fix: require explicit runtime trust replacement

This commit is contained in:
Keisuke Hirata 2026-07-26 23:18:51 +09:00
parent d994268a6b
commit 9a14f1b6fc
No known key found for this signature in database

View File

@ -257,7 +257,9 @@ fn run_identity_command(args: Vec<String>) -> Result<(), Box<dyn std::error::Err
_ => return Err(Box::new(CliError(format!("unknown identity init argument `{flag}`")))), _ => 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(); let path = server_identity_path();
if read_server_identity_file(&path)?.is_some() && !replace { if read_server_identity_file(&path)?.is_some() && !replace {
return Err(Box::new(CliError(format!( return Err(Box::new(CliError(format!(
@ -316,6 +318,7 @@ fn run_trust_runtime_command(args: Vec<String>) -> Result<(), Box<dyn std::error
let mut base_url = None; let mut base_url = None;
let mut public_key = None; let mut public_key = None;
let mut display_name = None; let mut display_name = None;
let mut replace = false;
while let Some(arg) = args.pop_front() { while let Some(arg) = args.pop_front() {
let (flag, inline_value) = split_flag_value(arg)?; let (flag, inline_value) = split_flag_value(arg)?;
match flag.as_str() { match flag.as_str() {
@ -323,6 +326,10 @@ fn run_trust_runtime_command(args: Vec<String>) -> Result<(), Box<dyn std::error
"--base-url" | "--endpoint" => base_url = Some(take_value(&flag, inline_value, &mut args)?), "--base-url" | "--endpoint" => base_url = Some(take_value(&flag, inline_value, &mut args)?),
"--public-key" => public_key = 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)?), "--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}`")))), _ => return Err(Box::new(CliError(format!("unknown trust-runtime add argument `{flag}`")))),
} }
} }
@ -330,6 +337,7 @@ fn run_trust_runtime_command(args: Vec<String>) -> Result<(), Box<dyn std::error
let base_url = base_url.ok_or_else(|| CliError("trust-runtime add requires --base-url".to_string()))?; let base_url = base_url.ok_or_else(|| CliError("trust-runtime add requires --base-url".to_string()))?;
let public_key = public_key.ok_or_else(|| CliError("trust-runtime add requires --public-key".to_string()))?; let public_key = public_key.ok_or_else(|| CliError("trust-runtime add requires --public-key".to_string()))?;
decode_public_key(&public_key)?; decode_public_key(&public_key)?;
ensure_trusted_runtime_replace_allowed(&store, &runtime_id, replace)?;
let now = Utc::now().to_rfc3339(); let now = Utc::now().to_rfc3339();
store.upsert_trusted_runtime(&TrustedRuntimeRecord { store.upsert_trusted_runtime(&TrustedRuntimeRecord {
runtime_id: runtime_id.clone(), runtime_id: runtime_id.clone(),
@ -398,6 +406,24 @@ fn run_trust_runtime_command(args: Vec<String>) -> Result<(), Box<dyn std::error
} }
} }
fn ensure_trusted_runtime_replace_allowed(
store: &SqliteWorkspaceStore,
runtime_id: &str,
replace: bool,
) -> Result<(), Box<dyn std::error::Error>> {
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<String>), CliError> { fn split_flag_value(arg: String) -> Result<(String, Option<String>), CliError> {
if let Some((flag, value)) = arg.split_once('=') { if let Some((flag, value)) = arg.split_once('=') {
if flag.is_empty() { if flag.is_empty() {
@ -743,7 +769,7 @@ fn parse_listen(value: &str) -> Result<SocketAddr, CliError> {
fn print_help() { fn print_help() {
println!( println!(
"yoi-workspace-server\n\nUsage:\n yoi-workspace-server init [OPTIONS]\n yoi-workspace-server config <COMMAND> [OPTIONS]\n yoi-workspace-server skills <COMMAND> [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 <COMMAND> [OPTIONS]\n yoi-workspace-server identity init --server-id <SERVER_ID> [--replace]\n yoi-workspace-server identity show [--json]\n yoi-workspace-server trust-runtime add --runtime-id <RUNTIME_ID> --base-url <URL> --public-key <KEY> [--display-name <NAME>] [--replace]\n yoi-workspace-server trust-runtime list [--json] [--include-revoked]\n yoi-workspace-server trust-runtime revoke --runtime-id <RUNTIME_ID>\n yoi-workspace-server skills <COMMAND> [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] #[tokio::test]
async fn init_creates_identity_local_config_and_server_records() { async fn init_creates_identity_local_config_and_server_records() {
let temp = tempfile::tempdir().unwrap(); let temp = tempfile::tempdir().unwrap();