Skip to content

Commit d17b90c

Browse files
hanwenchengclaude
andcommitted
fix(cli): #19 codex P2 v2 — pre-flight --env validation before backend I/O
Two correctness issues from the second codex pass on PR #19: 1. Pre-flight --env validation ran AFTER the master-session list_credentials() call, so a malformed `--env NO_EQUALS` could still produce a backend round-trip and a list/DENIED audit row before the parse error fired. Move the validation loop above the list_credentials branch so the pre-flight guarantee in the comment actually holds for the master-session path. 2. The validation only checked for the presence of '=', so `--env =foo` (empty KEY) and `--env BAR=` (empty SERVICE) were accepted and then blew up later as opaque runtime errors (empty env-var name passed to process spawn / empty service name passed to read_credential). Reject both up front with a clear "KEY must not be empty" / "SERVICE must not be empty" message. Tests: cmd_run_env_flag_empty_key_rejected, cmd_run_env_flag_empty_service_rejected. Test: cargo test -p agentkeys-cli -- --test-threads=1 → 20 passed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1a09a69 commit d17b90c

2 files changed

Lines changed: 70 additions & 17 deletions

File tree

crates/agentkeys-cli/src/lib.rs

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,31 @@ pub async fn cmd_run(
160160

161161
let backend = ctx.backend();
162162

163+
// Pre-flight validation: reject any invalid --env entries BEFORE any credential
164+
// I/O (no network round-trips or audit log entries for a partial invocation).
165+
// Must run before list_credentials so a malformed override does not produce a
166+
// backend round-trip / DENIED audit row on the master-session path (codex P2 v2).
167+
for raw in env_overrides {
168+
let eq_pos = raw.find('=').ok_or_else(|| {
169+
anyhow!(
170+
"Invalid --env format '{}': expected KEY=SERVICE (no '=' found)",
171+
raw
172+
)
173+
})?;
174+
if eq_pos == 0 {
175+
return Err(anyhow!(
176+
"Invalid --env format '{}': KEY must not be empty",
177+
raw
178+
));
179+
}
180+
if eq_pos + 1 == raw.len() {
181+
return Err(anyhow!(
182+
"Invalid --env format '{}': SERVICE must not be empty",
183+
raw
184+
));
185+
}
186+
}
187+
163188
let services_to_try: Vec<String> = if let Some(scope) = &session.scope {
164189
scope.services.iter().map(|s| s.0.clone()).collect()
165190
} else {
@@ -172,17 +197,6 @@ pub async fn cmd_run(
172197
.collect()
173198
};
174199

175-
// Pre-flight validation: reject any invalid --env entries BEFORE any credential
176-
// I/O (no network round-trips or audit log entries for a partial invocation).
177-
for raw in env_overrides {
178-
if raw.find('=').is_none() {
179-
return Err(anyhow!(
180-
"Invalid --env format '{}': expected KEY=SERVICE (no '=' found)",
181-
raw
182-
));
183-
}
184-
}
185-
186200
// Track which services we've already fetched in the auto-injection pass.
187201
// The --env loop below reuses these values instead of issuing a second
188202
// read_credential for the same service, which would double-count audit
@@ -214,12 +228,7 @@ pub async fn cmd_run(
214228
}
215229

216230
for raw in env_overrides {
217-
let eq_pos = raw.find('=').ok_or_else(|| {
218-
anyhow!(
219-
"Invalid --env format '{}': expected KEY=SERVICE (no '=' found)",
220-
raw
221-
)
222-
})?;
231+
let eq_pos = raw.find('=').expect("pre-flight validation already rejected entries without '='");
223232
let env_key = raw[..eq_pos].to_string();
224233
let service = &raw[eq_pos + 1..];
225234

crates/agentkeys-cli/tests/cli_tests.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,47 @@ async fn cmd_run_env_flag_invalid_format() {
367367
"unexpected error message: {err}"
368368
);
369369
}
370+
371+
// Test 19 (codex P2 v2): --env with empty KEY (e.g. "=github") rejected up
372+
// front, no backend round-trip and no DENIED audit row.
373+
#[tokio::test(flavor = "multi_thread")]
374+
async fn cmd_run_env_flag_empty_key_rejected() {
375+
let backend = create_test_backend();
376+
let (wallet, session) = init_session_direct(&backend).await;
377+
let context = ctx_with_session(backend, session);
378+
379+
let result = agentkeys_cli::cmd_run(
380+
&context,
381+
&wallet,
382+
&["=github".to_string()],
383+
&["true".to_string()],
384+
)
385+
.await;
386+
let err = result.expect_err("empty KEY must be rejected").to_string();
387+
assert!(
388+
err.contains("KEY must not be empty"),
389+
"unexpected error: {err}"
390+
);
391+
}
392+
393+
// Test 20 (codex P2 v2): --env with empty SERVICE (e.g. "MY_KEY=") rejected
394+
// up front, no backend round-trip for an empty service name.
395+
#[tokio::test(flavor = "multi_thread")]
396+
async fn cmd_run_env_flag_empty_service_rejected() {
397+
let backend = create_test_backend();
398+
let (wallet, session) = init_session_direct(&backend).await;
399+
let context = ctx_with_session(backend, session);
400+
401+
let result = agentkeys_cli::cmd_run(
402+
&context,
403+
&wallet,
404+
&["MY_KEY=".to_string()],
405+
&["true".to_string()],
406+
)
407+
.await;
408+
let err = result.expect_err("empty SERVICE must be rejected").to_string();
409+
assert!(
410+
err.contains("SERVICE must not be empty"),
411+
"unexpected error: {err}"
412+
);
413+
}

0 commit comments

Comments
 (0)