Contents

MAGI: A Risk-Reduction System Inside an Agentic Loop (pt.2)

Table of Contents

  • Introduction
  • Context
    • The platform
    • Setup of the run
  • How the run unfolded
    • The pipeline as executed
    • Timeline
    • Live log
    • Effort by agent
  • The MAGI board in detail
    • Independent analyses
    • The three proposals
    • Cross-review with authors masked
    • Preventive review by QA, SRE, Security and the Tech Writer
    • Claim verification and selection
  • Implementation: the code written by the developers
    • DEV1, the writer
    • DEV2, the pair
  • Verification and reviews of the implemented change
    • QA: acceptance scenarios
    • SRE
    • Security
    • Tech Writer
    • Final review by the three architects
  • Functional requirements: from request to evidence
    • Traceability
    • Requirement-by-requirement outcome
    • Findings overall
  • Discussion
    • What the architecture changed in this run
    • Threats to validity
    • Cost of robustness
  • Second run: the same request on Amazon Bedrock AgentCore
    • What AgentCore changes
    • The deployment in the AWS console
    • The run: a second design round
    • What it took to get a SUCCESS
    • What the gates did not see
    • Independent check outside the platform
    • Two runs side by side
  • Conclusion
  • Appendix: mandatory gates

Well, here we are again. After the autopsy of the agentic loop I wanted to see the engine on something bigger than a Python upgrade: a long, demanding change request, written by a human, in Italian, for a service that already exists. Not a toy, not a fix. A feature.

Introduction

This is a single end-to-end run of magi-system, an autonomous software-change platform. Three independent “architect” agents (Melchior, Balthasar, Casper) analyze a change request, propose plans, review each other’s proposals with the authors masked, and have every factual claim checked against the repository by tools before code computes which plan is admissible.

Two developer agents (writer and pair) implement the selected plan. QA, SRE and Security verify it, a Tech Writer documents it, the three architects review the final candidate in a fresh context, and a deterministic gate decides the outcome.

The request asked for a three-tool “Dev/SRE Toolbox” inside an existing FastAPI service (pytbak). The run ended with SUCCESS after 19 min 19 s and US$ 1.05:

  • all ten mandatory gates passed on the final commit
  • a 21-file change (+2,340 / -51 lines) with 67 new tests
  • no regression in the 79 existing ones
  • no push, no merge, no deployment

In this post I follow the run as it happened: what each agent produced, how each requirement went from the request to code and evidence, and where the safeguards changed the course of the run. Including the places where the platform got it wrong, because there are a few.

Context

The platform

magi-system is a LangGraph application. The graph state is JSON-native and checkpointed in SQLite, agents call an LLM through a provider adapter, and every command that touches the target runs in a container with no network. Three properties matter here:

  1. Facts are checked, not voted on. Architects state claims in a typed form (file exists, symbol exists, text present, package version, policy key, command succeeds). A verifier checks them against the baseline commit. A plan that relies on a refuted critical claim, an unresolved critical unknown or a confirmed blocker is inadmissible, however well it scores.
  2. Evidence is bound to a commit. Each check result carries the commit it ran on, and the gate reads only the final candidate’s results. Any new commit invalidates earlier evidence.
  3. No human in the loop, explicit failure. Every loop has a budget, and every run ends in either a verified candidate or a typed failure state.

Setup of the run

ItemValue
Targetpytbak 2.0.0 (FastAPI 0.143.1, Pydantic 2.14.1, Python 3.11 or later), 139 tracked files, baseline e3920b5
Target config.magi/config.yaml: build python -m compileall -q app, test python -m pytest -q, lint ruff --select E9,F63,F7,F82
RunnerDocker, image with the target’s dependencies preinstalled, --network=none
Modeldeepseek/deepseek-v4.1-flash for all nine agents, temperature 0, 32,000 output tokens per call
RoutingOpenRouter provider: {sort: throughput, require_parameters: true}, schema-constrained output
Budget3,000 tool calls, 600 LLM calls, 30 M tokens, US$ 5, 3,600 s
Request230 lines in Italian

This is not the first attempt. The configuration is the result of eleven previous runs on the same target, and each one exposed a problem that became a fix. This is run twelve.

How the run unfolded

The pipeline as executed

flowchart TD A[Intake: snapshot e3920b5, 139 files, config from .magi/config.yaml] --> B subgraph MAGI[MAGI board, round 0] B[3 independent analyses, 135 requirements traced to quotes] --> C[3 proposals: A Casper, B Melchior, C Balthasar] C --> D[Cross-review with authors masked, plus QA SRE Security Tech Writer preventive review] D --> E[Claim verification by tools: 290 claims, 240 supported, 42 refuted, 8 unverifiable] E --> F{Code computes admissibility and rank} end F -->|B admissible, rank 1, score 4.59| G[DEV1 writer: 20 files, commit be60190] F -.->|A admissible, rank 2, tie on score| X1[not selected] F -.->|C inadmissible| X2[rejected] G --> H[Initial checks: build PASS, lint PASS, test FAIL 1 of 146] H --> I[DEV2 pair: 11 concerns, fixes the wrong test expectation, commit d93229e] I --> J[All checks: build, test, lint, 11 QA scenarios, integrity, scope PASS] J --> K[Specialist review on the code: QA, SRE, Security PASS] K --> L[Tech Writer: README and docs/14-dev-sre-toolbox.md, commit 5cce890] L --> M[Evidence invalidated: every check re-run on 5cce890, PASS] M --> N[Final review by the 3 architects, fresh context, PASS] N --> O[Deterministic gate: 10 of 10 mandatory gates PASS] O --> P([SUCCESS: patch and local branch, not pushed])

Read it top to bottom and notice where models talk and where code decides. The models write analyses, proposals and code. Admissibility, rank, gates and the final verdict are computed.

Timeline

