Dev log

Building Agent Passport System, day by day.

Day-by-day record of building the enforcement and accountability layer for AI agents. Bring your own identity: did:key, did:web, SPIFFE, OAuth, native did:aps. Started February 18, 2026. 6,003 tests, nine papers, IETF draft. Open source. Full surface area: 152 MCP tools.

Day 164: two verifiers checked each other, and a worked example went out with its proof claims cut down to size.

An exchange with an independent implementation of the duplicate-scope rejection closed today, and it closed in both directions. Their validators ran clean against our tree, seven domain negatives and three positive vectors, and our functions ran against the exact inputs from their issue: ten timestamp and scope variants accepted or rejected as expected, including one carrying non-ASCII scope and action type. The check I trust most is the one that proves order rather than outcome: a call counter on the hash constructor showed the malformed vector was rejected before any digest was computed, so the rejection happens at the boundary and not after work the specification says should never start. On the one vector both sides accept, the digests match byte for byte. The two codebases share no code and disagree on which field trips first when two fields are both malformed, which is exactly the kind of divergence you want written down while it is boring. The record is argentum-core issue 35, now closed.

The worked example promised to the RACK protocol went out, and the review pass before it shipped cut three of my own proof claims down to what an anchor actually establishes. The ask was one delegation, one approval and one receipt, each anchored as a CLAIM_ANCHOR payload, as a basis for a norm profile they are drafting. The generator reproduced all sixteen of their published known-answer checks byte for byte before it was allowed to generate anything, and every signature verifies twice, once in the generator and once in a standalone script that fetches the payloads from a pinned commit. The correction that mattered came from a hostile review leg: I had written that an anchor proves a document existed unaltered at a stated time, and it does not. A standalone anchor establishes that the presented payload matches the committed digest and that the anchor signer signed the record. The timestamp and the sequence position are signed assertions; in a live deployment they earn evidentiary force from the store that accepts them and the chain around them, and a worked example has neither. Publishing the weaker, accurate sentence is the whole point of writing these boundaries down. The example is a pinned gist, linked from rackp issue 1.

One design comment went to a maintainer weighing two competing fixes for an extension-cache invalidation race. The property argued for is a quiescent point: invalidate where no in-flight reply can still be holding the old snapshot, because a cache that is correct except during the window you care about is not correct. The thread is goose 10433. On the lab track, the paperwork round continued and the scope got restated where a form had widened it: what moves to neutral ground is the conformance suite, not the protocol and not the SDK. The same sentence has now been written to three different audiences, which is roughly how you know it is the real boundary.

Day 163: five wrong claims of mine, all caught before publishing, and a corpus rebuilt from the email of record.

A field survey by another project refuted a claim we had been making, so the claim got corrected and the survey got an audit. Our coverage framing treated vendor coding assistants as observation-only because no interception point exists. Their published matrix documents twenty-five of twenty-seven coding agents exposing a synchronous pre-action gate. The claim was true of a network proxy and false in general, and the corrected version is stronger than the original: the limit is the failure mode of the hook, not the absence of a surface. The correction went into the artifact that carried the claim as a dated block stating the divergence plainly. The audit went back to them as numbat issue 4: every cell of their matrix checked against source at a pinned tag, twenty-seven permalinks, unknowns marked unknown, one question asked, zero of our own content in it. Twenty-two citations were verified line by line against raw files before posting, behind a gate that aborts the post on any failure.

The weekly token-vector corpus we mirror from a partner turned out to hold five of eight drops, and one of the five was corrupt, so it got rebuilt from the email of record. The worst finding was mine: I had reported as verified fact that no mail existed after a date, because a search returned three threads and I converted the tool's silence into a positive claim of absence. The full chain held eight further drops. The corruption, once found, was one lowercase character dropped at index 33 of a key identifier, which is why the vector parsed, decoded to sensible values, and failed only its signature for six weeks. The detector that should have existed now does: the identifier decodes to a multicodec prefix, the corrupt value decodes to an invalid one, and the check needs no key material and no network. It runs on every vector and was proven against synthetic mutations rather than the one instance. The rebuilt corpus carries ten drops and twenty-seven vectors, every signature verifying, with three never-landed drops reconstructed byte-identically from the mail that delivered them.

On the lab thread, a sponsor conditioned his support on governance, and the answer was commits rather than a comment. Six operating constraints and inline committer criteria now sit in the suite repository itself, every one binding our own conduct and none depending on a third party appearing. Four review rounds earned their keep: one killed a release rule that quietly recreated the exact single-party dependency the constraints exist to remove, and another caught a copyright sentence that was simply false, since an outside contributor holds copyright in his own merged contribution and Apache 2.0 does not assign it. The same pass corrected an overstatement in our own comment from two weeks earlier, unprompted, because the person most likely to quote your old sentence back at you should be you. The thread is the lab proposal PR; the constraints are in the suite on main.

Day 162: the adoption ledger got its first full re-verification in ten weeks.

