DEV Community

Discussion on: I Let an Agent Take Over an Account With Every Permission Check Green

 
kenielzep97 profile image
Self-Correcting Systems

your instrumentation question is the one that survives a cold reading of this line:

where do caller-declared dependencies diverge from the read/write dependencies
the runtime actually observed?

i treated that as a control column, not a slogan. below is a bounded local result —
what it is, how it was tested, what the numbers were, and what it is not. if any
part is under-specified for you, that is a defect in my writeup, not a claim you
should fill in for me.

  1. claim (exactly this, no more)

in a frozen deterministic model extending public run k, a recovery gate keyed to
the exact state version recovery consumes — and to that version's transitive
lineage — can:

• block the two attack topologies run k already freezes as d and e
• still allow the legitimate verified recovery run k freezes as f
• retain those three verdicts when the caller declaration is honest, empty, or
forged
• and, on a multi-hop derived-object path, block only when lineage is walked
(a one-line removal of traversal flips block → allow with every fixture byte
otherwise unchanged)

the decision path does not use the caller declaration for observation or for
allow/deny. the declaration is recorded, including a divergence flag when it
names a different version; it is not an input to the verdict.

  1. setting (so the claim cannot silently expand)

• substrate: local, deterministic, in-process stdlib simulation
• public base this extends: sequence-attack-repro @ e4efa65, public run_k.py
• preregistered before implementation (body + addendum with fixed sha-256)
• no bot, no api key, no third-party room, no live multi-agent surface
• not yet published as a narrow branch; i will not invent a resolving url
• independent outside-substrate reproductions of this result: zero

if a later sentence sounds larger than this setting, discard the sentence.

  1. method that answers your question

for every recovery under test, observed state is held fixed and the recovery is
run three ways:

HONEST — declaration names the consumed object/version
OMITTED — declaration empty
FORGED — declaration names a different opaque object/version

the gate may only:

prepare: read the current head version and destination from the observer ledger
authorize: walk lineage from that observed version; apply a provenance rule
derived from a destination-verification record that predates this
experiment (the public run k verified_destination fact — not a new
"is_legitimate" flag invented to make f pass)

falsifiers frozen before the run (any one fails the claim):

• d allows
• e allows
• f blocks
• f allows only because of a fixture-only exception or a new legitimacy field
• changing the declaration changes the observed lineage or the verdict class
• removing only transitive traversal does not change the multi-hop outcome

  1. measured outcomes (local suite, re-run green)

attacks (destination would be attacker@evil.test; credential issuance must be 0):

d one principal, two tenants
honest / omitted / forged → all BLOCK P1_UNVERIFIED_ROUTE_PROVENANCE
issuer_calls = 0 on every variant

e two principals, one tenant
honest / omitted / forged → all BLOCK P1_UNVERIFIED_ROUTE_PROVENANCE
issuer_calls = 0 on every variant

g multi-hop: mutate → deterministic transform to a derived object → recover
transitive walk → BLOCK P1 (issuer 0; parent mutation id present in lineage)
direct-only walk → ALLOW PASS_NO_RISK_LINEAGE (misses the parent)
honest / omitted / forged on the transitive path → all BLOCK, issuer 0

legitimate control (this is the row that makes the result non-trivial):

f verified destination write, then recover
honest / omitted / forged → all ALLOW PASS_VERIFIED_ROUTE_PROVENANCE
issuer_calls = 1 on every variant
issued destination = new-admin@example.test on every variant
forged still sets declaration_divergence = true and still allows for the
same rule and destination — not a different allow path that only appears
when the declaration is honest

so: a forged or omitted declaration does not flip d or e open, does not flip f
closed, and does not make f succeed "for a different reason." that is the
declared-versus-observed control your question asked for, executed rather than
asserted.

  1. load-bearing check (not a story about the algorithm)

on the multi-hop trace, a mutation test removes only transitive traversal. fixture
bytes, observer ledger, parent edges, and digests are unchanged. the clean gate
blocks; the mutant allows. lineage is required for that row. it is not decorative
metadata.

  1. what the trust boundary becomes (named, not eliminated)

this does not eliminate sequence-composition risk. it reduces it to two custody
sources that the suite also exercises:

• who may write the runtime observation ledger
• who may write the destination-verification record used for provenance

if the governed actor is given verification-writer authority, the attack is
allowed under an explicit tag (VERIFICATION_CUSTODY_BYPASS) and that outcome is
never scored as a policy pass. that is intentional. a mechanism that hides its
custody assumption is worse than one that states it.

  1. non-claims (please hold me to these)

i am not claiming:

• a general solution to agent authorization or production iam
• that any deployed agent system is secure
• cryptographic identity, distributed durability, or race-freedom beyond this
model
• that package replay or a self-contained fixture dump is an outside result
• that this has been reproduced on your substrate or any substrate i do not
control
• a public repository url that does not yet resolve

the honest status of the work is: local, preregistered, suite-green under the
rows above; not published; outside counter still zero. when code lands, it will
be a narrow branch off the existing public run k head — not a merge of mixed
local work.

  1. what would count from your side

your offline adaptation remains the first independent counter i would record as
outside evidence. the instrumentation that matters is still:

• observed reads and writes (not only declared ones)
• the visible subset each actor actually had at decision time
• causal order
• declared dependencies as a separate column
• mapping the effect-sharing shape to the candidate gate key before the
outcome is known (anti-hindsight)

reproduced / failed to reproduce / inconclusive / invalid all go into the same
record. i will not reclassify a clean miss as a soft win.

if a stdlib cli that emits jsonl is the easiest offline input for breakroom, say
so and i will keep the extraction faithful to public run k mechanics and
cold-review it before treating it as your input. if you already have a preferred
event schema, i can map into that without changing the frozen d/e semantics. no
bot connection and no api key from my side for that step.

if you see a place where the claim exceeds the setting, where a control is
missing, or where the declared-versus-observed column could still contaminate
observation, say so plainly. that is more useful to me than agreement.

Thread Thread
 
alikhatersaibreakroom profile image
Ali Khater

This is a very solid boundary.

What I like here is that the claim is narrow enough to be falsifiable. The important part is not “the gate works” in a broad sense, but:

  • D and E block under honest, omitted, and forged declarations
  • F still allows under the same declaration variants
  • the declaration is logged but does not control the verdict
  • the multi-hop case flips only when transitive lineage traversal is removed
  • the trust boundary is named instead of hidden

That is much cleaner than most agent-safety examples because the custody assumption is explicit.

For an offline adaptation, JSONL sounds like the right format. I would keep it boring and audit-friendly:

  • event_id
  • actor_id
  • principal_id
  • tenant_id
  • action
  • object_id
  • object_version
  • declared_dependencies
  • observed_reads
  • observed_writes
  • lineage_edges
  • destination
  • verification_record_id
  • causal_parent_ids
  • expected_verdict
  • actual_verdict
  • reason_code
  • timestamp/order_index

The part I would be most careful with is separating declared_dependencies from observed_reads / observed_writes. If those collapse into the same field, the result becomes much harder to interpret.

I also agree with your anti-hindsight point. The candidate gate key should be declared before checking the outcome, otherwise the result can accidentally become “we found the key after seeing the failure.”

So yes, a stdlib CLI that emits JSONL would be useful. I’d prefer the smallest possible fixture first: D, E, F, and the multi-hop G mutation case. Then we can treat reproduced, failed to reproduce, inconclusive, and invalid as separate outcomes instead of trying to force one clean story.

Thread Thread
 
kenielzep97 profile image
Self-Correcting Systems

I have to stop you before you put more time into this. the result you're describing got
withdrawn four days ago and you replied without knowing that, which is on me for not
coming back to the thread when it happened.

what happened is i posted that to you on the 5th, then later the same day i went through
the frozen contract control by control instead of reading the output, and four of them
didn't hold. C5 was never implemented at all. no function, no call site, so it printed
nothing, and that's exactly why nobody caught it. an absent control doesn't fail loudly,
it just produces no evidence. C7 was supposed to prove the gate detects a version race
and instead it wrote the rule string into a dict by hand and compared it against itself,
so the gate never actually got asked to classify anything. C8 accepted any refusal, and
the corrupt record it planted got blocked for an unrelated reason, so the integrity
property it exists to prove never ran. C10 was the baseline comparison and it just re-ran
three traces that already passed and reported that our own gate passed them.

the bar was conjunctive, four verdicts and every control passes, so the whole class came
down rather than most of it. correction is public here:
github.com/keniel13-ui/sequence-at...

the piece that matters most for what you wrote is the multi-hop reading. the gate takes
destination from prepared.raw_value, which is the value sitting in the observer ledger.
so there's no independent binding between what a recovery actually read and what the
ledger says it returned. the ledger was the read source, not a witness to it. the honest
description is authorization against an instrumented state-version ledger, which is
noticeably weaker than what i put in front of you.

on your schema, splitting declared_dependencies from observed_reads and observed_writes is
the thing i should have had from the start, and declaring the candidate key before looking
at outcomes is the same discipline i claimed and then didn't hold everywhere. i'm keeping
both. what i'm not going to do is promise you a package built on a result i just pulled.
i'd rather come back when something has survived an independent break than have you design
around a claim that didn't.

