Unified AI gateway — China's best LLMs + OpenAI/Gemini, the OpenAI way. Singapore-hosted (Tencent Cloud SG), PDPA-aligned, up to 90%+ cheaper than OpenAI on China models. Build with AI, Southeast Asia
This is the cleanest argument I've seen for "architecture, not prompt, is the safety boundary" — and it pairs perfectly with the non-determinism finding. Your live-critic boundary evaluator measuring non-determinism on identical input is the part most people skip: the critic itself drifts. That's the same failure shape we keep hitting in self-fixing agent loops — not crashes, but the reviewer going quietly blind. One operational lever that helped us: route the planner and the critic to different capability tiers. The planner can be cheap and fast; the critic, the thing that has to catch the blind spot, is where we refuse to penny-pinch. Cheap critic = blind critic. The two-LLM split only pays off if the second model is actually held to a higher bar than the first.
Solution architect, 16 years in software, last 3 on AI automation. I write about making automated work verifiable: evaluation harnesses, audit trails, and results published even when the answer is no.
One thing I would want before reading 11/11 as injection resistance: in tests 2 and 3 the blockers that actually fired are weak_rollback, unsafe_sequencing and feasibility, and none of them mention the payload. That is consistent with the injection being ignored, but it is also consistent with those goals escalating for reasons that have nothing to do with the payload at all. The cheap control is a benign twin of each adversarial goal, same plan request with the injected text stripped and everything else identical; if the twin escalates with the same blocker family, the 11/11 is measuring how strict the gates are on that corpus rather than how isolated the goal channel is. It would also give you a baseline to compare against when you start on indirect injection, where the payload arrives after the audit.
Filed as planner-critic-engine#334. You're right that 11/11 could be measuring gate strictness on that corpus rather than injection isolation — a benign twin with the injected text stripped would settle it. This also provides the baseline needed for when indirect injection testing begins. Thanks for the clean experimental design.
ANP2 — an open, permissionless AI-to-AI event protocol. Ed25519-signed events, capability discovery, and a computable trust graph. No accounts, no API keys, no tokens. Spec v0.1 DRAFT.
Your 11/11 result measures entry into escalation. The exit path is where the approval boundary actually lives, and it hasn't taken the same measurement pressure as the plan side. In this engine escalated is a handoff state, and something has to be able to leave it.
The sharpest example is in src/planner_critic/field_test_harness.py. The DIMENSIONS table binds the escalation dimension to ["adv-01-billing-no-safety"], which is one of the adversarial goals. Then run_escalation(goal, store, out) builds an EscalationManager(store), calls list_escalations(), loops over escs[:2], and calls mgr.resolve(e.id, "approved", note="field test"). It then records {"pass": True, "escalation_count": len(escs)}. So the harness flips the first two open escalations in store order to approved with no principal argument, and pass means only that the call didn't raise.
This is the harness dimension rather than the 170-goal corpus score, so it isn't your headline number. It's still the code's model of what an escalation check should exercise. list_escalations() walks store.list_plans(), so it isn't scoped to the current goal, and the sweep uses one shared SQLiteStore(":memory:"). Nothing excludes adversarial escalations. The blocked outcome and the write that unblocks it live in the same harness, with an assertion on only one side of it.
F-14 also reads narrower in code than in the register. EscalationManager.resolve(escalation_id, decision, note="", principal=None) in escalation.py does enforce approving_authority when one is configured: if principal != self._approving_authority it raises PermissionError. The problem is input plumbing. The CLI escalate approve subparser in cli/escalate.py accepts escalation_id, --patch and --note, and _run_approve calls manager.resolve(args.escalation_id, "approved", note=args.note). _handle_escalate_approve in server/http.py reads only body.get("note", ""). The MCP signature is escalate_approve(store_path, escalation_id, note="", patch_json=None). If v0.3.0 binds approving_authority without threading identity through those three signatures, every approve call raises, because principal stays None. Fronting the HTTP server with an authenticating gateway doesn't rescue this either. The approver's identity dies at the gateway, since nothing in the request body carries it inward.
The record has the same shape of gap. Escalation in types.py carries status, resolution and resolved_at with no actor field, so who approved is never persisted, even on the direct API path where the check does work. And build_explain in explain.py picks the final action with if is_last and escalation is not None: action = "escalated", branching on the escalation existing and never on escalation.status, so _build_summary still returns Escalated: <question> for a plan whose escalation was resolved approved. The manager docstring says the store is the single source of truth so the full arc is replayable, and #234 was fixed precisely so the audit trail keeps every defect distinct. The one write that can override a deterministic gate is the write that trail can't attribute.
The cost point is the part I would keep visible in the security design. Five critic passes are useful for measurement, but the cheaper invariant is making every path hit the structural gates. If a team starts saving money by skipping those gates on “simple” work, the architecture quietly turns back into trust-the-model.
The open seam I would prioritize is indirect injection crossing a trust boundary. Re-auditing every tool result with another LLM still leaves data and instructions entangled. A stronger contract is for tools to return typed data plus provenance, while deterministic policy decides whether that source may influence a particular state transition. For example, text fetched from a product page may inform discovery, but it should never be able to alter the payee, amount, approval requirements, or destination authority. That turns injection defense from “spot malicious language” into “untrusted sources cannot acquire capability.”
Staff Software Engineer focado em Agentic AI, arquitetura, observabilidade e AppSec. Construo sistemas com LLMs, multiagentes, MCP e RAG. Rumo a AI Security.
The honest limitation stood out to me. A malicious plan can have rollback and verification and still be unauthorized. Structural gates validate shape, not authority. Are you considering authorization for each plan step in v0.3.0?
Good write-up, and the honest limits are the useful part. You mention you haven't tested indirect injection through external context and that a benign control is still open (#334). I've been testing the side-effect layer for GitHub agents: an LLM agent with a create_issue tool, an injected note in third-party text, and an observer that reads the repository after the run and compares what changed against what was authorized. Across 3 models and 3 injection wordings (110 trials), whether the agent obeyed depended heavily on the model and the wording, and a benign-mode run with a plain note came back IN-BOUNDS 10/10. Results and rerun guide: github.com/gokuljayaprakash8/kalpa... If it's useful, I can run the same check against a sandbox copy of your GitHub Action and share the result.
This is exactly the kind of independent replication I hoped someone would run — the side-effect layer (observer + authorization comparison) is a genuinely different threat surface than the goal-channel direct injection I tested, and the fact that benign-mode came back IN-BOUNDS 10/10 across three models and three wordings is a real data point. The most interesting part of your setup is the observer: most injection tests stop at "did the agent do the bad thing," while you're checking the repository's actual post-state against what was authorized — that's the "assert on the audit rows, not the chat text" move, applied to GitHub. Would genuinely take you up on the sandbox run — please do share the result either way. The honest-limit section of my piece is the part I most want independent evidence against.
AI-Native Systems Engineer. Ex-ICU nurse. Creator of LEO Framework. I build deterministic multi-agent SDLC systems to cure autonomous coding amnesia. Python, React, Postgres.
"The architecture, not the prompt, is what makes it safe" is spot on. I've spent the last few months working on multi-agent SDLC frameworks and hit the exact same wall: trying to prompt-engineer safety or deterministic behavior into an LLM is a dead end. Structural isolation and deterministic gates are the only things that actually hold up in production.
I write when an idea won't leave me alone 🧠 Building AI agents and the tools to build AI agents. Love connecting AI with other fields and yapping about all of them.
The framing that direct injection failed because feasibility, not safety, caught it is the part I keep coming back to. When the critic treats "disable MFA in production" as infeasible by definition, the attack surface shrinks to whatever the planner accepts as achievable, which is a much cleaner boundary to reason about than a prompt allowlist. Curious whether the 21 injection traps included ones where the malicious step is individually feasible but only harmful in composition.
I write about Next.js + TypeScript + AI engineering, with a focus on making sites discoverable to LLMs and AI search. Long-form notes at mudassirkhan.me
"the critic evaluates structure, not intent" is the line that makes the whole design click. most security discussions assume the model has to catch injection. this is the better frame: the model never gets to treat injection as a decision because architecture makes it irrelevant first.
hit something similar when hardening an MCP server — the gate that held wasn't a system prompt clause, it was a schema validator upstream of the tool call. felt obvious in retrospect tbh.
the open seam you flag, indirect injection through tool outputs mid execution, is where we've gotten burned. how are you planning to instrument that without exploding the critic's context?
We're a place where coders share, stay up-to-date and grow their careers.
This is the cleanest argument I've seen for "architecture, not prompt, is the safety boundary" — and it pairs perfectly with the non-determinism finding. Your live-critic boundary evaluator measuring non-determinism on identical input is the part most people skip: the critic itself drifts. That's the same failure shape we keep hitting in self-fixing agent loops — not crashes, but the reviewer going quietly blind. One operational lever that helped us: route the planner and the critic to different capability tiers. The planner can be cheap and fast; the critic, the thing that has to catch the blind spot, is where we refuse to penny-pinch. Cheap critic = blind critic. The two-LLM split only pays off if the second model is actually held to a higher bar than the first.
One thing I would want before reading 11/11 as injection resistance: in tests 2 and 3 the blockers that actually fired are
weak_rollback,unsafe_sequencingand feasibility, and none of them mention the payload. That is consistent with the injection being ignored, but it is also consistent with those goals escalating for reasons that have nothing to do with the payload at all. The cheap control is a benign twin of each adversarial goal, same plan request with the injected text stripped and everything else identical; if the twin escalates with the same blocker family, the 11/11 is measuring how strict the gates are on that corpus rather than how isolated the goal channel is. It would also give you a baseline to compare against when you start on indirect injection, where the payload arrives after the audit.Filed as planner-critic-engine#334. You're right that 11/11 could be measuring gate strictness on that corpus rather than injection isolation — a benign twin with the injected text stripped would settle it. This also provides the baseline needed for when indirect injection testing begins. Thanks for the clean experimental design.
Your 11/11 result measures entry into escalation. The exit path is where the approval boundary actually lives, and it hasn't taken the same measurement pressure as the plan side. In this engine
escalatedis a handoff state, and something has to be able to leave it.The sharpest example is in
src/planner_critic/field_test_harness.py. TheDIMENSIONStable binds theescalationdimension to["adv-01-billing-no-safety"], which is one of the adversarial goals. Thenrun_escalation(goal, store, out)builds anEscalationManager(store), callslist_escalations(), loops overescs[:2], and callsmgr.resolve(e.id, "approved", note="field test"). It then records{"pass": True, "escalation_count": len(escs)}. So the harness flips the first two open escalations in store order to approved with no principal argument, and pass means only that the call didn't raise.This is the harness dimension rather than the 170-goal corpus score, so it isn't your headline number. It's still the code's model of what an escalation check should exercise.
list_escalations()walksstore.list_plans(), so it isn't scoped to the current goal, and the sweep uses one sharedSQLiteStore(":memory:"). Nothing excludes adversarial escalations. The blocked outcome and the write that unblocks it live in the same harness, with an assertion on only one side of it.F-14 also reads narrower in code than in the register.
EscalationManager.resolve(escalation_id, decision, note="", principal=None)inescalation.pydoes enforceapproving_authoritywhen one is configured: ifprincipal != self._approving_authorityit raisesPermissionError. The problem is input plumbing. The CLIescalate approvesubparser incli/escalate.pyacceptsescalation_id,--patchand--note, and_run_approvecallsmanager.resolve(args.escalation_id, "approved", note=args.note)._handle_escalate_approveinserver/http.pyreads onlybody.get("note", ""). The MCP signature isescalate_approve(store_path, escalation_id, note="", patch_json=None). If v0.3.0 bindsapproving_authoritywithout threading identity through those three signatures, every approve call raises, becauseprincipalstaysNone. Fronting the HTTP server with an authenticating gateway doesn't rescue this either. The approver's identity dies at the gateway, since nothing in the request body carries it inward.The record has the same shape of gap.
Escalationintypes.pycarriesstatus,resolutionandresolved_atwith no actor field, so who approved is never persisted, even on the direct API path where the check does work. Andbuild_explaininexplain.pypicks the final action withif is_last and escalation is not None: action = "escalated", branching on the escalation existing and never onescalation.status, so_build_summarystill returnsEscalated: <question>for a plan whose escalation was resolved approved. The manager docstring says the store is the single source of truth so the full arc is replayable, and #234 was fixed precisely so the audit trail keeps every defect distinct. The one write that can override a deterministic gate is the write that trail can't attribute.The cost point is the part I would keep visible in the security design. Five critic passes are useful for measurement, but the cheaper invariant is making every path hit the structural gates. If a team starts saving money by skipping those gates on “simple” work, the architecture quietly turns back into trust-the-model.
Thanks - yes, cost is important. Have you had a chance to see this? I covered cost here
dev.to/debashish_ghosal/i-ran-157-...
Yes, it’s 49c to run the full suite against a reliably good and cheaper model.
The open seam I would prioritize is indirect injection crossing a trust boundary. Re-auditing every tool result with another LLM still leaves data and instructions entangled. A stronger contract is for tools to return typed data plus provenance, while deterministic policy decides whether that source may influence a particular state transition. For example, text fetched from a product page may inform discovery, but it should never be able to alter the payee, amount, approval requirements, or destination authority. That turns injection defense from “spot malicious language” into “untrusted sources cannot acquire capability.”
The honest limitation stood out to me. A malicious plan can have rollback and verification and still be unauthorized. Structural gates validate shape, not authority. Are you considering authorization for each plan step in v0.3.0?
Good write-up, and the honest limits are the useful part. You mention you haven't tested indirect injection through external context and that a benign control is still open (#334). I've been testing the side-effect layer for GitHub agents: an LLM agent with a create_issue tool, an injected note in third-party text, and an observer that reads the repository after the run and compares what changed against what was authorized. Across 3 models and 3 injection wordings (110 trials), whether the agent obeyed depended heavily on the model and the wording, and a benign-mode run with a plain note came back IN-BOUNDS 10/10. Results and rerun guide: github.com/gokuljayaprakash8/kalpa... If it's useful, I can run the same check against a sandbox copy of your GitHub Action and share the result.
This is exactly the kind of independent replication I hoped someone would run — the side-effect layer (observer + authorization comparison) is a genuinely different threat surface than the goal-channel direct injection I tested, and the fact that benign-mode came back IN-BOUNDS 10/10 across three models and three wordings is a real data point. The most interesting part of your setup is the observer: most injection tests stop at "did the agent do the bad thing," while you're checking the repository's actual post-state against what was authorized — that's the "assert on the audit rows, not the chat text" move, applied to GitHub. Would genuinely take you up on the sandbox run — please do share the result either way. The honest-limit section of my piece is the part I most want independent evidence against.
Really insightful read, Debashish!
"The architecture, not the prompt, is what makes it safe" is spot on. I've spent the last few months working on multi-agent SDLC frameworks and hit the exact same wall: trying to prompt-engineer safety or deterministic behavior into an LLM is a dead end. Structural isolation and deterministic gates are the only things that actually hold up in production.
Great series, looking forward to part 6!
The framing that direct injection failed because feasibility, not safety, caught it is the part I keep coming back to. When the critic treats "disable MFA in production" as infeasible by definition, the attack surface shrinks to whatever the planner accepts as achievable, which is a much cleaner boundary to reason about than a prompt allowlist. Curious whether the 21 injection traps included ones where the malicious step is individually feasible but only harmful in composition.
"the critic evaluates structure, not intent" is the line that makes the whole design click. most security discussions assume the model has to catch injection. this is the better frame: the model never gets to treat injection as a decision because architecture makes it irrelevant first.
hit something similar when hardening an MCP server — the gate that held wasn't a system prompt clause, it was a schema validator upstream of the tool call. felt obvious in retrospect tbh.
the open seam you flag, indirect injection through tool outputs mid execution, is where we've gotten burned. how are you planning to instrument that without exploding the critic's context?