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
20 changes: 17 additions & 3 deletions orchestrator_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
Coordinate isolated agents to satisfy the authenticated caller goal. Do not do
worker, ops, scout, or reviewer work yourself.

The authenticated caller request is the goal authority. The orchestrator decides the DAG.
The supervisor enforces role isolation, evidence bindings, and phase gates.

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.
Expand All @@ -18,8 +21,10 @@ On a clean launch:

Only when `MULTIAGENT_RESUME=1`, load
`prompts/playbooks/recovery.md` before restoring work.
Do not inspect recovery state on a clean launch. When MULTIAGENT_RESUME=1,
inspect it only in the recovery workflow.

## Choose roles
## Role Catalog

| Need | Role |
| --- | --- |
Expand All @@ -31,6 +36,7 @@ Only when `MULTIAGENT_RESUME=1`, load
External access always belongs to ops. A scout may analyze an immutable artifact
returned by ops, but cannot call Slack, GitHub, Grafana, AWS, Kubernetes,
prod-mcp, or another deployed service.
External access is an authority boundary, not a mutability classification.

## Build the DAG

Expand All @@ -48,19 +54,27 @@ 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.
- Load reusable role and lifecycle modules from `MULTIAGENT_PROMPT_MODULE_ROOT`.
Use `prompts/playbooks/agent-spawning.md` for spawning mechanics.
- 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
`$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.
When selecting ops, load only prompts/playbooks/reviewed-ops-cycle.md.
- 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.
inspect unrelated logs, transcripts, role homes, or operation directories to
rediscover its result. The ops continuation itself verifies the exact
immutable request and digest-bound runbook and may inspect its exact receipt.
- If an accepted immutable request is not executed because the ops continuation
reports a structural blocker, do not repeat that unchanged request with a new
reviewer or ops context. Surface the blocker; a new reviewed cycle requires a
materially distinct request.
- 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
Expand Down
18 changes: 12 additions & 6 deletions prompts/playbooks/reviewed-ops-cycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ This command:

Do not reconstruct these mechanics manually. Prior panes, transcripts, final
messages, and native provider resume state are intentionally excluded from the
continuation boundary.
continuation boundary. A fresh context must not execute opaque instructions: it
reads and verifies the exact supervisor-published immutable request and its
digest-bound runbook before execution.

Never pass the supervisor-owned published artifact back as `--request-file`;
that path is intentionally outside the ops identity directory. On rejection or
Expand All @@ -52,13 +54,17 @@ and reviewer. A review correction may use a fresh reviewer on the same immutable
request. Never create a second ops identity.

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
tail, grep, find, or list unrelated agent logs, transcripts, role homes, or
operation directories. The ops continuation must inspect the exact immutable
request and bound runbook before execution, and may inspect the exact receipt
path returned by execution. 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
conclusion. If an accepted immutable request is not executed because the ops
continuation reports a structural blocker, do not submit that unchanged request
to another review cycle. 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
Expand Down
13 changes: 10 additions & 3 deletions prompts/roles/ops-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,19 @@ When restored, use the exact command supplied by the supervisor:
multiagent ops execute --request-file PATH --reviewer REVIEWER_NAME
```

This is a fresh provider context restored into the existing OS-enforced ops
identity. Verify that identity, then read the exact supervisor-published
immutable request and its digest-bound runbook before execution. The command
independently verifies the request, reviewer approval, and signed permit. Never
bypass it with direct provider access.

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
`receiptPath`; inspect both. Do not search unrelated logs, transcripts, role
homes, or operation directories. If you do not execute an accepted request,
report one exact structural blocker and never ask the supervisor to retry the
same immutable request with another reviewer or ops context. 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
Expand Down
8 changes: 4 additions & 4 deletions src/authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,7 @@ impl AuthorityRequest {
| AuthorityOperation::GateCheck => uid == config::ORCHESTRATOR_UID,
AuthorityOperation::OpsDescribe
| AuthorityOperation::OpsPublish
| AuthorityOperation::OpsExecute => {
uid == config::OPS_UID
}
| AuthorityOperation::OpsExecute => uid == config::OPS_UID,
AuthorityOperation::OpsPublishBound => uid == config::ORCHESTRATOR_UID,
AuthorityOperation::FindingCreate => uid == config::READER_UID,
AuthorityOperation::FindingDismiss | AuthorityOperation::TodoClose => {
Expand Down Expand Up @@ -251,7 +249,9 @@ 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("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());
Expand Down
65 changes: 31 additions & 34 deletions src/prod_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ fn operation_capability<'a>(response: &'a Value, operation_id: &str) -> 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())));
return Err(format!(
"prod-mcp capabilities failed: {}",
Value::Object(result.clone())
));
}
let operations = result
.get("structuredContent")
Expand Down Expand Up @@ -201,8 +204,8 @@ pub(crate) fn publish_bound_request(
state: &Path,
request_file: &Path,
) -> Result<PublishedRequest, String> {
let state = fs::canonicalize(state)
.map_err(|error| format!("resolve multiagent state: {error}"))?;
let state =
fs::canonicalize(state).map_err(|error| format!("resolve multiagent state: {error}"))?;
let (_, bytes) = read_reviewable_request(&state, request_file)?;
let template: Value = serde_json::from_slice(&bytes)
.map_err(|error| format!("decode bound ops request: {error}"))?;
Expand Down Expand Up @@ -235,10 +238,7 @@ fn bind_request_template(
"runbookDocument".into(),
Value::String(runbook_document.into()),
);
object.insert(
"runbookContentSha256".into(),
Value::String(digest.clone()),
);
object.insert("runbookContentSha256".into(), Value::String(digest.clone()));
validate_request_template(&template)?;
Ok((template, digest))
}
Expand All @@ -248,10 +248,7 @@ fn publish_request_bytes(state: &Path, bytes: &[u8]) -> Result<PublishedRequest,
return Err("ops request must contain between 1 and 65536 bytes".into());
}
let hex = format!("{:x}", Sha256::digest(bytes));
let directory = state
.join("operations")
.join("requests")
.join(&hex);
let directory = state.join("operations").join("requests").join(&hex);
fs::create_dir_all(&directory)
.map_err(|error| format!("create operation request store: {error}"))?;
secure_publication_path(&directory, true)?;
Expand All @@ -268,11 +265,7 @@ fn publish_request_bytes(state: &Path, bytes: &[u8]) -> Result<PublishedRequest,
.duration_since(UNIX_EPOCH)
.map_err(|error| error.to_string())?
.as_nanos();
let temporary = directory.join(format!(
".request.{}.{}.tmp",
std::process::id(),
unique
));
let temporary = directory.join(format!(".request.{}.{}.tmp", std::process::id(), unique));
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
Expand Down Expand Up @@ -310,33 +303,34 @@ fn publish_request_bytes(state: &Path, bytes: &[u8]) -> Result<PublishedRequest,
}

