mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-10-22 14:59:49 +00:00
* feat(core): gate scheduled tasks on a shell command run outside the model A scheduled task can carry a `gate`: a shell command the scheduler runs at each fire before the prompt reaches the session. Silence skips the fire with no model turn; output delivers the fire with the output attached as data; a failure is reported once per streak and counted. The gate runs inside CronScheduler so every fire kind and every consumer (TUI, ACP, headless, daemon-bound sessions) shares one path. It is confirmed like a Monitor command when the task is created and checked again against the session's approval mode and permission rules at fire time. Durable tasks persist the gate together with a downgrade fuse in the legacy `condition` field, so an older binary withholds the task instead of firing it unconditionally every slot. Also routes cron fires away from the composer's shell mode in the TUI. Design: docs/design/2026-10-21-scheduled-task-gate.md Refs #15056 * fix(core): normalize the gate rule hint and refuse a gate on a one-shot task The rule a blocked fire suggests is now derived from the same normalized command the cron_create dialog offers and the permission manager matches, so a wrapper-style gate such as `bash -c "./probe.sh"` names `Monitor(./probe.sh *)` rather than a `bash *` rule that never matches. A one-shot task has no next slot, so a quiet gate would consume it with nothing delivered and no trace; cron_create now refuses `gate` with `recurring: false` and says why, and the design records the rule. * fix(core): close the gate's permission, cancellation and lifetime gaps Review of the gate found seven ways a gated task could run, keep running or stall when it should not. All seven are closed here. - An explicit `Monitor(...)`/`Bash(...)` deny rule now blocks the gate in every approval mode: the runner evaluates the command before the yolo/auto shortcut, the way an ordinary tool call is denied before the mode approves it, so a persisted gate stops the moment the rule is saved. - In auto mode a bare `cron_create` allow rule takes the allow fast path around the classifier, so nobody looked at the command. `cron_create` now detects that case (a tool-only permission lookup answering `allow`) and holds the gate to the default-mode check: a `Monitor(...)`/`Bash(...)` rule or the read-only command, else the call is refused. - A manual run of a gated task has no scheduler in the loop and used to dispatch the raw prompt. The daemon's per-run `/run` and the Web Shell's "Run now" now send a manual-run prompt that asks the model to run the gate itself first and stop when it prints nothing; the task view carries `gate` and `gateStatus` so the client can do that for a tool-created task. - A gate's output goes to the consumer installed when it settles, not when it started: ACP re-installs its callback through `start()` after every turn, and that used to drop a gate that was mid-run. Only `stop()` ends the subscription, and it aborts the gate. - A tasks-file reload that drops, disables or rewrites a job now aborts its running gate, as `delete()` does, instead of letting the command finish and discarding the result. - A quiet final gate no longer pins a headless run: `sessionSize` counts a session-only one-shot or final fire until its gate settles, and a new `setGateSkippedListener` reports settlements that deliver nothing so the hold-open is re-checked. - A gated catch-up persists its `lastFiredAt` stamp at once but books its run only when the gate delivers, as a `catch-up` run, instead of recording a run that reached no session and a second one on delivery. Tests cover each case (runner, tool, scheduler, headless run, daemon route, run prompt, Web Shell dialog); the design docs and the user guide describe the new rules. * fix(core): match the gate's auto-mode lookup to the permission flow and tighten the manual-run text - In auto mode `cron_create` asked the permission manager about the tool name alone to tell whether an allow rule had skipped the classifier. A rule scoped to the call's parameters (`cron_create(prompt:…)`) takes the same fast path but matched only the real flow's context, so the gate was accepted unreviewed. The lookup now evaluates the same context the flow built for this call (`buildPermissionCheckContext` with the tool params), pinned by a test with a real permission manager. - The manual-run prompt told the model to follow the task whenever the gate printed something, so partial output before a failure counted. It now follows the task only on exit 0 with output, reports the gate printed nothing on a silent exit 0, and reports the failure and stops on a non-zero exit, a timeout or a cancellation — the runner's three outcomes. The Web Shell copy, the tests and the docs say the same. * fix(core): refuse a denied gate at creation and show the gate exactly as it runs `cron_create` now evaluates the gate as a `monitor` command before any other check, so an explicit `Monitor(...)`/`Bash(...)` deny refuses the call in every mode, a confirmed dialog included. The runner already checks that deny first at each fire, so such a task could only ever be blocked. The exec dialog showed Monitor's re-spelled spawn command while the scheduler persists and runs the trimmed gate. They differed for a gate whose wrapper script ends in `&` (`bash -c './probe.sh &'`), which the background check let through: the dialog showed `bash -c './probe.sh'` and every fire would have detached the probe. That gate is now refused at validation, and the dialog shows the persisted string itself. The design doc's approval section said the tool's default permission followed Monitor's read-only check; it is `ask` unconditionally. * fix(core): hold headless runs for running gates and close the wrapper-& gap - Headless: the hold-open also waits for a gate still running, durable ones included. A durable catch-up flushed by start() is stamped before its gate decides, so stopping the scheduler there aborted the gate and lost the slot (`CronScheduler.hasGatesInFlight`). - Validation: `backgroundsAnyPart` looks for a shell wrapper behind any prefix the Monitor parser does not unwrap (`nohup`, `sudo -u ci`, `command`, `env FOO=1`) and checks its script, nested scripts too, so `nohup bash -c './probe.sh &'` is refused like `bash -c './probe.sh &'`. - Runner: the 16 KiB cap cuts on a character boundary instead of leaving a lone lead byte that decodes to U+FFFD; binary output stops the command and fails the fire instead of settling quiet with nothing kept. - Web Shell: pin the manual-run prompt to the same literal as the daemon copy, so the two cannot drift apart; drop the test-only export. * fix(core): re-check headless completion after a cancelled gate A delete or disk reload that cancels the last running gate left the headless run waiting: the cancelled settlement returned before reaching the gate-skipped listener, so nothing re-checked the hold-open that hasGatesInFlight() had kept open. The settlement now reports to the listener (still delivering nothing) unless stop() cancelled it. |
||
|---|---|---|
| .. | ||
| channels | ||
| _meta.ts | ||
| agent-board.md | ||
| approval-mode.md | ||
| arena.md | ||
| auto-mode.md | ||
| batch.md | ||
| browser-use.md | ||
| code-review.md | ||
| commands.md | ||
| computer-use.md | ||
| context-cost.md | ||
| cross-session-protocol.md | ||
| dual-output.md | ||
| followup-suggestions.md | ||
| goals.md | ||
| headless.md | ||
| hooks.md | ||
| language.md | ||
| lsp.md | ||
| markdown-rendering.md | ||
| mcp.md | ||
| mem0.md | ||
| memory.md | ||
| multi-agent-coordination.md | ||
| omni-fixed-policies-preset.json | ||
| omni-media-policies.md | ||
| output-styles.md | ||
| rules.md | ||
| sandbox.md | ||
| scheduled-tasks.md | ||
| skills.md | ||
| status-line.md | ||
| structured-output.md | ||
| sub-agents.md | ||
| terminal-images.md | ||
| tips.md | ||
| token-caching.md | ||
| tool-use-summaries.md | ||
| worktree.md | ||