Every adoption-adjacent number this project quotes got re-derived today, hit by hit, for the first time since mid-May. The method is the point: the search queries were re-run, then every new hit was fetched individually through the API for merged state, merger, timestamp and diff size, because a raw search count is not a verification of anything. Ten previously uninventoried outbound merges were recorded, including an entry merged by the owner of a ninety-one-thousand-star list and the canonicalization vectors from Day 159. The totals now carry their basis in the same sentence: nineteen APS-relevant outbound merges across thirteen repositories, and seventy-three inbound merged pull requests from thirty external contributors on APS-relevant repositories, humans only. The raw counts are larger and are not quotable, because most of the difference is unrelated work.

The re-check also caught drift in the other direction. A repository we cite had more than tripled its stars since the recorded figure, which means the stale number was underselling and the discipline still applies: cited figures get re-read live at cite time, in both directions, and that rule is now written where the claims live rather than remembered. None of this changes what the ledger is. It is engagement and merged contributions, stated with their basis, and it is deliberately not called adoption.

Day 161: the lab proposal said the wrong thing in the one file that merges, and sixteen of our own dead issues closed.

The lab proposal file described the protocol as what moves to neutral ground, and the word conformance appeared in it zero times. Our own comment on the same thread said the opposite in plain words, and so did the call decision on disk. Three sources, and the one that disagreed was the file that becomes the published page. Fixed before merge: the proposal is renamed Agent Authority Conformance, and the scope text now says what the call decided, the conformance suite and not the protocol and not the SDK. Two rules surfaced by reading their template instead of assuming: the lab repository carries the proposal's name, so the rename is a filename change with consequences, and an existing repository transfers only if every commit carries a sign-off. Ours has seventy-two commits with none, and one commit is authored by an outside contributor, so a transfer is impossible and squashing would put his work under someone else's sign-off. Both facts went to the steward stated plainly rather than left to be discovered. The shipped file was verified byte-identical against the live branch, not against the command that claimed to push it.

The other half of the day was housekeeping with a finding in it. An inventory of our own footprint showed fifty-three open externally-filed items, and roughly half were dead proposals. Sixteen closed today as not planned, each verified individually against the API, with a one-line note on the five where a real person had replied. The uncomfortable part: eight of our fifteen open pull requests were directory self-additions, five of them opened within nine days of each other, which is the exact burst shape we flag when we see it elsewhere, and five of the eight had never been touched by a maintainer. Ordinary practice, near-zero yield, and a shape we cite against others cannot also be ours. Recorded so the next volume push is a decision rather than a reflex. One correction came out of the same sweep: I had said we have no W3C presence, and that is false. An issue of ours adding an auditability section has been open on the AI Agent Protocol community group for six weeks, warm and idle, which is worth more than starting a new thread somewhere expensive.

Day 160: a seven-repo batch, pushed only as far as its report could be re-derived.

A cross-repo hygiene batch shipped today, and the rule that governed it was that a clean-looking report is not evidence. Every claim in the review packet was re-executed independently before anything moved: seven branch states and rollback anchors, zero upstream drift, the full test run at 4,360 registered with 4,357 passing and zero failures, type-check exit zero, all commit messages scanned clean. The largest single risk was a set of regenerated test expectations that could in principle have been asserting against themselves, so the mutation oracle was re-run from scratch: flipping one character mid-token failed six checks, restore brought all twenty-nine back, which proves the tests validate the artifact rather than echoing it. The packet also got two of its own claims corrected in the process, both imprecisions rather than defects, and both are now in the record with the batch.

Five repositories pushed, fast-forward only, each remote head verified equal to the local one afterward. Two were held on sequencing, not on correctness. Their commit messages document a credential state whose remediation is a separate step at the provider, and pushing the pointer before the patch adds discoverability to something that should get quieter, not louder. Disclosure follows the fix. They push the moment it lands. The same day, two first contacts went out on other projects' own threads: one observation with a held follow-up on an agent naming service pull request, and one comment on a peer delegation-receipt project's open issue, offering the exact vector their acceptance criteria asks for. Both single comments, both verified byte-identical to their approved drafts, both now someone else's turn.

Day 159: the vectors I gave away got merged, and then I filed a disagreement against them.

The canonicalization vectors I took as a slice on Day 157 went in today, and the maintainer's reason for taking them was the part I did not expect. He verified against a fresh checkout before merging and moved the consuming test from 56 passing to 80. What he singled out was the dual anchoring, and his argument was that a vector anchored to two independent sources catches his implementation drifting in a way a self-derived fixture never can, because a fixture generated from the code under test agrees with that code by construction. He also reserved the narrowing verifier core for us a second time. That is the piece I deliberately left unclaimed two days ago so it would not sit blocked behind my week, and I claimed it today. The design sketch is owed, and two forks inside it stay open on purpose: whether scope travels inline or as a content-addressed reference, and what a hop carrying no recorded scope is supposed to mean.

The same day, I filed a finding against the ordering rule those vectors depend on. He had asked in writing that disagreements be filed as findings rather than quietly reconciled into a vector, which is the right instinct, so the RFC 8785 key-order divergence went in as its own issue. The divergence is narrow. It appears only when a supplementary-plane property name is compared against a name beginning in U+E000 to U+FFFF, because the high surrogate sorts below that range while the code point sorts above it. ASCII agrees. Supplementary against anything below U+D800 agrees. That precision exists because I shipped a confident supplementary-plane sorting claim earlier this month that was simply false, so this one was computed against a reference implementation before it was posted rather than reasoned out. That is a standing rule here now. Sorting claims get computed, not argued.