fn read_ops_draft(path: &Path) -> Result<Vec<u8>, String> {
let agents = fs::canonicalize(PathBuf::from(required_env("MULTIAGENT_LOG_DIR")?).join("agents"))
.map_err(|error| format!("resolve ops agents log directory: {error}"))?;
let canonical = fs::canonicalize(path)
.map_err(|error| format!("resolve ops draft file: {error}"))?;
let agents =
fs::canonicalize(PathBuf::from(required_env("MULTIAGENT_LOG_DIR")?).join("agents"))
.map_err(|error| format!("resolve ops agents log directory: {error}"))?;
let canonical =
fs::canonicalize(path).map_err(|error| format!("resolve ops draft file: {error}"))?;
if !canonical.starts_with(&agents) {
return Err("ops draft file must be inside MULTIAGENT_LOG_DIR/agents".into());
}
let (bytes, _metadata) =
read_bounded_file(&canonical, MAX_OPERATION_REQUEST_BYTES, true)?;
let (bytes, _metadata) = read_bounded_file(&canonical, MAX_OPERATION_REQUEST_BYTES, true)?;
if bytes.is_empty() {
return Err("ops request must contain between 1 and 65536 bytes".into());
}
#[cfg(target_os = "linux")]
if _metadata.uid() != crate::config::OPS_UID || _metadata.mode() & 0o022 != 0 {
return Err("ops draft must be owned by the ops UID and not group- or world-writable".into());
return Err(
"ops draft must be owned by the ops UID and not group- or world-writable".into(),
);
}
Ok(bytes)
}

fn read_reviewable_request(state: &Path, path: &Path) -> Result<(PathBuf, Vec<u8>), String> {
let canonical = fs::canonicalize(path)
.map_err(|error| format!("resolve ops request file: {error}"))?;
let canonical =
fs::canonicalize(path).map_err(|error| format!("resolve ops request file: {error}"))?;
if !canonical.starts_with(state) {
return Err("ops request file must be inside MULTIAGENT_STATE_DIR".into());
}
let (bytes, _metadata) =
read_bounded_file(&canonical, MAX_OPERATION_REQUEST_BYTES, true)?;
let (bytes, _metadata) = read_bounded_file(&canonical, MAX_OPERATION_REQUEST_BYTES, true)?;
if bytes.is_empty() {
return Err("ops request must contain between 1 and 65536 bytes".into());
}
Expand Down Expand Up @@ -379,7 +373,10 @@ fn read_bounded_file(
.read_to_end(&mut bytes)
.map_err(|error| format!("read {}: {error}", path.display()))?;
if bytes.len() as u64 > limit {
return Err(format!("{} exceeds the configured byte limit", path.display()));
return Err(format!(
"{} exceeds the configured byte limit",
path.display()
));
}
Ok((bytes, metadata))
}
Expand Down Expand Up @@ -1081,7 +1078,9 @@ fn verified_runbook_content(template: &Value) -> Result<String, String> {
}
if let Some(target) = canonical_runbook_target(&bytes)? {
if object.get("target") != Some(&target) {
return Err("ops request target does not match the exact Markdown runbook bytes".into());
return Err(
"ops request target does not match the exact Markdown runbook bytes".into(),
);
}
}
Ok(actual)
Expand Down Expand Up @@ -1550,10 +1549,9 @@ fn base64_decode(value: &str) -> Result<Vec<u8>, String> {
mod tests {
use super::{
base64_decode, base64url_encode, build_request, canonical, curl_command, ecdsa_der_to_raw,
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,
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,
};
use chrono::{TimeZone, Utc};
use serde_json::json;
Expand Down Expand Up @@ -1782,5 +1780,4 @@ mod tests {
let _ = fs::remove_file(request_headers);
let _ = fs::remove_file(response_headers);
}

}
Loading
Loading