Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/system-specs/modules/learn-cron-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ Deterministic cron jobs that bypass the LLM entirely:
- **Script-mode MCP identity**: a script cron runs as the principal `cron:<job id>` — the same key `ScriptContext` presents to the gateway over HTTP (`X-Session-Key` / `caller_session`) and the key an agent cron's session runs under, so ownership and audit see one principal per job whichever surface the job uses. It is delivered on two hops: `run_script_sandboxed` exports `KIROCREW_SESSION_KEY=cron:<job id>` into the script child, and `McpToolClient` hard-assigns the same key on the env of every MCP server `ctx.call_tool()` spawns — after both the inherited-env and per-server `env` overlays, so a script that rewrites its own `os.environ` cannot present a different principal (best-effort friction on the same footing as the rest of the env-based identity contract, not a containment boundary). This is load-bearing rather than cosmetic: a direct MCP spawn is not routed through gatewayd, so it gets no caller block, and nobody publishes a signed `session_pid_*` sidecar for the launcher pid — leaving a script cron with **none** of the three sources `_resolve_session_key_strict()` accepts, which fails closed for writes only. Every state-mutating tool then returns its refusal as an ordinary result string while read-only calls keep working, so the job reports `ok` and writes nothing. Two consequences to keep in mind: `cloud.aws.assert_human_action` keys on the same env var, so destructive cloud verbs from a script cron are refused exactly as they are from an agent session; and job ownership is unchanged, so a script cron reaches only the jobs recorded under its own key (`cron_trigger` against another job stays refused, as it is for agent crons). The general rule this is one instance of: **a spawn path that hands a child MCP access must hand it an identity too**, because the strict resolver fails closed for writes alone and the omission is therefore silent.
- **Command mode**: `command` field specifies a shell command to run (mutually exclusive with `script`). Stdout captured as result.
- **Timeout**: configurable per job (default 30s for scripts, 300s for commands).
- **Safety**: scripts must live under `~/.kiro/crew/crons/`. `is_sensitive_path()` blocks credential file access. SEL audit on every invocation. Auto-pause after 5 consecutive failures (`_AUTO_PAUSE_THRESHOLD`, single-sourced in `CronJob.record_failure`/`record_success`). The auto-pause is **persistent**: an execution-owned `auto_paused` flag (distinct from `user_paused`) is written by `_save`, propagated by `_merge_job_result`, and folded into the effective `enabled` derivation in `_load` — so a failing job stays paused across a daemon restart; `enable_job(True)` or a later success clears it (SEL-audited transitions). Concurrent execution guard prevents double-fire.
- **Safety**: scripts must live under `~/.kiro/crew/crons/`. `is_sensitive_path()` blocks credential file access. SEL audit on every invocation. Auto-pause after 5 consecutive failures (`_AUTO_PAUSE_THRESHOLD`, single-sourced in `CronJob.record_failure`/`record_success`). The auto-pause is **persistent**: an execution-owned `auto_paused` flag (distinct from `user_paused`) is written by `_save`, propagated by `_merge_job_result`, and folded into the effective `enabled` derivation in `_load` — so a failing job stays paused across a daemon restart; `enable_job(True)` or a later success clears it (SEL-audited transitions). Concurrent execution guard prevents double-fire. The script **body** is scanned as well — at authoring time and again at every fire, via `_vet_script_file` on the freshly re-resolved path — for credential-path references, protected secret environment variables, exfiltration URLs, and the shared sensitive-path matcher. Two shell-grammar heuristics are deliberately excluded there, because a body is Python source rather than a command line: `is_denied` (tool-name semantics such as `*git*push*`) and the raw-text separator-run collapse (in source a backslash run is an escape, so collapsing it manufactures paths the body never contained). The collapse is **replaced, not dropped** — `security.is_sensitive_source_body()` owns both halves, applying the same fence checks to each decoded string or bytes literal and exonerating one only when it sits provably in the pattern operand of a pattern-consuming call that is the outermost expression it reaches, and the body has neither rebound `re` or one of its attributes nor read a pattern back off an `re` object, so a run that survives into a real path (`open(r"…\\kiro-cli\\c.json")`, which Win32 collapses at open time) is still refused; docstrings are scanned, because Python retains them as `__doc__` where a body can read them back; see [security.md](security.md). A body that does not parse, or one too deep to traverse, keeps the raw-text collapse instead. A fire-time denial keeps the job and does not feed the auto-pause counter, so a body the scan misjudges is denied on every tick until it is edited.
- **Kind tag**: `cron_list` labels each job as `script`, `command`, or `agent` based on which mode is configured.

#### Auto-pause applies to `agent` (message) crons too
Expand Down
Loading
Loading