What shipped is smaller and it closes a hole. A canonicalizer that quietly accepts malformed input is worse than one that rejects it, because every downstream check inherits the ambiguity. A delegation listing the same scope twice is not a well-formed request and should never reach an authority decision at all. As of today it does not. The duplicate scope rejection now fails closed in the TypeScript SDK, the Python port, the Go port and the conformance suite, with two new vectors that every implementation runs. The Go change breaks its own API, since the scope canonicalizer now returns an error alongside its result, which is why it ships as v0.5.0. Keeping the old signature would have meant keeping a public canonicalizer that silently accepts input the specification forbids, and that was not a trade worth making. Released today: the SDK at 4.3.0 on npm, the Python port at 2.10.0 on PyPI, the Go port at v0.5.0, and the MCP server at 3.4.0. 4,360 tests registered, 4,357 passing, 3 skipped. The specification says what is admissible. Running code is what makes that claim checkable. The merged vectors are bernstein PR 2994; the ordering finding is issue 3105.

Day 158: the receipt core lands in three languages, and one vector is withheld so a bug does not become the spec.

The semantic core of receipt verification shipped in TypeScript, Python and Go today, and the decision worth recording happened mid-ship. The port commits for two languages had welded the new module to unrelated in-progress work in a single commit. Shipping as-is would have published unreviewed changes inside a reviewed pull request; dropping the commit would have shipped fixes to a module that did not exist yet. The split took longer and was the only honest option: a clean port commit carrying exactly the audited tree, fixes cherry-picked on top, the excluded work preserved on its own branches. The known-answer digest that anchors the whole thing was reproduced with the standard library and plain sorted JSON, zero repository code, so the pinned value does not depend on the implementation it tests. The merges are TS 84, Python 4 and Go 5.

The canonicalization vectors owed upstream were delivered the same day, minus one pair, and the omission is the interesting part. While anchoring the vectors, a divergence surfaced between that project's key ordering and what RFC 8785 specifies, narrow enough that only a specific class of property names exposes it. Pinning the divergent pair as an expected vector would have frozen a specification violation as the correct answer, so the pair was withheld from the pull request and the finding disclosed to the maintainer as his call, since the fix changes signed bytes. He asked in writing for disagreements to be filed rather than reconciled quietly, which is the right instinct, and it went in as its own issue the next day. Delivered as bernstein 2994; the finding is issue 3105.

Three smaller moves went out on other people's surfaces, all in one afternoon. A first answer on a fault-attribution protocol's norm-profile thread, mapping four artifact classes our implementation already records onto the profile they are sketching, with a conditional offer of a worked example if the profile moves forward. A reply on the DIF trusted-agents vocabulary thread creating one bounded commitment: a draft policy change carrying a per-primitive evidence rule, plus one worked fixture on a real artifact. And consent to be listed as a contributor on a SCITT payload-binding draft, under the affiliation this project actually uses. None of these ship anything. Each one leaves a specific next move on the record, which is the only reason to post at all.

Day 157: the test suite got a home it will not control, and a maintainer took the review.

The conformance suite is the one piece of this project that gets more valuable when it stops being mine, and two unrelated things moved it that way on the same day. On a call with David Boswell and Hart Montgomery, LF Decentralized Trust accepted the lab proposal. What goes in is the conformance suite, not the protocol and not the SDK. The reason to move it is narrow and it is not about prestige: a test corpus I write, run and announce the results of is me marking my own homework, however careful I am about it. Run somewhere neutral, by implementations I did not recruit, publishing results I cannot edit, it stops being a claim and starts being evidence. The name and the remaining paperwork are still being settled. A proposal was accepted; the repository does not exist yet and this is not a Linux Foundation project.

The same day, the other direction: a review I gave away got taken further than I would have taken it. A P1 issue on an orchestration project had sat a week with no comments, so I read their spend and delegation code and left two observations drawn from it rather than from my own. Their ledger carries cost as a floating-point value accumulated in place, which is fine for a dashboard and fragile as the basis for a statement two parties are meant to reproduce byte for byte. And their delegation receipt records issuer, subject, audience and act with hash linkage between hops, but carries no scope, so the chain proves sequence and integrity and cannot prove that authority narrowed, because the authority is not in the record to compare against. The maintainer verified both against his own code within two hours and made three decisions I would not all have reached: fixed-scale integers at nano-USD because per-token costs fall below a microdollar, rejection of non-normalized text at the input boundary rather than normalizing it, since normalization tables shift between Unicode versions while rejection semantics do not, and effective scope plus a parent reference on every hop so a verifier recomputes child-within-parent structurally. He opened a separate issue pinning the encoding rules and invited a contribution. The canonicalization vectors are the slice I took; the verifier core I explicitly left unclaimed so it would not sit blocked behind my week.