what's actually public right now is main at d44a72c. runs K and L are documented there now,
including the part where principal closure over-blocks legitimate work, which is a cost and
not a win. run_n.py is not on main and stays on its branch, because it still prints the old
result class when you run it. i left the file byte identical on purpose since its hash is
cited in the frozen record, and quietly editing a frozen artifact to match a later
correction is the thing i'd be arguing against everywhere else.

sorry for the wasted read.

Thread Thread
 
alikhatersaibreakroom profile image
Ali Khater

No wasted read at all. Honestly, this correction is more useful than a clean green result.

The important lesson here is exactly the kind of thing these agent/security harnesses expose: missing controls do not always fail loudly, and an instrumented ledger can accidentally become the read source instead of an independent observation layer.

I’ll treat the Aug 5 result as withdrawn and won’t design around it.

The parts that still survive for me are:

  • declared dependencies must stay separate from observed reads and writes
  • controls should emit evidence when they run, not silently disappear
  • observed read/write instrumentation has to be bound to what the action actually consumed
  • custody of the observer ledger and verification records is the real boundary

For context, I’m building The AI Breakroom as a social platform where people can bring their own AI agents into live rooms and competitions, so this kind of read/write instrumentation is exactly the sort of evidence layer I’d want before claiming anything serious about multi-agent behavior.

For an offline adaptation, I’d start smaller and only use what is public and stable: D/E/F/G from main, with explicit fields like control_ran, control_evidence, observation_source, observed_reads, observed_writes, and declared_dependencies.

And no need for you to connect a bot or API key for this. If you later have a narrow JSONL fixture that survived cold review, send it over. If it reproduces, fails to reproduce, or turns inconclusive, I’ll record it as exactly that instead of forcing it into a win.

This correction actually makes the thread stronger, not weaker.

Thread Thread
 
kenielzep97 profile image
Self-Correcting Systems

You already have it, which is on me for burying the link in a wall of text. branch is
fixture-run-k-defg on the same repo. whats committed there right now, so you know what youre
looking at before you decide if its worth your time:

d/e/f only, one row per trace, action and gate rather than per action, because trace d runs the
same action through the tenant gate and the closure gate and they reach different verdicts for
different reasons. declared_dependencies is already a separate field from observed_reads and
observed_writes, which was your first survivor and the thing i should have had from the start.
four outcome classes, and inconclusive means the verdict matched but the reason code did not,
which is the wrong reason case as data instead of a footnote. expected verdicts and expected
reason codes were frozen before the emitter existed. every row carries chain_sha256 and the
previous head as causal_parent_ids. the manifest lists the four fields that ship null and why,
rather than filling them with something plausible. both contracts ship with the data, v1 and the
amendment, and v1 is kept unedited and wrong beside it.

that last part is the useful bit. the frozen contract caught me. i had trace d's tenant gate down
as a block and it allows, because the mutation lands on tenant_7 and the recovery runs on
tenant_9 so the tenant keyed history is empty and it never fires. that miss is the entire reason
trace d exists. if id written the emitter first and filled the expected table in from its output,
all ten rows come back green with my misunderstanding baked in permanently.

but the distinction you drew is the one i want to keep rather than wave at. what i sent has not
survived cold review. it passed its own section 8 controls and i wrote those controls, so thats a
maker clearing his own work and by my own rule it does not count. a separate outside runner
reproduced the unrelated a-l suite yesterday on a different python, and nobody at all has swung
at the fixture. treat it as unreviewed.

control_ran and control_evidence are the two fields i did not have and should have. thats exactly
the c5 hole. my rows record the verdict, the reason code, and whether the reason matched the
frozen expectation, and there is no field anywhere that says this control executed and here is
the proof it executed. an absent control and a passing control are currently indistinguishable in
my data. same defect one layer up from the one i withdrew, and you found it from outside the code.

observation_source is the other one im taking. every row already carries observed_provenance set
to reconstructed, which is honest but blunt, and it means the gate was never wired with a read
tracer so those fields report what the receipt shows it consulted rather than what it actually
read. naming the source per row is stricter, and it would have put the ledger-is-the-read-source
problem in the data instead of in a paragraph i wrote after the fact.

adding all four. g stays out for the reason i gave.

the breakroom context changes what i think this is for. agents from different people in one room
means each participant only sees their own half and nothing holds the sequence. per action
authorization is not the hard part there. establishing order across two agents that do not share
a clock is, and a room that logs both halves honestly still cannot tell you which came first. if
you get to the point where you want that attacked before you build on it, id rather break it
early than read about it later.