From 9a14f1b6fc120a462516afa9ae90730680624f8b Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 26 Jul 2026 23:18:51 +0900 Subject: [PATCH] fix: require explicit runtime trust replacement --- crates/workspace-server/src/main.rs | 63 ++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/crates/workspace-server/src/main.rs b/crates/workspace-server/src/main.rs index 5ec9d219..fc64698a 100644 --- a/crates/workspace-server/src/main.rs +++ b/crates/workspace-server/src/main.rs @@ -257,7 +257,9 @@ fn run_identity_command(args: Vec) -> Result<(), Box return Err(Box::new(CliError(format!("unknown identity init argument `{flag}`")))), } } - let server_id = server_id.unwrap_or_else(|| "server-main".to_string()); + let server_id = server_id.ok_or_else(|| { + CliError("identity init requires --server-id".to_string()) + })?; let path = server_identity_path(); if read_server_identity_file(&path)?.is_some() && !replace { return Err(Box::new(CliError(format!( @@ -316,6 +318,7 @@ fn run_trust_runtime_command(args: Vec) -> Result<(), Box) -> Result<(), Box base_url = Some(take_value(&flag, inline_value, &mut args)?), "--public-key" => public_key = Some(take_value(&flag, inline_value, &mut args)?), "--display-name" => display_name = Some(take_value(&flag, inline_value, &mut args)?), + "--replace" => { + ensure_no_inline_value(&flag, inline_value.as_deref())?; + replace = true; + } _ => return Err(Box::new(CliError(format!("unknown trust-runtime add argument `{flag}`")))), } } @@ -330,6 +337,7 @@ fn run_trust_runtime_command(args: Vec) -> Result<(), Box) -> Result<(), Box Result<(), Box> { + if store + .list_trusted_runtimes(true)? + .iter() + .any(|runtime| runtime.runtime_id == runtime_id) + && !replace + { + return Err(Box::new(CliError(format!( + "trusted runtime `{runtime_id}` already exists; pass --replace to update it" + )))); + } + Ok(()) +} + fn split_flag_value(arg: String) -> Result<(String, Option), CliError> { if let Some((flag, value)) = arg.split_once('=') { if flag.is_empty() { @@ -743,7 +769,7 @@ fn parse_listen(value: &str) -> Result { fn print_help() { println!( - "yoi-workspace-server\n\nUsage:\n yoi-workspace-server init [OPTIONS]\n yoi-workspace-server config [OPTIONS]\n yoi-workspace-server skills [OPTIONS]\n yoi-workspace-server serve [OPTIONS]\n\nOptions:\n -h, --help Print help" + "yoi-workspace-server\n\nUsage:\n yoi-workspace-server init [OPTIONS]\n yoi-workspace-server config [OPTIONS]\n yoi-workspace-server identity init --server-id [--replace]\n yoi-workspace-server identity show [--json]\n yoi-workspace-server trust-runtime add --runtime-id --base-url --public-key [--display-name ] [--replace]\n yoi-workspace-server trust-runtime list [--json] [--include-revoked]\n yoi-workspace-server trust-runtime revoke --runtime-id \n yoi-workspace-server skills [OPTIONS]\n yoi-workspace-server serve [OPTIONS]\n\nOptions:\n -h, --help Print help" ); } @@ -813,6 +839,39 @@ mod tests { ); } + #[test] + fn server_identity_init_requires_explicit_server_id() { + let error = run_identity_command(vec!["init".to_string()]).unwrap_err(); + assert_eq!(error.to_string(), "identity init requires --server-id"); + } + + #[test] + fn trusted_runtime_add_requires_replace_for_existing_record() { + let temp = tempfile::tempdir().unwrap(); + let store = SqliteWorkspaceStore::open(temp.path().join("server.db")).unwrap(); + let public_key = RuntimeIdentityMaterial::generate("runtime-a") + .unwrap() + .public_key; + store + .upsert_trusted_runtime(&TrustedRuntimeRecord { + runtime_id: "runtime-a".to_string(), + display_name: "Runtime A".to_string(), + base_url: "http://127.0.0.1:18080".to_string(), + public_key, + created_at: "2026-07-26T00:00:00Z".to_string(), + updated_at: "2026-07-26T00:00:00Z".to_string(), + revoked_at: None, + }) + .unwrap(); + + let error = ensure_trusted_runtime_replace_allowed(&store, "runtime-a", false).unwrap_err(); + assert_eq!( + error.to_string(), + "trusted runtime `runtime-a` already exists; pass --replace to update it" + ); + ensure_trusted_runtime_replace_allowed(&store, "runtime-a", true).unwrap(); + } + #[tokio::test] async fn init_creates_identity_local_config_and_server_records() { let temp = tempfile::tempdir().unwrap();