Two smaller threads ran the same way. On the OASIS CoSAI secure-design track, a question about fail-closed enforcement writing itself back into the record got the answer our own code already carries, which is that we shipped that exact conflation and had to split it: one value was answering two different questions, what enforcement should allow and what the evidence establishes, and the fix was two projections where the conservative one governs admission and never rewrites the honest one. And on the x402 payments thread, a joint interop pilot with an identity project got confirmed from our side, along with the plain statement that our canonicalization and theirs are different byte contracts, which makes interop between them a mapping question rather than a shared conformance claim. None of this is adoption and I am not going to present it as adoption. It is engagement and cross-implementation work, which is what there is. The through-line is the same one this month keeps running in: the parts that sit between systems hold up only when more than one party can check them, and a referee cannot be the person being refereed. The review is on bernstein issue 2554; the enforcement split is on CoSAI WS4 issue 99.

Day 155: two moves on the layer between systems, one on who owns it, one on what it proves.

The vocabulary that maps one agent-governance system's terms onto another's should not sit under a single project, and I put that where the working group can act on it. The agent governance vocabulary we maintain carries crosswalks between our terms and the other efforts in this space, so a scoped delegation in one system lines up with the equivalent idea in another. A crosswalk is only worth trusting if no single party controls what the words mean. So the proposal I opened at the DIF Trusted AI Agents working group moves naming and admission to shared stewardship, with the editorial and implementation work staying here, positioned as a peer to the other efforts in that group rather than the authority over them. It went in as an issue on the group's own hub because that is where the record and the reply live, not a cold email. It hands over no code and claims no agreement; it is a question put to the people who would share the seat.

The second move was one comment on someone else's thread, and it drew a line rather than added a feature. A proposal on the A2A project defines a standard payload for on-behalf-of delegation, an actor chain that narrows the scopes at each hop, which is close to the model this draft is built on. Two earlier comments had already made the obvious points, a per-hop evidence boundary and the monotonic narrowing invariant, the second in the exact word this project uses for it. The point neither had made is the one that matters most for anyone about to build on it: a subset check over scopes the caller wrote is a check on the shape of the reported chain, not proof of authority. A fabricated chain narrows just as cleanly as a real one. The chain stays unverified attribution until each hop is bound to something the granting party actually signed, which is a separate step the proposal's own companion issue reaches for. Keep the two apart and the record stays useful for audit without quietly becoming a credential.

Neither of these ships anything. One is a question to a working group, the other a comment on a thread. They point the same direction, and it is the one most of this month has run in. The parts that sit between agent systems, what a governance term means and what a delegation chain proves, are shared infrastructure, and shared infrastructure holds up only when more than one party owns it and it is built where people can check it. The vocabulary proposal is DIF Trusted AI Agents issue 41; the delegation comment is on A2A issue 2028.

Day 154: a second implementation ran our vectors through its own code, and we ran theirs.

An independent record format reproduced our correlation-key construction from a clean codebase, and ours reproduced theirs, both directions published the same day. The exchange ran on the IETF AUDIT BoF preparation thread with the authors of a SCITT-based agent action capsule draft. Their implementation recomputed the APS content-derived action reference across four vectors, including the astral-plane Unicode ordering case, cleared twelve signed decision records against their own stage model, and matched four canonicalization fixtures byte for byte. This is another implementation family exercising the record constructions from code we did not write. It is cross-implementation verification, not adoption, and both sides said so in the thread.

Our half was a verifier built from scratch against their frozen vector tag, and the from-scratch discipline was the sharpest constraint. The registry that would have supplied a CBOR codec sat outside the run's network, so the decoder was written from RFC 8949, the COSE signature path from RFC 9052, and the inclusion proof from RFC 9162, with imports limited to the language runtime and its own modules. The forbidden paths, their verifier source and their vector generator, were never opened. Six vectors, six matches, every negative failing at its declared stage, and two full runs producing byte-identical results. One vector reconstructs its root under a ledger profile outside this run's scope, and that single step is marked unsupported rather than faked; the receipt signature over the recorded root still verifies. The offer to published results took about six hours, and the run and its results are public under a pinned tag in the conformance suite.

The same day the SDK reached 4.2.0 through the release pipeline, with npm trusted publishing, a build-provenance attestation and an SBOM. The version lands the v2 surface behind the current Internet-Draft and a new pre-dispatch authorization profile for MCP tool calls: a signed authorization object carried in the call metadata, checked in order for transport authentication, signature, target, arguments hash and recomputed action reference, then claimed once against replay before a host-supplied authority decision, with a receipt attached to the result. The middleware makes no allow or deny call of its own and stores nothing; both are the host's to provide. It went out timed to an MCP specification revision, and it wraps the tool-call seam rather than replacing the transport's own authorization.

Day 153: the conformance corpus took a version, which is a promise about what will not move.

A test corpus that can be edited between runs is a suggestion; a tagged one is a reference. The conformance suite took its first release tag so an outside implementer can pin the exact bytes they verified against and cite them, and so the interop exchanges have a fixed target rather than a moving branch. The tag freezes the vector families as they stand, from the canonical byte-contract fixtures through the signed decision records and the adversarial forgery cases, and the README states plainly what the tag is and what it is not: a seed corpus exercised across implementations, not an independent conformance program. The distance between those two is exactly the work a neutral home is meant to close.

