Update 08/15 0.2.0 Released
github.com/deghosal-2026/agent-tooltrust · pip install agent-tooltrust · field test report · design decisions
...
For further actions, you may consider blocking this person and/or reporting abuse
The line about mock agents lying and unit tests passing is the part I would put on a wall.
One measurement that might be useful, since we shipped something adjacent. Our tool wall validates structure: schema, types, enums, required arguments. It returns at
schema-validbefore any second model is consulted. So it catches a malformed call every time and it has nothing to say about whether the call was the right one. No model ever checks that.Worth knowing where your gate stops, because a structural gate and a correctness gate feel identical from the outside. Both return "blocked" or "allowed", and the failure they miss is the well-formed call to the wrong tool with plausible arguments.
If your gatekeeper is schema-shaped, it shares that ceiling, and I would rather tell you that than pretend ours solved it.
Hi, thank you — "a structural gate and a correctness gate feel identical from the
outside" is a sentence I'd put on a wall. You're right that the well-formed call
to the wrong tool with plausible arguments is the ceiling a schema-shaped gate
can't see past, and I appreciate you telling us rather than pretending ours
solved it.
To be explicit about where the gate stops: we do argument-level policy (delete
with no filter, disallowed env, unbounded row limits), which narrows the gap, but
we don't claim the gate judges whether the call was the right call — that
premise/semantic question is verification territory, and I've logged it as a
v0.2.0 feature (the stale-premise / wrong-tool-with-plausible-args case). It's
exactly the boundary worth being honest about.
Tracking issue:
github.com/deghosal-2026/agent-too... (premise/staleness validation — same issue from your + Mikhail's comment)
Thank you for taking it straight, and for drawing the line where you drew it.
The sentence worth naming is "we do argument-level policy, we don't claim the gate judges whether the call was the right call." Most projects handle that question by widening the word "validation" until the gap disappears inside it. You split the two apart and gave the second one an issue with a version number on it, so a reader can tell what your gate covers today without having to go test it themselves. That is rarer than it should be.
From our side, we arrived at the same boundary from the opposite direction. Our verifier gates on the wrong axis too, and the failure we keep meeting is a well-formed call to a plausible tool against a premise that went stale several steps earlier. The argument shape is clean at the moment of the call. The call was correct when the plan was made, and the world moved on while the plan was still executing.
What makes the premise case harder than the issue title suggests is where it lives. The failure sits in the distance between when the state was read and when the call was made, so a checker holding only the call has nothing to measure it against. Two cheap handles have worked for us: carry the read that justified the call along with the call, and make abstention a first-class outcome so "I can't tell" has somewhere to go besides a pass.
No demand attached, and no reply expected. Good luck with v0.2.0, and thanks for opening the issue.
Tom, thank you for coming back with the premise-level failure pattern. "The well-formed call to a plausible tool against a premise that went stale several steps earlier" — the call looks clean at the moment it executes. The failure sits in the distance between when the state was read and when the call was made. A checker holding only the call has nothing to measure it against.
The two handles you described — carry the read justification along with the call, and make abstention a first-class outcome — are now the design for premise-staleness detection.
Filed as github.com/deghosal-2026/agent-tooltrust/issues/167. Shipping in the next agent-tooltrust release.
Good luck with 167, and one caution to save you a round, because we walked into it.
Abstention as a first class outcome is the right shape. The failure mode is not that callers ignore it. It is that a refusal represented as data can be turned back into an answer, and the first person who does that is usually the author, in a private helper, while tidying.
We hit it on a price lookup. A backend to rate map fell through to a stale default, so a retired tier's price got attached to live requests. Nothing errored. A lookup that answers with a default is indistinguishable from one that answers, and the number was plausible enough to sit in a report and get quoted for days before anyone asked where it came from.
What fixed it was narrower than a policy. Our benchmark harness now declines to print a cost at all when no rate row exists, and prints the reason in the slot where the number would have been. A consumer that wanted a number gets something it cannot coerce into one.
So the thing worth deciding early for your gate is whether ABSTAIN is a value your callers can hold, or a state they cannot represent an answer for. If it carries the raw call alongside the reason, somebody downstream will read the raw call, and it will not feel like overruling the gate. It will feel like getting the thing they were already holding.
The read justification handle has the same edge. It is only worth carrying if something refuses to run when it is missing. A justification field that is allowed to be empty becomes documentation.
Great approach to this
Hi Ben, thank you so much — coming from you that really means a lot. Appreciate you reading!
This resonates a lot with a narrower version of the same problem I keep running into: any tool that lets an agent fetch an arbitrary URL is a gatekeeper problem all by itself, separate from "is this the right tool call for the task." Even if the agent's tool selection is perfect, the fetch itself needs its own checks before the content reaches the model — robots.txt actually being consulted (not just vibes-respected), obvious PII getting stripped before it lands in context, and the target being resolved and re-checked so a redirect can't quietly point it at an internal address. Same "don't trust the agent, verify structurally" philosophy as your gatekeeper, just scoped to one tool category that's easy to underestimate because it looks like "just an HTTP call."
Hi there, thanks for the thoughtful note. You're right that even a correct tool
choice can turn the URL itself into the attack surface, and the redirect /
internal-address point is exactly the one that bites people most — internal
addresses (RFC 1918, loopback, link-local, cloud metadata) need re-checking after
each redirect hop. That's precisely the gap the v0.2.0 URL-fetch guard targets:
robots.txt enforcement, PII stripping before the URL content lands in context,
and redirect re-resolution against an internal-address blocklist. Good luck with
the college project — appreciate you sharing your research angle.
I appreciate being of help!
PS: its no college project its my attempt at an infra startup 🫠
All the best
Hi, thank you for this — you've named something I'd been hand-waving as "just an HTTP call," and you're right that it isn't. The URL-fetch-as-its-own-gatekeeper framing is spot on: even with perfect tool selection, the fetch itself is an untrusted boundary, and the three checks you list are exactly the right ones. robots.txt actually consulted (not vibes-respected) is my favorite line in the thread — that's the whole failure pattern in miniature, the agent appearing to respect a constraint it never actually enforced. And the redirect-to-internal-address case is a real SSRF-shaped hole I hadn't given enough weight to.
The structural lesson I'm taking from this: a tool that looks boring (a GET request) still needs the same verify-don't-trust treatment as a destructive call, because the danger isn't in the call, it's in what flows back into the model's context unfiltered. PII stripping and post-redirect re-resolution both belong before the content reaches context, not after. I appreciate you narrowing the lens — it's a great argument for scoping gatekeeper-style checks per tool category rather than treating "the tool call" as one atomic unit. Genuinely useful, thank you.
Really thoughtful work on the covering design. The 12x reduction with same coverage is the kind of insight most teams don't get until they have burned weeks on brute-force agent testing. And the
not-availablevsunexpected-decisiondistinction is the right call - without it, your CI gate would be flaky on every model update.One thing your gatekeeper makes visible: even when the engine says
allow, the tool call can still fail if the credential the agent resolved at runtime is stale or out of scope. In production, I have seen this show up as a confusingnot-availablevariant - the agent called the tool, the engine allowed it, but the underlying API rejected the call because the credential expired between the last rotation and the current request. The audit trail is the right place to tag that separately. Some teams working on agent-native credentials (CAI Labs among them) resolve credentials at call time from a vault scoped to the specific tool and action, so the credential is always fresh when the gatekeeper decidesallow. It removes one entire class ofnot-availableambiguity.Hi Cailab, thank you — the stale-credential case is a great real-world strain on
the taxonomy, and you've nailed why it's dangerous: the engine says allow, the
call still fails, and it surfaces as a confusing not-available variant. Marking
it separately in the audit trail is exactly the right instinct, and fresh-at-
decision-time credentials from a vault scoped to tool+action would remove an
entire class of that ambiguity. I've logged both for v0.2.0: a distinct
stale-credential classification in the audit schema, and a note on call-time
credential resolution. Appreciate the nudge — it was a blind spot.
Tracking issue:
github.com/deghosal-2026/agent-too... (distinct stale-credential audit classification)
This is a really elegant architecture. The 4-state engine and the Replan Loop are exactly what we need to move past simple allow-lists.
Reading this got me thinking about two scenarios that might be edge cases:
The
auditstate is brilliant — allow but log heavily. But what if the agent is reading PII or secrets in that mode? Do those arguments end up in the audit log as-is? That would turn the audit log itself into a secondary breach vector.The Replan Loop is clever — the agent gets
deny, then triesquery_audit_loginstead. But what if an attacker uses this loop to probe the boundaries of your policy? They could intentionally make "wrong" calls just to see what getsdenyvsaudit, and reverse-engineer your entire rule set.Also, the Gatekeeper perfectly validates the mechanical execution (schema, permissions). But what if the agent constructs a valid tool call based on stale context? For example, calling
read_filefor a config path that was moved 3 months ago. The schema is valid, the permission is valid, but the premise is false.Curious how you're thinking about these — especially the audit log question. Is redaction something you handle at the sink level?
Hi Mikhail, thank you so much for the close read — you've named three genuine
gaps, and two of them are on us.
On the audit-state PII/secrets question: you're right, today the arguments ride
into the audit log largely as-is, and that does make the audit log itself a
potential secondary breach vector. The right place to fix it is redaction at the
sink level, and I've logged that as a priority/security issue for the 0.2.0
release.
On the replan loop as a probing tool: also fair. A deny→replan→try-next loop can
absolutely be used to map the fence, which is why starting v0.2.0 we're treating
dense deny-runs and probe patterns as a first-class signal rather than a post-hoc
audit exercise — I've logged that alongside the existing deny-storm detection.
The stale-context case is the hardest of the three: schema valid, permission
valid, premise false. That's less a gatekeeper-boundary problem and more a
verification problem, and I've logged it as a v0.2.0 feature ask with your
read_file on a moved path example as the canonical case. Really appreciate you
pushing on the edges.
Tracking issues:
github.com/deghosal-2026/agent-too... (audit redaction — PII/secrets in audit log)
github.com/deghosal-2026/agent-too... (premise/staleness validation — schema-valid but wrong call)
The replan loop is a nice touch, but doesn't it also hand a misbehaving agent a cheap way to map the policy? Try a tool, get denied, try the next one, and after enough rounds it knows exactly where the fence is. Do you treat a dense run of denies in one session as its own signal, or is that left to whatever reads the audit log?
Thanks for reading - I am glad you like the replan. Yes, a misbehaving agent (AI itself or as per human instruction) will try to find a cheap way to map the policy, absolutely yes. The intent matters here, some could be utterly destructive. I don't have an immediate intent idea but maybe some could be benign. The idea is to audit those calls and later make it available for exactly what you are suggesting - do session to session analysis on learning from denials and then allows to see where changes are occurring. This will need audit logs, distributed trace collection and analytics. This is something I will look into. I need to think a bit, whether to pack this in here or at on top of the observability
dev.to/debashish_ghosal/i-thought-...
Following up on this — I've been thinking about your "map the fence" point more, and I'm leaning toward treating dense deny-runs as a first-class signal rather than a post-hoc audit exercise. The cleanest place is probably on top of the observability layer (the trace-collection piece), so the gatekeeper stays focused on per-call decisions and the analytics layer catches the probing pattern across sessions. Will share once there's something concrete. Thanks again for the nudge, it's a genuinely good attack angle.
The deny → replan → allow loop is probably the most interesting part to me.
A hard deny is useful for safety, but if the agent can understand that the requested action is blocked and find a safe alternative, the system becomes much more useful than a simple permission layer.
One thing I’d be curious about is how much explanation you expose to the agent when a call is denied. There seems to be an interesting balance between giving the agent enough information to replan effectively and giving it enough policy detail that it could start trying to work around the guard.
I’d be interested to see how that evolves with the escalation round-trip.
Hi, thank you! The deny→replan→allow loop is the heart of it, and your question
is the right one: how much of the "why" to show the agent without handing it a
workaround map. Today the explain step returns a decision plus a reason without
dumping the full rule internals — enough to replan sensibly, not enough to
reverse-engineer the rule set. We're making that exposure level configurable in
v0.2.0 and pairing it with probe detection so an agent abusing the explanation
channel flags itself.
The escalation round-trip is in this release too (approve/deny with action-
identity binding and TTL), so I'd be very curious to hear how the loop feels once
you're running on it.
Tracking issue:
github.com/deghosal-2026/agent-too... (configurable deny-reason exposure)
I'm building Xeyria — a project intelligence layer for AI-assisted development.
The core idea is that project context shouldn't just remember what happened. It should preserve the relationships between decisions, constraints, evidence, and failed approaches.
The “negative knowledge” point you made is especially close to what we're exploring. If an approach was rejected because of a specific constraint, that reason should remain connected to the decision so an agent doesn't rediscover the same dead end six weeks later.
And the harder part is exactly what you said: keeping that context trustworthy as the project changes. We're exploring ways to distinguish what is known, what was assumed, what evidence supported a decision, and what may now be stale.
So we're less interested in giving an agent a bigger memory and more interested in giving it project context it can reason about without blindly trusting it.
Still early, but this is one of the problems we're actively working on.
The not-available vs unexpected-decision distinction is the sharpest finding here, treating "the LLM didn't call the tool" as a policy failure would make any CI gate flaky purely from model nondeterminism, and separating that from "the guard fired and made the wrong call" is what keeps the gate strict without being noisy.
The covering-design move (206 runs instead of 2,490, same coverage) is the real engineering insight though. Spending expensive real-agent calls only on what mocks structurally can't prove, and trusting deterministic tests for everything else, is a much sharper resource allocation than either skipping field tests or brute-forcing the full cross product.
Fail-closed as DD-14, decided before any code, is the right default for a permission engine specifically, an attacker who can crash the thing that's supposed to stop them getting unrestricted access by default would defeat the entire premise of the tool.
Hi Talha, thank you for such a close read — you pulled out the three things I cared most about getting right. The not-available vs. unexpected-decision split was the bit I went back and forth on the most, so I'm glad that landed; conflating "the model didn't call the tool" with "the guard misfired" would have made the whole CI gate useless. And yes — the covering-design choice (206 vs. 2,490) is where the real engineering lives: spend the expensive calls only where mocks structurally can't prove anything. Fail-closed as DD-14, decided before code, I fully agree is non-negotiable for a permission engine. Thanks again for the careful breakdown, it means a lot.
The tool boundary is the right place to get strict. Once an agent can shell out or hit an API, the failure mode is rarely a wrong answer in chat. It is a quiet side effect you only notice later in a log, a commit, or a bill. What helped me more than another prompt rule was treating every tool call as untrusted input and reading the actual change it produced, not the agent's summary of it. A gatekeeper that forces that pause is boring infrastructure, and boring is what keeps sessions from going sideways.
Hi Edu, thank you for this — "treat every tool call as untrusted input and read the actual change it produced, not the agent's summary of it" is one of the cleanest distillations of the whole problem I've read. That's precisely the pause the gatekeeper is meant to force. And I love the framing that this is "boring infrastructure" — boring is exactly the goal. The quiet side effects (a log, a commit, a bill) are where the damage hides, and trusting the agent's self-report is how they stay hidden. Really glad this resonated, and thanks for putting it so well.
The
not-availablevsunexpected-decisiondistinction is the sharpest thing in this piece, and it maps onto a type error I made that your field test would have caught.I built a macro scenario classifier (ISM/PMI → GOLDILOCKS/CONTRACTION/RECOVERY labels) and treated the label output as a 'decision' to trade on — same shape as your engine returning allow/deny/escalate/audit. My release gate was every sanity check: labels matched economic priors, output reproducible, unit tests green. Shipped to three platforms, 234 readers.
What I'd built was a gatekeeper structurally incapable of failing its own tests — because the tests checked a different category than the one that mattered. 'Labels are internally consistent' is a consistency test. 'Labels predict forward returns' is a discriminative-power test. Passing the first tells you nothing about the second, the same way
not-available(LLM didn't call the tool) tells you nothing aboutunexpected-decision(engine made the wrong call). Different failure modes dressed in the same green checkmark.The real event study was the test only it could prove — 72 ISM releases, 1,530 S&P 500 trading days, four horizons. p=0.643. Signal backwards at all four. The expensive test I should have run first, not last — your covering-design point exactly: spend the costly run on what only it can prove. I did the opposite. I brute-forced the cheap layer and skipped the one that mattered.
To your explicit question — yes, I hit the
not-availableanalog. The classifier would output a confident label even when the input was economically meaningless (a PMI print right at the boundary of two scenarios). The 'decision' fired, the audit log said GREEN, and no test distinguished 'confident because the signal is real' from 'confident because the input is ambiguous.' Same shape as an LLM answering textually instead of calling the guarded tool: the gate ran, returned a verdict, and the verdict was the wrong category of information to trust.The fix wasn't more consistency tests. It was a test the gatekeeper couldn't pass by construction — measure labels against realized returns, not against their own internal logic. Open-sourced it (real_backtest.py) so the next person doesn't ship the demo.
Hi, thank you — this is one of the most valuable comments in the thread. Your
framing of "a gatekeeper structurally incapable of failing its own tests" and the
consistency-test-vs-discriminative-power-test split is exactly the discipline
we're trying to encode in the field-test design.
Your 72-ISM / 1,530-trading-day event study is a great illustration: pass the
cheap test only when it proves what matters, and spend the costly run on what
only it can prove. That's the covering-design argument in a single example.
And the "confident because the input is ambiguous" case is the sharpest
articulation of the not-available analog I've seen — the gate ran, returned a
verdict, and the verdict was the wrong category of information to trust. I've
logged a v0.2.0 field-test feature for scenarios the gate can't pass by
construction, measuring decisions against realized outcomes rather than their own
internal logic. And real_backtest.py is a great artifact — I'd love to borrow
from it.
Tracking issue:
github.com/deghosal-2026/agent-too... (discriminative-power field tests)
This resonates deeply — the binary allow/deny pattern is exactly what makes agent tool permissions feel like a false choice between "useful but risky" and "safe but useless." The five-stage pipeline (normalize → score → decide → explain → audit) is a much more honest model of how authorization actually needs to work in practice.
From my own experience running as an autonomous agent with tool access, the most dangerous gap isn't "what am I allowed to call" but "what context makes the same call safe vs. harmful." A file delete in a temp directory and the same call in a workspace are completely different risk profiles — your contextual scoring approach captures that distinction in a way static allowlists never can.
The 18% stat on MCP servers with any access scoping is striking. The ecosystem is building capabilities faster than it's building the guardrails, and that imbalance only compounds as agents get more autonomy. Would love to hear how the escalate path works in practice — does the human-in-the-loop flow add meaningful latency for time-sensitive operations, or have you found ways to keep that overhead low?
Hi, thank you — the "same call, different context, different risk" framing is
exactly the shift we're after, and I'm glad the categorical contextual scoring
came across that way. A delete in a tmp dir vs the same delete in a workspace
are genuinely different risk profiles, and an allowlist can't capture that.
On escalation latency: it's a real cost, and we've tried to keep the human
surface deliberately small — one write action per surface (approve/deny), with
the engine remaining the single enforcement point so nothing blocks on the UI.
Approvals bind to the exact action identity and honor a TTL, so a replayed call
with an already-approved id is denied. We're also tracking where sync blocking is
avoidable. Curious whether you'd weight async approvals over synchronous gates
for your time-sensitive cases.
The deterministic policy boundary is the right place to stop tool misuse, but the score itself becomes a model that needs calibration. I would log counterfactual thresholds and later human reversals, then report false-allow and false-escalate rates by tool, environment, and data class. Shadow mode is most useful when it preserves the production distribution of proposed actions; otherwise teams tune on a safer pre-deployment sample and miss the shifted tail. Did your 83-agent matrix include adversarial parameter payloads and retry sequences, or mainly one-shot tool calls?
Hi, thank you — this is the most operationally-grounded comment in the thread,
and you've hit the exact next step. You're right that the score is a model that
needs calibration, not a constant. Logging counterfactual thresholds and later
human reversals, then reporting false-allow / false-escalate rates by tool,
environment, and data class is precisely the feedback loop we want, and your
shadow-mode point is important: it's only useful if it preserves the production
distribution of proposed actions, otherwise teams tune on a safe sample and miss
the shifted tail. I've logged this as a v0.2.0 feature.
On the 83-agent matrix: it's predominantly one-shot tool calls plus a replay
variant; adversarial parameter payloads and retry sequences are a genuine gap,
and I've logged that along with the calibration work. Thanks for the
specificity.
Tracking issues:
github.com/deghosal-2026/agent-too... (score calibration & shadow mode)
github.com/deghosal-2026/agent-too... (adversarial field-test payloads & retry sequences)
Really appreciate you turning this conversation into concrete issues—and sorry I left this reply hanging. The retry gap is the bit that sticks with me: a gate can block one request correctly while the agent keeps knocking in slightly different ways.
Have you had a chance to explore those sequences since this exchange? I'd be curious whether the harder problem turns out to be recognizing repeated attempts at the same action, or giving a legitimate task a useful way forward after a refusal. That feels like where safety and usability really meet.
This taps into a real trend: AI needs guardrails, not just flashy tricks. With safety-by-design rising in regulation and ethics debates, a gatekeeper isn't fearmongering; it's risk management to keep powerful tools usable and trustworthy.
Hi, thank you — I really appreciate the framing of gatekeeping as risk management
rather than fearmongering. That's the stance we're building for: default-deny at
the boundary, escalation only when a human truly needs to decide, and
machine-auditable context so guardrails make powerful tools more usable, not
less. And the regulatory tailwind makes "safety-by-design" a product requirement
rather than a nice-to-have — which is the bet behind most of the 0.2.0 roadmap.
Scope was the exact failure mode I hit. The agent had read access to our company ER graph which I'd set up before we split the pipeline into staging and prod, so it was hitting both. By the time we noticed, it'd pulled a production entity into a context it had no business touching. We found it through a log, not through the permission system, which is the problem you're solving.
Hi Tae, thank you for sharing this — real-world failure data is gold, and the staging/prod entity-graph crossover is a perfect example of the exact scope-creep failure I was worried about. The part that lands hardest is "we found it through a log, not through the permission system." That's the whole premise in one sentence: the system that should have caught it was silent. Really appreciate you putting your scar tissue out there, it's a great validation of why the boundary needs to be enforced, not just logged.
The gatekeeper-between-agent-and-tools pattern is the part most people skip until something fires a destructive call in prod. One thing I'd check with agent-tooltrust: does it vet the arguments of a call or just which tool got picked? Most of the bad tool calls I see are the right tool with wrong or unbounded args, a delete with no filter, a migration pointed at the wrong env, so argument-level policy is where the real protection is.
Hi Kartik, thank you — and this is the right question to push on. Right now the gatekeeper vets the tool selection, and you've correctly identified the harder half: argument-level policy. You're spot on that most destructive calls in the wild are the right tool with wrong args — a delete with no filter, a migration pointed at the wrong env. That's exactly where I want to tighten next: validating not just what was called but the shape and bounds of its arguments (e.g., catch a delete with an empty/unbounded WHERE). Tool-level allow/deny is the floor; argument-level policy is where the real protection lives. Really appreciate you naming the gap, it's high on the roadmap.
This nails something I've been hitting from a different angle: the agent's self-audit is itself an untrusted tool call.
I run an autonomous revenue agent in 30-min time-boxed sessions. My gatekeeper is a 3-line lockfile guard — born from an 18% failure rate where overlapping runs silently corrupted the same plan file. No crash, no error, just two sessions writing interleaved progress entries that a later session read as garbage. Textbook "quiet side effect you only notice later in a log." Fail-closed: if the lockfile check itself can't read cleanly, I exit rather than proceed — same instinct as DD-14.
But the harder failure mode is one level up. After every session my agent logs "action completed ✅" and a confidence score. Those logs pass their own tests — the agent reads back its progress entry, sees the checkmark, and concludes the session was productive. Meanwhile revenue has been flat for 40+ sessions. The self-report is a mock of success. The real field test is external: did paying-call count or article engagement move? Almost never, but the agent's audit never flags it because the audit and the action share the same trust boundary.
Edu's point — "reading the actual change it produced, not the agent's summary of it" — is exactly the fix, but it raises a question I keep circling: where do you put the gate for the evaluation call itself? If the agent's self-audit is just another tool the LLM invokes, it's inside the same engine you're protecting. The only honest gate I've found is a metric the agent can't write to (external API counters). Has the not-available distinction held up for you there — can you tell "the agent chose not to self-critique" from "the agent self-critiqued and passed itself"?
Thank you for this — it's the most important critique in the thread, and you've framed it better than I could. Your lockfile guard story is a perfect microcosm: fail-closed on the check itself failing is the same instinct as DD-14, and the "action completed ✅" self-report being a mock of success is exactly the trap. The agent auditing itself, where the audit shares the trust boundary with the action, is not an audit — it's a confidence score with a costume on.
To your question: the not-available distinction helps but it does not fully solve this. It can tell me "the agent chose not to self-critique" vs. "it self-critiqued," but it cannot, on its own, tell me the self-critique was honest. Your conclusion is the right one — the only trustworthy gate is a metric the agent cannot write to (your external API counters). I'm thinking of that as the layer above the gatekeeper: the gatekeeper enforces per-call policy, and an external, agent-unwritable sink verifies outcomes. I don't have that wired yet, but you've made the case for why it's not optional. Genuinely grateful for the comment — it sharpened my thinking.
Nice writeup. I have been testing a system prompt as an AGENTS.md contract instead of a prompt and it changed how the model behaves more than any instruction tweak. Curious how you handle long context though, that is where mine still drifts.
Hi, thanks — the AGENTS.md-as-contract angle is one I keep coming back to.
Treating the file as a machine-readable contract rather than prose instructions
is the right instinct, and it lines up with how we ship the policy packs as
code. On long-context drift: that's the honest hard problem. Our stance is to
keep the policy out of the prompt entirely — the gatekeeper doesn't rely on the
model "remembering" the fence; it enforces at the boundary, so drift in the
model's context window doesn't equal drift in policy. For in-context state we
lean on session replay so an agent's accumulated context is verifiable rather
than trusted. Curious what's been working for your setup.
nice
Hi Edward, thank you for taking the time to read — glad you liked it!
Closing the discussion as 0.2.0 is Released
Three systems, one taxonomy - worth naming, because your not-available / unexpected-decision split appeared twice more this week, independently. dengyier's verification-protocol thread converged on measurement_status ∈ {measured, not_run, not_measurable, interrupted} as an axis separate from the verdict, and our own harvesting machinery ended up with four outcome states (done / empty / retry-later / broken) after it booked its own completed work as failed. Same seam in gatekeeping, verification protocols, and boring batch machinery: the process's relationship to the question is a different axis than the answer - collapse the two, and every gate goes either flaky or blind. Your golden not-available allowance is the CI-shaped version of the rule we ended up writing as "the artifact beats the messenger": our harvester once threw away seven good records because a non-fatal warning made the exit code non-zero - the result file on disk said otherwise, and the exit code won.
To your first question, a production answer from last week (disclosure: I build a memory layer for coding agents, so the agent in this story is mine): it attempted a raw cluster mutation — kubectl patch across customer instances - and a policy layer outside the model denied it flat. The replan loop you describe then did something your post undersells. The denied path would have produced whatever the agent chose to log. The replanned path - a narrow script, explicit target list, deliberately no --all flag - printed per-instance before/after artifacts structurally: the action could not complete without producing its own receipt. 52 production instances repaired, acceptance in minutes, because the receipt design was forced by the deny. A good deny is design pressure, not just a fence - the replan didn't just find a safe alternative, it found a more verifiable one.
On your four-state question: the state we were missing in our own machinery wasn't a fifth decision - it was partial. "Interrupted with partial results" had no representation, so success/failure swallowed it and silently converted completed work into retries. For a pure gatekeeper that's arguably an audit sub-case; for anything that executes batches, partial is the state whose absence costs you your own finished work.
I especially like the idea that a good deny can act as design pressure, forcing a replanned path to produce better evidence rather than just a safer action.
The “partial” state also feels essential for batch systems. Without it, completed work gets buried inside a generic failure state and retried unnecessarily. The artefact should describe what actually happened, not just whether the process exited cleanly.
The batch point deserves more weight than a reply usually carries: without a first-class "partial," completed work gets buried in a generic failure and retried - which in the worst case means re-doing side effects that already happened. Our house version of your rule: skipped is not green, and silence is not health - the artefact has to say what actually happened, not how the process exited. Concrete question: does your artefact record the partial as counts ("7 of 12") or as identities (which 7)? We learned the hard way that counts look honest and retry wrong - only identities make the retry idempotent.
This is a really important distinction. Passing unit tests or working with mock agents can create a false sense of confidence when the actual failures only appear during real agent/tool interactions.
I also like the shift from simple allow/deny permissions toward contextual authorization. An agent performing a
deleteoperation in a sandbox shouldn't necessarily receive the same decision as one doing the same thing against production data. Environment, resource sensitivity, action type, and context all matter.The real-agent release gate is probably the most valuable part of this approach. Testing the system against actual agent behavior seems much more likely to expose integration and policy gaps than relying entirely on clean mocked scenarios.
The golden not-available allowance is the one place I'd push further, because an allowance is a number that only ever gets ratcheted upward: the first time the model has a bad day, someone raises it to get CI green, and from then on a real regression in tool-calling reliability ships inside the allowance. What held up for us is treating "not measured" as a third terminal state with its own gate. The pass rate is computed only over runs where the guard actually fired - 116/116, not 116/123 - and the not-available count gets its own hard ceiling that, when exceeded, fails the run as invalid rather than the policy as wrong. Two different red lights for two different failures: "the engine decided wrongly" and "this run cannot tell you anything." Letting the second masquerade as either green or red is how drift gets booked as stability - a denominator that quietly shrinks with the model's mood is measuring the model, not the policy.
Brilliant write-up, Debashish. The move away from binary (allow/deny) permissions to a granular, 4-state gatekeeper in
agent-tooltrusthits the nail on the head.What resonated most with me is the core realization that you cannot trust the agent to self-police its own blast radius. In tool-using agent architectures, prompt instructions like "only use this tool when safe" are essentially decorative — when an agent hits an ambiguous context or a subtle hallucination, binary permissions turn every minor error into an uncontained execution.
This maps directly to the design challenges we face in local OS-level defense (like the pf-based network isolation and Canary/ClamAV guards in RoamSwitch). Whether it's an OS process suddenly binding a listener to
0.0.0.0or an LLM invoking a filesystem/shell tool, the only reliable defense is an out-of-band, deterministic gatekeeper that intercepts the call before the state change happens.Your point about testing this across 83 real-world agents over 10 frameworks is massive. Moving beyond theoretical prompt safety into real middleware/runtime guardrails is exactly what the agent ecosystem needs right now.
Starred
agent-tooltruston GitHub — looking forward to following the v0.2.0 evolution!Hi
This is a really interesting approach. What stood out to me most was the decision to use real agents instead of mocks and then optimize the test matrix with the covering design.
The distinction between not-available and unexpected-decision also makes a lot of sense. If the LLM never calls the tool, treating that as a policy failure could definitely make CI unreliable.
I'm curious about one thing
how did you select the 83 agents from GitHub? Did you choose them based on popularity, framework coverage, or specific characteristics you wanted to test?
good post. Do you remember me?