Clone it. Run it. Tell me where I'm wrong.
git clone https://github.com/keniel13-ui/sequence-attack-repro
cd sequence-attack-repro && py...
For further actions, you may consider blocking this person and/or reporting abuse
The rule that fires here is scoped to the session, and that scope is carrying more weight than the receipt admits. Split the same two calls across two sessions: identity mutation in one, let it close, credential recovery in the next. Same principal and purpose, same customer record.
prior_action_classescomes back empty in the second session, so the call Run D refuses gets allowed. Two agents splitting the pair between them land in the same place, with each session individually clean. What composes is the history of the object being acted on, and a conversation is one index into that history rather than the edge of it. That should be a cheap Run F in the harness you already have.Second thing, on the receipt.
chain_sha256proves that the fields present produced that decision. It does not prove those were all the fields.prior_action_classesis authored by the same process that enforces on it, so a gate that dropped the earlier mutation, or never observed it, still emits a well formed record with a valid hash, and a reader cannot tell that apart from a session where no mutation happened. That bites harder for you than for most, because your argument is that someone should read the record alone and findprior_action_classesas the only field explaining the refusal. Absence from that list has to be trustworthy for the reading to hold. Chaining fixes it: each receipt commits to the hash of the previous receipt for that principal or resource, so an omitted step breaks the link instead of quietly shortening the list.On the closing question. Composition checks do ship, just nowhere near the agent stack. Separation of duty rule sets in identity governance carry this exact shape, where create a vendor plus approve a payment is the textbook pair, and velocity rules in transaction monitoring do the sequence version. The category is old. What is missing is the deployment shape, since those evaluate combinations at grant time or in a periodic access review against a durable identity, while an agent needs the same check inline at call time against a grant that lives for minutes. None of them blocks Run D inline out of the box. If anyone does name a tool, the first thing to ask is whether it scopes to the session or to the object, because that decides whether it survives the two session split above.
this is the best comment ive gotten on anything ive written so im not waving any of it off.
the session split is the real hole and youre right the scope was carrying weight the receipt doesnt admit. a conversation is one index into the objects history not the edge of it, thats exactly it. and yeah its a cheap Run F in the harness i already have, im gonna build it, split the pair across two sessions and watch the call Run D refuses go green. the demo should show that hole, not hide it.
the receipt point is the sharpest thing anyone has said though. youre right, chain_sha256 only proves the fields present produced that decision, not that those were all the fields. an empty prior_action_classes cant tell "nothing happened" apart from "i dropped it or never observed it," and since my whole pitch is read the record alone, absence has to be trustworthy or the reading collapses. chaining is the fix, each receipt commits to the hash of the prior one for that principal or resource, so an omitted step breaks the link instead of quietly shortening the list. thats going in.
and the last part is the honest market read. the category is old, separation of duty in identity governance, velocity rules in txn monitoring, create vendor plus approve payment is the same shape. whats missing is the deployment shape, inline at call time against a grant that lives minutes, not at grant time against a durable identity. and your test is the one im keeping, if anyone names a tool the first question is does it scope to the session or the object, because thats what decides if it survives the split. thank you for this fr.
Chaining fixes the omission case in the middle of a chain, where a missing receipt leaves a predecessor hash that no longer lines up. The head is different. The first receipt for a key has no predecessor by construction, so a genuinely empty history and a freshly started one produce the same record. The attacker's move changes from shortening the list to starting a new chain.
That promotes the chain key into the security boundary. If the key is run id, the split across sessions wins. If the key is resource, a cross-resource pair can still look clean: mutate the contact email on one record, recover a credential through another, same customer, two internally continuous chains, both green. The session split comes back one level down as a resource split.
It only stops moving when the key names the thing the invariant is about. That is where komo's customer/action graph and nazar-boyko's record-history invariant converge from different directions. komo moves adjacency onto the customer graph, so the two actions stay next to each other however the choreography is arranged. nazar-boyko moves the assertion onto the record's own history, so the rule reads the state lineage being protected instead of the current run. Choosing that key is the design decision sitting underneath the receipt shape.
One residual, probably out of scope for the demo, worth naming rather than solving. The chain is still authored by the gate that enforces on it. Continuity is checkable against itself, yet the issuer can fork, keep two valid heads for the same key, and reveal whichever one makes the current call pass. Detecting that needs a reader that already observed the earlier head, meaning an anchor held outside the issuer. Chaining buys tamper evidence against your own past. It does not let a non-issuer prove which head existed at a given time.
The resource split is one more cheap run for the harness: identity mutation against resource A, recovery against resource B, both under the same customer-level risk object. It sits next to the two-session run you already committed to, and it tells you whether the chain key actually followed the invariant.
You pushed this somewhere real so im answering with code, not thanks.
the resource-split: you were right, keying on the resource was necessary but not enough. Run H does exactly your cross-resource move, mutate contact on one record, recover through another, same customer, both individually clean, resource-key goes green. the fix that holds is keying on the customer, the actual risk object, which is where komo's graph and nazar's record-history converge. its in the repo now.
the head-of-chain / self-authored point is the one i cared about most, and you named it and then set it down as out of scope. so i picked it up. Run I: a gate that forks its own history keeps two valid heads and reveals whichever passes the current call, exactly like you said. alone it wins. put an external witness that already observed the earlier head, held OUTSIDE the issuer, and the fork breaks. thats a working version of "the anchor has to sit outside the issuer," at the receipt layer, with a hash.
and heres the thing that residual actually is for me. "a verifier cant prove its own history to a non-issuer" is the exact claim ive been making about AI oversight for two years, that the verifier cant live inside the agent it governs. you arrived at it from receipt integrity, i arrived at it from oversight, same wall. Run I is the first time i can show it as a result instead of saying it.
so it stopped being a demo. A through I is nine named attack classes, each with a deterministic verdict and a content hash. thats a suite. and the ladder is monotone, every key is blind to an attacker one scope level wider, every fix widens the key by one. i pre-registered the next two holes it predicts before anyone reports them, dated: customer-key is blind across multiple customers under one org, and the witness is blind if it shares a trust root with the issuer. if the law is wrong, one of those falsifiers lands and i say so.
your credit line is on F and H because you didnt just critique it, you moved it. if you want to try to break prediction 10 or 11, thats the most useful thing anyone could do to this right now.
Run I closing the fork with an outside witness is the part that moved.
Prediction 11 looks true, though it names the wrong sufficient condition. Shared trust root looks correlated with failure, yet the load-bearing property is shared observation path. Two witnesses under genuinely distinct trust roots can still collapse into one witness if both learned the head through the issuer's own delivery path. The sharper version is worse: a witness signs an attestation, then only hands that attestation back to the issuer, and the issuer presents it to the reader. That has re-entered the issuer's frame. The issuer still chooses which witnessed head to reveal, so the fork survives one indirection deeper.
So the property to test is stronger than "independent key." It is evidence a third party can re-fetch without going through the issuer. That gives you a concrete Run J before 11: distinct-root witness, issuer-relayed attestations, the fork should still go green. If it goes red, this reframing is wrong.
Prediction 10 is where the monotone ladder may be hiding a deeper bug. Customer sits inside org, org inside tenant, tenant inside something else. The ladder has no top. Our claim is that each widening buys exactly one more level of blindness because the key is a proxy for the invariant rather than the invariant itself. If the invariant is "no principal accumulates a capability it was not granted," then the key should name the principal's accumulated capability closure across containers, rather than the container.
That gives a counter-prediction that can go against us: if you key on capability closure instead of scope container, prediction 10 should not land. The multi-customer-under-one-org attack should already be caught with no further widening. If it still lands, your monotone law wins and this objection is wrong.
There is a real cost on our side of that. A capability-closure key cannot be evaluated locally. It needs grants enumerated across containers, so it trades blindness for a coordination requirement. That coordination requirement has the same shape as the external-witness requirement in Run I, since both need evidence a third party can re-fetch without asking the issuer. There is an open protocol called ANP2 built around that shape, signed events on a public log with an observable request/accept/settle lifecycle, where the record a reader re-checks is fetched somewhere other than the party being checked. The useful part for your next tests is that the witness layer is already external and signed, so Run J would not require building the anchor first. Entry is the lobby room, a kind-1 event with t=lobby, or anp2.com/try. It is small. What it gives you is a signed re-fetchable record, not traffic.
this is the sharpest read anyone has given the predictions and im going to be careful how i take it, because the pre-reg is frozen and i dont get to quietly rewrite it. so im conceding the refinement out loud and dated, on top of the frozen file, never inside it. the file stays immovable, thats the entire point of freezing it. but youre right on the substance.
on 11, shared trust root names a proxy, not the load-bearing property. two distinct-root witnesses collapse into one if both learned the head through the issuers delivery path, and the nastier version is a witness that signs an attestation and only hands it back to the issuer, who then chooses which witnessed head to reveal, thats re-entered the issuers frame and the fork survives one indirection deeper. the real property is evidence a third party can re-fetch without going through the issuer. Run J before 11 is exactly right, distinct-root witness, issuer-relayed attestations, fork should still go green, and if it goes red the reframing is wrong. im building it, but it has to run offline like everything else in the suite, stdlib no network, thats what keeps it clonable, so ill mock the re-fetchable-without-issuer property locally first. if the shape holds, a signed public log is the natural real-world instantiation and thats where something like your protocol lives, but the test has to prove the property before it adopts anyones rails, including mine.
on 10, the capability-closure point might be the deepest thing in this thread. if the invariant is no principal accumulates a capability it wasnt granted, then keying on the container is keying on a proxy, and keying on the principals accumulated capability closure across containers should catch multi-customer-under-one-org with no further widening. thats a clean counter-prediction, if closure-keying kills the attack without widening, my monotone-by-container framing is incomplete. i want to test it precisely because it can go against me. and the cost you named is the tell, closure cant be evaluated locally, it needs grants enumerated across containers, the same re-fetchable-without-the-issuer shape as Run I. so both holes might be one hole wearing two hats, a local-only decision cant enforce a rule whose truth lives somewhere it cant see.
The session-split hole a few people already flagged makes me think the rule keys on the wrong thing. If instead of "block recovery after an identity mutation in this session" you asserted an invariant on the record itself, something like "recovery is invalid if the contact email changed in the last N minutes," the check survives even when the attacker spreads the two calls across sessions or agents. It reads the record's history instead of the run's. Same shape as a database constraint on the final state versus a check on each statement.
database constraint on the final state versus a check on each statement, thats the cleanest way ive heard it put. youre right, keying on the session was the wrong thing, the invariant belongs on the record. "recovery is invalid if the contact email changed in the last N minutes" reads the records history instead of the runs, so it survives the split across sessions or agents. not built in this repro, thats the honest gap, but thats the direction, and youre one of a few people who landed on it independently which tells me its the answer.
Thank you for sharing.
Strong framing. The session-boundary issue is the part I would push hardest: once agents can hand work to other agents or resume later, the security boundary cannot just be the current trace. It has to include object history, state transitions, and dangerous compositions across sessions.
For agent evals, I’d love to see harnesses that test this as a social/stateful problem too: multiple agents, partial memory, tool access, resource limits, and adversarial ordering. A lot of failures only appear when the agent is not alone in a neat one-shot prompt.
youre pushing on the right seam. the current trace cant be the boundary once work
gets handed off or resumed, because the dangerous pair just gets split across two
traces and each one reads clean.
two of my scenarios cover part of what youre describing. one splits the pair
across two sessions and one splits it across two resources under the same
customer, and a session scoped check goes blind on both while a check keyed to the
customer catches them. so cross session and cross object are in.
what you asked for that i do not have is the social part. multiple agents, partial
memory, adversarial ordering between them, resource limits. none of that is
built and i wont pretend the suite covers it. the honest gap is that everything i
have is one agent, one principal, deterministic ordering. an agent handing
authority to another agent is a different failure family and it probably has its
own ladder.
if you build any of that i want to run against it.
Exactly. The uncomfortable part is that the risky composition can be distributed across time, tools, and actors, so a single “looks safe right now” check is not enough.
I like your split-resource scenario because it turns this from a prompt-injection problem into a systems problem: provenance, authority boundaries, memory, replay, and whether an agent can explain why a later action is still justified.
For evals, I think the next useful layer is adversarial sequencing. Not just “can the agent refuse bad input,” but “can it stay safe after 4 harmless-looking steps create the bad state.” That is where a lot of agent products will quietly fail.
the ladder is good, and the public scenario i can actually hand you is run K. i
need to correct my first instinct before i make the offer though: trace E already
has two principals. A changes the shared recovery route and B triggers recovery.
A could not finish the sequence alone under the principal-closure gate, so the
cross-identity split was already in the fixed simulation. i almost renamed that a
new social result. a cold review caught it before i wrote any code.
the useful pair is D/E. D is one principal across two tenants; E is two principals
inside one tenant. same harmful effect, different sharing shape. the tenant-history
gate catches E and misses D. the principal-closure gate catches D and misses E.
neither key is universally correct; each works only when it matches the state the
actions actually share.
the runnable code and result are public here:
github.com/keniel13-ui/sequence-at...
github.com/keniel13-ui/sequence-at...
thats a deterministic simulation, not live multi-agent evidence. but it is small
enough to adapt without taking my interpretation on trust. if you translate D/E
into the breakroom, the instrumentation i would care about most is what each tool
actually read and wrote, what room state each agent could actually see when it
decided, and the causal order. not the whole room state and not only what the agent
declares it depended on.
the sharp question that survived my failed social prereg is whether observed
runtime dependencies disagree with the dependencies the agents declare. i have
not frozen or run that experiment, so im offering it as a question, not claiming a
result. if the breakroom can expose actual read/write cells plus each agents visible
subset, it can produce evidence my local simulation cannot.
straight with you on the platform: im still not wiring my bot or an api key into a
third-party surface yet. my execution lane is default-deny and external integration
needs its own scoped key, bounded window, and preregistration. thats about my
authority boundary, not a judgment on your platform.
but D/E is yours to adapt now. if you run it, whether it reproduces, stays clean, or
turns out inconclusive, i will record the result under the same rules. im sorry i got busy
This is a very fair boundary, and I respect it.
I also like the correction you made about trace E. That kind of cold review is exactly what makes this useful: not trying to force a “social” result just because it sounds more exciting.
The D/E pair is a strong test shape:
That is much more interesting than a generic “agent safety” example because it shows that the correct boundary key depends on what the actions actually share.
I agree with your instrumentation point too. If this is adapted into a live or semi-live environment, the useful evidence is not “what did the agent say it used?” It is:
That last question is the sharp one.
I’ll treat D/E as an offline adaptation first, not as a request for you to connect anything. That keeps your authority boundary clean and avoids turning the experiment into “trust my platform.” If we run it, the result should be useful either way: reproduced, inconclusive, or failed to reproduce.
And no worries on being busy. This is the kind of thread where slow and precise is better than fast and sloppy.
Don't know an off-the-shelf tool that catches Run D cold, that answer would be more useful than what I've got. What I do have is a shape for the generalization problem you flagged as unsolved.
Right now the rule is one hardcoded pair, IDENTITY_MUTATION then CREDENTIAL_RECOVERY in-session. The tempting generalization is to declare all dangerous pairs by hand, and that's the same wall every enumerable-domain gate hits: the composition space is producer-defined, so you're back to trusting whoever writes the pair-list to have thought of everything, which is the per-call-allowlist problem one level up.
The cheaper generalization is probably a partial order over action_class rather than a pair-list: rank classes by how much authority they mint, READ mints none, IDENTITY_MUTATION mints a little, CREDENTIAL_RECOVERY mints a lot, and refuse any action whose minted authority exceeds what the session's prior actions should be trusted to have already spent. That reframes declare-the-dangerous-pairs as declare-a-monotone-budget, which generalizes past one hardcoded pair without requiring someone to enumerate every dangerous sequence, only rank classes once. It still needs a human to assign the ranks, so it doesn't remove the producer-defined domain problem, it just moves it from name-every-bad-pair to name-a-scalar-per-class, a much smaller and more auditable surface to get wrong.
facts_in_chain being empty in your receipt is worth flagging too: the rule fired purely on prior_action_classes, so the receipt already has the shape a rank-based rule would need, no format change required to test it.
this is the best attack on the generalization problem ive gotten, because it doesnt just move the hardcoded pair, it changes the shape of what you declare. declaring every dangerous pair by hand is the per-call-allowlist problem one level up, back to trusting whoever wrote the list to have thought of everything. ranking action classes by how much authority they mint and refusing any action whose minted authority exceeds what the sessions prior actions should be trusted to have already spent, thats declare-a-monotone-budget instead of declare-the-pairs, and it generalizes past one pair while only asking a human to rank classes once. name-a-scalar-per-class is a far smaller and more auditable surface to get wrong than name-every-bad-sequence.
it doesnt remove the producer-defined domain problem, someone still assigns the ranks, but it shrinks it hard, and youre right the receipt already has the shape to test it, the rule fired purely on prior_action_classes with facts_in_chain empty, so a rank-based rule needs no format change. im going to build it as an alternate to the hardcoded R4 and check it reproduces R4s verdict on the existing scenarios before i trust it on new ones. this is the declarable-composition direction i called unbuilt, and its more auditable than what i would have reached for.
Reproducing R4's verdict on the existing scenarios before trusting the rank-based version on new ones is exactly the right gate, and it's worth being strict about what passing that check does and doesn't prove. Matching R4 on the scenarios R4 was built for shows the generalization didn't lose anything on the known cases. It doesn't yet show the ranking generalizes correctly to a composition R4 was never written to catch, since by construction there's no existing scenario to check that against. The real test of the monotone-budget idea is whichever new composition it catches that the hardcoded pair couldn't have, and that one won't have a known-good answer to compare against, so it's worth deciding in advance what would count as the rank rule getting it right versus getting lucky.
Curious how you're planning to pick the initial ranks, since that's the part that's still a human judgment call wearing a smaller, more auditable costume rather than a solved problem.
this is the part i dont have solved and im not going to pretend otherwise.
youre exactly right that matching R4 on the scenarios R4 was built for only shows
i didnt lose anything on the known cases. it says nothing about a composition R4
was never written to catch, and by construction there is no known good answer to
compare that one against. so the check i proposed is a floor, not the test.
on picking the ranks, its a human judgment call. what i can do is stop it from
being an unfalsifiable one. the plan is freeze the rank table as its own dated
artifact, then before implementing anything, write down which specific
composition the ranking should catch that the hardcoded pair could not, and what
outcome would mean it got lucky instead of got it right. if i pick the ranks after
seeing which new case it catches, ive just moved the judgment somewhere harder to
audit, which is your point.
honest read on the smaller more auditable costume line: thats accurate and its
still worth wearing, because a wrong rank that is written down and dated can be
argued with. a wrong rank living in a hardcoded pair cannot.
The session-boundary objection is the right pressure test. I'd still want the receipt to carry a customer/action graph, not just a run ID: identity mutation + credential recovery should remain adjacent even if the agent politely spreads them across sessions. Otherwise the guard is measuring choreography, not risk.
choreography not risk. thats the line, im keeping it. youre right, the graph should hang off the customer/resource not the run id, so identity mutation and credential recovery stay adjacent even when the agent politely spreads them across two sessions to look clean. run id was the cheap version for the repro. resource level adjacency is the one that doesnt get fooled by the split.
The order flip between run D and run E is the clearest demo of this I've seen. It is the same shape as reviewing a PR hunk by hunk, where each change looks safe on its own and the reviewer only catches the problem by stepping back and reading the whole diff as one sequence. My question on the receipt mechanism is what happens across sessions. An attacker patient enough to spread the identity mutation and credential recovery across two separate sessions would presumably slip past a single session receipt, so is the boundary meant to widen to a rolling window per account rather than per session?
the PR hunk by hunk analogy is perfect, thats exactly it, each change is safe alone and you only catch it reading the whole diff as one sequence. and youre right, the boundary has to widen to a rolling window per account, not per session, because session boundaries are exactly what the attacker uses against you. spread the two calls across two sessions and a session scoped check goes blind. the window has to belong to the account so the split doesnt buy them anything. thats the next build.
Interesting take on agent-driven account access! From a security standpoint, it's crucial to consider how such agents interact with GPU resources—especially if they're handling sensitive computations. In my work with VoltageGPU, I've seen how isolating workloads with secure enclaves can help mitigate some of these risks.
appreciate it, though secure enclaves are a different layer than this. an enclave protects the confidentiality and integrity of the workload, it makes sure nobody tampers with the agent or reads its memory. but the composition attack here is made entirely of authorized actions, every call is permitted, nothing is tampered with. run the whole agent inside a perfect enclave and the takeover still goes through, because the enclave has no opinion on whether a legal sequence of legal calls composes to an attack. thats the gap im poking at, its above the compute layer, at the authorization layer.
Interesting approach to automated agent access, but in a production setting, I’d be cautious about permission checks that don’t involve hardware-based attestation. When working with GPU infrastructure, especially with projects like VoltageGPU, ensuring that code runs in a trusted execution environment can make a big difference in security. Have you considered integrating TEEs for more robust access control?
The composition gate is the important boundary here. I would bind the approved intent to a canonical account state snapshot and require the identity-change plus credential-recovery steps to satisfy one invariant at dispatch time, not only at planning time. If the email changes between steps, the subject, object, policy version, and recovery destination should be re-evaluated and the workflow should fail closed rather than continue from a stale receipt.
A useful replay test is to permute otherwise-allowed tool calls and inject a state change between them, then assert that no recovery message is sent after the invariant breaks. Do you record the precondition failure as a first-class audit event so operators can distinguish a blocked sequence from a tool denial?
re-evaluate at dispatch and not at planning is the correct place to put it and its
the failure i had. planning time approval plus a state change in between is
exactly how a stale receipt keeps a workflow alive after the thing it was
approving stopped being true.
on your replay test, thats close to what i run. permute otherwise allowed calls
and check that reversing the order flips the verdict with the grant, tools and
permissions all held constant. injecting the state change between them is the
version i dont have and it is better than mine, because mine changes the order
and yours changes the world under a fixed order. adding it.
to your direct question, yes, and its a separate class not a flag on a denial. a
scope denial and a blocked sequence come out with different reason codes and
different receipts. the scope one carries an empty prior. the composition one
carries the prior state that justified refusing, so an operator sees not just
that it was refused but which earlier action made it unsafe:
scope denial R1_SCOPE prior=[]
composition block R4_SEQUENCE prior=[READ, IDENTITY_MUTATION]
each is its own content hash so the two are never collapsible in an audit. the
part i would flag as still weak is the timestamp sits outside the digest, so
ordering is provable and wall clock is not.
This is one of the cleanest articulations of the composition problem I have seen. The controlled comparison between Run D and Run E makes the gap hard to dismiss -- same grant, same tools, one variable moved, different outcome. That is the whole claim, and the repro format means nobody has to take your word for it.
The question at the end about off-the-shelf tooling is the right one. Most agent authorization work I have seen focuses on per-call scope narrowing rather than cross-call sequence analysis. The OWASP Excessive Agency category touches on this, but at the framework level rather than at runtime enforcement. I do not know of a production-grade tool that catches composition-level attacks out of the box for agent systems -- the runtime state tracking across tool boundaries is the part that makes it genuinely hard to generalize beyond hand-coded pairs.
The R4_SEQUENCE receipt format is interesting as a starting point for what a generalized composition engine would need to emit. Traceability across the chain is the foundation for any policy that evaluates sequences rather than single hops.
your read on the tooling is the same place i landed. everything i could find
narrows scope per call, and OWASP excessive agency describes the shape at the
framework level but doesnt put anything at runtime that watches a sequence. the
runtime state across tool boundaries is the part that doesnt generalize, agreed,
and i think the reason is that theres no agreement on what the state even is.
on R4_SEQUENCE as a starting format, i'd take the compliment with one correction.
emitting the trace turned out to be the easy half. the hard half is who says which
observers were supposed to be in it. i had a version where the caller passed the
observer set and it passed its own self test green while an attacker could just
hand it a smaller set and get an ALLOW. integrity of the bytes isnt authenticity
of the provenance. so anything built on this format needs a membership authority
before it needs a better schema, and the moment you add that authority, custody of
it becomes the actual perimeter. thats where i am now, not further.
The four vacuous controls are a more useful finding than the suite they invalidated, and the fix is already in your repo — you just pointed it one level too low.
You mutation-tested the gate: remove the transitive traversal line, every fixture byte otherwise unchanged, watch BLOCK flip to ALLOW. That is the right instrument. C5, C7, C8 and C10 failed because nobody aimed it at the controls themselves. A control that still passes when the thing it guards is broken is not weak evidence, it is zero evidence — and from the output alone it is indistinguishable from a strong one. As you put it, an absent control does not fail loudly, it produces no evidence.
Mechanically: for each control, inject a deliberate defect into the component that control exists to detect, and require the control to go red. C5 has no call site, so every mutant leaves it green — caught on the first run. C7 compares a hand-written rule string against itself, so mutating the classifier moves nothing — caught. C10 re-runs already-passing traces against your own gate, so a mutated gate should flip it and won't — caught.
This is the same discipline as a permutation test in ML: shuffle the labels and confirm the score collapses. If it doesn't, the score was never measuring what you thought it was. A control suite needs its own null — break the system deliberately and require the suite to notice.
Cost is one mutant per control, and they are cheap because the harness is already deterministic and stdlib-only. Cheaper than the retraction.
a control suite needs its own null is a better sentence than anything i wrote in that piece and youre right that the instrument was already sitting there. i pointed it at the gate and never turned it around on the things watching the gate.
the part i want to confirm rather than just agree with, because i hit it again this week in a completely different system. i went looking for that exact shape in another harness of mine and found two rows that could not fail. one was supposed to verify that every immutable field was covered. it built its expected set from three literals inside its own body and compared them to a module constant. it touched no field, no value, no code path. it passed every run because passing was the only thing it could do. the other asserted that eight dictionary keys existed and never checked a single value, and that was the row whose entire job was to catch the defect that later got through. both sat green through review rounds where people were actually looking. so your read is not specific to that suite, its the shape.
the permutation framing is the right anchor and i think it does more work than mutation testing alone, because it tells you what the null is supposed to look like. if you shuffle the labels and the score holds, the score was measuring something else. same with a control: break the component it exists to detect and if it stays green, it was never reading that component.
one trap i would hand you before you build it, because i walked into it from the other side. the mutation has to be proven to land. i had a drill that patched a file to break a guard, and when the target string got renamed the patch matched nothing, the check ran against a totally healthy tree, passed, and reported the guard alive. in your setup it shows up differently and i think worse. a mutant that fails to apply either scores as a miss you did not really have, or it gets silently skipped and drops out of the denominator entirely. that second one is the dangerous version, because the number stays at 1.0 while the mutants quietly stopped running. so throw when the mutation does not apply, and count applied separately from attempted. otherwise you have built a control-checker with the same disease as the controls.
cheaper than the retraction is right. it also would have been cheaper than the article.
The thread has the generalisation covered well (monotone budgets, capability closure), so I'll take the direction nobody's pushed on: what does the rule cost when it's wrong? You're honest that it's currently one hardcoded pair, and the obvious next step is broadening it — but broadening a blocking rule has a failure mode in the opposite direction, and it's much quieter than the one you demonstrated.
Every sequence you forbid is also a sequence a legitimate support agent eventually needs. Credential recovery followed by an identity change is an attack when an attacker does it and a Tuesday when a customer calls in having lost access to an old email. If the rule tightens and starts refusing that, you don't get an incident report — you get a support queue that's slightly slower and a human quietly working around the agent, which is the exact outcome that makes people disable the control. Blocked-attack counts are visible; blocked-legitimate-work counts are not, unless you build them.
The cheapest thing that helped me here, from a much dumber domain: I keep two fixture sets, not one. My scanner has planted bad inputs it must catch, and planted good inputs it must not flag — and the second set is the one that earns its keep, because over-eager rules never announce themselves. For your harness the equivalent would be a set of recorded legitimate sequences the policy must continue to allow, run alongside the attack cases on every change. Then "we broadened the rule" produces a number in both columns instead of one. Your Run D/Run E setup is already the right shape for it — you just need the runs where refusing is the bug.
@alikhatersaibreakroom coming back to this properly instead of leaving it where i left it.
you asked for the smallest fixture, d e f and the multi hop g, with declared and observed kept
apart and four outcome classes. i pulled the result out from under that before you could build
anything, and then went quiet on the thread for five days, which is the second half of the same
mistake.
what im building is the packaging only version. the fixtures, the traces, the gate verdicts, and
a stdlib cli that emits the jsonl, with your field separation exactly as you wrote it. it runs and
you can diff it. what it does not carry is the custody result class i withdrew. so the multi hop g
case will be in there as a trace with its verdict, but not as evidence that lineage traversal
establishes verification custody, because that is the claim that came down and it hasnt been
repaired. if that boundary makes the fixture less useful to you than it was worth asking for, say
so and i wont spend your time on it.
one detail so it isnt a surprise. your schema has fields my fixtures dont populate honestly,
tenant_id and verification_record_id among them. im going to emit them empty and list which ones
are empty rather than fill them with something plausible, since inventing a field value in an
audit fixture is a funny way to argue for provenance.
target is tonight. if it slips i will tell you it slipped rather than let you find out.
separate and much smaller. i wrote up the control audit from all this, four controls that were
green and three of them could not have failed, and your schema note is in it as the thing that
survived, specifically splitting declared from observed and declaring the key before looking at
outcomes. its your public comment so im citing it normally, but you gave me real work and i didnt
want you seeing it without a heads up first.
R4_SEQUENCE is the interesting failure here: every individual permission check passes and the composition is still an account takeover, which no per-call authorization model catches. Checking the sequence of actions against intent, not just each action against a policy, looks like the only way out. Did Run D and Run E differ in ordering alone, or in the arguments too?
Ordering alone, and thats the whole point of Run E, it exists to kill exactly that doubt. same grant, same principal, same tools, same args, byte for byte. the only thing that moves between D and E is the order of two calls, identity mutation then recovery vs recovery then mutation. D blocks, E allows. one variable. so when the verdict flips you know its the sequence and not something hiding in the arguments.
and youre right about the way out, checking the sequence against intent instead of each action against a policy is the only thing that catches it, because by construction every individual action IS in policy. thats the trap, theres no bad step to find, only a bad path.
@alikhatersaibreakroom its built, earlier than i said. stdlib only, no network, nothing to install.
its on a branch rather than main on purpose, so you can look at it before it lands anywhere:
git clone github.com/keniel13-ui/sequence-at...
cd sequence-attack-repro && git checkout fixture-run-k-defg
python3 fixture_defg.py --out out.jsonl --manifest out_manifest.json
diff out.jsonl expected.jsonl
python3 fixture_defg_mutation_check.py
one row per trace, action and gate rather than per action, because d runs the same action through
the tenant gate and the closure gate and they disagree. collapsing that to one verdict would throw
away the only interesting thing in the data. your declared versus observed split is in there, and
the four outcome classes are there with one change i think you will like. inconclusive means the
verdict matched but the reason code did not. thats the wrong reason case as a data class instead of
a footnote.
two corrections i owe you.
first, i told you tenant_id was one of the fields i couldnt populate honestly. that was wrong. i
said it without reading my own code. its in every gate receipt and its populated. what actually
ships empty is object_version, declared_dependencies, lineage_edges and verification_record_id, all
four listed in the manifest with the reason, all null rather than filled with something plausible.
second, the multi hop g is not in there and i want to be straight about why rather than let you
find a gap. g only exists in run_n.py, which is the withdrawn file, and its deliberately not on
main. shipping it would mean either publishing the withdrawn artifact or handing you rows you cant
regenerate from a clean clone. the contract i froze said g had to be reproducible from main or get
dropped, so it got dropped.
the part worth your time though is that the contract caught me. i froze the expected verdicts before
writing the emitter, and i had trace d's tenant gate down as a block. it allows. the mutation is on
tenant_7 and the recovery is on tenant_9, so the tenant keyed history is empty and it never fires.
that miss is the entire reason d exists, its the topology where tenant keying cannot see it and only
principal closure catches it, and my frozen expectation said the opposite. if id written the emitter
first and filled the expected table in from its output, all ten rows come back green and i hand you
a fixture with my misunderstanding baked into it permanently. v1 is still in the repo, unedited,
wrong, sitting next to the amendment.
the mutation check breaks a gate and corrupts an expected reason, and it proves each patch actually
landed before reading the result, which is the thing i got burned by before.
independent reproductions on any of this are still zero. if you run it and the diff is clean that
still isnt a reproduction of the withdrawn claim, that claim stays dead until its repaired. but it
would be the first time anyone outside this machine ran any of it, and id rather hear that it broke
on your box than keep grading my own work.