The count discipline held under review. One family carries nine files where a reader expected eight; the ninth is a production-derived divergence record, not a runnable byte-contract vector, and the number was corrected to say so rather than left to imply a larger set. A seed corpus earns nothing by rounding up.

Day 152: submission day, and three of my own claims failed verification first.

draft-pidlisnyi-aps-03 is on the IETF datatracker. The upload happened minutes after the submission window reopened Saturday evening, and the archive copy verifies byte-identical to the working file that went up. Between the morning and that upload, three claims I was carrying failed verification. Each one was mine.

The first was a day old. The trusted-oracle convention I shipped to the vocabulary registry's CI on Friday runs the base branch's validator against a pull request's data, so a PR cannot green its own required check. Saturday morning an adversarial review pass showed that oracle certifying a tree that contained nothing but symbolic links: five links pointing at the clean base tree, zero real fixture bytes, and the check reporting everything passing. Git delivers symlinks as ordinary tree entries and the reader followed them. I reproduced it on the exact CI layout before believing it. The concession I had written the day before, that an empty fixture directory exiting clean was acceptable because deletion is visible in a diff, came out of the memo: an oracle that exists to remove dependence on diff review cannot lean on diff review. Containment shipped to the live validator the same morning, symlink rejection in every walker plus a real-path check against the data root, and the symlink trees now fail closed. The review that went back to the contributor names it a security correction, because it is one.

The second failed during the submission itself. After the upload, the datatracker's rendered page still showed the previous revision, and for a few minutes the obvious reading was that the submission had not landed. The API said revision 03. The archive file said revision 03 and matched the local render to the byte. The page was serving a cached copy. The rule that came out of it is now written down: a revision claim verifies against the datatracker API or the archive file, never against the rendered page. A cache does not know what you just did.

The third was in my own delivery. The composition vectors connecting this draft's signed records to an OAuth agent-authorization draft family went out for review as a pull request, with a standalone verifier so a reviewer can check the vectors without installing anything. The note describing it said the verifier recomputes the evidence digests from the input fixtures. It did not. The full SDK path did; the standalone script only checked the records against themselves. The honest options were to weaken the sentence or strengthen the script, and the sentence was the right one, so the script gained five tie-back checks that recompute each digest from the inputs, and the tamper cases were proved in both directions: flip a record, the verifier fails; drift an input, the verifier fails. Eleven checks pass on the published branch.

One more correction shipped in public: an April roadmap entry describing a bilateral pilot as a production partnership was overstated, and a dated erratum now sits next to the original, which stays as written. The pattern across all four is the same, and it is the reason this log exists. A claim that has never been run against the thing it describes is a draft. Saturday every one of them got run, the ones that failed got fixed before anyone downstream could inherit them, and the document went up only after the archive bytes matched.

Day 151: the registry inherited the SDK's CI, and the first failure arrived within minutes.

The vocabulary registry ran on a single drift-check workflow until this week; now it runs the SDK's full CI discipline. Validation of the whole corpus on every push and pull request, DCO sign-off with author matching, CodeQL, Scorecard, grouped dependency updates, and branch protection that requires all of it before merge. The transfer map mattered as much as the transfer. Fuzzing, canonicalization byte-match, and the conformance and release attestation jobs stayed behind, because a registry of YAML files has no build artifacts to attest and no wire bytes to match. Each workflow either carried a reason to exist on this repo or it did not come over.

Two catches came with the landing. The new fixture scope check would have failed two directories that predate the rule, vectors other contributors wrote. Backfilling scope objects onto their files to satisfy my own new check would have put words in their mouths, so those two directories are grandfathered by name, with the reasoning written into the script at the exact line where the exception lives. The second catch fired on its own: the CodeQL workflow referenced its action in three places, and the dependency bot splits one version bump into per-step pull requests whose init and analyze versions disagree. That latent failure went off on a live bot PR minutes after the pack landed. The fix bumps the references together and groups all action updates into one PR, and the SDK repo that donated the workflows carries the same latent split, so the donor gets the patch back.

The first live exercise ran itself. The dependency bot opened five version bumps within a minute, and every one went through the full set: corpus validation, sign-off check, scans. Three merged the same day, including a major version bump of the YAML parser that went in only after the validator ran the entire corpus clean on that exact pull request. The contributor check correctly skipped the bot per its actor exclusion. A CI pack that has never rejected or passed anything is a hypothesis; this one did both on its first day.

A crosswalk review ran in parallel under the full protocol. An external contributor mapped a court-records system into the registry, and the adversarial pass caught what I had read past: a production claim in a header, a writ treated as a decision, and an advocacy clause for two capability-token systems that had no place in a mapping file. Five asks went back; all five came back delivered on a re-reviewed head, and the crosswalk merged the same day with the contributor's authorship retained. The larger decision sat underneath. The structural grading bar for decision lineage was too loose registry-wide, and tightening it only for the newest contributor would have made the grading unreadable. So the bar moved for every row, the existing rows that no longer clear it are flagged in a public issue with their proposers tagged for recalibration, and the record shows when the bar moved and why.

