From 923534621e9b0ce47e9f531bc2fbde57acc0fa53 Mon Sep 17 00:00:00 2001 From: shiva Date: Thu, 13 Aug 2026 18:34:26 +0530 Subject: [PATCH 1/2] feat(work): refuse to run agentflare work when invoked by an AI agent agentflare work is meant for a human's own terminal or the daemon's in-process WorkItemExecutor -- not an AI agent shelling out to it, which bypasses the claim/queue bookkeeping the dashboard and agent_jobs table rely on and risks racing the daemon's own dispatch. WorkArgs::run now checks the same AI-agent marker env vars bashenv.rs's shim dispatcher already uses (CLAUDECODE, CURSOR_AGENT, CODEX_CLI_SESSION, GEMINI_SESSION, CODEBUDDY, LEAN_CTX_AGENT) before calling execute_work, and denies with a message naming why and what to do instead. Left as a strict, unconditional deny per scope -- a comment at the guard site flags the known tension with human-authorized-but-agent-executed recovery runs for a future override decision. Agentflare-Agent: claude-code Agentflare-Branch: task/113-agentflare-work-must-refuse-to-run-when Agentflare-Item: 113 --- src/cli/work.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/cli/work.rs b/src/cli/work.rs index fa8bb7c9..1d916737 100644 --- a/src/cli/work.rs +++ b/src/cli/work.rs @@ -616,10 +616,49 @@ fn notify(recipient: &str, body: &str, item_id: &str) { impl WorkArgs { pub fn run(self) { + if let Some(marker) = ai_agent_env_marker() { + eprintln!( + "error: `agentflare work` is a human-only command — it bypasses the daemon's \ + claim/queue tracking that the dashboard and autonomous self-repair depend on \ + (detected {marker} set in this process's environment).\n\n\ + If you're an AI agent: don't run this directly. Either wait for the daemon's \ + discovery tick to dispatch the item (it will, once `ready-for-work` is set and \ + nothing blocks it), or ask a human to run this command for you if the item is \ + genuinely stuck." + ); + // Known tension (item #113): this deny is strict and has no override. This + // session's own recovery of #104/#107 (claims that outlived their TTL past the + // daemon's 3-attempt self-repair cap) needed a human-authorized `agentflare work` + // run executed by an AI agent after explicit sign-off -- a path this guard now + // closes entirely, even with a human in the loop. Left unresolved on purpose; an + // override (e.g. `--i-am-a-human`, or a prompt requiring real terminal input) is a + // deliberate future decision, not something to route around here. + std::process::exit(1); + } std::process::exit(execute_work(self, &mut std::io::stdout()).exit_code); } } +/// Name of the first AI-agent marker env var found set (non-empty) in this process's +/// environment, or `None` on a human's own terminal. Mirrors the marker list +/// `bashenv.rs`'s shim dispatcher already uses to detect an agent's own tool-execution layer +/// running the shell -- same signal, same list, kept in sync by hand since one lives in a +/// bash string template and the other in Rust. +fn ai_agent_env_marker() -> Option<&'static str> { + const MARKERS: &[&str] = &[ + "CLAUDECODE", + "CURSOR_AGENT", + "CODEX_CLI_SESSION", + "GEMINI_SESSION", + "CODEBUDDY", + "LEAN_CTX_AGENT", + ]; + MARKERS + .iter() + .copied() + .find(|var| std::env::var(var).is_ok_and(|v| !v.is_empty())) +} + /// `execute_work`'s result: the process exit code (0 = success), plus — set /// only when the failure was classified as rate-limit shaped — a hint for /// how long the job queue should wait before retrying this item. @@ -1093,6 +1132,40 @@ mod tests { } } + /// Both assertions live in one test (rather than two) because env vars are + /// process-global state and `cargo test` runs tests in parallel threads by + /// default -- a separate "set CLAUDECODE" test and "assert none set" test + /// would race each other's set/remove calls. + #[test] + fn ai_agent_env_marker_detects_claudecode_and_clears() { + let markers = [ + "CLAUDECODE", + "CURSOR_AGENT", + "CODEX_CLI_SESSION", + "GEMINI_SESSION", + "CODEBUDDY", + "LEAN_CTX_AGENT", + ]; + // SAFETY: test-only; these env vars aren't touched by any other test in + // this process, and set/remove here always run on the same thread. + unsafe { + for var in markers { + std::env::remove_var(var); + } + } + assert_eq!(ai_agent_env_marker(), None); + + unsafe { + std::env::set_var("CLAUDECODE", "1"); + } + assert_eq!(ai_agent_env_marker(), Some("CLAUDECODE")); + + unsafe { + std::env::remove_var("CLAUDECODE"); + } + assert_eq!(ai_agent_env_marker(), None); + } + #[test] fn build_prompt_includes_name_description_and_comments() { let item = test_item(); From 14f0d36b393d63b7499a69ec3a1f8ca3a4f9eca9 Mon Sep 17 00:00:00 2001 From: shiva Date: Fri, 14 Aug 2026 11:50:44 +0530 Subject: [PATCH 2/2] fix(work): detect AI agents via agent_detector instead of a bespoke env-var list Item #113's original guard reimplemented bashenv.rs's marker env-var list. agent_detector::agent_name() is already the established primitive for this (claims.rs::owner_id, mcp_server.rs::identity, review.rs::submitter_name, cli/hook.rs::resolve_agent) and is strictly more robust -- it also walks the parent process tree, so it isn't defeated by an agent whose wrapper doesn't forward its marker env var through to a subprocess. Agentflare-Agent: claude-code Agentflare-Branch: task/113-agentflare-work-must-refuse-to-run-when Agentflare-Item: 113 --- src/cli/work.rs | 62 ++++++++++++------------------------------------- 1 file changed, 15 insertions(+), 47 deletions(-) diff --git a/src/cli/work.rs b/src/cli/work.rs index 1d916737..35e756da 100644 --- a/src/cli/work.rs +++ b/src/cli/work.rs @@ -616,11 +616,11 @@ fn notify(recipient: &str, body: &str, item_id: &str) { impl WorkArgs { pub fn run(self) { - if let Some(marker) = ai_agent_env_marker() { + if let Some(agent) = agent_detector::agent_name() { eprintln!( "error: `agentflare work` is a human-only command — it bypasses the daemon's \ claim/queue tracking that the dashboard and autonomous self-repair depend on \ - (detected {marker} set in this process's environment).\n\n\ + (detected this process is running under the {agent} AI agent).\n\n\ If you're an AI agent: don't run this directly. Either wait for the daemon's \ discovery tick to dispatch the item (it will, once `ready-for-work` is set and \ nothing blocks it), or ask a human to run this command for you if the item is \ @@ -639,26 +639,6 @@ impl WorkArgs { } } -/// Name of the first AI-agent marker env var found set (non-empty) in this process's -/// environment, or `None` on a human's own terminal. Mirrors the marker list -/// `bashenv.rs`'s shim dispatcher already uses to detect an agent's own tool-execution layer -/// running the shell -- same signal, same list, kept in sync by hand since one lives in a -/// bash string template and the other in Rust. -fn ai_agent_env_marker() -> Option<&'static str> { - const MARKERS: &[&str] = &[ - "CLAUDECODE", - "CURSOR_AGENT", - "CODEX_CLI_SESSION", - "GEMINI_SESSION", - "CODEBUDDY", - "LEAN_CTX_AGENT", - ]; - MARKERS - .iter() - .copied() - .find(|var| std::env::var(var).is_ok_and(|v| !v.is_empty())) -} - /// `execute_work`'s result: the process exit code (0 = success), plus — set /// only when the failure was classified as rate-limit shaped — a hint for /// how long the job queue should wait before retrying this item. @@ -1132,38 +1112,26 @@ mod tests { } } - /// Both assertions live in one test (rather than two) because env vars are - /// process-global state and `cargo test` runs tests in parallel threads by - /// default -- a separate "set CLAUDECODE" test and "assert none set" test - /// would race each other's set/remove calls. + /// `WorkArgs::run`'s guard denies whenever `agent_detector::agent_name()` returns + /// `Some`, so exercising that same primitive here is what actually proves the guard + /// fires -- there's no separate marker list of our own left to drift out of sync. + /// Only the "detects" direction is asserted: unlike the env var it sets and clears, + /// `agent_detector::agent_name()` also walks the parent process tree, which a sandboxed + /// dev session (this one included) can make non-empty even with every marker env var + /// cleared, so asserting the "clear -> None" side here would be flaky by environment + /// rather than by test bug. #[test] - fn ai_agent_env_marker_detects_claudecode_and_clears() { - let markers = [ - "CLAUDECODE", - "CURSOR_AGENT", - "CODEX_CLI_SESSION", - "GEMINI_SESSION", - "CODEBUDDY", - "LEAN_CTX_AGENT", - ]; - // SAFETY: test-only; these env vars aren't touched by any other test in - // this process, and set/remove here always run on the same thread. - unsafe { - for var in markers { - std::env::remove_var(var); - } - } - assert_eq!(ai_agent_env_marker(), None); - + fn agent_detector_flags_the_claudecode_marker_run_denies_on() { + // SAFETY: test-only; CLAUDECODE isn't touched by any other test in this + // process, and set/remove here always run on the same thread. unsafe { std::env::set_var("CLAUDECODE", "1"); } - assert_eq!(ai_agent_env_marker(), Some("CLAUDECODE")); - + let detected = agent_detector::agent_name(); unsafe { std::env::remove_var("CLAUDECODE"); } - assert_eq!(ai_agent_env_marker(), None); + assert_eq!(detected.as_deref(), Some("claude-code")); } #[test]