fix(cron): give script crons a session identity for state-mutating MCP calls - #8232
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design review verified: the identity gap is real (script crons had none of the three sources Design-Verdict: PASS Root-cause fix through the channel agent crons already use; scope, side effects, and deferred gaps are all named and defensible. [DESIGN-REVIEWED] 1dd6195 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All claims verified against the repository. Writing the review now. First-Principles-Verdict: PASS A cause-level fix: the one identity channel every launcher already uses now reaches script crons, and every rider is a declared comment/spec sync. What this change shipsIntent: make a script cron's state-mutating MCP calls actually write instead of silently no-oping — a FIX.
WatchThe silence that hid this bug — refusals arriving as ordinary result strings that [FIRST-PRINCIPLES-REVIEWED] 1dd6195 |
1ad22d8 to
f566fc8
Compare
f566fc8 to
f790598
Compare
2694bce
f790598 to
2694bce
Compare
…P calls Every state-mutating MCP tool resolves its caller through mcp_core._resolve_session_key_strict, which accepts three sources: the gateway-injected caller block, KIROCREW_SESSION_KEY, or KIROCREW_HOST_PID plus its signed sidecar. A script cron had none of them -- nothing routes its direct MCP spawns through gatewayd, nobody publishes a sidecar for the launcher pid, and run_script_sandboxed never set the env var. On Linux the sandbox launcher's KIROCREW_HOST_PID therefore pointed at an unsigned pid and every write came back "the signed pid mapping did not verify"; on macOS/Windows there was no channel at all. Read-only calls worked, so a script that swallowed the refusal string reported ok while writing nothing. Fix: the launcher injects KIROCREW_SESSION_KEY=cron:<job id> into the child env -- the same channel acp/client.py gives every agent subprocess, agent crons included, and the same key ScriptContext already presents over HTTP -- and the MCP bridge hard-pins that key on the server spawn so script code rewriting its own os.environ cannot present another session's identity (the pattern ScriptContext.notify already uses for caller_session). The cron spec gains the script-cron identity contract as its durable home, including the general rule it instances: a spawn path that hands a child MCP access must hand it an identity, since the strict resolver fails closed for writes alone and the omission is otherwise silent. The script-cron launcher is a second injector of KIROCREW_SESSION_KEY, so the computer-use spec and shim stop calling the ACP spawn path the only one. The claim those passages rest on is unchanged and now stated directly: both env sources come from a launcher above the process, and a GUI-launched kiro-cli has none, which is why the shim does not gate on identity. That fact is stated once in the shim's module docstring and pointed at from the two comments that restated it, so the next injector syncs one copy. The same docstring described the audit trail as recording an empty key while the code forwards the UNRESOLVED_SESSION_PREFIX placeholder -- the placeholder its own constant exists to provide -- so it now says so.
2694bce to
1dd6195
Compare
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
Problem / Motivation
A script cron that calls a state-mutating MCP tool (
ctx.call_tool("kirocrew-cron", "cron_trigger", ...),learn_add,artifact_save, ...) does not raise — the call reaches the handler and comes back with the fail-closed refusal as a plain string:Read-only calls work, so the job looks healthy: a script that swallows the string reports
okon every run while writing nothing.Why it matters
This is the second half of #6431. That fix (
boot_platformin the launcher preamble) made script-cron MCP calls reach the server again; this one makes the server trust them. Without it, cron→cron orchestration from a script cron (a selector waking an investigator viacron_trigger), and every other script-driven write, silently no-ops on every install — Linux included, where the refusal names a "signed pid mapping" the operator has no way to produce.What changed (motivation → approach → change)
mcp_core._resolve_session_key_strict— the gate behind every mutating tool — accepts exactly three sources: the gateway-injected caller block,KIROCREW_SESSION_KEY, orKIROCREW_HOST_PIDplus its HMAC sidecar. A script cron had none of them:run_script_sandboxednever setKIROCREW_SESSION_KEY;session_pid_<pid>.sigfor its launcher pid, so on Linux the sandbox launcher'sKIROCREW_HOST_PIDpointed at an unsigned pid (hence the "did not verify" wording), and on macOS/Windows noHOST_PIDis exported at all.Approach: use the channel the gateway already gives every agent subprocess.
acp/client.pyinjectsKIROCREW_SESSION_KEY=<session>into agent processes — agent crons included, where the key iscron:<job id>. Script crons now get the same:run_script_sandboxedsetsKIROCREW_SESSION_KEY=cron:<job id>in the child env. This is the keyScriptContextalready presents to the gateway over HTTP (X-Session-Key/caller_session), so ownership and audit see one principal per job regardless of surface.McpToolClienttakes an optionalsession_keyand hard-assigns it on the server spawn env AFTER the inherited-env and spec-env overlays.ScriptContext.call_toolpassescron:<job id>. Same reasonScriptContext.notifyhard-assignscaller_session: the bridge's env is the script child's ownos.environ, which user code can rewrite before callingctx.call_tool. The CLI preview path constructs the bridge bare and is unchanged.Why env rather than publishing a signed sidecar for
proc.pid: the sidecar channel exists for the kiro backend's session-unboundAcpRuntime, where one process multiplexes N sessions and an env var is wrong by construction. A script cron is one process per job, so the env var is the correct channel — it is what agent crons use, it works on macOS/Windows (no launcher exportsHOST_PIDthere), and it leaves no per-runsession_pid_*files behind. Provenance is identical to the ACP path: gateway-authored env on a direct child.Side effect, intended:
cloud.aws.assert_human_actionkeys on the same var, so destructive cloud verbs from a script cron are now refused like they are from an agent session.Out of scope, deliberately:
_check_cron_job_ownershipstill requires the caller's key to equal the target job's owner. With this change a script cron is identified ascron:<its own id>, which is not the owner of any job it did not create, socron_triggeron a different job is still refused (as it is for agent crons). Whether cross-job orchestration should get a scoped exemption is a design question, not an identity bug; happy to open an issue if maintainers want it tracked.Specs
docs/system-specs/modules/learn-cron-dashboard.mdis the durable home for the identity contract: script crons run ascron:<job id>, the two hops that deliver it, why the omission was silent (strict resolution fails closed for writes only), the two consequences named above, and the general rule this is one instance of — a spawn path that hands a child MCP access must hand it an identity.The launcher is a second injector of
KIROCREW_SESSION_KEY, sodocs/system-specs/modules/computer-use.mdand themcp_computershim stop calling the ACP spawn path the only one. The claim those passages actually rest on is unchanged and now stated directly: both accepted env sources come from a launcher above the process, and a GUI-launched kiro-cli has none, which is why the shim does not gate on identity. That fact was restated in five places this diff had to keep in sync, so it is now stated once in the shim's module docstring and pointed at from the two comments and the test docstring that repeated it — the next injector syncs one copy. While there: the same docstring described the audit trail as recording an empty key, which theUNRESOLVED_SESSION_PREFIXconstant thirty lines below it exists to prevent, so it now names the placeholder the code actually forwards.Deliberately not in this PR, both raised as follow-ups rather than fixed here:
ScriptContext.call_toolstill audits"ok"on a refusal that arrives as an ordinary result string, so a future identity or ownership gap would be as quiet as this one was; and the cross-jobcron_triggerownership question below.Tests
test/test_cron_script_identity.py(new, 11 tests, mutation-verified: 10/11 fail with the production change reverted):KIROCREW_SESSION_KEY=cron:<job>; equals theX-Session-KeyScriptContext._postsends; a forged inherited key is overwritten, not keptos.environand over a spec-envblock; passes nothing when given nothing (CLI preview);call_toolpasses the job's keymcp_core._resolve_session_key_strict()andmcp_cron._authz_session_key()evaluated in the child env returncron:<job>, and the same env minus the key reproduces the refusal ("")test/test_cron_script_more_coverage.py: threeMcpToolClientstubs accept the new kwarg.Full cron suites green locally (419 passed, 1 skipped); flake8 clean.
Manual verification
N/A — the consumer-side tests evaluate the actual strict resolver against the actual child env; the refused string in the report is exactly
_resolve_session_key_strict() == "".Related Issues
Follow-up to #6431 (same launcher, second gate).
Pattern harvest
Rule candidate: review-prompt
Pattern: "gateway-spawned child that calls session-keyed MCP tools without the gateway setting KIROCREW_SESSION_KEY (or publishing its pid sidecar)" — any new spawn path that hands a child MCP access must also hand it an identity, or the strict resolver fails closed only for writes and the failure is silent.
Checklist
KIROCREW_SESSION_KEY