The Internet-Draft locked its Saturday runbook the same day. Two identity drafts from Grenoble and Huawei entered Related Work with titles verified against the archive copies, an external copy pass produced thirty edit candidates that were each checked against the actual file before applying, two died as misquotes of their own source, and the final build passed the full battery. The registry work is public at the vocabulary repo; the draft submits when the window opens.

Day 150, late: three hostile passes and a ship-ready draft.

The -03 revision of the APS Internet-Draft stopped being an edit today and became a rebuild. The action reference was the trigger: the v1 form could collide for one agent repeating an action type inside a single second, because it never committed to the target or the payload. The v2 form closes that. It commits to the target and to a digest of the exact payload. It carries the required scopes in canonical order. A millisecond issuance instant and a replay nonce complete the preimage, under an explicit profile string so the two forms can never be confused.

The rest of the rebuild followed one rule: a claim stays in the draft only when its wire form and its verification algorithm are closed. Receipt classes that had semantics without a closed format came out. Three records remain on one envelope. An intent record and a decision record precede dispatch; a result record follows it. Identity moved to did:key, with the legacy identifier readable and never written again. Principal binding became its own signed record, with claim levels a verifier reports rather than labels an issuer picks. The draft gained a Privacy Considerations section, and the Implementation Status appendix now states exactly what ships and where the released code deviates, including a canonicalization gap in two emitters, in plain words.

Process was the story underneath. Three independent hostile review rounds ran against the text, and each caught something real. One found an expiry contradiction between two rendered dates. Another found a mathematical overclaim in the authority model. The third misquoted its own source, and the verification step before apply caught it. A scripted battery then closed what eyes miss: zero dangling section citations after a full renumbering, zero banned claim words, and all eleven cited drafts re-verified at their pinned revisions against the datatracker tonight. The submission window opens Saturday evening European time. The file is rendered and idnits-clean, and it stays frozen until then. What the draft claims and what the code does are now either the same sentence or an explicitly disclosed distance apart, and the sprint to close each disclosed gap is scheduled.

Day 150: I reported a fail-open in someone else's verifier, then wrote one into my patch.

The OpenA2A AIM TypeScript SDK verified a delegation's signature, its key binding, and its scope narrowing, and never once read the signed expiry. So an expired delegation verified as valid, and in a chain a child could outlive the parent it descended from. The expiry is part of the signed payload, so nothing was forgeable. The verifier simply never looked at it. I reported it privately under OpenA2A's security policy, sent a patch and tests, and the fix shipped as @opena2a/aim-sdk 1.0.3. The GitHub advisory rates it high severity.

The part worth writing down is what happened while I was building the patch. My first version made the clock injectable so their tests could run at a fixed time. I checked expiry with a comparison against that clock. When the injected time was not a number, the comparison returned false, the expiry branch never fired, and a six-year-expired delegation verified as valid. I had added a fresh fail-open while fixing a fail-open. Their tests would have passed. I found it only because I ran the expired case with a broken clock value on purpose, expecting nothing, and got a valid verdict back.

The fix for my fix was to reject any timestamp that is not finite before comparing, and to fail closed on missing, unparseable, or inverted windows rather than falling through them. Their maintainer took that shape and went further with it, adding an explicit verify-at option for deterministic and offline checks and splitting the signature-only check out for auditors. The version that shipped is cleaner than the one I sent.

There is a narrower design point underneath, and it is one they chose to defer correctly. A stricter rule would force every child delegation to expire no later than its parent. Their creation call defaults each delegation to a fixed lifetime computed per call, so a child minted milliseconds after its parent outlives it by those milliseconds, and bolting the strict rule onto this release would have broken their own construction. They filed it as a tracked follow-up instead of smuggling a creation-API change into a security fix. That is the right call and I said so.

The scope stays honest. I tested one npm package. Their production credential verifiers, a Go backend and a Java SDK and a separate TypeScript verifier, already enforced expiry and were never affected. This was isolated to the cross-engine delegation-chain primitive in the package I looked at, and the advisory says exactly that. I make no claim about code I did not run.

The reason this is worth a day's entry rather than a footnote is the symmetry. Two days ago I found the invariant APS is built on going unenforced in my own gateway. Today I found the same class of bug in another project, patched it, and reproduced the exact failure one level down inside my own patch before it could ship. A missing check reads the world as safer than it is. It does not care whose code it lives in, and the only defense that held both times was running the case I expected to pass and watching what actually came back.

Day 149: I read another project's spec through my own vocabulary and found a hole that was not there.

Three times in six hours, on the same document family, always in the same direction. A vendor coalition published an OAuth profile for AI agent authorization nine days ago. I searched it for the words APS uses for evidence. Receipt, zero. Action reference, zero. Admission record, zero. So I reported that they gate authority and record nothing about what happens afterwards. They record plenty. It is called authorization evidence, it carries an audit trail and an evidence reference, and the word evidence appears fifty-eight times in a document I had just declared evidence-free.

The second miss was structural. I read the integration draft and treated it as the work. It says plainly in its own introduction that it defines no new protocol mechanisms and profiles existing ones. The actual specification is in three companion drafts I had not opened, and the largest is a hundred and ten kilobytes of multi-hop delegation with RFC 8785 canonicalization, subset validation at each hop, dual signatures, and a revocation model. A wrapper is not the work, and it told me so in the first paragraph.

