diff --git a/src/appkit.rs b/src/appkit.rs deleted file mode 100644 index 2859d31..0000000 --- a/src/appkit.rs +++ /dev/null @@ -1,243 +0,0 @@ -//! Compatibility wrapper for the Cloudflare-backed internal Block App Kit CLI. -//! -//! This powers `sq agent-tools appkit` and `bl tools appkit`. It is separate -//! from the root `bl apps` command used by the external Apps Platform pilot -//! and must remain available while the internal experience is unchanged. - -use std::io; -use std::process::{Command, ExitStatus, Stdio}; - -use anyhow::{anyhow, Result}; - -use crate::cli::{global_arg_skip_count, APPKIT_COMMAND_NAME}; - -pub(crate) fn is_appkit_command(command_tokens: &[String]) -> bool { - command_tokens.first().map(String::as_str) == Some(APPKIT_COMMAND_NAME) -} - -pub(crate) fn should_run_before_bootstrap(raw_args: &[String]) -> bool { - let Some(position) = appkit_command_position(raw_args) else { - return false; - }; - - position.after_separator - || !raw_args[position.index + 1..] - .iter() - .any(|arg| arg == "--describe-commands") -} - -pub(crate) fn run(raw_args: &[String]) -> Result<()> { - let args = raw_args_after_appkit(raw_args); - exec_appkit(&args) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct AppkitCommandPosition { - index: usize, - after_separator: bool, -} - -fn appkit_command_position(raw_args: &[String]) -> Option { - let mut index = 0; - while index < raw_args.len() { - match raw_args[index].as_str() { - APPKIT_COMMAND_NAME => { - return Some(AppkitCommandPosition { - index, - after_separator: false, - }); - } - "--" => { - return (raw_args.get(index + 1).map(String::as_str) == Some(APPKIT_COMMAND_NAME)) - .then_some(AppkitCommandPosition { - index: index + 1, - after_separator: true, - }); - } - value if is_bootstrap_metadata_prefix(value) => return None, - value => { - let skip_count = global_arg_skip_count(value); - if skip_count == 0 { - return None; - } - index += skip_count; - } - } - } - - None -} - -fn is_bootstrap_metadata_prefix(arg: &str) -> bool { - matches!( - arg, - "--describe-commands" | "--summary" | "--write-extensions" - ) || arg.starts_with("--write-extensions=") -} - -fn raw_args_after_appkit(raw_args: &[String]) -> Vec<&str> { - let mut index = 0; - while index < raw_args.len() { - match raw_args[index].as_str() { - APPKIT_COMMAND_NAME => { - return raw_args[index + 1..].iter().map(String::as_str).collect(); - } - "--" => { - if raw_args.get(index + 1).map(String::as_str) == Some(APPKIT_COMMAND_NAME) { - return raw_args[index + 2..].iter().map(String::as_str).collect(); - } - index += 1; - } - value => index += global_arg_skip_count(value).max(1), - } - } - - Vec::new() -} - -fn exec_appkit(args: &[&str]) -> Result<()> { - match child_status(APPKIT_COMMAND_NAME, &[], args) { - Ok(status) => std::process::exit(child_exit_code(status)), - Err(err) if err.kind() == io::ErrorKind::NotFound => {} - Err(err) => return Err(command_error(APPKIT_COMMAND_NAME, err)), - } - - match child_status( - "uvx", - &["--from", "mcp_block_app_kit", APPKIT_COMMAND_NAME], - args, - ) { - Ok(status) => std::process::exit(child_exit_code(status)), - Err(err) if err.kind() == io::ErrorKind::NotFound => anyhow::bail!( - "appkit or uvx not found. Install appkit on PATH, or install uv so sq agent-tools can run mcp_block_app_kit on demand." - ), - Err(err) => Err(command_error("uvx", err)), - } -} - -fn child_status(binary: &str, prefix_args: &[&str], args: &[&str]) -> io::Result { - Command::new(binary) - .args(prefix_args) - .args(args) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .status() -} - -fn command_error(binary: &str, err: io::Error) -> anyhow::Error { - anyhow!("failed to execute `{binary}`: {err}") -} - -#[cfg(unix)] -fn child_exit_code(status: ExitStatus) -> i32 { - use std::os::unix::process::ExitStatusExt; - - status - .code() - .or_else(|| status.signal().map(|signal| 128 + signal)) - .unwrap_or(1) -} - -#[cfg(not(unix))] -fn child_exit_code(status: ExitStatus) -> i32 { - status.code().unwrap_or(1) -} - -#[cfg(test)] -mod tests { - #[cfg(unix)] - use std::process::ExitStatus; - - #[cfg(unix)] - use super::child_exit_code; - use super::{is_appkit_command, raw_args_after_appkit, should_run_before_bootstrap}; - - #[test] - fn appkit_command_is_detected_before_extension_loading() { - let tokens = vec!["appkit".to_string(), "deploy".to_string()]; - - assert!(is_appkit_command(&tokens)); - } - - #[test] - fn appkit_command_can_run_before_bootstrap() { - let raw_args = ["appkit", "deploy", "--timeout", "not-a-number", "--summary"] - .into_iter() - .map(String::from) - .collect::>(); - - assert!(should_run_before_bootstrap(&raw_args)); - } - - #[test] - fn appkit_command_before_bootstrap_skips_global_flag_values() { - let raw_args = ["--base-url", "appkit", "appkit", "deploy"] - .into_iter() - .map(String::from) - .collect::>(); - - assert!(should_run_before_bootstrap(&raw_args)); - } - - #[test] - fn appkit_describe_commands_stays_on_metadata_path() { - let nested_describe = ["appkit", "deploy", "--describe-commands"] - .into_iter() - .map(String::from) - .collect::>(); - let root_describe = ["--describe-commands", "appkit"] - .into_iter() - .map(String::from) - .collect::>(); - - assert!(!should_run_before_bootstrap(&nested_describe)); - assert!(!should_run_before_bootstrap(&root_describe)); - } - - #[test] - fn raw_args_after_appkit_preserves_appkit_owned_flags() { - let raw_args = [ - "--base-url", - "https://kgoose.example.test", - "appkit", - "deploy", - "--timeout", - "30", - "--version", - ] - .into_iter() - .map(String::from) - .collect::>(); - - assert_eq!( - raw_args_after_appkit(&raw_args), - vec!["deploy", "--timeout", "30", "--version"] - ); - } - - #[test] - fn raw_args_after_appkit_ignores_appkit_in_global_flag_values() { - let raw_args = [ - "--base-url", - "appkit", - "--playpen=appkit", - "appkit", - "deploy", - ] - .into_iter() - .map(String::from) - .collect::>(); - - assert_eq!(raw_args_after_appkit(&raw_args), vec!["deploy"]); - } - - #[cfg(unix)] - #[test] - fn child_exit_code_preserves_signal_status() { - use std::os::unix::process::ExitStatusExt; - - assert_eq!(child_exit_code(ExitStatus::from_raw(2)), 130); - assert_eq!(child_exit_code(ExitStatus::from_raw(7 << 8)), 7); - } -} diff --git a/src/bl/apps.rs b/src/bl/apps.rs index 03a6d45..8a2a643 100644 --- a/src/bl/apps.rs +++ b/src/bl/apps.rs @@ -1,10 +1,7 @@ //! External BuilderLab Apps Platform control-plane commands. //! -//! This module serves only the external BuilderLab Apps Platform pilot in -//! `bb-block` and the multi-tenant `bb-public` environment. It does not replace the -//! existing Cloudflare-backed internal Block App Kit CLI exposed through -//! `bl tools appkit`, and it does not migrate the separate internal Compose -//! workflow. Both internal paths remain unchanged. +//! This module serves the BuilderLab Apps Platform in `bb-block` and the +//! multi-tenant `bb-public` environment. //! //! The CLI sends its stored bbidentity session only to the allowlisted Compose //! control-plane origins. Public ingress authorizes that session through kgoose @@ -70,14 +67,10 @@ const TRUSTED_CONTROL_PLANE_HOSTS: &[&str] = &[ ]; pub fn command() -> Command { + let about = "Manage apps through the BuilderLab Apps Platform."; Command::new("apps") .about("Manage apps through Apps Platform") - .long_about( - "Manage apps through the BuilderLab Apps Platform control plane on Compose in \ - `bb-block` and multi-tenant `bb-public`. This does not replace the \ - Cloudflare-backed internal App Kit CLI (`bl tools appkit`) or migrate the separate internal \ - Compose workflow.", - ) + .long_about(about) .subcommand_required(true) .arg_required_else_help(true) .disable_help_subcommand(true) diff --git a/src/cli.rs b/src/cli.rs index 10cc276..e8fda3f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -16,12 +16,6 @@ pub const ROOT_BIN_NAME: &str = "sq agent-tools"; pub const ROOT_SUMMARY: &str = "Discover auth-backed tool extensions"; pub const TOOLS_COMMAND_NAME: &str = "tools"; pub const BL_TOOLS_BIN_NAME: &str = "bl tools"; -pub const APPKIT_COMMAND_NAME: &str = "appkit"; -pub const APPKIT_COMMAND_ABOUT: &str = "Cloudflare-backed internal Block App Kit CLI (local exec)"; -pub const APPKIT_COMMAND_LONG_ABOUT: &str = - "Proxies to the Cloudflare-backed internal appkit CLI. This is separate from the external \ - BuilderLab Apps Platform control plane exposed at root `bl apps`.\n\ - Requires appkit on PATH, or uvx to run mcp_block_app_kit on demand."; pub const EXTENSION_DESCRIBE_COMMAND_NAME: &str = "describe"; pub const EXTENSION_DESCRIBE_COMMAND_ABOUT: &str = "Print the full extension description/instructions."; @@ -81,28 +75,11 @@ impl GlobalValueFlag { } } -pub fn global_arg_skip_count(arg: &str) -> usize { - if GlobalValueFlag::from_name(arg).is_some() { - 2 - } else if split_global_value_assignment(arg).is_some() || is_global_switch_flag(arg) { - 1 - } else { - 0 - } -} - fn split_global_value_assignment(arg: &str) -> Option<(GlobalValueFlag, &str)> { let (name, value) = arg.split_once('=')?; GlobalValueFlag::from_name(name).map(|flag| (flag, value)) } -fn is_global_switch_flag(arg: &str) -> bool { - matches!( - arg, - "--describe-commands" | "--summary" | "--version" | "-V" - ) -} - pub fn bootstrap_args(args: I) -> Result where I: IntoIterator, @@ -313,11 +290,9 @@ pub fn build_tools_command( command = command.arg(arg); } - command = command.subcommand(appkit_command()); - for extension in extensions .iter() - .filter(|extension| extension.name != APPKIT_COMMAND_NAME) + .filter(|extension| is_supported_extension(&extension.name)) { let mut subcommand = Command::new(leak_str(extension.name.clone())) .about(leak_str(extension.about.clone())) @@ -340,12 +315,10 @@ pub fn build_tools_command( command } -fn appkit_command() -> Command { - Command::new(APPKIT_COMMAND_NAME) - .about(APPKIT_COMMAND_ABOUT) - .long_about(APPKIT_COMMAND_LONG_ABOUT) - .arg_required_else_help(true) - .disable_help_subcommand(true) +// App Kit belongs to the private employee CLI, including when an old catalog +// or backend still advertises it. It must not fall through to remote dispatch. +pub fn is_supported_extension(name: &str) -> bool { + name != "appkit" } fn extension_describe_command() -> Command { @@ -863,8 +836,8 @@ mod tests { ); let help = command.render_long_help().to_string(); - assert!(help.contains("appkit")); - assert!(help.contains("Block App Kit CLI (local exec)")); + assert!(!help.contains("appkit")); + assert!(!help.contains("Block App Kit CLI (local exec)")); assert!(help.contains("utils")); assert!(help.contains("Extensions")); assert!(help.contains("--timeout")); diff --git a/src/lib.rs b/src/lib.rs index 93d8d71..82620e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -mod appkit; mod bl; mod catalog; mod cli; @@ -16,10 +15,9 @@ use clap::error::ErrorKind; use clap::{ArgMatches, Command}; use cli::{ bool_false_id, bool_true_id, bootstrap_args, bootstrap_args_with_default_base_url, - build_tools_command, parameter_id, ToolCommandConfig, APPKIT_COMMAND_ABOUT, - APPKIT_COMMAND_NAME, BL_TOOLS_BIN_NAME, EXTENSION_DESCRIBE_COMMAND_ABOUT, - EXTENSION_DESCRIBE_COMMAND_NAME, ROOT_BIN_NAME, ROOT_COMMAND_NAME, ROOT_SUMMARY, - TOOLS_COMMAND_NAME, + build_tools_command, is_supported_extension, parameter_id, ToolCommandConfig, + BL_TOOLS_BIN_NAME, EXTENSION_DESCRIBE_COMMAND_ABOUT, EXTENSION_DESCRIBE_COMMAND_NAME, + ROOT_BIN_NAME, ROOT_COMMAND_NAME, ROOT_SUMMARY, TOOLS_COMMAND_NAME, }; use runtime::{ load_extension, load_extensions, load_extensions_with_auth_status, LoadedExtension, @@ -48,7 +46,6 @@ struct ToolsCliConfig { command: ToolCommandConfig, describe_root_name: &'static str, version_bin_name: &'static str, - nested_appkit_help: &'static str, builderlab_mode: bool, } @@ -61,7 +58,6 @@ fn agent_tools_config() -> ToolsCliConfig { }, describe_root_name: ROOT_COMMAND_NAME, version_bin_name: ROOT_BIN_NAME, - nested_appkit_help: "sq agent-tools appkit --help", builderlab_mode: false, } } @@ -75,7 +71,6 @@ fn bl_tools_config() -> ToolsCliConfig { }, describe_root_name: TOOLS_COMMAND_NAME, version_bin_name: BL_TOOLS_BIN_NAME, - nested_appkit_help: "bl tools appkit --help", builderlab_mode: true, } } @@ -205,10 +200,6 @@ fn build_bl_command() -> Command { } fn run_tools_cli(argv0: &str, raw_args: &[String], config: ToolsCliConfig) -> Result<()> { - if appkit::should_run_before_bootstrap(raw_args) { - return appkit::run(raw_args); - } - let proxy_base_url = if config.builderlab_mode { blox_tools_base_url()? } else { @@ -219,6 +210,7 @@ fn run_tools_cli(argv0: &str, raw_args: &[String], config: ToolsCliConfig) -> Re } else { bootstrap_args(raw_args.iter().cloned())? }; + ensure_supported_extension(&bootstrap.command_tokens)?; let mut kgoose_config = KgooseConfig { base_url: bootstrap.base_url.clone(), @@ -278,14 +270,6 @@ fn run_tools_cli(argv0: &str, raw_args: &[String], config: ToolsCliConfig) -> Re return Ok(()); } - // The Cloudflare-backed internal App Kit deploy needs direct access to the - // local workspace so it can tar files and upload them. Keep this compatibility - // path as a local process; root `bl apps` is the separate Apps Platform - // control-plane client. - if appkit::is_appkit_command(&bootstrap.command_tokens) { - return appkit::run(raw_args); - } - let selected_extension_name = bootstrap .command_tokens .first() @@ -462,6 +446,7 @@ fn load_command_description( command_tokens: &[String], tools_cli: ToolsCliConfig, ) -> Result { + ensure_supported_extension(command_tokens)?; let command_path = command_tokens .iter() .take_while(|token| !token.starts_with('-')) @@ -470,13 +455,6 @@ fn load_command_description( match command_path.as_slice() { [] => load_root_command_description(client, config, tools_cli), - [APPKIT_COMMAND_NAME] => Ok(appkit_command_description()), - [APPKIT_COMMAND_NAME, ..] => { - anyhow::bail!( - "`--describe-commands` does not inspect nested appkit commands; run `{}`", - tools_cli.nested_appkit_help - ) - } [extension_name] => load_extension_command_description( client, config, @@ -606,18 +584,15 @@ fn root_command_description( extensions: &[runtime::ExtensionSummary], root_name: &str, ) -> CommandDescription { - let mut commands = vec![appkit_command_description()]; - commands.extend(extensions.iter().filter_map(|extension| { - if extension.name == APPKIT_COMMAND_NAME { - None - } else { - Some(CommandDescription { - name: extension.name.clone(), - summary: sq_command_summary(&extension.about), - commands: Vec::new(), - }) - } - })); + let commands = extensions + .iter() + .filter(|extension| is_supported_extension(&extension.name)) + .map(|extension| CommandDescription { + name: extension.name.clone(), + summary: sq_command_summary(&extension.about), + commands: Vec::new(), + }) + .collect(); CommandDescription { name: root_name.to_string(), @@ -626,12 +601,11 @@ fn root_command_description( } } -fn appkit_command_description() -> CommandDescription { - CommandDescription { - name: APPKIT_COMMAND_NAME.to_string(), - summary: sq_command_summary(APPKIT_COMMAND_ABOUT), - commands: Vec::new(), +fn ensure_supported_extension(command_tokens: &[String]) -> Result<()> { + if let Some(name) = command_tokens.first() { + anyhow::ensure!(is_supported_extension(name), "unknown extension `{name}`"); } + Ok(()) } fn tool_command_description(tool: &RuntimeTool) -> CommandDescription { @@ -950,12 +924,11 @@ mod tests { use serde_json::json; use super::{ - agent_tools_config, appkit_command_description, bl_tools_config, build_tool_request, - extension_command_description, load_command_description, parse_bool_value, parse_header, - render_tool_response, root_command_description, sq_command_summary, - tool_command_description, CommandDescription, + agent_tools_config, bl_tools_config, build_tool_request, extension_command_description, + load_command_description, parse_bool_value, parse_header, render_tool_response, + root_command_description, sq_command_summary, tool_command_description, CommandDescription, }; - use crate::cli::{build_command, APPKIT_COMMAND_NAME, ROOT_SUMMARY}; + use crate::cli::{build_command, ROOT_SUMMARY}; use crate::kgoose::{ CallToolResponse, ExtensionInfo, KgooseClient, KgooseConfig, ListExtensionsResponse, ListToolsResponse, ToolConfig, @@ -1206,94 +1179,42 @@ mod tests { "agent-tools", ); - assert_eq!( - description, - CommandDescription { - name: "agent-tools".to_string(), - summary: ROOT_SUMMARY.to_string(), - commands: vec![ - CommandDescription { - name: "appkit".to_string(), - summary: "Cloudflare-backed internal Block App Kit CLI (local exec)" - .to_string(), - commands: Vec::new(), - }, - CommandDescription { - name: "slack".to_string(), - summary: "Slack tools for chat".to_string(), - commands: Vec::new(), - }, - CommandDescription { - name: "utils".to_string(), - summary: "Utility helpers".to_string(), - commands: Vec::new(), - }, - ], - } - ); - } - - #[test] - fn appkit_command_description_is_static() { - assert_eq!( - appkit_command_description(), - CommandDescription { - name: APPKIT_COMMAND_NAME.to_string(), - summary: "Cloudflare-backed internal Block App Kit CLI (local exec)".to_string(), - commands: Vec::new(), - } - ); - } - - #[test] - fn appkit_description_does_not_load_extension_metadata() { - let description = load_command_description( - &PanickingKgooseClient, - &kgoose_config(), - &["appkit".to_string()], - agent_tools_config(), - ) - .expect("load appkit command description"); - - assert_eq!(description, appkit_command_description()); - } - - #[test] - fn nested_appkit_describe_commands_uses_appkit_specific_error() { - let error = load_command_description( - &PanickingKgooseClient, - &kgoose_config(), - &[ - "appkit".to_string(), - "deploy".to_string(), - "my-site".to_string(), + let expected = CommandDescription { + name: "agent-tools".to_string(), + summary: ROOT_SUMMARY.to_string(), + commands: vec![ + CommandDescription { + name: "slack".to_string(), + summary: "Slack tools for chat".to_string(), + commands: Vec::new(), + }, + CommandDescription { + name: "utils".to_string(), + summary: "Utility helpers".to_string(), + commands: Vec::new(), + }, ], - agent_tools_config(), - ) - .expect_err("nested appkit description should fail locally"); - - let message = error.to_string(); - assert!(message.contains("does not inspect nested appkit commands")); - assert!(message.contains("sq agent-tools appkit --help")); + }; + assert_eq!(description, expected); } #[test] - fn nested_appkit_describe_commands_uses_bl_tools_error_for_bl_tools() { - let error = load_command_description( - &PanickingKgooseClient, - &kgoose_config(), - &[ - "appkit".to_string(), - "deploy".to_string(), - "my-site".to_string(), - ], - bl_tools_config(), - ) - .expect_err("nested appkit description should fail locally"); - - let message = error.to_string(); - assert!(message.contains("does not inspect nested appkit commands")); - assert!(message.contains("bl tools appkit --help")); + fn removed_local_extension_metadata_never_contacts_backend() { + for config in [agent_tools_config(), bl_tools_config()] { + for tokens in [ + vec!["appkit".to_string()], + vec!["appkit".to_string(), "deploy".to_string()], + ] { + let error = load_command_description( + &PanickingKgooseClient, + &kgoose_config(), + &tokens, + config, + ) + .expect_err("removed extension must fail locally"); + assert_eq!(error.to_string(), "unknown extension `appkit`"); + } + } } #[test] diff --git a/tests/bl_e2e.rs b/tests/bl_e2e.rs index dee6d11..72ff9a7 100644 --- a/tests/bl_e2e.rs +++ b/tests/bl_e2e.rs @@ -3652,9 +3652,6 @@ fn bl_apps_help_distinguishes_external_and_internal_paths() { "Apps Platform", "bb-block", "bb-public", - "Cloudflare-backed internal App Kit", - "bl tools appkit", - "separate internal Compose workflow", "list", "get", "versions", @@ -3668,6 +3665,13 @@ fn bl_apps_help_distinguishes_external_and_internal_paths() { "help did not explain {expected:?}: {stdout}" ); } + for internal_help in [ + "Cloudflare-backed internal App Kit", + "bl tools appkit", + "separate internal Compose workflow", + ] { + assert!(!stdout.contains(internal_help), "{stdout}"); + } } #[test] @@ -4839,23 +4843,17 @@ fn bl_tools_describe_commands_uses_configured_extensions_and_connection_status() assert!(output.status.success(), "stderr was: {stderr}"); let description = serde_json::from_str::(&stdout).expect("parse describe output"); assert_eq!(description["name"], json!("tools")); - assert_eq!( - description["commands"], - json!([ - { - "name": "appkit", - "summary": "Cloudflare-backed internal Block App Kit CLI (local exec)" - }, - { - "name": "secret", - "summary": "[not connected] Needs more auth" - }, - { - "name": "utils", - "summary": "Utility helpers" - } - ]) - ); + let expected_commands = json!([ + { + "name": "secret", + "summary": "[not connected] Needs more auth" + }, + { + "name": "utils", + "summary": "Utility helpers" + } + ]); + assert_eq!(description["commands"], expected_commands); assert!(stderr.is_empty(), "stderr was: {stderr}"); assert_eq!(requests.len(), 1); assert_eq!(requests[0].path, "/api/v3/list-extensions"); diff --git a/tests/cli_e2e.rs b/tests/cli_e2e.rs index 780dab0..5afd12b 100644 --- a/tests/cli_e2e.rs +++ b/tests/cli_e2e.rs @@ -10,8 +10,6 @@ use sha2::{Digest, Sha256}; #[cfg(unix)] use common::temp_test_dir; -#[cfg(unix)] -use common::write_fake_executable; use common::{ calculate_tool_schema, list_tools_response, output_text, post_message_tool_schema, tool_response, tool_response_with_extension_description, unique_suffix, @@ -622,190 +620,21 @@ fn describe_commands_uses_static_catalog_without_network() { assert!(output.status.success(), "stderr was: {stderr}"); let description = serde_json::from_str::(&stdout).expect("parse describe output"); assert_eq!(description["name"], json!("agent-tools")); - assert_eq!( - description["commands"], - json!([ - { - "name": "appkit", - "summary": "Cloudflare-backed internal Block App Kit CLI (local exec)" - }, - { - "name": "secret", - "summary": "Needs more auth" - }, - { - "name": "utils", - "summary": "Utility helpers" - } - ]) - ); + let expected_commands = json!([ + { + "name": "secret", + "summary": "Needs more auth" + }, + { + "name": "utils", + "summary": "Utility helpers" + } + ]); + assert_eq!(description["commands"], expected_commands); assert!(stderr.is_empty(), "stderr was: {stderr}"); assert!(requests.is_empty(), "requests were: {requests:#?}"); } -#[cfg(unix)] -#[test] -fn appkit_no_args_passes_through_to_real_cli() { - let fake_bin = temp_test_dir("appkit-no-args-passthrough"); - write_fake_executable( - &fake_bin, - "appkit", - "#!/bin/sh\nprintf 'appkit:%s\\n' \"$*\"\n", - ); - let server = MockServer::start(vec![]); - - let output = server - .command() - .env("PATH", &fake_bin) - .args(["appkit"]) - .output() - .expect("run appkit no args"); - let requests = server.finish(); - let (stdout, stderr) = output_text(&output); - fs::remove_dir_all(&fake_bin).expect("remove fake bin"); - - assert!(output.status.success(), "stderr was: {stderr}"); - assert_eq!(stdout.trim(), "appkit:"); - assert!(requests.is_empty(), "requests were: {requests:#?}"); -} - -#[cfg(unix)] -#[test] -fn appkit_help_flag_passes_through_to_real_cli() { - let fake_bin = temp_test_dir("appkit-help-passthrough"); - write_fake_executable( - &fake_bin, - "appkit", - "#!/bin/sh\nprintf 'appkit:%s\\n' \"$*\"\n", - ); - let server = MockServer::start(vec![]); - - let output = server - .command() - .env("PATH", &fake_bin) - .args(["appkit", "--help"]) - .output() - .expect("run appkit --help"); - let requests = server.finish(); - let (stdout, stderr) = output_text(&output); - fs::remove_dir_all(&fake_bin).expect("remove fake bin"); - - assert!(output.status.success(), "stderr was: {stderr}"); - assert_eq!(stdout.trim(), "appkit:--help"); - assert!(requests.is_empty(), "requests were: {requests:#?}"); -} - -#[cfg(unix)] -#[test] -fn appkit_on_path_is_preferred_over_uvx() { - let fake_bin = temp_test_dir("appkit-preferred"); - write_fake_executable( - &fake_bin, - "appkit", - "#!/bin/sh\nprintf 'appkit:%s\\n' \"$*\"\n", - ); - write_fake_executable(&fake_bin, "uvx", "#!/bin/sh\nprintf 'uvx:%s\\n' \"$*\"\n"); - let server = MockServer::start(vec![]); - - let output = server - .command() - .env("PATH", &fake_bin) - .args(["appkit", "deploy", "my-site", "./build"]) - .output() - .expect("run fake appkit"); - let requests = server.finish(); - let (stdout, stderr) = output_text(&output); - fs::remove_dir_all(&fake_bin).expect("remove fake bin"); - - assert!(output.status.success(), "stderr was: {stderr}"); - assert_eq!(stdout.trim(), "appkit:deploy my-site ./build"); - assert!(!stdout.contains("uvx:")); - assert!(requests.is_empty(), "requests were: {requests:#?}"); -} - -#[cfg(unix)] -#[test] -fn appkit_owned_sq_flags_are_forwarded_before_bootstrap() { - let fake_bin = temp_test_dir("appkit-owned-flags"); - write_fake_executable( - &fake_bin, - "appkit", - "#!/bin/sh\nprintf 'appkit:%s\\n' \"$*\"\n", - ); - let server = MockServer::start(vec![]); - - let output = server - .command() - .env("PATH", &fake_bin) - .env("KGOOSE_TIMEOUT", "not-a-number") - .args([ - "appkit", - "deploy", - "--timeout", - "also-not-a-number", - "--summary", - ]) - .output() - .expect("run fake appkit with sq-looking args"); - let requests = server.finish(); - let (stdout, stderr) = output_text(&output); - fs::remove_dir_all(&fake_bin).expect("remove fake bin"); - - assert!(output.status.success(), "stderr was: {stderr}"); - assert_eq!( - stdout.trim(), - "appkit:deploy --timeout also-not-a-number --summary" - ); - assert!(requests.is_empty(), "requests were: {requests:#?}"); -} - -#[cfg(unix)] -#[test] -fn appkit_falls_back_to_uvx_and_preserves_environment() { - let fake_bin = temp_test_dir("appkit-uvx-fallback"); - write_fake_executable( - &fake_bin, - "uvx", - "#!/bin/sh\nprintf 'uvx:%s\\n' \"$*\"\nprintf 'sts:%s\\n' \"${STS_ACCESS_TOKEN:-}\"\n", - ); - let server = MockServer::start(vec![]); - - let output = server - .command() - .env("PATH", &fake_bin) - .env("STS_ACCESS_TOKEN", "test-sts-token") - .args(["appkit", "deploy", "my-site", "./build"]) - .output() - .expect("run fake uvx fallback"); - let requests = server.finish(); - let (stdout, stderr) = output_text(&output); - fs::remove_dir_all(&fake_bin).expect("remove fake bin"); - - assert!(output.status.success(), "stderr was: {stderr}"); - assert!(stdout.contains("uvx:--from mcp_block_app_kit appkit deploy my-site ./build")); - assert!(stdout.contains("sts:test-sts-token")); - assert!(requests.is_empty(), "requests were: {requests:#?}"); -} - -#[test] -fn appkit_missing_binary_prints_clear_error() { - let server = MockServer::start(vec![]); - - let output = server - .command() - .env("PATH", "") - .args(["appkit", "list"]) - .output() - .expect("run missing appkit command"); - let requests = server.finish(); - let (_stdout, stderr) = output_text(&output); - - assert!(!output.status.success()); - assert!(stderr.contains("appkit or uvx not found")); - assert!(stderr.contains("sq agent-tools can run mcp_block_app_kit on demand")); - assert!(requests.is_empty(), "requests were: {requests:#?}"); -} - #[test] fn inaccessible_extension_errors_are_humanized_in_e2e_flow() { let server = MockServer::start(vec![MockResponse::text( diff --git a/tests/removed_appkit_e2e.rs b/tests/removed_appkit_e2e.rs new file mode 100644 index 0000000..c0089c3 --- /dev/null +++ b/tests/removed_appkit_e2e.rs @@ -0,0 +1,139 @@ +//! Removed local extensions must not appear or execute through either binary. +#![cfg(unix)] + +mod common; + +use std::fs; +use std::path::Path; +use std::process::Command; + +use serde_json::{json, Value}; + +use common::{output_text, write_fake_executable, MockServer}; + +fn tools_command(server: &MockServer, builderlab: bool, temp: &Path) -> Command { + let mut command = if builderlab { + server.bl_tools_command() + } else { + server.command() + }; + command + .env("BL_AUTH_STORAGE", "file") + .env("BL_AUTH_STORAGE_FILE", temp.join("auth.json")); + command +} + +#[test] +fn removed_appkit_is_absent_from_help_and_discovery_even_if_advertised_remotely() { + let temp = tempfile::tempdir().expect("create temp directory"); + let catalog = temp.path().join("extensions.yaml"); + fs::write( + &catalog, + "- name: appkit\n about: Must not appear\n- name: utils\n about: Utility helpers\n", + ) + .expect("write static catalog"); + + for builderlab in [false, true] { + for arg in ["--help", "--describe-commands"] { + let responses = if builderlab { + vec![common::MockResponse::json(json!({"extensions": [ + {"name": "appkit", "description": "Must not appear"}, + {"name": "utils", "description": "Utility helpers"} + ]}))] + } else { + vec![] + }; + let server = MockServer::start(responses); + let output = tools_command(&server, builderlab, temp.path()) + .env("KGOOSE_EXTENSIONS_CATALOG", &catalog) + .arg(arg) + .output() + .expect("discover tools"); + let requests = server.finish(); + let (stdout, stderr) = output_text(&output); + + assert!(output.status.success(), "{builderlab} {arg}: {stderr}"); + assert!(!stdout.contains("appkit"), "{stdout}"); + assert!(!stdout.contains("Cloudflare"), "{stdout}"); + if arg == "--describe-commands" { + let description: Value = serde_json::from_str(&stdout).expect("parse discovery"); + assert_eq!(description["commands"][0]["name"], "utils"); + assert_eq!(description["commands"].as_array().unwrap().len(), 1); + } else { + assert!(stdout.contains("utils"), "{stdout}"); + } + assert_eq!(requests.len(), usize::from(builderlab)); + } + } + + let output = common::bl_command() + .args(["apps", "--help"]) + .output() + .expect("inspect Apps Platform help"); + let (stdout, stderr) = output_text(&output); + assert!(output.status.success(), "{stderr}"); + assert!(stdout.contains("Apps Platform")); + assert!(!stdout.contains("appkit"), "{stdout}"); + assert!(!stdout.contains("Cloudflare"), "{stdout}"); +} + +#[test] +fn removed_appkit_rejects_execution_and_targeted_metadata_without_local_or_remote_fallback() { + let temp = tempfile::tempdir().expect("create temp directory"); + let marker = temp.path().join("executed"); + let args: &[&[&str]] = &[ + &["appkit"], + &["appkit", "--help"], + &["appkit", "deploy"], + &["appkit", "deploy", "--timeout", "invalid", "--summary"], + &["--", "appkit", "deploy"], + &["--playpen", "appkit", "appkit", "deploy"], + &["--playpen=appkit", "appkit", "deploy"], + &["--timeout", "3", "appkit", "deploy"], + &["--describe-commands", "appkit"], + &["appkit", "--describe-commands"], + &["appkit", "deploy", "--describe-commands"], + &["--summary", "appkit"], + &["appkit", "--summary"], + &["--describe-commands", "--", "appkit"], + &["--summary", "--", "appkit"], + ]; + + for appkit_on_path in [false, true] { + let fake_bin = temp + .path() + .join(if appkit_on_path { "both" } else { "uvx-only" }); + fs::create_dir(&fake_bin).expect("create fake PATH"); + let script = "#!/bin/sh\nprintf invoked > \"$APPKIT_TEST_MARKER\"\nexit 71\n"; + write_fake_executable(&fake_bin, "uvx", script); + if appkit_on_path { + write_fake_executable(&fake_bin, "appkit", script); + } + + for builderlab in [false, true] { + for args in args { + let server = MockServer::start(vec![]); + let output = tools_command(&server, builderlab, temp.path()) + .env("PATH", &fake_bin) + .env("APPKIT_TEST_MARKER", &marker) + .args(*args) + .output() + .expect("run removed App Kit command"); + let requests = server.finish(); + let (stdout, stderr) = output_text(&output); + + assert!(!output.status.success(), "{builderlab} {args:?}: {stdout}"); + assert!(!stderr.is_empty(), "{builderlab} {args:?}: {stderr}"); + if !args.contains(&"invalid") { + assert!(stderr.contains("unknown extension `appkit`"), "{stderr}"); + } + assert!(stdout.is_empty(), "{stdout}"); + assert!(!marker.exists(), "local executable ran for {args:?}"); + assert!( + requests.is_empty(), + "remote fallback for {args:?}: {requests:?}" + ); + } + } + } +}