gantt title run-20261015-205501-e0a70f, wall clock, 15 Oct 2026 dateFormat HH:mm:ss axisFormat %H:%M section MAGI board Analysis (3 in parallel) :a1, 20:55:01, 124s Proposals (3 in parallel) :a2, 20:57:10, 97s Cross-review and preventive review :a3, 20:58:47, 87s Claim verification and selection :a4, 21:00:14, 11s section Implementation DEV1 writer :b1, 21:00:25, 178s Initial checks (container) :b2, 21:03:23, 7s DEV2 pair (assess and fix) :b3, 21:03:30, 166s section Verification Checks and QA scenarios :c1, 21:06:16, 34s Specialist review :c2, 21:06:50, 13s Tech Writer :c3, 21:07:03, 116s Re-verification on docs commit :c4, 21:09:00, 81s Final review (3 architects) :c5, 21:10:21, 239s Gate :milestone, 21:14:20, 0s

The MAGI board, the phase that decides what to build, took 5 min 24 s (28% of the run). Implementation took 5 min 51 s. Verification, documentation and the final review took the remaining 8 minutes.

The final review was the single longest step. One architect’s output was truncated by the 32,000-token cap twice (a temperature-0 repetition loop) and needed a technical retry. More on that in the discussion.

Live log

I followed the run live through its event stream (events.jsonl). The commentary below is what I was told during the run, translated and lightly condensed. It shows what an operator sees and has to judge at each step.

20:55:01, run_start. Run 12 started: commit e3920b5, environment container:docker:...pytbak-magi:local, policy default. The three architects start their analysis in parallel, each with read-only tools and without seeing the others’ work.

20:57:05, analysis collected. 135 requirements accepted (111 explicit, 24 derived), every explicit one carrying a verbatim Italian quote of the request. 5 rejected: 3 paraphrased quotes not found in the request, 2 derived “requirements” with no traceable source (for example “protect the endpoints with Basic Auth”: an architect’s interpretation, correctly kept out of the requirements).

