Agentic fix orchestrator
When you click Dispatch fix in the admin console, the report becomes a task for an agent running in an isolated sandbox.
Flow
Sandbox providers
| Adapter | When to use |
|---|---|
local-noop | Tests + local dev (no network at all) |
e2b | Default for cloud (managed Firecracker) |
modal | Heavy compute / long-running fixes |
cloudflare | Edge-routed, sub-second cold starts |
The adapter abstraction means swapping providers is a config change, not a
code change. Every sandbox invocation writes to sandbox_audit_log
(provider, image digest, network policy, exit code) so a SOC 2 auditor can
verify isolation guarantees.
Agent contract
We support two agent shapes:
McpFixAgent— speaks JSON-RPC 2.0 withtools/call, plus the SEP-1686 Tasks envelope so long-running fixes can stream progress.RestFixWorkerAgent— a simpler REST contract (POST /fix,GET /status) for teams that haven’t adopted MCP yet.
Both agents have the same set of tools available: read_file, write_file,
run_tests, git_commit, open_pr. Filesystem and network access are
brokered through the sandbox — the agent itself never gets a raw shell.
Credential scoping
Mushi’s GitHub App ships with per-repo, per-PR scoping: each fix
attempt installs the app only on the target repo, with contents:write and
pull-requests:write only. Tokens last 60 minutes and are revoked the
moment the PR is opened.
Spec traceability — inventoryAction end-to-end
Every fix carries the originating inventory Action from dispatch all
the way back to the admin UI. The 2026-05-09 release closed the
write-side U-turn that was the v2 system’s biggest open issue.
What the worker now does
- Recover the anchor. The worker reads
inventory_action_node_idfrom the dispatch row. If empty (legacy report or the caller didn’t specify), it walks thereports_againstgraph edge thatclassify-reportwrites when it can match a report to an Action — then back-fills the dispatch row so the next consumer doesn’t have to repeat the walk. - Thread
expected_outcomeinto the LLM prompt. A Markdown block rendered byrenderSpecContext()lists the action description, page, story, and every assertion in the contract. The reviewer prompt’s question 4 explicitly asks the agent to preserve every assertion — drift is treated as a regression, not a stylistic note. - Run
validateAgainstSpecas a deterministic pre-PR gate. Hard-fails the dispatch if the diff removes ajson_pathfield the contract asserts on; soft warnings (no changed file references the contract’s required DB table, no changed file mentions the action’s page route) are persisted tofix_attempts.spec_validation_warnings JSONBso the admin Fixes drawer can show “this fix didn’t reference the inventory’s requireduserstable — sanity-check before merging”. - Queue a targeted post-PR synthetic probe. A marker
synthetic_runsrow scoped toactionNodeIdis inserted the moment the PR opens. The synthetic-monitor cron drains the queue with priority on its next tick, runs an HTTP probe, and evaluatesexpected_outcome(status_inchecks + JSONPath assertions) against the live action. The result is a realsynthetic_runsrow that flips the Action toverifiedorregressedin the admin UI within minutes of merge.
expected_outcome shape
expected_outcome:
summary: 'POST /signup returns 200 and creates a user row'
response:
status_in: [200, 201]
json_path:
- { path: '$.user.id', op: 'exists' }
- { path: '$.error', op: 'not_equals', value: 'rate_limited' }
database:
table: 'users'
schema: 'public'
expect: 'row_exists'
ui:
route_change_to: '/dashboard'
visible_text: 'Welcome'Every field is optional except database.table (when database is set). The
synthetic monitor evaluates the HTTP-side assertions today; database +
UI assertions are recorded as unverified until the Playwright crawler
runtime lands. See
/v1/schemas/expected-outcome.json
for the canonical JSON Schema.
Carrying the anchor through external surfaces
| Surface | How the anchor flows |
|---|---|
REST POST /v1/admin/fixes/dispatch | Optional inventoryActionNodeId field on the request body (UUID-validated) |
MCP dispatch_fix tool | inputSchema.properties.inventoryActionNodeId; get_fix_context returns the full inventory_action (with expected_outcome) |
A2A POST /v1/a2a/tasks | input.inventoryActionNodeId at create time; surfaced on every GET /v1/a2a/tasks/:id as metadata.inventoryActionNodeId |
| GitHub Action | inventory-action-node-id input on command: dispatch-fix |