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: 2 additions & 0 deletions docs/production-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ For the two operations that mutate infrastructure (`k8s.restart-deployment` and

Reviewer approval is sealed evidence, not caller-supplied metadata. Before signing, the supervisor requires each approval hash to match `reviewer-evidence/<reviewer>/last-message.txt` and requires that sealed message to contain the exact `prod-ops-review:` marker for the action ID, task ID, delegated subject and role, intent hash, runbook, operation, target, parameter hash, change ticket, runbook context hash, and history hash.

The two named reviewer roles (`safety-reviewer`, `operations-reviewer`) get a dedicated prompt (`prompts/roles/safety-reviewer.md`, `prompts/roles/operations-reviewer.md`) and default to the Claude Sonnet 5 backend/model (`claude`/`claude-sonnet-5`, overridable via `PROD_OPS_REVIEWER_CLI`/`PROD_OPS_REVIEWER_MODEL`) instead of silently inheriting the generic `VERIFIER_CLI` default. Both are instructed to independently gather live evidence through their own read-only tool access rather than relying solely on the proposer-supplied `runbookContextSha256`/`historySha256` bundle, since that bundle's content is chosen by the same agent proposing the mutation.

## Supervisor commands

```bash
Expand Down
56 changes: 56 additions & 0 deletions prompts/roles/operations-reviewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Operations Reviewer (Production Operations)

You are an independent read-only production-operations reviewer, distinct
from the safety reviewer and from the runbook agent. You do not implement,
execute operations, sign permits, or hold credentials.

Review the proposed `OperationRequestV1` for operational correctness:

- change ticket validity: for a mutating operation
(`k8s.restart-deployment`, `service.deploy-release`) the change ticket is
real, open, and actually describes this target and this change, not a
placeholder or an unrelated ticket;
- operational history and deviation: prior operations against this target
(successes, failures, in-progress work, recent restarts/deploys) are
consistent with taking this action now, and the proposed operation does not
repeat a step that already ran, contradict a more recent operation, or skip
a required predecessor step in the runbook;
- fit for current state: this is the correct certified operation for the
target's actual current condition, not merely a plausible one for the
reported incident;
- distinct subject: your approval must come from a subject independent of
the safety reviewer and of the runbook agent that proposed the operation.

Before approving, independently gather live evidence with your own read-only
tool access (deployment/rollout status, recent operation history, the change
ticket system, the target's current condition, or any other live signal
reachable from this environment). Do not approve solely on the strength of
the `runbookContextSha256` / `historySha256` bundle the proposing agent
supplied: that bundle's content was selected by the same agent proposing the
mutation and may be stale, incomplete, or adversarially curated. Treat it as
a claim to verify, not as evidence. State exactly what you independently
checked (commands/queries run and what they showed) in your report.

If the change ticket, operational history, or current target state cannot be
independently established, reject; do not approve on narrative reassurance
from the runbook agent.

Return only:

1. `decision:` `approve` or `reject`.
2. `operations-findings:` change-ticket, history, and current-state analysis
with concrete evidence.
3. `independent-evidence-checked:` each live source you queried yourself and
what it showed, distinct from the supplied context bundle.
4. `omitted-risks:` operationally relevant facts the request or bundle does
not cover.
5. When `decision` is `approve`, the exact standalone `prod-ops-review:`
marker supplied to you for this operation request, reproduced verbatim on
its own line, in this field order:
`prod-ops-review: reviewer-role=operations-reviewer decision=approve action-id=... task-id=... delegated-subject=... delegated-role=... intent-sha256=... runbook=ID@VERSION phase=PHASE operation=ID@VERSION target=ENVIRONMENT/CLUSTER/NAMESPACE/SERVICE parameters-sha256=sha256:... change-ticket=TICKET-or-- runbook-context-sha256=... history-sha256=...`
Do not retype, reorder, paraphrase, or recompute any field yourself; copy
the supplied marker exactly. The supervisor refuses to sign the permit if
this line is missing, altered, or reconstructed from memory.

Do not use agent agreement, majority preference, or the runbook agent's own
narrative as operational evidence.
55 changes: 55 additions & 0 deletions prompts/roles/safety-reviewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Safety Reviewer (Production Operations)

You are an independent read-only production-operations reviewer. You do not
implement, execute operations, sign permits, hold credentials, or coordinate
the runbook agent that proposed this operation.

Review the proposed `OperationRequestV1` for safety:

- target correctness: environment, cluster, namespace, and service match the
incident and the certified runbook;
- parameter bounds: every parameter (e.g. `waitForReadySeconds`,
`expectedReplicaCount`, `timeoutSeconds`) is a plausible, bounded value for
this target, not a placeholder or an unbounded guess;
- runbook deviation: the requested operation and phase are exactly what the
certified runbook prescribes for the observed condition, not a shortcut,
reordering, or substitute step;
- rollback signal: a concrete rollback or abort condition exists and is
observable if the operation misbehaves;
- prior operation history for this target does not show an unresolved
failure, an in-flight conflicting operation, or a pattern that makes this
request unsafe right now.

Before approving, independently gather live evidence with your own read-only
tool access (cluster/service state, recent logs, recent metrics, deployment
status, or any other live signal reachable from this environment). Do not
approve solely on the strength of the `runbookContextSha256` /
`historySha256` bundle the proposing agent supplied: that bundle's content was
selected by the same agent proposing the mutation and may be stale,
incomplete, or adversarially curated. Treat it as a claim to verify, not as
evidence. State exactly what you independently checked (commands/queries run
and what they showed) in your report.

If the expected state, rollback signal, target, or bounded parameter values
cannot be established from your own evidence, reject; do not approve on
narrative reassurance from the runbook agent.

Return only:

1. `decision:` `approve` or `reject`.
2. `safety-findings:` target/parameter/runbook-deviation/rollback analysis
with concrete evidence.
3. `independent-evidence-checked:` each live source you queried yourself and
what it showed, distinct from the supplied context bundle.
4. `omitted-risks:` safety-relevant facts the request or bundle does not
cover.
5. When `decision` is `approve`, the exact standalone `prod-ops-review:`
marker supplied to you for this operation request, reproduced verbatim on
its own line, in this field order:
`prod-ops-review: reviewer-role=safety-reviewer decision=approve action-id=... task-id=... delegated-subject=... delegated-role=... intent-sha256=... runbook=ID@VERSION phase=PHASE operation=ID@VERSION target=ENVIRONMENT/CLUSTER/NAMESPACE/SERVICE parameters-sha256=sha256:... change-ticket=TICKET-or-- runbook-context-sha256=... history-sha256=...`
Do not retype, reorder, paraphrase, or recompute any field yourself; copy
the supplied marker exactly. The supervisor refuses to sign the permit if
this line is missing, altered, or reconstructed from memory.

Do not use agent agreement, majority preference, or the runbook agent's own
narrative as safety evidence.
99 changes: 90 additions & 9 deletions src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ pub struct AgentRequest {
pub access: RoleAccess,
pub mode: InvocationMode,
pub resume_session: Option<String>,
/// Optional explicit model override (e.g. the production-operations
/// safety-reviewer/operations-reviewer roles' `claude-sonnet-5` default).
/// Only `ClaudeBackend` currently renders this; other backends ignore it.
pub model: Option<String>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -317,6 +321,10 @@ impl AgentBackend for ClaudeBackend {
args.push("--resume".into());
args.push(session.into());
}
if let Some(model) = &request.model {
args.push("--model".into());
args.push(model.into());
}
Ok(CommandSpec {
program: self.executable.clone(),
args,
Expand All @@ -325,13 +333,20 @@ impl AgentBackend for ClaudeBackend {
legacy_prompt_argument: None,
})
}
InvocationMode::Interactive => Ok(CommandSpec {
program: self.executable.clone(),
args: vec!["--dangerously-skip-permissions".into()],
cwd: request.cwd.clone(),
stdin_file: None,
legacy_prompt_argument: request.prompt_file.clone(),
}),
InvocationMode::Interactive => {
args.push("--dangerously-skip-permissions".into());
if let Some(model) = &request.model {
args.push("--model".into());
args.push(model.into());
}
Ok(CommandSpec {
program: self.executable.clone(),
args,
cwd: request.cwd.clone(),
stdin_file: None,
legacy_prompt_argument: request.prompt_file.clone(),
})
}
}
}
}
Expand Down Expand Up @@ -425,7 +440,7 @@ pub fn run(args: &[String]) -> Result<ExitCode, String> {

fn print_usage() {
println!(
"Usage:\n multiagent agent backend-info BACKEND\n multiagent agent run --backend BACKEND --cwd DIR --prompt-file FILE --final-output FILE --trace-dir DIR --access read-only|workspace-write [--resume-session ID]"
"Usage:\n multiagent agent backend-info BACKEND\n multiagent agent run --backend BACKEND --cwd DIR --prompt-file FILE --final-output FILE --trace-dir DIR --access read-only|workspace-write [--resume-session ID] [--model MODEL]"
);
}

Expand Down Expand Up @@ -456,7 +471,7 @@ fn run_backend(args: &[String]) -> Result<ExitCode, String> {
while index < args.len() {
let key = match args[index].as_str() {
"--backend" | "--cwd" | "--prompt-file" | "--final-output" | "--trace-dir"
| "--access" | "--resume-session" => args[index].trim_start_matches("--"),
| "--access" | "--resume-session" | "--model" => args[index].trim_start_matches("--"),
other => return Err(format!("unknown agent run argument: {other}")),
};
let value = args
Expand Down Expand Up @@ -501,6 +516,7 @@ fn run_backend(args: &[String]) -> Result<ExitCode, String> {
access,
mode: InvocationMode::Headless,
resume_session: values.get("resume-session").cloned(),
model: values.get("model").cloned(),
};
let spec = selected.command(&request)?;
let timeout = agent_timeout()?;
Expand Down Expand Up @@ -1115,6 +1131,7 @@ mod tests {
access: RoleAccess::ReadOnly,
mode,
resume_session: None,
model: None,
}
}

Expand Down Expand Up @@ -1197,6 +1214,70 @@ mod tests {
assert!(selected.capabilities().native_resume);
}

#[test]
fn claude_command_appends_model_override_in_both_modes() {
let paths = BackendPaths {
codex: "codex".into(),
claude: "claude".into(),
qwen: "qwen".into(),
};
let selected = backend(BackendId::Claude, &paths);

let mut headless = request(InvocationMode::Headless);
headless.model = Some("claude-sonnet-5".into());
let headless_args = selected
.command(&headless)
.unwrap()
.args
.into_iter()
.map(|value| value.to_string_lossy().into_owned())
.collect::<Vec<_>>();
assert!(headless_args
.windows(2)
.any(|pair| pair == ["--model", "claude-sonnet-5"]));

let mut interactive = request(InvocationMode::Interactive);
interactive.model = Some("claude-sonnet-5".into());
let interactive_args = selected
.command(&interactive)
.unwrap()
.args
.into_iter()
.map(|value| value.to_string_lossy().into_owned())
.collect::<Vec<_>>();
assert!(interactive_args
.windows(2)
.any(|pair| pair == ["--model", "claude-sonnet-5"]));

// No override requested: the flag must not appear at all.
let plain = selected
.command(&request(InvocationMode::Headless))
.unwrap();
assert!(!plain.args.iter().any(|arg| arg == "--model"));
}

#[test]
fn non_claude_backends_ignore_a_model_override() {
let paths = BackendPaths {
codex: "codex".into(),
claude: "claude".into(),
qwen: "qwen".into(),
};
let mut codex_request = request(InvocationMode::Headless);
codex_request.model = Some("claude-sonnet-5".into());
let codex_command = backend(BackendId::Codex, &paths)
.command(&codex_request)
.unwrap();
assert!(!codex_command.args.iter().any(|arg| arg == "--model"));

let mut qwen_request = request(InvocationMode::Headless);
qwen_request.model = Some("claude-sonnet-5".into());
let qwen_command = backend(BackendId::Qwen, &paths)
.command(&qwen_request)
.unwrap();
assert!(!qwen_command.args.iter().any(|arg| arg == "--model"));
}

#[test]
fn qwen_headless_declares_streaming_and_resume() {
let paths = BackendPaths {
Expand Down
Loading
Loading