21:00:25, selection. Plan selected in round 0: Proposal B, Melchior. Balthasar’s was inadmissible: a critical claim was refuted (“the platform policy lists .github/workflows/** … as protected paths”) and a critical unknown about write scopes was unresolved. Note for later: that claim is true, but it was written as a policy_exists claim on a path, which the verifier looks up as a policy key. It did not block the run, since a plan was selected anyway.

21:03:23, handoff dev.writer. DEV1 wrote the code in 3 min, 4 escalations. Melchior’s structure follows the repo’s own convention: models in app/models/toolbox.py.

21:03:29, check project.test FAIL on be60190. Checked on a copy: 145 passed, 1 failed (test_empty_documents_are_skipped), the same test that failed in run 9 with a different plan and author. A recurring trap with empty YAML documents.

21:06:16, handoff dev.pair. DEV2 changed the test, not the code. That could be a legitimate correction or a weakened test, so I checked the diff: only the expected value changes (documents == 3 to == 2), with a comment explaining PyYAML’s behavior, and the actual assertion (resources_inspected == 1) stays. Verified independently: PyYAML returns 2 documents ([None, dict]) for ---\n---\n<doc>. DEV2 was right, DEV1’s expectation was wrong, and the code was correct.

21:07:03, specialists on d93229e. qa.acceptance PASS (11 scenarios), sre.review PASS (5 non-blocking findings), security.review PASS (0 findings).

21:09:04, check on 5cce890. The Tech Writer has finished, producing a new commit 5cce890. All checks are rerunning on it, and tests are already green. Next come the specialists and then the final review.

21:10:21, specialists on 5cce890. The specialists pass again on the final candidate: QA 11 scenarios, SRE 5 non-blocking findings, Security 8 non-blocking findings. Next is the final review by the three architects, then the gate.

21:12:56, agent_failure magi.balthasar. Balthasar’s final review was truncated twice at 32k, as in run 10 (a repetition loop). A technical retry is running for Balthasar only; Melchior’s and Casper’s reviews are kept.

21:14:20, magi.final_review PASS. Three final reviews completed, 7 findings, none confirmed blocking. run_end: SUCCESS, change_level: verified, final commit 5cce890.

Effort by agent

pie showData title Input tokens by agent, total 4.74 M "Melchior" : 600897 "Balthasar" : 739261 "Casper" : 830805 "DEV1 writer" : 543558 "DEV2 pair" : 765383 "Tech Writer" : 861686 "QA" : 132193 "SRE" : 132193 "Security" : 132190
AgentLLM callsInput tokensOutput tokensTool calls
Melchior17600,897106,20179
Balthasar20739,261137,38690
Casper21830,80579,98394
DEV1 writer12543,55862,29561
DEV2 pair13765,38343,75238
Tech Writer13861,68644,37631
QA / SRE / Security3 each132,193 each41,871 / 18,028 / 17,8430
Total1054,738,166551,735393

Input dominates (90% of tokens) because each tool-loop turn resends the transcript. The three architects account for 46% of input tokens. The analysis is the part most exposed to hallucination, and it is where the platform spends most. I think that is the right place.

The MAGI board in detail

Independent analyses

The three architects started from the same snapshot, each with read-only tools (list_files, read_file, grep) and without seeing the others’ work. Each analysis returns an understanding of the mandate, the requirements with their source quotes, typed claims about the repository, assumptions, open questions and risks.

Melchior (evolution, minimal change)Balthasar (failure modes)Casper (challenge)
Requirements extracted796427
Claims about the repository303418
Assumptions8107
Open questions7106
Risks8108
Duration62 s63 s123 s

The requirement sets overlap and are merged by id, so the union is the 135 requirements of the run.

Strange… the three analyses converged on the same open questions without seeing each other: the URL prefix (/api/v1/toolbox as written, or /api/toolbox following the repository), whether to reuse the Basic Auth of /api/debug/*, and whether to gate the feature behind a flag like DEBUG_ENDPOINTS_ENABLED.

Their risks show the different perspectives:

  • Melchior looked at fit with the existing app: the app-wide 400 handler strips error detail, so limit errors must not use 400. The error-injection middleware (?inject_error=) and the global slowapi rate limit (100/min) apply to the new endpoints and could interfere with tests.
  • Balthasar looked at what breaks: new routes change GET /api/mgmt/mappings and make the README’s “31 routes registered” stale without any test noticing. yaml.safe_load_all still expands anchors and aliases, so a hostile manifest can explode in memory. Offloading to the default thread pool shares it with the existing CPU-bound endpoints.
  • Casper challenged the scope: custom Prometheus metrics could introduce high-cardinality labels, and limits must be enforced before or during parsing, not after.

The three proposals

All three proposals adapted the prefix to /api/toolbox (the repository has a single /api prefix and no versioning, and the request explicitly allows adapting it), reused verify_credentials, offloaded parsing with loop.run_in_executor after the existing /api/fib/{x} precedent, pinned PyYAML and kept analysis code free of FastAPI. They differed in structure and in a few contracts:

B, Melchior (selected)A, CasperC, Balthasar
Analysis codeapp/toolbox/ (base, registry, tools/)app/toolbox/ + runner.pyapp/services/toolbox/ + runner.py
Modelsapp/models/toolbox.pyapp/models/toolbox.pyapp/models/toolbox.py
Planned file changes212320
Assumptions12128
Unknown tool404 via the app-wide handler422 with a readable reasonundecided (200 + TOOL_UNKNOWN finding, or 404)
Over-limit input413413413 for size; 200 + limit finding for documents/depth/findings
Findings capanalysis outcome, never an HTTP erroroutcome=error + dedicated finding200 + limit finding
Kill switchTOOLBOX_ENABLEDnonenone
Rollbackrevert, or TOOLBOX_ENABLED=falserevertrevert
Regression test moduleyesyesno (existing suites re-run)

Each proposal also listed its trade-offs explicitly. Melchior’s, for example:

Limit rejections use 413 rather than 400: the app-wide 400 handler would erase the specific reason; 413 keeps the message but is a slightly unusual code for “too many YAML documents”. And: Findings cap is an analysis outcome rather than an HTTP error: preserves “a finding is not an HTTP error” and “no silent truncation”, at the cost of two different limit-handling paths that must both be documented.

Cross-review with authors masked

Each architect scored the other two proposals, never its own, on the six rubric criteria of the platform policy, without knowing who wrote them (labels A, B, C).

Reviewer, proposalRequirement fit (x3)Simplicity (x2)Risk (x2)Operability (x1)Testability (x2)Maintainability (x1)Weighted
Balthasar on B (Melchior)5445544.55
Casper on B (Melchior)5445554.64
Melchior on A (Casper)5444544.45
Balthasar on A (Casper)5544554.73
Melchior on C (Balthasar)4434443.82
Casper on C (Balthasar)4434443.82
xychart-beta title "Peer score per proposal, weighted rubric, mean of two reviewers" x-axis ["B Melchior", "A Casper", "C Balthasar"] y-axis "Score (1-5)" 3 --> 5 bar [4.59, 4.59, 3.82]

Both reviewers of C gave it the same scores and the same reason: the unknown-tool contract is left undecided in the plan, and it carries an unresolved assumption about whether the runtime image installs PyYAML from requirements.txt or docker/requirements.txt. Casper’s note on C: “regression coverage is delegated to pre-existing tests rather than newly implemented”.

Preventive review by QA, SRE, Security and the Tech Writer

Before any code was written, the four specialists reviewed all three proposals: 49 findings in total, plus 16 from the architects’ cross-review.

xychart-beta title "Findings raised on the proposals, by reviewer" x-axis ["SRE", "Tech Writer", "QA", "Security", "Casper", "Balthasar", "Melchior"] y-axis "Findings" 0 --> 18 bar [17, 12, 10, 10, 7, 5, 4]
  • SRE raised the same three major findings against every proposal: no per-run timeout or concurrency bound on tool execution; run_id is not propagated to tracing, so response/log/trace correlation is not achieved; a new hard dependency imported at application-import time, so a missing install takes the whole app down. On C it also flagged the undecided unknown-tool contract.
  • Security confirmed on all three proposals that CPU-bound tool execution shares the process-wide default thread pool with other endpoints. On C, that PyYAML availability in the runtime image is an unresolved critical assumption (major). On B, that the 404 body for an unknown tool is masked by the app-wide 404 handler. Whether json.validate should echo the parsed value back in the response stayed unresolved.
  • QA wrote the 12 acceptance scenarios and raised findings on testability: OpenAPI presence only in the verification plan, not in a test; the no-network guarantee asserted only by static import inspection. Two of its objections were refuted by verification, for example “allowPrivilegeEscalation absent is mapped to severity error” on A, when the plan maps it to a warning. An objection without support is not a veto.
  • The Tech Writer required that curl examples must carry the Basic Auth header to be “working” (the request asks for working examples), and that the new settings be documented in .env.example.

Findings are not votes. Each one is resolved by verifying its supporting claims: it becomes CONFIRMED, REFUTED or UNRESOLVED, and only a confirmed blocker excludes a proposal.

Claim verification and selection

The verifier checked 290 claims against e3920b5 in 11 seconds: 240 supported, 42 refuted, 8 unverifiable (operational guarantees and business assumptions, which cannot be checked mechanically and block only when critical). Typical supported claims: verify_credentials defined in app/auth.py; loop.run_in_executor used in app/routers/api.py; APIRouter(prefix="/api" in the existing routers; DEBUG_ENDPOINTS_ENABLED handled in tests/conftest.py.

Code then computed admissibility and rank:

ProposalAdmissibleScoreRankReason
B, Melchioryes4.59091none
A, Casperyes4.59092none
C, Balthasarnon/an/arefuted critical claim about protected paths; unresolved critical unknown about write scopes

A and B tied exactly. The policy’s tie-break (fewer_planned_changes, fewer_assumptions, magi_order) chose B: 21 planned changes against 23. No vote was taken at any point.

Implementation: the code written by the developers

DEV1, the writer

DEV1 received the selected plan, the requirements and the write scopes, and implemented it in 12 LLM calls and 61 tool calls (3 min). Its handoff is explicit about what is not done:

The work is NOT fully green: the full suite reports 1 failed / 145 passed / 25 skipped, and the README.md update command failed, so README.md is unchanged. Both are reported below as escalations with evidence; I could not fix them in this turn.

I like that. A writer that says “not green” in its own handoff is a writer I can work with.

The code follows the plan closely. The common contract, in app/toolbox/base.py:

class Tool(Protocol):
    """Common contract every toolbox tool implements."""

    name: str
    version: str
    description: str
    input_model: type[BaseModel]

    def execute(self, params: BaseModel, limits: ToolboxLimits) -> ToolResult:
        ...

The explicit registry rejects empty and duplicate identifiers and returns tools in a deterministic order:

def register(tool: Tool) -> None:
    """Register a tool. Raises ValueError on an empty or duplicate identifier."""
    if not tool.name:
        raise ValueError("tool name must not be empty")
    if tool.name in _REGISTRY:
        raise ValueError(f"duplicate tool name: {tool.name}")
    _REGISTRY[tool.name] = tool

The router keeps the four error channels the request asked to keep apart: 422 for invalid tool input, 404 for an unknown tool, 413 for limits, a sanitized 500 for anything unexpected. Findings never change the HTTP status. Parsing runs in the thread pool, and the log line carries only run_id, tool, status, outcome and duration:

    try:
        result = await loop.run_in_executor(None, tool.execute, params, limits)
    except ToolLimitExceeded as exc:
        ...
        raise HTTPException(status_code=413, detail=str(exc)) from exc
    except ToolInputError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc
    except Exception:
        logger.exception(
            "toolbox run failed | run_id={} tool={} status={} duration_ms={}",
            run_id, tool.name, ExecutionStatus.FAILED.value, duration_ms,
        )
        raise HTTPException(status_code=500, detail="Internal server error") from None

Balthasar’s risk about alias expansion was handled in the YAML loader itself:

class _NoAliasSafeLoader(yaml.SafeLoader):
    """SafeLoader that refuses YAML aliases.

    Aliases make it possible to build a small document that expands to a huge
    object graph (a "billion laughs" bomb). The toolbox never needs them, so
    they are rejected instead of being expanded.
    """

The request’s rule “init containers: no probe rules” is a single explicit branch, while security checks still apply to init containers:

        # --- probes: regular containers only, never init containers ---
        if not is_init:
            if not container.get("readinessProbe"):
                add("K8S_MISSING_READINESS_PROBE", Severity.WARNING, ...)

The distinction between missing and explicitly risky configuration is encoded as two codes with different severities: allowPrivilegeEscalation: true gives K8S_ALLOW_PRIVILEGE_ESCALATION_ENABLED (error), while a missing field gives K8S_ALLOW_PRIVILEGE_ESCALATION_NOT_DISABLED (warning).

DEV1 wrote 67 tests in five files:

FileTestsCovers
test_toolbox_k8s_manifest.py29clean and dirty manifests, multi-document, empty documents, unsupported kind, missing kind, unexpected structure, non-mapping document, requests/limits, probes, untagged / latest / digest images, privileged, privilege escalation true vs absent, non-root at pod level, init containers (no probes, security still checked), StatefulSet and DaemonSet, unsafe YAML tag, aliases, invalid YAML as negative outcome, document/depth/findings/size limits, determinism
test_toolbox_api.py13auth on catalog and runs, wrong credentials, uniform schema, finding not an HTTP error, unknown tool 404, malformed 422, invalid tool input 422, over-limit 413, sanitized internal error, OpenAPI exposure, determinism
test_toolbox_json_validate.py10valid object and scalar, error position, empty and whitespace input, URL-like and path-like strings treated as text, oversized input, determinism
test_toolbox_text_diff.py9identical, changed, empty inputs, context 0, context above limit, oversized left/right, determinism
test_toolbox_regression.py6contexts, fibonacci, health, debug auth, unchanged 400 envelope, route mappings

The sanitized-error test, for example, injects a tool that raises RuntimeError("secret internal detail") and asserts that the response contains neither the message nor a traceback.

DEV2, the pair

DEV2 first assessed DEV1’s candidate independently, without seeing DEV1’s handoff, from the mandate, the plan, the diff and the check results. It raised 11 concerns, among them:

BLOCKING: project.test FAILS. test_empty_documents_are_skipped expects output['documents'] == 3 but the tool returns 2 for '---\n---\n' + GOOD_DEPLOYMENT. And: The custom loader rejects all YAML aliases/anchors, so a syntactically valid manifest that uses anchors is reported as K8S_YAML_INVALID. This is a defensible hardening choice but is a false-negative risk that should be documented. And: The findings cap keeps max_findings-1 findings plus a notice; it is not silent, but confirm the dropped count is reported.

Then it read the whole package, ran a direct yaml.safe_load_all probe in the container, and concluded that the tool was right and the test wrong:

 def test_empty_documents_are_skipped():
+    # PyYAML yields one document per '---' marker: the leading '---\n---\n'
+    # produces a single empty (None) document followed by the Deployment, so
+    # the stream holds 2 documents and only 1 is inspected.
     manifest = "---\n---\n" + GOOD_DEPLOYMENT
     result = _run(manifest)
-    assert result.output["documents"] == 3
+    assert result.output["documents"] == 2
     assert result.output["resources_inspected"] == 1

DEV2 also fixed the order of the docs index and escalated what it could not do: the README update, blocked because a helper script it tried to write at the repository root was outside the write scopes. The Tech Writer later updated the README.

Verification and reviews of the implemented change

QA: acceptance scenarios

QA had defined 12 acceptance scenarios during the preventive review, each linked to requirement ids and to a command. Eleven had a command and were run in the container on d93229e and again on 5cce890. The twelfth (“no superfluous infrastructure is introduced”) has no mechanical check and was assessed in the reviews.

ScenarioCommandVerdict
acc-01 the app still compiles and startspython -m compileall -q appPASS
acc-02 new and existing tests passpython -m pytest -q -p no:cacheproviderPASS
acc-03 lint gate passesruff check --select E9,F63,F7,F82 app testsPASS
acc-04 three tools in the catalog and runnablepython -m pytest -q -k toolboxPASS
acc-05 typed requests and responses in OpenAPIpython -m pytest -q -k toolboxPASS
acc-06 uniform result schemapython -m pytest -q -k toolboxPASS
acc-07 no network, cluster or credentialspython -m pytest -q -k toolboxPASS
acc-08 hostile or over-limit inputs give controlled errorspython -m pytest -q -k toolboxPASS
acc-09 json.validate contractpython -m pytest -q -k toolboxPASS
acc-10 text.diff contractpython -m pytest -q -k toolboxPASS
acc-11 k8s.manifest.inspect contractpython -m pytest -q -k toolboxPASS
acc-12 no superfluous infrastructurenoneassessed in review

Eight scenarios share the same command, which runs every toolbox test. They are weaker than they look: each one passes if the whole toolbox suite passes, not because a test specific to that scenario passed.

SRE

On the code, SRE confirmed what was solved and what was not. Solved: “Findings cap behaviour matches the no-silent-truncation requirement”, “No new infrastructure, persistence or retrieval endpoint introduced”.

Open, non-blocking: “Observability log line is claimed but not asserted by any test”; “New toolbox limits are not surfaced in .env.example”; an edge case where max_findings=0 still returns one finding (the notice); and an unresolved doubt that the request-size cap relies on Content-Length and could be bypassed with chunked transfer encoding, with the per-field input limit as the backstop.

Of the three major concerns raised before implementation, the hard-import concern remained a design choice (the dependency is pinned), and no per-run timeout or tracing propagation was added.

Security

Security confirmed by tests that the endpoints reuse Basic Auth (401 without and with wrong credentials), that unsafe YAML tags and aliases are rejected, and that internal errors are sanitized.

It raised two non-blocking findings of substance: "json.loads recursion depth is not visibly guarded" (a deeply nested JSON document can raise RecursionError; the request asks for depth limits, and only YAML has one), and the max_findings=0 edge case.

Tech Writer

The Tech Writer read the implemented code, not the plan, and wrote docs/14-dev-sre-toolbox.md (360 lines: feature description, curl examples with Basic Auth, response examples, configuration and limits, how to add a tool, limitations of the Kubernetes checks, architecture note). It also updated the README, the docs index and the CHANGELOG.

Its commit 5cce890 invalidated all previous evidence, and every check was re-run. Yes, for a Markdown-only commit. That is on purpose.

Final review by the three architects

The three architects reviewed the final diff in a fresh context. Melchior and Casper completed in 93 s and 16 s. Balthasar’s review was truncated twice by a repetition loop and needed a technical retry.

The gate recorded “three final reviews completed; 7 finding(s), none confirmed blocking”. Verification confirmed Balthasar’s “json.validate has no recursion/depth guard”, matching Security’s finding independently. It refuted “Pinned PyYAML version differs between plan and diff” as a defect (the plan said 6.0.2, the code pins 6.0.3: a documentation mismatch, not a bug).

And one refutation is wrong. Balthasar reported “Image reference with an empty tag is not flagged”, and verification refuted it. Reading the code shows the finding was correct: for nginx: the last path segment contains :, so the “no tag” branch is skipped, and the empty tag is not latest, so nothing is reported:

            if ":" not in last_segment:
                add("K8S_IMAGE_TAG_MISSING", Severity.WARNING, ...)
            elif last_segment.rsplit(":", 1)[1] == "latest":
                add("K8S_IMAGE_TAG_LATEST", ...)

The finding was minor and non-blocking, so the outcome would not have changed. But it is a real defect that the run shipped and that the platform marked as refuted.

Functional requirements: from request to evidence

Traceability

The request was decomposed into 135 requirements. An explicit requirement is accepted only if its source_quote occurs verbatim (whitespace- and case-normalized) in the request. That is what prevents an architect from inventing a requirement. QA then turned the requirements into 11 acceptance scenarios, each bound to a command run in the container on the candidate commit.

flowchart LR R[Request, Italian, 230 lines] --> Q[Verbatim quotes] Q --> REQ[135 requirements: 111 explicit and 24 derived] REQ --> COV[Proposal coverage: every mandatory requirement covered] REQ --> SC[11 acceptance scenarios from QA preventive review] SC --> CMD[Command in container on the candidate commit] CMD --> EV[Evidence bound to commit 5cce890] EV --> G{Gate qa.acceptance}

Requirement-by-requirement outcome

Functional requirementImplementation in the final commitVerified by
Modular feature inside the existing app, no new service, no rewritePackage app/toolbox/ + router app/routers/toolbox.py, mounted in create_app() behind TOOLBOX_ENABLED (default true); app/main.py +8 linesScenario “application compiles/starts without regressions”; regression tests; 79 pre-existing tests pass
Reuse conventions and dependenciesPrefix adapted to the repo: /api/toolbox/... (the repo has no /api/v1); settings in the existing Pydantic Settings; loop.run_in_executor pattern copied from /api/fib/{x}; PyYAML pinned in requirements.txtClaims verified at analysis (loop.run_in_executor present in app/routers/api.py, verify_credentials in app/auth.py)
Common tool contract and explicit registryTool protocol (name, version, input model, execute(params, limits) -> ToolResult) in app/toolbox/base.py; app/toolbox/registry.py with a dict keyed by unique id, duplicate registration rejected, deterministic orderCatalog scenario; unit tests
json.validate: JSON text, parsed value or error with position, no URL/pathapp/toolbox/tools/json_validate.py; invalid JSON is a negative outcome (HTTP 200, outcome: fail, error finding with line/column); URL-like strings are treated as plain text, never fetchedtests/test_toolbox_json_validate.py; smoke test: HTTP 200 + outcome: fail
text.diff: two strings, unified diff, bounded context linesapp/toolbox/tools/text_diff.py (difflib.unified_diff), fields left/right, context lines bounded by TOOLBOX_MAX_DIFF_CONTEXT_LINEStests/test_toolbox_text_diff.py; smoke test: wrong field names give 422 with a structured error
k8s.manifest.inspect: multi-document, Deployment/StatefulSet/DaemonSet, static onlyapp/toolbox/tools/k8s_manifest_inspect.py (473 lines); YAML loaded with a SafeLoader subclass that also refuses aliases (no “billion laughs”); depth and document-count limits; unsupported kinds and non-mapping documents reported as findingstests/test_toolbox_k8s_manifest.py (349 lines)
Report missing requests/limits, probes, latest/untagged images, privileged, allowPrivilegeEscalation, non-root18 finding codes, including K8S_CONTAINER_MISSING_REQUESTS/LIMITS, K8S_MISSING_READINESS/LIVENESS_PROBE, K8S_IMAGE_TAG_LATEST, K8S_IMAGE_TAG_MISSING, K8S_PRIVILEGED_CONTAINER, K8S_RUN_AS_NON_ROOT_NOT_SETSmoke test on a bad Deployment: all 8 expected codes, 1 error + 7 warnings
Distinguish missing from explicitly riskySeparate codes: K8S_ALLOW_PRIVILEGE_ESCALATION_NOT_DISABLED (missing, warning) vs K8S_ALLOW_PRIVILEGE_ESCALATION_ENABLED (explicit true, error)Unit tests; doc table
Init containers handled separately, no probe rulesContainers and initContainers walked separately (is_init); probe checks skipped for init containersUnit tests “init containers”
GET /tools: id, description, version, input schemaToolListResponse, input schema generated from each tool’s Pydantic modelCatalog and OpenAPI scenarios
POST /runs: synchronous, HTTP 200 with result and run_id, no queuerun_tool awaits run_in_executor(tool.execute); UUID run_id per call; no persistence, no GET /runs/{id}API tests; smoke test
Uniform typed result; status vs outcome vs validation vs internal error kept distinctToolRunResponse (run_id, tool, tool_version, status, outcome, summary, findings, output, duration_ms); 422 for malformed requests and invalid tool input, 404 unknown tool, 413 over limit, 500 sanitized ("Internal server error", no stack trace)Scenario “uniform schema”; tests “malformed”, “sanitized internal error”
Reuse auth; no public bypassAPIRouter(..., dependencies=[Depends(verify_credentials)])Smoke test: 401 without credentials; auth tests
Configurable limits, controlled rejection, no silent truncationTOOLBOX_ENABLED plus six TOOLBOX_MAX_* limits (request bytes, input bytes, YAML documents, YAML depth, findings, diff context lines); a finding-cap notice when findings are cappedScenario “hostile or over-limit inputs”; final-review findings discuss the cap edge cases
Logs without payloads; reuse observabilitylogger.info with run_id, tool, status, duration only; no new observability stackSRE review (5 non-blocking findings)
Tests: units per tool, API, Kubernetes cases, regression5 new test files, 67 new testsproject.test: 146 passed, 25 skipped
Documentation: feature, curl, responses, limits, how to add a tool, k8s limitations, ADR notedocs/14-dev-sre-toolbox.md (360 lines), README section, docs/README.md index, CHANGELOG.mddocs.updated gate; Tech Writer preventive review asked for Basic Auth in every curl example

Findings overall

The 101 findings raised during the run show where the specialists added value beyond the tests. Before code was written, SRE raised the same three major concerns against all three proposals (no per-run timeout or concurrency bound, run_id not propagated to tracing, a hard import-time dependency on PyYAML). The Tech Writer required every curl example to carry the Basic Auth header to be “working”, as the request demands.

During implementation, DEV1 raised a blocker against its own work (the failing test) and a major finding (README not updated after a failed edit command), both handled downstream.

On the final candidate, 24 findings were raised and none was confirmed blocking. Examples: json.loads recursion depth not visibly guarded; new TOOLBOX_* settings not added to .env.example; an edge case where max_findings=0 still returns one (notice) finding; observability guarantees documented but not asserted by any executed check.

Several final-review findings were refuted by verification, one of them wrongly (the empty image tag). The confirmed ones are recorded in the report as residual risks. They did not stop the run because the policy blocks only on confirmed blockers.

pie showData title Findings by status, 101 in total "Confirmed" : 66 "Refuted by verification" : 25 "Unresolved" : 10

Discussion

What the architecture changed in this run

  • Plan choice by facts. Two proposals tied on the rubric score (4.59). The tie was broken by the deterministic rule in the policy, not by a vote. The third proposal was excluded before any code existed.
  • Pair review as a correctness check, not a formality. The only failing test came from a wrong expectation in a test written by DEV1. DEV2 fixed the test with a justification that an operator could verify in one command, and no assertion was weakened.
  • Evidence invalidation. The documentation commit changed only Markdown, yet every check was re-run on 5cce890. The gate never accepts results from an earlier commit. If it isn’t there, it can’t lie.
  • Fresh-context final review. The three architects re-read the final change. In this run they found nothing blocking. In the previous run (run 10) the same step found a real bug that all 12 QA scenarios had missed (summary counts computed before findings truncation).

Threats to validity

I’d rather list them myself than have you find them:

  • One run, one target, one model. The same request produced different plans in runs 9, 10 and 12 (Balthasar’s, Casper’s and Melchior’s). The result is an existence proof, not a success rate.
  • Self-consistency risk. All nine agents used the same model, so common-mode errors are possible. Tool-based verification and container checks reduce this risk, they do not remove it.
  • Verifier coverage. 42 claims were refuted. Most were real model errors, but at least one class is a verifier limitation: a claim that a path is protected, expressed as policy_exists on the path, is refuted although true. It contributed to one proposal’s inadmissibility.
  • Lint scope. The configured lint gate checks only syntax-level errors (E9,F63,F7,F82), because the target does not pass full ruff at baseline. Type checking is not configured in the target and was not run.
  • Reviewers saw a truncated diff. The specialists and the final reviewers receive the last 60,000 characters of the diff (diff[-60000:] in engine/nodes_verify.py). This change is about 99,000 characters, so the router, models, base, registry, settings and main.py were cut off, and several reviewers said so in their findings (“Provided diff is truncated”). Their conclusions on those files rest on tests and claims, not on reading the code. This is a platform limitation, and it matters for large changes.
  • False refutations still exist. Besides the protected-paths claim, a correct final-review finding (nginx: with an empty tag is not flagged) was refuted by verification. A refuted finding drops out of the decision. When the refutation is wrong, a real defect is silently accepted.
  • Coarse acceptance scenarios. Eight of the eleven executed scenarios share the command pytest -k toolbox, so they do not discriminate between requirements.
  • Operator judgment. The test change by DEV2 was checked by a human observer (me) during the run. The platform itself would have accepted it on the evidence alone.

Cost of robustness

Six model outputs were rejected because they hit the 32,000-token cap. The excerpts of earlier runs show these are repetition loops at temperature 0 (for example “No X. No Y. No Z. …” for about 150,000 characters). The cap plus a retry with feedback recovered every case, at the price of about four minutes of the final review and a share of the output tokens.

Second run: the same request on Amazon Bedrock AgentCore

A single run is an existence proof, so I did the obvious thing: same request, same kind of target (a public FastAPI repository, fastapi-agentcore), but the whole pipeline moved from my laptop to Amazon Bedrock AgentCore. Same three-tool Dev/SRE Toolbox, same ten gates, same model family through OpenRouter. What changes is where everything runs.

It ended SUCCESS in 25 min 44 s and US$ 1.35 of model cost, plus about US$ 0.12 of infrastructure. Final commit 067510f, 21 files (+2,329 / -49), 54 new passing tests (133 passed, 25 skipped, against 79 before), ten of ten gates, no push, no merge, no deployment.

What AgentCore changes

One run is one AgentCore Runtime session. The graph is the same, the surroundings are not:

ConcernLocal runAgentCore run
Orchestratorlocal processone Runtime microVM per run (2 vCPU, 8 GB), kept alive with /ping = HealthyBusy
Workspace and artifactslocal diskAmazon EFS, /magi/<run_id>/...
Run state and checkpointsSQLiteDynamoDB, plus S3 for large checkpoints
Target commandsDocker, --network=noneAgentCore Code Interpreter in VPC mode, no internet, per-run EFS access point
Toolchain in the sandboximage with the dependencieswheels installed by the Runtime (which has NAT) into .git/magi-toolchain
Code deploymentuv runimage with the locked dependencies, code bundle in S3 loaded at session start
ObservabilityfilesCloudWatch Logs, 15 metric filters, a dashboard filtered by run id

The sandbox is the part I care about. It has no internet, so the Runtime installs the toolchain for it, and the platform’s rule stays the same: a gate is PASS only if a command was executed and succeeded.

The deployment in the AWS console

Screenshots taken after the run, account id blacked out, region eu-north-1.

Network. One VPC, six subnets over two availability zones (public, isolated and private in each), an internet gateway, a NAT gateway and the S3 and DynamoDB gateway endpoints. The Runtime and the sandbox sit in the private subnets, and the NAT gateway is the only way out.

/images/magi-a-risk-reduction-system-inside-an-agentic-loop/agentcore-vpc-resource-map.webp

Runtime. Version 8 of the magi_runtime is ready, with the DEFAULT endpoint, started from the image magi-agentcore:base-1, an IAM service role of its own and IAM inbound authentication.

/images/magi-a-risk-reduction-system-inside-an-agentic-loop/agentcore-runtime-version.webp

Image and code are separate. The base image (178 MB: Python, git, uv and the locked dependencies, no magi-system code) is pushed once. The code lives in the S3 code/ prefix, a zip and a small JSON pointer per version, so shipping a fix does not mean rebuilding the image. This run used bundle 1ff47e1, the first one with the preflight.

/images/magi-a-risk-reduction-system-inside-an-agentic-loop/agentcore-ecr-image.webp

/images/magi-a-risk-reduction-system-inside-an-agentic-loop/agentcore-s3-code-bundles.webp

Observability. The magi-runs dashboard, filtered on this run: model cost against infrastructure cost, tokens, calls and latency, gate checks by verdict, tables per run and per agent. The numbers agree with the platform’s own report: 116 calls, 5,474,396 input and 663,977 output tokens, US$ 1.3517 (computed and provider-reported are identical), 1,636 active seconds.

/images/magi-a-risk-reduction-system-inside-an-agentic-loop/agentcore-cloudwatch-dashboard.webp

The “Run outcomes” panel shows no data in my capture: the end event fell outside the selected time window. I kept the screenshot as it is.

The run: a second design round

The first run selected a plan in round 0. This one did not. None of the three round-0 proposals was admissible, the board went into a revision round, and the plan was chosen in round 1. That cost about 4 more minutes and US$ 0.19 of architect calls, on top of US$ 0.42 for round 0. The architects alone are US$ 0.60 of the 1.35.

Why they were rejected is the interesting part. Verification of the 491 claims of the run: 359 supported, 77 refuted, 55 unverifiable. In round 0:

ProposalWhy inadmissible
Melchior22 critical claims refuted, 2 critical unknowns unresolved (PyYAML not installed, the 400 handler)
Balthasar1 mandatory requirement not covered, 1 critical claim refuted (the protected .github/workflows/**)
Casper8 critical claims refuted, 2 critical claims unresolved (no network needed, no payload logged)

Two different causes hide here. The first is real: an uncovered requirement, unresolved unknowns. The second is a defect of the platform, not of the agents. Nearly all the refuted critical claims of Melchior and Casper have the form “the router module exists after the change”, or “the documentation page exists”. The architects wrote claims in the future tense, the verifier checks the baseline, and on the baseline those files do not exist yet. In round 0, file_exists had 6 supported and 20 refuted, text_present 29 and 11. In round 1 the same kinds were all supported, because the architects restated them in a form the baseline can confirm.

The platform did the safe thing, and the policy allowed one revision round, so the run recovered. But a claim about a file the plan creates should be a plan item, not a baseline fact. That is on my list.

Can I blame something I changed between the runs? I looked. The MAGI phase code, the prompts, the selection logic and the verification tools are unchanged, and the preflight, the S3 bundle and the toolchain all act before or after the board. The likeliest explanation is model variance on an existing weakness. Not proven: one run per case, no controlled comparison.

The selected plan (Balthasar’s, round 1, rank score 4.68 against 4.41) mounts the router at the flat /api/toolbox prefix behind verify_credentials, parses YAML through a custom SafeLoader with a depth budget and an alias-expansion budget enforced during composition, dispatches CPU-bound work with run_in_executor wrapped in asyncio.wait_for, and answers over-limit input with 413 naming the limit.

Casper, as usual, asked something the others did not: the original Italian text was not visible to it, only an English translation.

What it took to get a SUCCESS

This was the tenth run on AgentCore and the first SUCCESS. The earlier nine failed for platform reasons, not model ones:

DefectFix
The sandbox had no toolchain (no pytest, no git)the Runtime installs aarch64 wheels into .git/magi-toolchain, which survives git clean -fdx and is not scanned by flake8 .
Heredocs and tools waited on stdincommands run with stdin closed
QA scenarios that searched source text or guessed -k selectors gave false negativestext scans and empty selections are advisory, QA can correct a failed scenario once
Claims written as app/auth.py instead of app/auth.py::verify_credentials were refutedrejected at the schema, so the agent retries
Warm containers kept serving old code after a new bundle was publishedthe Runtime reports commit@bundle, the launcher compares it with the published pointer and stops a stale start before it spends (4 of 5 starts in this run were stale)
The previous run (US$ 1.44) passed every executed gate and ended FAILED_VERIFICATION: the target had no build or lint commandpreflight at intake: a mandatory project gate that cannot run stops the run at cost zero, and the target got a .magi/config.yaml

The last row is the one I like. A pipeline that spends a dollar and a half to discover that the target has no lint command is not a risk-reduction system.

QA also corrected one of its own scenarios. acc-toolbox-routes-registered first looked for “toolbox” in create_app().routes, which fails because the app mounts sub-applications. QA rewrote it to check app.openapi()['paths'], and it passed. The verdict is still the result of an executed command, what changed is that a scenario testing an implementation detail was replaced by one testing behaviour.

What the gates did not see

Ten of ten gates passed, and the patch still has things I would not merge as they are:

  • The generated documentation has no code fences. docs/14-dev-sre-toolbox.md has five bare bash lines where a fence should open. The docs.updated gate checks that documentation changed, not how it renders. QA and a couple of architects noticed it as a minor finding, nobody was required to fix it.
  • A failing test was rewritten. dev.writer rewrote test_document_budget after it failed with DID NOT RAISE ToolLimitExceeded, and reported it itself as a major finding. Reviewers saw the final test and the guard is now exercised, but the platform has no rule that tells “the test was wrong” from “the test was weakened to pass”. In the first run the same situation ended well, with a human checking the diff.
  • Reviewers saw truncated diffs again. The pair, Security and all three final reviewers say the diff did not contain the router, main.py, models and settings.
  • Known minors shipped. text.diff advertises context_lines up to 50 while the effective default is 10, so a client following the schema gets a 413. max_findings is checked after all findings are materialised. The 504 path has no test and cannot cancel the worker thread. And the empty tag nginx: is treated as clean, the same defect as in the first run. No gate failed, so no remediation cycle ran.

Also, of the 64 mandatory explicit requirements, 29 are demonstrated by an executable scenario. The other 35 are process requirements (“read the repository instructions”, “report what could not be verified”), satisfied by what the agents did and reported, not by a command. The platform does not claim otherwise.

Independent check outside the platform

Same idea as before: do not trust the report, run the thing. The patch was applied to a fresh clone of the target, the 133 tests passed, the Docker image built, the container answered /api/mgmt/ready with 200, and the three tools answered over HTTP with Basic auth. The k8s tool reports 7 warnings on an nginx:latest Deployment as findings, not as an HTTP error, which was the requirement.

/images/magi-a-risk-reduction-system-inside-an-agentic-loop/agentcore-toolbox-k8s-curl.webp

This check exercised the tools with sample inputs, not the edge cases above.

Two runs side by side

Local runAgentCore run
OutcomeSUCCESSSUCCESS
Wall clock19 min 19 s25 min 44 s
Model costUS$ 1.05US$ 1.35 (+ about 0.12 infrastructure)
Design rounds12
Model calls105116
Claims checked290 (42 refuted)491 (77 refuted)
Change21 files, +2,340 / -5121 files, +2,329 / -49
Tests67 new, 146 passed54 new, 133 passed
Mandatory gates10 of 1010 of 10
Shared defectempty image tag not flaggedempty image tag not flagged

Two runs, one pass each, a different plan chosen each time. It is still not a success rate. What it does say is that the move to a managed runtime did not change the verdicts: the gates stay executed commands, the evidence stays bound to a commit, and the same blind spots show up in the same places.

Conclusion

On a real FastAPI service and a demanding 230-line change request, magi-system delivered a verified change autonomously: requirements traced to the request, a plan selected by checked facts, code written and corrected by two developer agents, eleven acceptance scenarios and three specialist reviews passed, documentation updated, all evidence re-run on the final commit, and a final review by three architects. About one US dollar, 19 minutes.

The change was not integrated. The patch and the local branch are artifacts for a human to review and merge, and that is by design. What I take from the run is not the dollar or the minutes. It is that the two real defects (the empty image tag and the truncated diff) were found by reading the evidence, not by the pipeline. The pipeline is good at not lying about what it checked. It is not yet good at knowing what it did not check.

The second run on AgentCore says the same thing from another angle. Different infrastructure, a different plan, a second design round, and the same blind spots: the empty image tag, the truncated diff, a test rewritten to pass.

Appendix: mandatory gates

All ten, on the final commit 5cce890:

GateVerdictDetail
project.buildPASSpython -m compileall -q app, exit 0
project.testPASSpython -m pytest -q -p no:cacheprovider, 146 passed, 25 skipped
project.lintPASSruff check --select E9,F63,F7,F82 app tests
qa.acceptancePASS11 scenarios, no confirmed blocker
security.reviewPASS8 findings, none blocking
sre.reviewPASS5 findings, none blocking
docs.updatedPASSREADME.md, docs/14-dev-sre-toolbox.md
magi.final_reviewPASS3 reviews, 7 findings, none blocking
integrity.protected_pathsPASSno protected change
scope.write_scopesPASSall changes inside authorized scopes

Machine-specific paths in the run artifacts were replaced by placeholders before writing this post.

If you want the engine’s design first, start from Autopsy of an Agentic Loop. The gh-aw workflows that started the whole story are in Git on AI Steroids.