From fe377baf103a55889897278456f3727384962357 Mon Sep 17 00:00:00 2001 From: Bo Wu Date: Mon, 24 Aug 2026 15:34:50 -0700 Subject: [PATCH 01/11] Add live GitHub review E2E --- control-server/package.json | 3 +- .../test/github-unreviewed-pr.e2e.mjs | 126 ++++++++++++++++++ runbooks/github-repository-work.md | 4 +- 3 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 control-server/test/github-unreviewed-pr.e2e.mjs diff --git a/control-server/package.json b/control-server/package.json index 1e49b5e..4ef8884 100644 --- a/control-server/package.json +++ b/control-server/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "start": "node src/server.mjs", - "test": "node --test test/*.test.mjs" + "test": "node --test test/*.test.mjs", + "test:e2e:github": "node test/github-unreviewed-pr.e2e.mjs" }, "dependencies": { "ws": "8.21.3" diff --git a/control-server/test/github-unreviewed-pr.e2e.mjs b/control-server/test/github-unreviewed-pr.e2e.mjs new file mode 100644 index 0000000..78d0d69 --- /dev/null +++ b/control-server/test/github-unreviewed-pr.e2e.mjs @@ -0,0 +1,126 @@ +import assert from "node:assert/strict"; + +const baseUrl = new URL(process.env.MULTIAGENT_E2E_URL || "http://127.0.0.1:18080"); +const repository = process.env.MULTIAGENT_E2E_REPOSITORY || "multiagent"; +const githubRepository = process.env.MULTIAGENT_E2E_GITHUB_REPOSITORY || "aptos-labs/aptos-core"; +const timeoutMs = Number(process.env.MULTIAGENT_E2E_TIMEOUT_MS || 30 * 60_000); +const sessionId = process.env.MULTIAGENT_E2E_SESSION_ID || `github-pr-e2e-${Date.now().toString(36)}`; + +const expected = await latestOpenPullRequestWithoutReviews(githubRepository); +console.log(`oracle: ${expected.html_url} (${expected.title})`); + +let cookie = process.env.MULTIAGENT_E2E_COOKIE || ""; +if (!cookie) { + const login = await request("/api/login", { + method: "POST", + body: { + username: required("MULTIAGENT_E2E_USERNAME"), + password: required("MULTIAGENT_E2E_PASSWORD"), + }, + }); + cookie = login.headers.get("set-cookie")?.split(";", 1)[0] || ""; + assert.ok(cookie, "control server did not issue an authentication cookie"); +} + +const repositories = await request("/api/repositories", { cookie }); +assert.ok(repositories.body.repositories.includes(repository), `repository is not configured: ${repository}`); + +const task = [ + `Read the latest open pull request in ${githubRepository} that has no submitted pull-request reviews.`, + "Use the GitHub Markdown runbook and access GitHub only through prod-mcp.", + "Return its PR number, title, author, URL, creation timestamp, and explicit evidence that its submitted review list is empty.", + "Do not clone or modify the repository.", +].join(" "); +await request("/api/sessions", { + method: "POST", + cookie, + body: { id: sessionId, repository, task }, + expectedStatus: 201, +}); +console.log(`session: ${sessionId}`); + +const deadline = Date.now() + timeoutMs; +let lastStatus = "pending"; +let lastReport = ""; +while (Date.now() < deadline) { + const sessions = await request("/api/sessions", { cookie }); + const session = sessions.body.sessions.find((candidate) => candidate.id === sessionId); + assert.ok(session, `session disappeared: ${sessionId}`); + if (session.status !== lastStatus) { + lastStatus = session.status; + console.log(`status: ${lastStatus}`); + } + try { + const report = await request(`/api/sessions/${sessionId}/report`, { cookie }); + lastReport = String(report.body.report || ""); + if (reportMatches(lastReport, expected)) { + console.log(lastReport); + console.log("GitHub unreviewed PR E2E passed"); + process.exit(0); + } + } catch {} + if (["failed", "archived"].includes(session.status)) { + throw new Error(`session ended with status ${session.status}\n${lastReport}`); + } + await sleep(10_000); +} +throw new Error(`timed out waiting for ${sessionId}; last status: ${lastStatus}\n${lastReport}`); + +function reportMatches(report, pullRequest) { + const number = String(pullRequest.number); + return report.includes(pullRequest.html_url) + && (report.includes(`#${number}`) || report.includes(`PR ${number}`) || report.includes(`pull request ${number}`)) + && /(?:zero|no|empty).{0,80}(?:submitted )?reviews?/is.test(report); +} + +async function latestOpenPullRequestWithoutReviews(repositoryName) { + const headers = { + accept: "application/vnd.github+json", + "user-agent": "multiagent-live-e2e", + "x-github-api-version": "2022-11-28", + }; + if (process.env.GITHUB_TOKEN) headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + const pulls = await githubJson(`https://api.github.com/repos/${repositoryName}/pulls?state=open&sort=created&direction=desc&per_page=30`, headers); + for (const pull of pulls) { + const reviews = await githubJson( + `https://api.github.com/repos/${repositoryName}/pulls/${pull.number}/reviews?per_page=1`, + headers, + ); + if (reviews.length === 0) return pull; + } + throw new Error(`no unreviewed open pull request found in the latest ${pulls.length} PRs`); +} + +async function githubJson(url, headers) { + const response = await fetch(url, { headers }); + const body = await response.json().catch(() => null); + if (!response.ok) throw new Error(`GitHub oracle failed with HTTP ${response.status}: ${JSON.stringify(body)}`); + return body; +} + +async function request(path, options = {}) { + const headers = { accept: "application/json", origin: baseUrl.origin }; + if (options.body) headers["content-type"] = "application/json"; + if (options.cookie) headers.cookie = options.cookie; + const response = await fetch(new URL(path, baseUrl), { + method: options.method || "GET", + headers, + body: options.body ? JSON.stringify(options.body) : undefined, + }); + const body = await response.json().catch(() => ({})); + const expectedStatus = options.expectedStatus || 200; + if (response.status !== expectedStatus) { + throw new Error(`${options.method || "GET"} ${path} returned ${response.status}: ${JSON.stringify(body)}`); + } + return { response, headers: response.headers, body }; +} + +function required(name) { + const value = process.env[name]; + if (!value) throw new Error(`${name} is required`); + return value; +} + +function sleep(milliseconds) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +} diff --git a/runbooks/github-repository-work.md b/runbooks/github-repository-work.md index ad9e0c6..dcf1a10 100644 --- a/runbooks/github-repository-work.md +++ b/runbooks/github-repository-work.md @@ -16,8 +16,8 @@ pull request. GitHub credentials remain inside prod-mcp. ## Read phase 1. Set the phase to `read` and operation to `github.read`. -2. Identify the exact `owner/repository` and choose `get-repository`, `get-file`, `get-pull-request`, or `list-pull-requests`. -3. Bound file paths, refs, pull-request numbers, state, and result limits to the original goal. +2. Identify the exact `owner/repository` and choose `get-repository`, `get-file`, `get-pull-request`, `list-pull-requests`, or `list-pull-request-reviews`. +3. Bound file paths, refs, pull-request numbers, state, and result limits to the original goal. Compose multiple read requests when the goal requires correlating pull requests with their submitted reviews. 4. Persist the signed action ID and receipt. ## Materialize phase From ad36976dc2cba7f8a772689e2a71a069c2e8bdb7 Mon Sep 17 00:00:00 2001 From: Bo Wu Date: Mon, 24 Aug 2026 15:59:28 -0700 Subject: [PATCH 02/11] Expose the reviewed ops request schema --- prompts/roles/ops-agent.md | 23 ++++++++++++++++------ src/prod_ops.rs | 40 +++++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/prompts/roles/ops-agent.md b/prompts/roles/ops-agent.md index b5504d4..37813de 100644 --- a/prompts/roles/ops-agent.md +++ b/prompts/roles/ops-agent.md @@ -7,12 +7,23 @@ operations or encode provider-specific behavior in policy or source code. 1. Select the applicable runbook and derive the operation, target, parameters, and phase from it and the prod-mcp target contract. -2. Write one bounded JSON draft under - `$MULTIAGENT_LOG_DIR/agents/$MULTIAGENT_SUBAGENT_NAME/` using the generic - envelope: `taskId`, `goal`, `operation`, `target`, `parameters`, and - `runbook`. Add `changeTicket` only when required. Never add `approvals` or - calculate `runbookContentSha256`. -3. Publish it with: +2. Generate, then edit, one bounded JSON draft under + `$MULTIAGENT_LOG_DIR/agents/$MULTIAGENT_SUBAGENT_NAME/`: + +```bash +DRAFT_FILE="$MULTIAGENT_LOG_DIR/agents/$MULTIAGENT_SUBAGENT_NAME/request.json" +multiagent ops template > "$DRAFT_FILE" +``` + + Preserve the generated field shapes exactly. Set `taskId`, `goal`, + `operation.id`, `operation.version`, `parameters`, `runbook.id`, + `runbook.phase`, and `runbook.version` from the authenticated goal, + selected runbook, and prod-mcp tool contract. Do not add `target`; + publication derives the canonical four-field target from the Markdown + runbook. Add `changeTicket` only when required. Never add `approvals`, + `runbookDocument`, or `runbookContentSha256`. +3. If needed, run `multiagent ops --help`; do not infer a schema from + validation failures. Publish the completed draft with: ```bash multiagent ops publish --draft-file "$DRAFT_FILE" \ diff --git a/src/prod_ops.rs b/src/prod_ops.rs index 4732a6b..05b7298 100644 --- a/src/prod_ops.rs +++ b/src/prod_ops.rs @@ -14,6 +14,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; const MAX_OPERATION_REQUEST_BYTES: u64 = 65_536; const MAX_RUNBOOK_BYTES: u64 = 1_048_576; +const OPS_USAGE: &str = "usage:\n multiagent ops template\n multiagent ops publish --draft-file PATH --runbook-document PATH\n multiagent ops bind-runbook --request-file PATH --runbook-document PATH\n multiagent ops review-bind --request-file PATH\n multiagent ops execute --request-file PATH --reviewer NAME"; pub(crate) struct PublishedRequest { artifact_path: PathBuf, @@ -47,12 +48,49 @@ struct TrustedApproval { pub fn run(args: &[String]) -> Result { match args.first().map(String::as_str) { + Some("template") => template(&args[1..]), Some("bind-runbook") => bind_runbook(&args[1..]), Some("publish") => publish(&args[1..]), Some("execute") => execute(&args[1..]), Some("review-bind") => review_bind(&args[1..]), - _ => Err("usage: multiagent ops bind-runbook --request-file PATH --runbook-document PATH | multiagent ops publish --draft-file PATH --runbook-document PATH | multiagent ops review-bind --request-file PATH | multiagent ops execute --request-file PATH --reviewer NAME".into()), + Some("help" | "--help" | "-h") => { + print_ops_help(); + Ok(ExitCode::SUCCESS) + } + _ => Err(OPS_USAGE.into()), + } +} + +fn template(args: &[String]) -> Result { + if !args.is_empty() { + return Err("usage: multiagent ops template".into()); } + println!( + "{}", + serde_json::to_string_pretty(&json!({ + "taskId": "replace-with-stable-task-id", + "goal": "replace with the bounded operation goal", + "operation": { + "id": "replace.with.operation-id", + "version": "1.0.0" + }, + "parameters": {}, + "runbook": { + "id": "replace.with-runbook-id", + "phase": "replace-with-runbook-phase", + "version": "1.0.0" + } + })) + .map_err(|error| format!("encode ops request template: {error}"))? + ); + Ok(ExitCode::SUCCESS) +} + +fn print_ops_help() { + println!("{OPS_USAGE}"); + println!( + "\nDraft schema:\n taskId: non-empty stable string\n goal: bounded goal copied from the authenticated task\n operation: object with id and semantic version\n parameters: provider operation parameters\n runbook: object with id, phase, and semantic version\n\nGenerate a valid starting envelope with `multiagent ops template`. The publish command derives target, runbookDocument, and runbookContentSha256 from the exact Markdown runbook. Do not supply approvals." + ); } fn bind_runbook(args: &[String]) -> Result { From e51404f772f8d88ce6dab6cd448aefb12d9360f2 Mon Sep 17 00:00:00 2001 From: Bo Wu Date: Mon, 24 Aug 2026 16:09:28 -0700 Subject: [PATCH 03/11] Document the secure ops draft handoff --- prompts/roles/ops-agent.md | 4 +++- src/prod_ops.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/prompts/roles/ops-agent.md b/prompts/roles/ops-agent.md index 37813de..ef9acf5 100644 --- a/prompts/roles/ops-agent.md +++ b/prompts/roles/ops-agent.md @@ -23,9 +23,11 @@ multiagent ops template > "$DRAFT_FILE" runbook. Add `changeTicket` only when required. Never add `approvals`, `runbookDocument`, or `runbookContentSha256`. 3. If needed, run `multiagent ops --help`; do not infer a schema from - validation failures. Publish the completed draft with: + validation failures. Make the completed ops-owned draft readable by the + supervisor group and not group-writable, then publish it: ```bash +chmod 0640 "$DRAFT_FILE" multiagent ops publish --draft-file "$DRAFT_FILE" \ --runbook-document runbooks/SELECTED.md ``` diff --git a/src/prod_ops.rs b/src/prod_ops.rs index 05b7298..01e7e2c 100644 --- a/src/prod_ops.rs +++ b/src/prod_ops.rs @@ -89,7 +89,7 @@ fn template(args: &[String]) -> Result { fn print_ops_help() { println!("{OPS_USAGE}"); println!( - "\nDraft schema:\n taskId: non-empty stable string\n goal: bounded goal copied from the authenticated task\n operation: object with id and semantic version\n parameters: provider operation parameters\n runbook: object with id, phase, and semantic version\n\nGenerate a valid starting envelope with `multiagent ops template`. The publish command derives target, runbookDocument, and runbookContentSha256 from the exact Markdown runbook. Do not supply approvals." + "\nDraft schema:\n taskId: non-empty stable string\n goal: bounded goal copied from the authenticated task\n operation: object with id and semantic version\n parameters: provider operation parameters\n runbook: object with id, phase, and semantic version\n\nGenerate a valid starting envelope with `multiagent ops template`. After editing, run `chmod 0640 DRAFT_FILE` so the ops-owned draft is supervisor-readable and not group-writable. The publish command derives target, runbookDocument, and runbookContentSha256 from the exact Markdown runbook. Do not supply approvals." ); } From a871aa6bfccc5af0521122119db49bd6462203f1 Mon Sep 17 00:00:00 2001 From: Bo Wu Date: Mon, 24 Aug 2026 16:19:25 -0700 Subject: [PATCH 04/11] Bind GitHub runbook to canonical target --- runbooks/github-repository-work.md | 1 + 1 file changed, 1 insertion(+) diff --git a/runbooks/github-repository-work.md b/runbooks/github-repository-work.md index dcf1a10..d403cd2 100644 --- a/runbooks/github-repository-work.md +++ b/runbooks/github-repository-work.md @@ -6,6 +6,7 @@ - Version: `1.0.0` - Prod MCP operations: `github.read`, `github.clone`, `github.create-pr` - Operation version: `1.0.0` +- Set `target` to `{"cluster":"external-services","environment":"production","namespace":"github","service":"installation"}`. ## Goal From 6d941c185ab06ad838e2d5d66d69c0777013b1ce Mon Sep 17 00:00:00 2001 From: Bo Wu Date: Mon, 24 Aug 2026 16:38:58 -0700 Subject: [PATCH 05/11] Align reviewed ops handoff with runtime contract --- prompts/playbooks/reviewed-ops-cycle.md | 17 +++--- prompts/roles/ops-agent.md | 34 +++++++----- src/prod_ops.rs | 71 ++++++++++++++++++++++--- src/runtime.rs | 3 +- 4 files changed, 100 insertions(+), 25 deletions(-) diff --git a/prompts/playbooks/reviewed-ops-cycle.md b/prompts/playbooks/reviewed-ops-cycle.md index 5d6dd1d..47491d0 100644 --- a/prompts/playbooks/reviewed-ops-cycle.md +++ b/prompts/playbooks/reviewed-ops-cycle.md @@ -16,18 +16,21 @@ Do not replace this identity after review. ## Review and continue -After ops returns a published artifact descriptor: +After ops returns its bound request path and digest lines, pass that exact +ops-owned file to the supervisor-owned cycle. For the standard role contract it +is `$MULTIAGENT_LOG_DIR/agents/OPS_NAME/request.json`: ```bash multiagent subagent reviewed-ops-cycle OPS_NAME \ - --request-file "$PUBLISHED_REQUEST_PATH" \ + --request-file "$MULTIAGENT_LOG_DIR/agents/OPS_NAME/request.json" \ --reviewer ops-reviewer-NN \ --timeout 900 ``` Use a fresh reviewer name for each immutable request. This command: -1. publishes a safe legacy request when necessary; +1. validates that the bound request belongs to the named ops identity and + publishes it as a supervisor-owned immutable artifact; 2. binds the reviewer to the immutable request and exact runbook; 3. passes only a bounded artifact descriptor to the reviewer; 4. finalizes accepted review evidence before execution; and @@ -38,6 +41,8 @@ Do not reconstruct these mechanics manually. Prior panes, transcripts, final messages, and native provider resume state are intentionally excluded from the continuation boundary. -On rejection or preflight failure, report the blocker. A changed request needs a -new publication and reviewer. A review correction may use a fresh reviewer on -the same immutable request. Never create a second ops identity. +Never pass the supervisor-owned published artifact back as `--request-file`; +that path is intentionally outside the ops identity directory. On rejection or +preflight failure, report the blocker. A changed request needs a new publication +and reviewer. A review correction may use a fresh reviewer on the same immutable +request. Never create a second ops identity. diff --git a/prompts/roles/ops-agent.md b/prompts/roles/ops-agent.md index ef9acf5..9b4778d 100644 --- a/prompts/roles/ops-agent.md +++ b/prompts/roles/ops-agent.md @@ -5,8 +5,16 @@ operations or encode provider-specific behavior in policy or source code. ## Prepare -1. Select the applicable runbook and derive the operation, target, parameters, - and phase from it and the prod-mcp target contract. +1. Select the applicable runbook and operation. Read the operation's live + prod-mcp contract before constructing parameters: + +```bash +multiagent ops describe OPERATION_ID +``` + + Use the returned description, JSON schema, examples, and authorization + requirements exactly. Do not infer provider fields from public APIs, + repository source, validation failures, or prior operations. 2. Generate, then edit, one bounded JSON draft under `$MULTIAGENT_LOG_DIR/agents/$MULTIAGENT_SUBAGENT_NAME/`: @@ -18,23 +26,25 @@ multiagent ops template > "$DRAFT_FILE" Preserve the generated field shapes exactly. Set `taskId`, `goal`, `operation.id`, `operation.version`, `parameters`, `runbook.id`, `runbook.phase`, and `runbook.version` from the authenticated goal, - selected runbook, and prod-mcp tool contract. Do not add `target`; - publication derives the canonical four-field target from the Markdown + selected runbook, and the `ops describe` result. Do not add `target`; + runbook binding derives the canonical four-field target from the Markdown runbook. Add `changeTicket` only when required. Never add `approvals`, `runbookDocument`, or `runbookContentSha256`. -3. If needed, run `multiagent ops --help`; do not infer a schema from - validation failures. Make the completed ops-owned draft readable by the - supervisor group and not group-writable, then publish it: +3. Bind the completed draft to a normalized path relative to the framework + root, then make the bound ops-owned request readable by the supervisor group + and not group-writable: ```bash -chmod 0640 "$DRAFT_FILE" -multiagent ops publish --draft-file "$DRAFT_FILE" \ +multiagent ops bind-runbook --request-file "$DRAFT_FILE" \ --runbook-document runbooks/SELECTED.md +chmod 0640 "$DRAFT_FILE" ``` -Correct only the unpublished draft if validation fails. The returned descriptor -identifies the supervisor-owned immutable request; never copy or modify it. -Report that descriptor and stop for independent review. +Correct only this ops-owned request if binding fails. Report the exact +`DRAFT_FILE` path and the two digest lines returned by `bind-runbook`, then stop +for independent review. Do not call `ops publish`; the supervisor-owned +`reviewed-ops-cycle` publishes the immutable artifact after validating that the +bound request belongs to this ops identity. ## Execute after review diff --git a/src/prod_ops.rs b/src/prod_ops.rs index 01e7e2c..5665a5c 100644 --- a/src/prod_ops.rs +++ b/src/prod_ops.rs @@ -14,7 +14,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; const MAX_OPERATION_REQUEST_BYTES: u64 = 65_536; const MAX_RUNBOOK_BYTES: u64 = 1_048_576; -const OPS_USAGE: &str = "usage:\n multiagent ops template\n multiagent ops publish --draft-file PATH --runbook-document PATH\n multiagent ops bind-runbook --request-file PATH --runbook-document PATH\n multiagent ops review-bind --request-file PATH\n multiagent ops execute --request-file PATH --reviewer NAME"; +const OPS_USAGE: &str = "usage:\n multiagent ops describe OPERATION_ID\n multiagent ops template\n multiagent ops bind-runbook --request-file PATH --runbook-document PATH\n multiagent ops publish --draft-file PATH --runbook-document PATH\n multiagent ops review-bind --request-file PATH\n multiagent ops execute --request-file PATH --reviewer NAME"; pub(crate) struct PublishedRequest { artifact_path: PathBuf, @@ -48,6 +48,7 @@ struct TrustedApproval { pub fn run(args: &[String]) -> Result { match args.first().map(String::as_str) { + Some("describe") => describe(&args[1..]), Some("template") => template(&args[1..]), Some("bind-runbook") => bind_runbook(&args[1..]), Some("publish") => publish(&args[1..]), @@ -61,6 +62,39 @@ pub fn run(args: &[String]) -> Result { } } +fn describe(args: &[String]) -> Result { + if args.len() != 1 || args[0].is_empty() { + return Err("usage: multiagent ops describe OPERATION_ID".into()); + } + let response = call_prod_mcp_tool("operations_capabilities", json!({}))?; + let operation = operation_capability(&response, &args[0])?; + println!( + "{}", + serde_json::to_string_pretty(operation) + .map_err(|error| format!("encode prod-mcp operation capability: {error}"))? + ); + Ok(ExitCode::SUCCESS) +} + +fn operation_capability<'a>(response: &'a Value, operation_id: &str) -> Result<&'a Value, String> { + let result = response + .get("result") + .and_then(Value::as_object) + .ok_or("prod-mcp capabilities response has no result object")?; + if result.get("isError").and_then(Value::as_bool) == Some(true) { + return Err(format!("prod-mcp capabilities failed: {}", Value::Object(result.clone()))); + } + let operations = result + .get("structuredContent") + .and_then(|value| value.get("operations")) + .and_then(Value::as_array) + .ok_or("prod-mcp capabilities response has no operations array")?; + operations + .iter() + .find(|operation| operation.get("id").and_then(Value::as_str) == Some(operation_id)) + .ok_or_else(|| format!("prod-mcp does not advertise operation {operation_id}")) +} + fn template(args: &[String]) -> Result { if !args.is_empty() { return Err("usage: multiagent ops template".into()); @@ -89,7 +123,7 @@ fn template(args: &[String]) -> Result { fn print_ops_help() { println!("{OPS_USAGE}"); println!( - "\nDraft schema:\n taskId: non-empty stable string\n goal: bounded goal copied from the authenticated task\n operation: object with id and semantic version\n parameters: provider operation parameters\n runbook: object with id, phase, and semantic version\n\nGenerate a valid starting envelope with `multiagent ops template`. After editing, run `chmod 0640 DRAFT_FILE` so the ops-owned draft is supervisor-readable and not group-writable. The publish command derives target, runbookDocument, and runbookContentSha256 from the exact Markdown runbook. Do not supply approvals." + "\nCall `multiagent ops describe OPERATION_ID` before constructing parameters; it returns prod-mcp's live description, JSON schema, examples, and authorization requirements.\n\nDraft schema:\n taskId: non-empty stable string\n goal: bounded goal copied from the authenticated task\n operation: object with id and semantic version\n parameters: exact provider operation parameters from `ops describe`\n runbook: object with id, phase, and semantic version\n\nGenerate a valid starting envelope with `multiagent ops template`, then bind it with a normalized framework-relative runbook path such as `runbooks/name.md`. After binding, run `chmod 0640 DRAFT_FILE` so the ops-owned request is supervisor-readable and not group-writable. The reviewed-ops-cycle publishes the immutable request. Do not supply target, approvals, runbookDocument, or runbookContentSha256 yourself." ); } @@ -1035,7 +1069,7 @@ fn exact_runbook_bytes(relative: &str) -> Result, String> { .components() .any(|component| !matches!(component, std::path::Component::Normal(_))) { - return Err("runbookDocument must be a normalized relative path".into()); + return Err("runbookDocument must be a normalized path relative to MULTIAGENT_FRAMEWORK_ROOT, for example runbooks/name.md".into()); } let framework_root = fs::canonicalize(required_env("MULTIAGENT_FRAMEWORK_ROOT")?) .map_err(|error| format!("resolve multiagent framework root: {error}"))?; @@ -1111,6 +1145,10 @@ fn sign_permit(payload: &[u8]) -> Result { } fn call_prod_mcp(permit: &str) -> Result { + call_prod_mcp_tool("operations_execute", json!({"permit": permit})) +} + +fn call_prod_mcp_tool(name: &str, arguments: Value) -> Result { let url = required_env("PROD_MCP_URL")?; if !(url.starts_with("http://") || url.starts_with("https://")) { return Err("PROD_MCP_URL must use HTTP or HTTPS".into()); @@ -1122,7 +1160,7 @@ fn call_prod_mcp(permit: &str) -> Result { .map_err(|error| format!("create prod-mcp temporary directory: {error}"))?; let request_headers = private_temp_path(&temporary_dir, "prod-mcp-request-headers", "txt")?; let response_headers = private_temp_path(&temporary_dir, "prod-mcp-response-headers", "txt")?; - let call = json!({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"arguments":{"permit":permit},"name":"operations_execute"}}); + let call = json!({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"arguments":arguments,"name":name}}); let result = (|| { write_mcp_headers(&request_headers, &token, None)?; write_private_file(&response_headers, b"")?; @@ -1132,7 +1170,7 @@ fn call_prod_mcp(permit: &str) -> Result { let _ = fs::remove_file(response_headers); let result = result?; if let Some(error) = result.get("error") { - return Err(format!("prod-mcp execution failed: {error}")); + return Err(format!("prod-mcp tool {name} failed: {error}")); } Ok(result) } @@ -1454,7 +1492,7 @@ fn base64_decode(value: &str) -> Result, String> { mod tests { use super::{ base64_decode, base64url_encode, build_request, canonical, curl_command, ecdsa_der_to_raw, - parse_mcp_body, private_temp_path, review_binding_marker, review_binding_matches, + operation_capability, parse_mcp_body, private_temp_path, review_binding_marker, review_binding_matches, review_binding_value, review_evidence_is_bound, reviewer_accepted, runbook_content_digest, validate_request_template, write_mcp_headers, TrustedApproval, @@ -1612,6 +1650,27 @@ mod tests { ); } + #[test] + fn operation_capability_selects_the_exact_live_contract() { + let response = json!({ + "result": { + "structuredContent": { + "operations": [ + {"id":"github.read","parameterSchema":{"type":"object"}}, + {"id":"slack.read","parameterSchema":{"type":"object"}} + ] + } + } + }); + let operation = operation_capability(&response, "github.read").unwrap(); + assert_eq!(operation["id"], "github.read"); + assert_eq!(operation["parameterSchema"]["type"], "object"); + assert_eq!( + operation_capability(&response, "github.write").unwrap_err(), + "prod-mcp does not advertise operation github.write" + ); + } + #[test] fn shared_action_permit_fixture_matches_the_rust_contract() { let fixture: serde_json::Value = diff --git a/src/runtime.rs b/src/runtime.rs index 5bbf5a9..cbe29a2 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1824,7 +1824,8 @@ fn reviewed_ops_cycle(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String .map_err(io_error("resolve ops agent log directory"))?; if !request_file.starts_with(&ops_logs) { return Err(format!( - "reviewed ops request must belong to ops identity {ops_name}" + "reviewed ops request must be the ops-owned bound request under {}; do not pass the supervisor-owned published artifact", + ops_logs.display() )); } let published = crate::prod_ops::publish_bound_request(&cfg.state, &request_file)?; From 6a6b2af1b4aab65ab581a1d7141a8c8fe4c315d2 Mon Sep 17 00:00:00 2001 From: Bo Wu Date: Mon, 24 Aug 2026 16:51:10 -0700 Subject: [PATCH 06/11] Route capability discovery through supervisor --- src/authority.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/authority.rs b/src/authority.rs index 50a30b3..55f5ef9 100644 --- a/src/authority.rs +++ b/src/authority.rs @@ -1,7 +1,7 @@ use crate::config; use serde::{Deserialize, Serialize}; -/// The complete mutation surface accepted by the authority supervisor. +/// The complete privileged surface accepted by the authority supervisor. /// /// CLI parsing happens before a request crosses the Unix socket. The server /// authorizes this enum instead of independently interpreting command strings, @@ -44,6 +44,7 @@ enum AuthorityOperation { ValidationLeaseShow, ValidationLeaseList, GateCheck, + OpsDescribe, OpsPublish, OpsExecute, } @@ -54,6 +55,9 @@ impl AuthorityRequest { "workflow" => (AuthorityOperation::Workflow, args), "decision" => (AuthorityOperation::Decision, args), "dag" => (AuthorityOperation::Dag, args), + "ops" if args.first().map(String::as_str) == Some("describe") => { + (AuthorityOperation::OpsDescribe, &args[1..]) + } "ops" if args.first().map(String::as_str) == Some("publish") => { (AuthorityOperation::OpsPublish, &args[1..]) } @@ -140,7 +144,9 @@ impl AuthorityRequest { | AuthorityOperation::TodoAssign | AuthorityOperation::TodoStatus | AuthorityOperation::GateCheck => uid == config::ORCHESTRATOR_UID, - AuthorityOperation::OpsPublish | AuthorityOperation::OpsExecute => { + AuthorityOperation::OpsDescribe + | AuthorityOperation::OpsPublish + | AuthorityOperation::OpsExecute => { uid == config::OPS_UID } AuthorityOperation::FindingCreate => uid == config::READER_UID, @@ -203,6 +209,7 @@ impl AuthorityRequest { AuthorityOperation::ValidationLeaseShow => ("subagent", Some("validation-lease-show")), AuthorityOperation::ValidationLeaseList => ("subagent", Some("validation-lease-list")), AuthorityOperation::GateCheck => ("subagent", Some("gate-check")), + AuthorityOperation::OpsDescribe => ("ops", Some("describe")), AuthorityOperation::OpsPublish => ("ops", Some("publish")), AuthorityOperation::OpsExecute => ("ops", Some("execute")), }; @@ -263,6 +270,14 @@ mod tests { .expect("ops request"); assert!(ops.authorized_for(config::OPS_UID)); assert!(!ops.authorized_for(config::ORCHESTRATOR_UID)); + let describe = AuthorityRequest::from_cli("ops", &strings(&["describe", "github.read"])) + .expect("ops describe request"); + assert!(describe.authorized_for(config::OPS_UID)); + assert!(!describe.authorized_for(config::ORCHESTRATOR_UID)); + assert_eq!( + describe.into_cli(), + ("ops".to_string(), strings(&["describe", "github.read"])) + ); } #[test] From 1f372f51cc94cff6deedd8be9303c5ec0e8f0dc5 Mon Sep 17 00:00:00 2001 From: Bo Wu Date: Mon, 24 Aug 2026 17:03:12 -0700 Subject: [PATCH 07/11] Publish reviewed requests through supervisor --- src/authority.rs | 13 +++++++++++++ src/prod_ops.rs | 26 ++++++++++++++++++++++---- src/runtime.rs | 33 +++++++++++++++++++++++++++------ 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/authority.rs b/src/authority.rs index 55f5ef9..7f09e48 100644 --- a/src/authority.rs +++ b/src/authority.rs @@ -45,6 +45,7 @@ enum AuthorityOperation { ValidationLeaseList, GateCheck, OpsDescribe, + OpsPublishBound, OpsPublish, OpsExecute, } @@ -58,6 +59,9 @@ impl AuthorityRequest { "ops" if args.first().map(String::as_str) == Some("describe") => { (AuthorityOperation::OpsDescribe, &args[1..]) } + "ops" if args.first().map(String::as_str) == Some("publish-bound") => { + (AuthorityOperation::OpsPublishBound, &args[1..]) + } "ops" if args.first().map(String::as_str) == Some("publish") => { (AuthorityOperation::OpsPublish, &args[1..]) } @@ -149,6 +153,7 @@ impl AuthorityRequest { | AuthorityOperation::OpsExecute => { uid == config::OPS_UID } + AuthorityOperation::OpsPublishBound => uid == config::ORCHESTRATOR_UID, AuthorityOperation::FindingCreate => uid == config::READER_UID, AuthorityOperation::FindingDismiss | AuthorityOperation::TodoClose => { matches!(uid, config::ORCHESTRATOR_UID | config::READER_UID) @@ -210,6 +215,7 @@ impl AuthorityRequest { AuthorityOperation::ValidationLeaseList => ("subagent", Some("validation-lease-list")), AuthorityOperation::GateCheck => ("subagent", Some("gate-check")), AuthorityOperation::OpsDescribe => ("ops", Some("describe")), + AuthorityOperation::OpsPublishBound => ("ops", Some("publish-bound")), AuthorityOperation::OpsPublish => ("ops", Some("publish")), AuthorityOperation::OpsExecute => ("ops", Some("execute")), }; @@ -278,6 +284,13 @@ mod tests { describe.into_cli(), ("ops".to_string(), strings(&["describe", "github.read"])) ); + let publish_bound = AuthorityRequest::from_cli( + "ops", + &strings(&["publish-bound", "--request-file", "/state/request.json"]), + ) + .expect("ops publish-bound request"); + assert!(publish_bound.authorized_for(config::ORCHESTRATOR_UID)); + assert!(!publish_bound.authorized_for(config::OPS_UID)); } #[test] diff --git a/src/prod_ops.rs b/src/prod_ops.rs index 5665a5c..6b43308 100644 --- a/src/prod_ops.rs +++ b/src/prod_ops.rs @@ -23,10 +23,6 @@ pub(crate) struct PublishedRequest { } impl PublishedRequest { - pub(crate) fn path(&self) -> &Path { - &self.artifact_path - } - pub(crate) fn descriptor_json(&self) -> Result { serde_json::to_string(&json!({ "artifactPath": self.artifact_path, @@ -51,6 +47,7 @@ pub fn run(args: &[String]) -> Result { Some("describe") => describe(&args[1..]), Some("template") => template(&args[1..]), Some("bind-runbook") => bind_runbook(&args[1..]), + Some("publish-bound") => publish_bound(&args[1..]), Some("publish") => publish(&args[1..]), Some("execute") => execute(&args[1..]), Some("review-bind") => review_bind(&args[1..]), @@ -163,6 +160,27 @@ fn publish(args: &[String]) -> Result { Ok(ExitCode::SUCCESS) } +#[cfg_attr(not(target_os = "linux"), allow(unused_variables))] +fn publish_bound(args: &[String]) -> Result { + #[cfg(target_os = "linux")] + if unsafe { libc::geteuid() } != crate::config::SUPERVISOR_UID { + return Err("ops publish-bound is reserved for the authority supervisor".into()); + } + #[cfg(not(target_os = "linux"))] + return Err("ops publish-bound requires Linux".into()); + + #[cfg(target_os = "linux")] + { + let options = options(args)?; + let state = PathBuf::from(required_env("MULTIAGENT_STATE_DIR")?); + let request_file = PathBuf::from(required(&options, "--request-file")?); + let descriptor = publish_bound_request(&state, &request_file)?; + println!("{}", descriptor.descriptor_json()?); + Ok(ExitCode::SUCCESS) + } +} + +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] pub(crate) fn publish_bound_request( state: &Path, request_file: &Path, diff --git a/src/runtime.rs b/src/runtime.rs index cbe29a2..80ff00c 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1828,10 +1828,8 @@ fn reviewed_ops_cycle(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String ops_logs.display() )); } - let published = crate::prod_ops::publish_bound_request(&cfg.state, &request_file)?; - let request_file = published.path(); - let descriptor = published.descriptor_json()?; - let binding = crate::prod_ops::review_binding_for_request(request_file)?; + let (request_file, descriptor) = publish_reviewed_ops_request(&request_file)?; + let binding = crate::prod_ops::review_binding_for_request(&request_file)?; let reviewer_instruction = reviewed_ops_reviewer_instruction(&request_file, &descriptor, &binding); spawn( @@ -1856,9 +1854,9 @@ fn reviewed_ops_cycle(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String )); } finalize(cfg, std::slice::from_ref(&reviewer))?; - crate::prod_ops::preflight_reviewed_request(request_file, &reviewer)?; + crate::prod_ops::preflight_reviewed_request(&request_file, &reviewer)?; - let execute_instruction = reviewed_ops_execute_instruction(request_file, &reviewer); + let execute_instruction = reviewed_ops_execute_instruction(&request_file, &reviewer); restore( cfg, &[ @@ -1873,6 +1871,29 @@ fn reviewed_ops_cycle(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String Ok(()) } +fn publish_reviewed_ops_request(request_file: &Path) -> Result<(PathBuf, String), String> { + let request_file = request_file + .to_str() + .ok_or("reviewed ops request path is not valid UTF-8")?; + let output = run_self_output(&[ + "ops", + "publish-bound", + "--request-file", + request_file, + ])?; + let descriptor = String::from_utf8(output.stdout) + .map_err(|error| format!("decode published ops request descriptor: {error}"))?; + let descriptor = descriptor.trim().to_string(); + let value: serde_json::Value = serde_json::from_str(&descriptor) + .map_err(|error| format!("decode published ops request descriptor: {error}"))?; + let artifact_path = value + .get("artifactPath") + .and_then(serde_json::Value::as_str) + .filter(|path| !path.is_empty()) + .ok_or("published ops request descriptor has no artifactPath")?; + Ok((PathBuf::from(artifact_path), descriptor)) +} + fn list_subagents(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String> { if !args.is_empty() { return Err("list takes no arguments".into()); From 21451cb94527337113bd4ad705eab29665c193a2 Mon Sep 17 00:00:00 2001 From: Bo Wu Date: Mon, 24 Aug 2026 17:17:31 -0700 Subject: [PATCH 08/11] Complete reviewed ops lifecycle routing --- orchestrator_prompt.md | 12 +++++++++--- src/authority.rs | 4 ++++ src/runtime.rs | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/orchestrator_prompt.md b/orchestrator_prompt.md index 33fb723..78bae6a 100644 --- a/orchestrator_prompt.md +++ b/orchestrator_prompt.md @@ -3,6 +3,10 @@ Coordinate isolated agents to satisfy the authenticated caller goal. Do not do worker, ops, scout, or reviewer work yourself. +All framework paths in this prompt resolve under `$MULTIAGENT_FRAMEWORK_ROOT`. +Read policies, role modules, playbooks, and runbooks only from that image-owned +root; never use same-named files from the cloned application repository. + ## Start On a clean launch: @@ -44,10 +48,12 @@ bindings, independent review, and phase completion. - Spawn roles with `multiagent subagent spawn`; provider-native agents do not establish the required Linux identity or evidence boundary. -- Source changes follow `prompts/playbooks/implementation-lifecycle.md`. +- Source changes follow + `$MULTIAGENT_FRAMEWORK_ROOT/prompts/playbooks/implementation-lifecycle.md`. - External-only work skips the source lifecycle and uses reviewed ops requests. -- For ops, load only `prompts/playbooks/reviewed-ops-cycle.md`. Keep one ops - identity for the session and invoke `multiagent subagent reviewed-ops-cycle` +- For ops, load only + `$MULTIAGENT_FRAMEWORK_ROOT/prompts/playbooks/reviewed-ops-cycle.md`. Keep one + ops identity for the session and invoke `multiagent subagent reviewed-ops-cycle` for every immutable request. - Use a fresh reviewer for each immutable ops request. Finalize the ops identity only when operational work completes or reaches a blocker. diff --git a/src/authority.rs b/src/authority.rs index 7f09e48..230c6a5 100644 --- a/src/authority.rs +++ b/src/authority.rs @@ -53,6 +53,9 @@ enum AuthorityOperation { impl AuthorityRequest { pub fn from_cli(command: &str, args: &[String]) -> Option { let (operation, forwarded) = match command { + // Typed workflow context is read-only and verifies the direct caller's + // kernel UID, so it must not be re-executed as the supervisor UID. + "workflow" if args.first().map(String::as_str) == Some("context") => return None, "workflow" => (AuthorityOperation::Workflow, args), "decision" => (AuthorityOperation::Decision, args), "dag" => (AuthorityOperation::Dag, args), @@ -248,6 +251,7 @@ mod tests { #[test] fn typed_api_excludes_runtime_and_arbitrary_execution() { assert!(AuthorityRequest::from_cli("workflow", &strings(&["status"])).is_some()); + assert!(AuthorityRequest::from_cli("workflow", &strings(&["context", "workflow-1"])).is_none()); assert!(AuthorityRequest::from_cli("subagent", &strings(&["assignment-create"])).is_some()); assert!(AuthorityRequest::from_cli("agent", &strings(&["run"])).is_none()); assert!(AuthorityRequest::from_cli("role-exec", &[]).is_none()); diff --git a/src/runtime.rs b/src/runtime.rs index 80ff00c..8930291 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1837,7 +1837,7 @@ fn reviewed_ops_cycle(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String &[ reviewer.clone(), "--role".into(), - "ops-reviewer".into(), + "reviewer".into(), "--instruction".into(), reviewer_instruction, ], From 74c34af412b89f13f488c94615217ff4f2d1ef1c Mon Sep 17 00:00:00 2001 From: Bo Wu Date: Mon, 24 Aug 2026 17:36:16 -0700 Subject: [PATCH 09/11] Enforce materialized reviewed ops continuations --- orchestrator_prompt.md | 5 +++++ prompts/playbooks/reviewed-ops-cycle.md | 8 ++++++++ prompts/roles/ops-agent.md | 9 +++++++-- src/runtime.rs | 11 ++++++----- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/orchestrator_prompt.md b/orchestrator_prompt.md index 78bae6a..6aa4093 100644 --- a/orchestrator_prompt.md +++ b/orchestrator_prompt.md @@ -57,6 +57,11 @@ bindings, independent review, and phase completion. for every immutable request. - Use a fresh reviewer for each immutable ops request. Finalize the ops identity only when operational work completes or reaches a blocker. +- After a reviewed ops cycle, treat any required follow-up operation as + incomplete until the same ops identity has materialized its complete bound + request at `$MULTIAGENT_LOG_DIR/agents/OPS_NAME/request.json`. A prose proposal + or `awaiting` report is not a result; restore that ops identity, then run a new + reviewed cycle with a fresh reviewer. - Load other playbooks only when their lifecycle is selected. Do not enumerate prompt files to discover known roles. diff --git a/prompts/playbooks/reviewed-ops-cycle.md b/prompts/playbooks/reviewed-ops-cycle.md index 47491d0..74d59d1 100644 --- a/prompts/playbooks/reviewed-ops-cycle.md +++ b/prompts/playbooks/reviewed-ops-cycle.md @@ -46,3 +46,11 @@ that path is intentionally outside the ops identity directory. On rejection or preflight failure, report the blocker. A changed request needs a new publication and reviewer. A review correction may use a fresh reviewer on the same immutable request. Never create a second ops identity. + +After each cycle, inspect the ops result before declaring operational work +complete. If it identifies another required operation, require the complete +bound request at `$MULTIAGENT_LOG_DIR/agents/OPS_NAME/request.json` and run a new +cycle with a fresh reviewer. A prose proposal, an `awaiting` statement, or a +draft under a private role-home path is incomplete work: restore the same ops +identity to materialize the canonical request rather than treating it as a +result or spawning a replacement. diff --git a/prompts/roles/ops-agent.md b/prompts/roles/ops-agent.md index 9b4778d..80313b6 100644 --- a/prompts/roles/ops-agent.md +++ b/prompts/roles/ops-agent.md @@ -56,8 +56,13 @@ multiagent ops execute --request-file PATH --reviewer REVIEWER_NAME Execute the reviewed request once. Interpret the structured outcome under the runbook and decide whether to finish, escalate, or prepare a distinct request. -Changed bytes always require a new review. Report the action ID, receipt path, -result, or exact blocker. +Changed bytes always require a new review. For every follow-up operation, rerun +`multiagent ops describe OPERATION_ID`, overwrite and bind the canonical +`$MULTIAGENT_LOG_DIR/agents/$MULTIAGENT_SUBAGENT_NAME/request.json`, run +`chmod 0640` on it, and report its exact path and digest lines. Never use a +private role-home path and never finish with only a proposed request. Report the +action ID, receipt path, final result, or exact blocker only when no follow-up +operation remains. Keep evidence in your trace directory. Credentials and signing authority remain with the supervisor and prod-mcp; missing credential environment variables are diff --git a/src/runtime.rs b/src/runtime.rs index 8930291..28a0f30 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1761,11 +1761,12 @@ fn reviewed_ops_reviewer_instruction( ) } -fn reviewed_ops_execute_instruction(request_file: &Path, reviewer: &str) -> String { +fn reviewed_ops_execute_instruction(request_file: &Path, reviewer: &str, ops_name: &str) -> String { format!( - "Continue the same runbook with the independently reviewed immutable request. Execute exactly:\n\nmultiagent ops execute --request-file {} --reviewer {}\n\nInspect the persisted structured outcome and decide from the runbook whether to stop, escalate, or propose another distinct reviewed operation. Never execute the same immutable request twice. Report the result or exact blocker. Do not create a replacement ops identity.", + "Continue the same runbook with the independently reviewed immutable request. Execute exactly:\n\nmultiagent ops execute --request-file {} --reviewer {}\n\nInspect the persisted structured outcome and decide from the runbook whether to stop, escalate, or prepare another distinct reviewed operation. Never execute the same immutable request twice. If another operation is needed, first run `multiagent ops describe OPERATION_ID`, then materialize and bind the complete next request at exactly `$MULTIAGENT_LOG_DIR/agents/{}/request.json`, run `chmod 0640` on it, and report that exact path plus the two digest lines. Do not use a role-home path, do not call `ops publish`, and do not finish with only a proposed request. If no operation remains, report the final result or exact blocker. Do not create a replacement ops identity.", shell_escape(&request_file.display().to_string()), - shell_escape(reviewer) + shell_escape(reviewer), + ops_name, ) } @@ -1856,7 +1857,7 @@ fn reviewed_ops_cycle(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String finalize(cfg, std::slice::from_ref(&reviewer))?; crate::prod_ops::preflight_reviewed_request(&request_file, &reviewer)?; - let execute_instruction = reviewed_ops_execute_instruction(&request_file, &reviewer); + let execute_instruction = reviewed_ops_execute_instruction(&request_file, &reviewer, ops_name); restore( cfg, &[ @@ -4190,7 +4191,7 @@ mod tests { assert!(!review.contains("Slack")); assert!(!review.contains("Grafana")); - let execute = reviewed_ops_execute_instruction(request, "ops-reviewer-01"); + let execute = reviewed_ops_execute_instruction(request, "ops-reviewer-01", "github-ops"); assert!(execute.contains( "multiagent ops execute --request-file /state/logs/agents/ops-primary/request.json --reviewer ops-reviewer-01" )); From ae68f2102ca61fde8a7f50129a165497f9fb62d0 Mon Sep 17 00:00:00 2001 From: Bo Wu Date: Mon, 24 Aug 2026 18:02:51 -0700 Subject: [PATCH 10/11] Preserve literal negative predicates --- orchestrator_prompt.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/orchestrator_prompt.md b/orchestrator_prompt.md index 6aa4093..83ac24c 100644 --- a/orchestrator_prompt.md +++ b/orchestrator_prompt.md @@ -62,6 +62,11 @@ bindings, independent review, and phase completion. request at `$MULTIAGENT_LOG_DIR/agents/OPS_NAME/request.json`. A prose proposal or `awaiting` report is not a result; restore that ops identity, then run a new reviewed cycle with a fresh reviewer. +- Preserve literal predicates from the authenticated goal. When the caller + requires an empty list, zero records, or no submitted items, any returned item + disqualifies that candidate regardless of its subtype or state. Do not weaken + the predicate by reclassifying records; continue the same bounded search until + the exact predicate is proven or a concrete blocker is reached. - Load other playbooks only when their lifecycle is selected. Do not enumerate prompt files to discover known roles. From b38905a6e2231caa115a693b566f958e33f6f067 Mon Sep 17 00:00:00 2001 From: Bo Wu Date: Mon, 24 Aug 2026 18:46:02 -0700 Subject: [PATCH 11/11] Reduce reviewed operations orchestration overhead --- orchestrator_prompt.md | 7 ++ prompts/playbooks/reviewed-ops-cycle.md | 28 ++++-- prompts/roles/ops-agent.md | 6 ++ prompts/roles/ops-reviewer.md | 3 + src/prod_ops.rs | 44 +++++++++- src/runtime.rs | 53 ++++++++++-- src/workflow.rs | 108 ++++++++++++++++++++++++ 7 files changed, 233 insertions(+), 16 deletions(-) diff --git a/orchestrator_prompt.md b/orchestrator_prompt.md index 83ac24c..d15e910 100644 --- a/orchestrator_prompt.md +++ b/orchestrator_prompt.md @@ -57,11 +57,18 @@ bindings, independent review, and phase completion. for every immutable request. - Use a fresh reviewer for each immutable ops request. Finalize the ops identity only when operational work completes or reaches a blocker. +- `reviewed-ops-cycle` waits for both review and the ops continuation. Consume + its compact result directly: never call `subagent wait` afterward and never + inspect logs, transcripts, role homes, operation directories, or receipts to + rediscover its result. - After a reviewed ops cycle, treat any required follow-up operation as incomplete until the same ops identity has materialized its complete bound request at `$MULTIAGENT_LOG_DIR/agents/OPS_NAME/request.json`. A prose proposal or `awaiting` report is not a result; restore that ops identity, then run a new reviewed cycle with a fresh reviewer. +- Complete successful external-only work with + `multiagent orchestrator complete --external-only`; do not enter source + lifecycle phases or write surrogate result files. - Preserve literal predicates from the authenticated goal. When the caller requires an empty list, zero records, or no submitted items, any returned item disqualifies that candidate regardless of its subtype or state. Do not weaken diff --git a/prompts/playbooks/reviewed-ops-cycle.md b/prompts/playbooks/reviewed-ops-cycle.md index 74d59d1..ba6e107 100644 --- a/prompts/playbooks/reviewed-ops-cycle.md +++ b/prompts/playbooks/reviewed-ops-cycle.md @@ -27,7 +27,9 @@ multiagent subagent reviewed-ops-cycle OPS_NAME \ --timeout 900 ``` -Use a fresh reviewer name for each immutable request. This command: +Use a fresh reviewer name for each immutable request. The reviewer is required; +it is the independent authority boundary, not optional orchestration overhead. +This command: 1. validates that the bound request belongs to the named ops identity and publishes it as a supervisor-owned immutable artifact; @@ -35,7 +37,9 @@ Use a fresh reviewer name for each immutable request. This command: 3. passes only a bounded artifact descriptor to the reviewer; 4. finalizes accepted review evidence before execution; and 5. continues the same ops identity in a fresh provider context with the exact - execute command. + execute command; and +6. waits for that continuation and prints one compact `ReviewedOpsCycleResult` + containing the ops conclusion or the next bound request. Do not reconstruct these mechanics manually. Prior panes, transcripts, final messages, and native provider resume state are intentionally excluded from the @@ -47,10 +51,16 @@ preflight failure, report the blocker. A changed request needs a new publication and reviewer. A review correction may use a fresh reviewer on the same immutable request. Never create a second ops identity. -After each cycle, inspect the ops result before declaring operational work -complete. If it identifies another required operation, require the complete -bound request at `$MULTIAGENT_LOG_DIR/agents/OPS_NAME/request.json` and run a new -cycle with a fresh reviewer. A prose proposal, an `awaiting` statement, or a -draft under a private role-home path is incomplete work: restore the same ops -identity to materialize the canonical request rather than treating it as a -result or spawning a replacement. +The cycle already waits. Do not call `subagent wait` afterward, and do not read, +tail, grep, find, or list agent logs, transcripts, role homes, operation +directories, or receipts. Use only the returned `opsResult` and +`followUpRequest`. If `followUpRequest` is non-null, run a new cycle on that +exact path with a fresh reviewer. If it is null, use `opsResult` as the durable +conclusion. A prose proposal, an `awaiting` statement`, or a draft under a +private role-home path is incomplete work: restore the same ops identity to +materialize the canonical request rather than treating it as a result or +spawning a replacement. + +For an external-only task with successful reviewed operations and no source +changes, finish with `multiagent orchestrator complete --external-only`. Do not +manufacture source lifecycle phases or files. diff --git a/prompts/roles/ops-agent.md b/prompts/roles/ops-agent.md index 80313b6..31a3f7b 100644 --- a/prompts/roles/ops-agent.md +++ b/prompts/roles/ops-agent.md @@ -56,6 +56,12 @@ multiagent ops execute --request-file PATH --reviewer REVIEWER_NAME Execute the reviewed request once. Interpret the structured outcome under the runbook and decide whether to finish, escalate, or prepare a distinct request. +The command prints a compact result and persists the full receipt at +`receiptPath`. Use the compact result directly. Do not search logs, transcripts, +role homes, or operation directories, and do not reread the receipt unless the +compact result explicitly reports missing or truncated evidence. An +`operationId` or `actionId` returned by execution is evidence, not an operation +capability ID; never pass it to `ops describe`. Changed bytes always require a new review. For every follow-up operation, rerun `multiagent ops describe OPERATION_ID`, overwrite and bind the canonical `$MULTIAGENT_LOG_DIR/agents/$MULTIAGENT_SUBAGENT_NAME/request.json`, run diff --git a/prompts/roles/ops-reviewer.md b/prompts/roles/ops-reviewer.md index a2f8010..42ae2f8 100644 --- a/prompts/roles/ops-reviewer.md +++ b/prompts/roles/ops-reviewer.md @@ -11,5 +11,8 @@ Before execution: - If `review-bind` fails for either schema or binding, reject the request. Manual digest calculation or visual comparison is not a substitute for successful deterministic validation. - If and only if the request matches the goal and runbook, make the first non-empty line exactly `Verdict: ACCEPTED`, then explain the decision without reproducing the binding artifact. - Otherwise make the first non-empty line `Verdict: REJECTED` and explain the deviation. +- Keep the explanation to at most three concise bullets. Do not restate the + request, runbook, schemas, digests, or evidence that the supervisor already + supplied. After execution, a separate reviewer invocation must inspect the persisted request and receipt under `MULTIAGENT_STATE_DIR/operations/ACTION_ID` and report any behavioral deviation or unexpected side effect. diff --git a/src/prod_ops.rs b/src/prod_ops.rs index 6b43308..73f0d04 100644 --- a/src/prod_ops.rs +++ b/src/prod_ops.rs @@ -65,9 +65,25 @@ fn describe(args: &[String]) -> Result { } let response = call_prod_mcp_tool("operations_capabilities", json!({}))?; let operation = operation_capability(&response, &args[0])?; + let mut compact = serde_json::Map::new(); + for key in [ + "id", + "version", + "description", + "access", + "allowedRunbooks", + "parameterSchema", + "parameterExamples", + "requireChangeTicket", + "requiredApprovalRoles", + ] { + if let Some(value) = operation.get(key) { + compact.insert(key.into(), value.clone()); + } + } println!( "{}", - serde_json::to_string_pretty(operation) + serde_json::to_string(&Value::Object(compact)) .map_err(|error| format!("encode prod-mcp operation capability: {error}"))? ); Ok(ExitCode::SUCCESS) @@ -597,9 +613,33 @@ fn execute(args: &[String]) -> Result { serde_json::to_vec_pretty(&result).map_err(|error| error.to_string())?, ) .map_err(|error| format!("persist operation receipt: {error}"))?; + let structured = result + .pointer("/result/structuredContent") + .cloned() + .unwrap_or(Value::Null); + let evidence = structured + .get("summary") + .and_then(Value::as_str) + .map(|summary| { + serde_json::from_str(summary).unwrap_or_else(|_| Value::String(summary.into())) + }) + .unwrap_or(Value::Null); + let compact = json!({ + "apiVersion": "multiagent.moveindustries.io/v1", + "kind": "OperationExecutionResult", + "actionId": action_id, + "operationId": structured.get("operationId").cloned().unwrap_or(Value::Null), + "requestedOperation": structured.get("requestedOperation").cloned().unwrap_or(Value::Null), + "state": structured.get("state").cloned().unwrap_or(Value::Null), + "outcome": structured.get("outcome").cloned().unwrap_or(Value::Null), + "code": structured.get("code").cloned().unwrap_or(Value::Null), + "message": structured.get("message").cloned().unwrap_or(Value::Null), + "evidence": evidence, + "receiptPath": operation_dir.join("receipt.json"), + }); println!( "{}", - serde_json::to_string_pretty(&result).map_err(|error| error.to_string())? + serde_json::to_string(&compact).map_err(|error| error.to_string())? ); Ok(ExitCode::SUCCESS) } diff --git a/src/runtime.rs b/src/runtime.rs index 28a0f30..5ca8ec0 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1023,16 +1023,23 @@ pub fn orchestrator(args: &[String]) -> Result { .iter() .any(|arg| matches!(arg.as_str(), "-h" | "--help")) { - println!("Usage:\n multiagent orchestrator complete\n\nRuns the normal-path completion gates for the active orchestrated workflow."); + println!("Usage:\n multiagent orchestrator complete [--external-only]\n\nRuns the supervisor completion gates. Use --external-only only for reviewed operations with no source implementation lifecycle."); return Ok(ExitCode::SUCCESS); } - if args != ["complete"] { + if args.first().map(String::as_str) != Some("complete") + || args.len() > 2 + || (args.len() == 2 && args[1] != "--external-only") + { return Err(format!("unknown command: {}", args[0])); } if config::lifecycle_enforced() { let workflow_id = env_nonempty("MULTIAGENT_WORKFLOW_ID") .ok_or_else(|| "lifecycle enforcement requires MULTIAGENT_WORKFLOW_ID".to_string())?; - let diff = crate::workflow::supervisor_complete(&workflow_id)?; + let diff = if args.get(1).map(String::as_str) == Some("--external-only") { + crate::workflow::supervisor_complete_external(&workflow_id)? + } else { + crate::workflow::supervisor_complete(&workflow_id)? + }; println!("workflow completed\t{workflow_id}\t{diff}\tauthority=supervisor"); } else { run_self_quiet(&["subagent", "gate-check"])?; @@ -1763,7 +1770,7 @@ fn reviewed_ops_reviewer_instruction( fn reviewed_ops_execute_instruction(request_file: &Path, reviewer: &str, ops_name: &str) -> String { format!( - "Continue the same runbook with the independently reviewed immutable request. Execute exactly:\n\nmultiagent ops execute --request-file {} --reviewer {}\n\nInspect the persisted structured outcome and decide from the runbook whether to stop, escalate, or prepare another distinct reviewed operation. Never execute the same immutable request twice. If another operation is needed, first run `multiagent ops describe OPERATION_ID`, then materialize and bind the complete next request at exactly `$MULTIAGENT_LOG_DIR/agents/{}/request.json`, run `chmod 0640` on it, and report that exact path plus the two digest lines. Do not use a role-home path, do not call `ops publish`, and do not finish with only a proposed request. If no operation remains, report the final result or exact blocker. Do not create a replacement ops identity.", + "Continue the same runbook with the independently reviewed immutable request. Execute exactly:\n\nmultiagent ops execute --request-file {} --reviewer {}\n\nUse the compact execution result printed by that command; the full receipt is already persisted at its receiptPath. Do not inspect agent logs, transcripts, operation directories, or the receipt unless the compact result explicitly reports missing or truncated evidence. Decide from the runbook whether to stop, escalate, or prepare another distinct reviewed operation. Never execute the same immutable request twice and never run `ops describe` on the returned operationId or actionId. If another operation is needed, first run `multiagent ops describe OPERATION_ID`, then materialize and bind the complete next request at exactly `$MULTIAGENT_LOG_DIR/agents/{}/request.json`, run `chmod 0640` on it, and report that exact path plus the two digest lines. Do not use a role-home path, do not call `ops publish`, and do not finish with only a proposed request. If no operation remains, report the final result or exact blocker. Do not create a replacement ops identity.", shell_escape(&request_file.display().to_string()), shell_escape(reviewer), ops_name, @@ -1829,7 +1836,12 @@ fn reviewed_ops_cycle(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String ops_logs.display() )); } - let (request_file, descriptor) = publish_reviewed_ops_request(&request_file)?; + let reviewed_request_sha256 = format!( + "sha256:{:x}", + Sha256::digest(fs::read(&request_file).map_err(io_error("read reviewed ops request"))?) + ); + let ops_request_file = request_file; + let (request_file, descriptor) = publish_reviewed_ops_request(&ops_request_file)?; let binding = crate::prod_ops::review_binding_for_request(&request_file)?; let reviewer_instruction = reviewed_ops_reviewer_instruction(&request_file, &descriptor, &binding); @@ -1869,6 +1881,35 @@ fn reviewed_ops_cycle(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String ], )?; wait(cfg, &[ops_name.to_string(), "--timeout".into(), timeout])?; + let ops_status = read_trimmed(&ops_dir.join("status")).unwrap_or_else(|| "unknown".into()); + let ops_result = fs::read_to_string(ops_dir.join("last-message.txt")) + .unwrap_or_else(|_| "ops agent produced no durable final message".into()); + let ops_result: String = ops_result.chars().take(16_384).collect(); + let follow_up_request = fs::read(&ops_request_file).ok().and_then(|bytes| { + let sha256 = format!("sha256:{:x}", Sha256::digest(&bytes)); + (sha256 != reviewed_request_sha256).then(|| { + serde_json::json!({ + "path": ops_request_file.display().to_string(), + "sha256": sha256, + "bytes": bytes.len(), + }) + }) + }); + println!( + "{}", + serde_json::to_string(&serde_json::json!({ + "apiVersion": "multiagent.moveindustries.io/v1", + "kind": "ReviewedOpsCycleResult", + "opsName": ops_name, + "reviewer": reviewer, + "opsStatus": ops_status, + "cycleWaitedForCompletion": true, + "additionalWaitRequired": false, + "opsResult": ops_result, + "followUpRequest": follow_up_request, + })) + .map_err(|error| format!("encode reviewed ops cycle result: {error}"))? + ); Ok(()) } @@ -4197,6 +4238,8 @@ mod tests { )); assert!(execute.contains("decide from the runbook")); assert!(execute.contains("Never execute the same immutable request twice")); + assert!(execute.contains("compact execution result")); + assert!(execute.contains("never run `ops describe` on the returned operationId or actionId")); } #[cfg(unix)] diff --git a/src/workflow.rs b/src/workflow.rs index deca437..970dbfa 100644 --- a/src/workflow.rs +++ b/src/workflow.rs @@ -1231,6 +1231,114 @@ pub fn supervisor_complete(id: &str) -> Result { Ok(diff) } +/// Seals a workflow that intentionally performed only independently reviewed +/// external operations and never entered the source implementation lifecycle. +pub fn supervisor_complete_external(id: &str) -> Result { + if config::lifecycle_enforced() + && std::env::var("MULTIAGENT_UID_SANDBOX").as_deref() == Ok("1") + && std::env::var("MULTIAGENT_AUTHORITY_SERVER_CHILD").as_deref() != Ok("1") + { + return Err("lifecycle completion must execute inside the authority supervisor".into()); + } + let store = Store::configured()?; + let p = store.paths(id)?; + let _lock = store.lock(&p)?; + let mut state = read_env(&p.state, id)?; + if state_value(&state, "phase") != "pre-implementation" { + return Err(format!( + "external-only completion requires phase=pre-implementation, got {}", + state_value(&state, "phase") + )); + } + if !state_value(&state, "preimplementation_gate").is_empty() + || !state_value(&state, "decision_id").is_empty() + || !state_value(&state, "candidate_diff_hash").is_empty() + { + return Err("external-only completion cannot bypass a started source implementation lifecycle".into()); + } + validate_original_task(&state)?; + let todos = read_todos(&p.todos)?; + let active_rows: Vec<&str> = todos + .iter() + .filter(|row| active(row.get(4))) + .map(|row| row.get(0)) + .collect(); + if !active_rows.is_empty() { + return Err(format!( + "external-only completion blocked by active TODOs: {}", + active_rows.join(",") + )); + } + let operations_dir = store.state_dir.join("operations"); + let mut successful_operations = 0usize; + if operations_dir.is_dir() { + for entry in fs::read_dir(&operations_dir) + .map_err(|error| format!("list external operation receipts: {error}"))? + { + let entry = entry + .map_err(|error| format!("read external operation receipt entry: {error}"))?; + let receipt_path = entry.path().join("receipt.json"); + if !receipt_path.is_file() { + continue; + } + let receipt: serde_json::Value = serde_json::from_slice( + &fs::read(&receipt_path) + .map_err(|error| format!("read external operation receipt: {error}"))?, + ) + .map_err(|error| { + format!( + "decode external operation receipt {}: {error}", + receipt_path.display() + ) + })?; + let structured = receipt + .pointer("/result/structuredContent") + .unwrap_or(&serde_json::Value::Null); + let succeeded = structured + .get("state") + .and_then(serde_json::Value::as_str) + == Some("succeeded") + && structured + .pointer("/outcome/disposition") + .and_then(serde_json::Value::as_str) + == Some("succeeded") + && structured + .pointer("/outcome/terminal") + .and_then(serde_json::Value::as_bool) + == Some(true); + if !succeeded { + return Err(format!( + "external-only completion requires successful terminal receipts; {} is not successful", + receipt_path.display() + )); + } + successful_operations += 1; + } + } + if successful_operations == 0 { + return Err( + "external-only completion requires at least one successful reviewed operation receipt" + .into(), + ); + } + crate::subagent::completion_gate_check()?; + let result = format!("external-only:{successful_operations}"); + state.insert("phase".into(), "complete".into()); + state.insert("candidate_diff_hash".into(), result.clone()); + state.insert("reviewed_diff_hash".into(), result.clone()); + state.insert("updated_at".into(), timestamp()); + write_env(&p.state, &state)?; + event( + &p.events, + "phase_transitioned", + &format!( + "from=pre-implementation\tto=complete\titeration={}\tauthority=supervisor\troute=external-only\toperations={successful_operations}", + state_value(&state, "iteration") + ), + )?; + Ok(result) +} + fn value(args: &[String]) -> Result<(), String> { if args.len() != 2 { return Err("value requires WORKFLOW_ID KEY".into());