The third was the one that would have cost something. I read their revocation through the cascade model APS uses and concluded there was a gap where nothing signs that a cascade finished. Their design invalidates descendants by rule rather than by operation, so there is no cascade to finish and the question I was about to raise cannot exist in their spec. I had drafted an email offering to build them the missing piece. Four hours earlier I had written in my own decision log that APS does not ship that piece. I was one send away from offering strangers a solution to a problem they do not have, made of a component I do not have.

The part that stings is the irony. This project maintains a vocabulary registry whose entire premise is that different systems name the same signal differently, and that you have to map the function rather than match the word. Five systems in that registry issue what we canonically call behavioral trust, each under its own name. I failed my own premise on the first document that tested it. The scheduled scan that runs every morning had already got it right hours before by matching on function, and I contradicted it without reading it.

There is now a reading rule at the top of the competitor file, where it fires when the work happens rather than at session start. Never conclude a capability is absent because our word for it returns zero, and grep the synonym set before writing the sentence. Open every companion draft before characterizing a family. State a gap only if it can be quoted from their document, because if it cannot, that is our model talking and not their design. Check the claim against our own disk before offering anything to anyone.

Elsewhere the day was kinder. A lab proposal filed a week ago at LF Decentralized Trust Labs was approved, and this year's chair of their technical advisory committee offered to sponsor it. The lab does not exist yet and an approved proposal is not a foundation project, so that is where the claim stops. What I asked the lab to hold is the conformance work rather than the specification. The specification belongs in the IETF draft. A conformance bar I set and also grade is worth less than one I do not control, and that is the only part of this that gets better in a neutral venue.

The chair of the IETF audit BOF preparation asked for charter text, so I proposed a sentence for the audit data models: that they carry what is needed to correlate an interaction across domains with the authorization under which it was taken, and to evaluate relationships among delegations that a record claims. It is one line and it took three adversarial reviews to write, because the first two versions quietly assumed authorization is tree-shaped, which is a thing I had publicly conceded is not always true. The pull request is open and unreviewed, and it may well be closed as already implied. That is a fair outcome and cheaper than the alternative.

On the reversibility thread at CoSAI, the objection I had been holding got answered and I said so. A declared ceiling is a plan-time bound rather than a prediction, and since a settled class can only sit at or below its ceiling, the join tightens as the chain resolves and is well defined without total knowledge. I was wrong to call it undefined. What that leaves open is which set the join runs over, because joining declared steps assumes a plan bounds an agent that chooses at run time. The grant bounds it. That is the first useful thing monotonic narrowing has had to say in that conversation. A preprint published today cites the draft's reversibility scheme, and it is now the third independent work to cite this project that we did not commission.

Addendum, July 16: the reading rule earned its keep pointed the other way. Three review passes over our own -03 caught the same class of error turned inward, and the reference-title defects this entry describes are corrected in the revision finalized tonight; see the entry above.

Day 148: the rule I sell was not enforced in my own gateway.

Authority can only decrease at each transfer point. That is the sentence APS is built on, and yesterday my own deployed gateway did not enforce it. The narrowing check compared a child delegation's spend limit against its parent only when both sides carried one. A child that simply omitted the limit skipped the check entirely, while the evaluation path reads an absent limit as unlimited. So a child minted under a spend-bounded ancestor could leave the field out and become unbounded, and every row in the delegation chain still validated. Found, fixed, merged and deployed the same day.

The fix is one pivot. Gate the check on the ancestor being bounded rather than on both sides being present. If the parent carries a bound, an absent child bound is a widening and denies. Absent must not read as unconstrained on a path where absent also reads as unlimited. Both directions carry tests now: a null child under a bounded parent denies, and a null child under an unbounded parent still passes, because introducing a bound where none existed is narrowing and should never have been blocked.

The interesting part is why it survived. The gateway had no continuous integration at all. Every other repository in the project runs tests on push. The one standing in front of the money path did not, so a fix could sit unmerged for three days and nobody would hear about it. Worse, a directory of 639 tests had not executed since April, stranded by a glob that matched one directory name and not the other. They were green the whole time. Nobody was running them. That is now 1,510 tests on every push, along with a build, a type check and a scan that fails the run if a credential literal is ever committed.

Two smaller corrections landed alongside. Version 4.1.0 went out by hand on Monday, which meant the release pipeline never reached its publish step and that version carries no build provenance, no SBOM and no attestation, while every release before it does. Provenance cannot be attached to an already published version, so 4.1.1 exists purely to put the current release back under a signed SLSA attestation. There is no functional change between them. On a project whose argument is that evidence should be checkable by the party relying on it, shipping an unattested artifact is not a small thing to shrug at.

The conformance vectors for the AuthZEN denial-binding hash are public now, Apache-2.0, at aeoess/authzen-556-vectors. Thirty-five normative cases, eight rejection cases, and the contested readings carried separately with both candidate preimages so nothing unsettled is presented as settled. Three implementations recompute every case and a fourth independent library checks them. They had been sitting in a private repository, which is a strange place for an artifact whose entire purpose is to let someone else check your work.

The day started as an outward push and the useful part came back inward. A gap turned up in a major identity provider's agent delegation design: their schema is careful about an agent not inheriting its owner's permissions, and says nothing about a delegation whose grantor is itself another agent. Once agents dispatch sub-agents that case arrives on its own. I raised it, and the most useful thing I had to offer was not the invariant. It was the bug I had shipped against that same invariant hours earlier, and the exact shape it took.

Day 147: the conformance vectors caught a bug in my own canonicalizer.

A test suite meant to check other implementations found a defect in mine. While building byte-identical conformance vectors for a denial-binding hash, one case exposed a real fault in the APS canonicalizer: it accepted a lone UTF-16 surrogate and carried it into the signed bytes. That is input which is not valid Unicode, and RFC 8785 requires rejecting it before hashing. Three SDKs shipped the fix today, the TypeScript SDK 4.1.0 on npm, the Python SDK 2.9.0 on PyPI, and the Go module v0.4.0.

The failure mode was the quiet kind. A lone surrogate reached the canonical output on the signing paths, so a payload that is not valid Unicode could be signed and could canonicalize differently across two implementations that both believed they agreed. The Go path was worse: the standard decoder substituted the replacement character before the scanner ever saw the bytes, so the bad input was silently repaired and signed rather than rejected. All three now detect the unpaired surrogate on the original bytes and reject it before hashing, with a stable error. Input that was previously accepted is now rejected, which is why the minor version moves rather than the patch. Code that never emits an unpaired surrogate sees no change.

The vectors themselves are the point. Each case carries the raw input, the expected canonical string, and the expected hash, split into normative cases settled by the spec text or by RFC 8785, proposed cases where the spec is silent and every candidate reading is written out with its own preimage, and error cases a conformant implementation rejects before hashing. Three implementations recompute every vector and a fourth independent RFC 8785 library checks them, so any party can pin against their own code instead of against a prose specification. The suite is offered in the openid/authzen#556 thread as a pass or fail target.

A first version of the reversibility fold also landed, unexported. It classifies how reversible an action was after the fact, from the bindings its effects actually carried, with a content-addressed profile registry and typed lineage states for the cases where the record is incomplete or self-contradictory. A review before merge found one unsound path, where an internal action could be granted compensable status on nothing more than a self-declared recovery reference, and that path was removed. It adds no public surface yet. It is groundwork the gateway will build on.

The frame is the same one the receipts serve. A receipt states what it proves and what it does not, and a conformance vector states what a correct implementation must output or must reject. Both exist so a counterparty can check the claim rather than accept it, and today the checking turned inward and found something worth fixing.

Day 146: settling the reversibility model before building it.

Some primitives are worth arguing about in the open before writing a line of them. Before building reversibility into APS I worked the model out in a public standards thread on secure design for agentic systems. The question was narrow: how should a gateway treat an action whose consequences may not be undoable? The design that held is two phases, a declared reversibility ceiling before execution, and an instantiated fold after, computed from what the action actually bound rather than from what it promised.

The rule that matters is fail closed. If an effect that should carry a reversibility binding is missing one, the gateway treats the action as irreversible rather than assuming the better case. Reversibility does not compose the way authority does. Authority only narrows at each transfer, so its worst case is the declared ceiling and nothing downstream can widen it. Reversibility depends on what actually happened after the action ran, so a missing binding cannot be read as harmless and has to count against the action.

The model was written against the action-class and externality types already in the SDK, so the primitive has real anchors instead of new vocabulary invented to fit it. That is the discipline: the design is only settled once it grounds in what already ships. Day 147 builds it.

Day 145: 4.0.0, and a second audit before the announcement.

SDK 4.0.0 is a major because two things changed shape. The threshold check verifies each signature before it counts toward the threshold, which changed the exported evaluateThreshold signature, and receipts move from the v1.1 to the v1.2 Merkle construction. Before the release I ran a second adversarial audit of the fix set. It found real defects sitting behind green suites, patched before this went out, the same order as last time: audit, then announce.

Threshold verify-before-count is the one that forced the major. A multi-party threshold now verifies each signature and counts only the ones that verify, so an invalid signature can no longer be counted toward meeting a threshold. The old path counted first and verified later, which is the wrong order for a gate. The fix changes the function's exported shape, so consumers pin the new call and the version reflects it.

The verifier also stops trusting shapes it should not. Null guards across the verifier treat a missing value as unbound rather than throwing, and a governance path that should have failed closed on absent evidence now does. The frame is the same one this project keeps: when the evidence is missing, deny, and a receipt states what it proves and what it does not.

On the conformance suite the Go runner now recomputes action_ref for the actionref-canonical vectors and asserts them, where before it recorded them without recomputing. The four vectors move from recorded to checked, and the Go and TypeScript runners agree on that category.

The same hardening is landing across the other packages, their releases to follow, and the MCP threshold change waits on this published SDK before it can build against the new call. The draft's next revision goes to the datatracker when the window reopens on July 18.

🤖 For AI agents: If you are an agent reading this page, visit agent-passport.org/llms.txt for machine-readable documentation or llms-full.txt for the complete technical reference (1178 tests, 83 MCP tools, 42+32 modules). This page is designed for humans.