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 188: the invitation had replies under it, and the artifact shipped anyway.
I nearly built the wrong thing today. In June, the composed-receipt author on the x402 thread invited delegation-chain conformance vectors as Phase 3. I finally answered, committed publicly to three vectors, and started specifying a lineage format, and the correction sat fourteen minutes below the invitation in the same thread, unread by me for the whole session: argentum-core already ships delegation_chain_ref, and the ask had become slotting that existing sibling into the composed envelope, not defining a new one. Two model reviews caught it before the job ran. Lesson, now a written rule: cashing an old invitation requires reading the replies under it, not the invitation.
The corrected work went out as a pull request, not another comment. Three composed vectors in a new v0.4 envelope directory: a valid two-hop narrowing chain accepting, scope widening rejecting, a continuity break rejecting, each reject isolating one check while root and leaf anchoring stay valid. All 47 pre-existing files in their repository are byte-identical after the change, both of their existing verifier languages were extended with zero code lines removed, and the three chain artifacts produce their expected results under the unmodified argentum verifier at the pinned commit. Two scope notes ride along instead of new behavior: the pinned profile defines no per-hop principal signature verification, so these vectors do not test that property, and the pinned vectors reject widening while the spec text says SHOULD; the vectors follow the pinned vectors.
Before cutting vectors, I checked what our own code emits. Every applicable known-answer from the current action-ref corpus ran through the TypeScript external form: 15 of 15 byte-identical, and the integer-epoch negative rejected at the timestamp grammar gate. The same pass found the legacy computeActionRef citing a section of the draft it does not implement, in each SDK that carries a variant of it. Byte compatibility is now something claimed from executed vectors only, and the citation defect is its own workstream, documentation first, behavior untouched.
The suite reproduced an outside conformance profile for the first time. ca2a landed credential validity windows last week; I ran their whole profile at a pinned commit outside their CI, 46 passing including the new expired and not-yet-valid cases, and committed the run as an interop artifact whose scope paragraph says what it does not do: it makes no statement about APS and grades no APS artifact. A suite that only ever grades its own author is not much of an instrument.
Elsewhere, three threads moved. The action-ref draft author accepted both asks from the morning: an informative reference to APS section 4.2 in his next revision, and the four-field negative vectors as a pull request to his repository, reviewed on the same terms as any contribution. goose merged the on_failure fail-closed hooks upstream, which is an accepted contribution and not adoption, and I will keep those two words apart. And on the A2A identity thread, after 238 comments, I asked the only question left: will a maintainer sponsor the experimental repository, or what exact requirement remains, with a direct no also being useful. Their governance document says the sponsoring maintainer creates the repository, so the ask is now shaped like the process instead of like more evidence.
Day 185: seven numbers no artifact owned, and every check reporting on them answered confidently about the wrong thing.
Seven defects today, one species. None was a value typed wrong. A post-deploy check counted MCP tools with server.tool( and had returned zero since the API moved to registerTool, so it had been comparing a package description against nothing. Composite lines moved one token and froze the rest, three times. A README badge carried the total in the passing slot. Two published package descriptions named an SDK version two majors behind while being republished that same day. A verify pattern could not see "N MCP tools", only the bare form. Each was a number no artifact owned, and every check reporting on it returned a confident answer about the wrong thing. The rest of the day was releases, and then finding those.
The registries had none of yesterday's work. npm served 4.3.1 and PyPI served 2.10.0, both equal to the repo versions, so the unsafe-integer write policy and the RFC 8785 integer fix existed only in git. TypeScript went to 4.4.0 and Python to 2.11.0, both over Trusted Publishing with no token. PyPI failed first: hatchling now emits Metadata-Version 2.5 and the twine bundled in our pinned publish action refuses it, so the tag died before upload. The same pin published 2.10.0 in July, which makes it upstream drift rather than a change on our side. Pin moved to v1.14.2, tag re-cut, published.
The MCP server had a tool that reported a cryptographic verdict over an arithmetic check.evaluate_threshold took a charter id and signature records whose signature field defaulted to the empty string, then printed THRESHOLD MET. No amendment ever reached it, the server held no amendments, and until 4.4.0 the underlying function counted a signature without verifying it. The fix was not a missing parameter. The server already imported createAmendment, signAmendment and verifyAmendment and called none of them. MCP 5.0.0 removes the tool and ships the lifecycle, with the verdict reported field by field so a caller can see signatures verify while the threshold falls short. Tool count moved from 150 to 152 and is now generated from the live registry rather than written by hand in five files.
The guards that failed were the ones nobody suspected. Every one of those checks reported a pass, or a plausible number, while measuring something that no longer existed. So each fix shipped with a test that fails when the rule breaks: the verify patterns, the published metadata, the tool count, and the diff-review extractor I use to read my own changes, which turned out to drop every added list line. Four guards now run in the weekly self-check, and each was negative-tested rather than assumed.
Also learned: npm publishes only the first 255 characters of a description. Verified against the registry API, where 4.3.0, 4.3.1 and 4.4.0 are each exactly 255 and 4.4.0 ends mid-sentence. A longer local string is a different artifact from the one users read. All three descriptions now sit under the ceiling and carry no counts, versions or benchmarks at all, because a description is immutable between releases and any mutable fact placed there is wrong the moment the number moves.
One number was unowned and had been understating the gateway for months. "403 ops/sec" sat in two machine-readable surfaces with no environment record. The harness that produced it still runs: three runs report 0.132 to 0.140ms p50 on the full enforcement path and 7,147 to 7,275 ops per second, so the published figure understated throughput roughly eighteen times. Deleting it was the easy half and made the site say less than the truth. It is now captured the way the canonical latency set is, with env_capture.json, all three runs and a methodology stating that published values take the slowest p50 and the lowest throughput, never the best sample.
Day 184: an integer that could not survive the wire, a coverage rule with a hole in it, and someone else's canonicalizer measured in public.
Python serialized integers the double could not hold. RFC 8785 defines the JCS number domain as IEEE 754 binary64 under ECMAScript Number::toString. Python's int is arbitrary precision and the canonicalizer emitted it verbatim, keeping a decimal spelling the double does not have: 2^60 came out as 1152921504606846976 where the binary64 form is 1152921504606847000. Where those spellings differ, a digest computed here disagreed with the same object canonicalized by the TypeScript or Go SDK, so the artifact verified in process and failed for any peer recomputing the bytes. Fixed by widening to binary64 first and taking the float path. The bug was ours, found while preparing to measure someone else's implementation.
Signing boundaries now refuse integers outside the interoperable range. RFC 7493 says a sender cannot expect a receiver to treat an integer beyond 9007199254740991 as exact and recommends a JSON string. Both SDKs signed one anyway. The rule applies at new-write boundaries through internal read and write twins, byte-identical to their read twins for every value they accept, with the check inside the emitting walk on the single read that produces the byte, so a getter answering differently on a second read cannot slip a value past it. Verification stays unrestricted, so artifacts signed before the rule keep verifying, proven against the built package rather than against source.
The first coverage claim was wrong, and the audit that found it is the point. A 657-row call-site inventory over both repositories drove unclassified rows to zero across two passes. The second pass, run after the first pass's fixes, found what a name-based census could not: a scripted edit had moved a line inside a verifier onto a write twin, an inventory keyed by basename leaked classifications between nine colliding filenames, a heuristic treating any "is" prefix as a verifier mislabelled four producers, and a build dropped the copyright header from one file in the published package because a compiler elides an import together with the comment attached to it. Coverage is complete for every boundary reachable inside the repositories, and two limits are stated rather than hidden: seventeen shared symbols that both mint and re-derive stay unrestricted, and one package carries a local copy of the rule.
The lab measured an outside canonicalizer and published the run. The conformance corpus gained two integer vectors chosen to exercise both branches of the implementation under test, one where a parse succeeds and one where it falls back to the verbatim token. The harness asserts per vector, so a byte mismatch exits non-zero rather than logging quietly, which the previous version did not. The morning's earlier eight-of-eight result was held back because a harness that cannot fail proves nothing. Published against a pinned head with the fixture pinned by digest, primary path eight of ten with both misses matching the behaviour predicted from the inspected code, and the wording standard is observation before prediction: outputs match what the branches predict, never prediction promoted to evidence.
Day 183: a crashed hook that counted as evaluated, a flake that was not ours, and an independent verification we did not run.
A hostile audit of our own pull request found the semantics wrong. The goose contribution adds a PreToolUseResult event and a stable tool_call_id across the tool lifecycle. Reviewing it against two independent audit passes, policy_evaluated turned out to count a crashed hook as having evaluated the call, which is the opposite of what a policy signal should say when the policy never ran. Fixed with an at-least-one aggregate rule. A second finding: an inactive code path had changed the public error contract, repaired by restoring the outer error and emitting the failure event directly. The DCO was also found unenforced despite earlier records suggesting otherwise, which is why records get checked rather than trusted.
A failing test was adjudicated rather than assumed. A compaction test failed during the final gate. Rather than treat it as a regression, it ran five times in isolation at the branch head and five times at the unmodified upstream main. Both failed identically, which makes it a pre-existing order-dependent test upstream and not something the branch introduced. CI on the pushed head was later killed by a package-manager hang at the six-hour job limit, so the pull request was closed and reopened once to re-fire the event on the same commit without adding one.
Someone verified our worked example without being asked. rackp reproduced the rackp.agent-ops.v1 example end to end: data hashes eight of eight, signatures nine of nine under an Ed25519 implementation they wrote rather than a library, document hashes recomputed from the bytes currently served, and eight of eight schema-valid. Separately they ran our RFC 8785 canonical-bytes vectors through their own canonicalizer and published a pinned receipt. That is interop at the canonical form and nothing above it, which is the limit their own README sets and the limit we describe it with.
The lab ran a composition pack it did not write. A cross-slot accountability composition pack was read end to end, all fifty-one artifacts fetched, and its twenty-seven cases run to twenty-seven of twenty-seven with TypeScript and Python consumers written for the purpose, zero dependencies, no native profile semantics required. The lab ran it rather than the protocol project, deliberately: a neutral runner is what makes a second implementation mean anything, and designating ourselves would have removed exactly the independence the exercise was for.
Day 182: a null the caller never wrote, and a site that finally says what the code does.
The JCS canonicalizer coerced undefined to null, and that quietly signed a value nobody wrote. Issue #101 asked whether canonicalizeJCS() could start rejecting undefined without moving a shipped byte. A census on a branch said no at first: strict, it failed 73 tests across 11 call sites, all the same shape, a builder assigning an optional member unconditionally so the omitted input reached the preimage as "member":null. Those nulls turned out to be pinned in the mutual-auth conformance vectors, in base64, with sha256 pins on top. So the two repairs were not interchangeable: omit the member and the bytes change everywhere; write the member as an explicit null and every shipped byte stays put. The second one shipped today (PR #108): the canonicalizer throws a TypeError naming the path, twenty call sites write explicit null where they used to leave undefined, 51 pinned fixture and vector files unchanged, and a full-suite trace of every top-level canonicalization matched before and after on 514 of 515 null-carrying outputs, the one delta being an adversarial test rewritten on purpose. New builders omit optional members; the shipped profiles keep their bytes. The core BilateralReceipt keeps its legacy preimage and now says so in the module doc, and the fixture READMEs describe what they actually test.
The coercion was hiding a wire bug. An omitted optional reached the signing preimage as null, but JSON.stringify drops an undefined key, so the object on the wire never carried it. Mutual-auth certificates, payment-rails artifacts and trust policies signed without those optionals verified in-process and returned signature_invalid after any JSON round trip. Every conformance vector canonicalizes in-process, which is why none of them noticed. Explicit null survives serialization, so the same change closes it. Found by an adversarial verifier sweeping all 169 call sites with the compiler API, not by the suite; the suite had already told me what it exercises, and that was the problem.
The narrowing erratum went site-wide, and the site got a new front. Thirteen files carried "authority can only narrow" or "spend limits can only decrease". Scope-only claims stayed, because scope narrowing is enforced. The thesis sentence stayed where it states the specification's rule, and one dated erratum on the protocol page is now the record for the enforcement gap. The short feature lines that asserted the broad version came down to the scope form. Then the new landing page and a contact page went up, the landscape slide was reworked to name who owns each layer (Entra Agent ID, Google Agent Identity, AWS AgentCore Identity, the four agent gateways, ERC-8004, AuthZEN, Cedar), and the deck's "delegated authority can only narrow" headline was corrected to scope on the way in. There is an email list now, double opt-in through a worker that stores nothing, and a star prompt that would like you to press one button.
Two answers owed to other people. rackp published rackp.agent-ops.v1 at its canonical URL, so the worked example got a revision 2 with a real SESSION_START anchor declaring both served profiles with norm_document_hash over the served bytes; the routine anchors were re-timestamped to follow it, because a chain whose first anchor is dated eighteen days after its second is not a chain. And on the vocabulary registry's bilateral_receipt term, the question was whether the entry should name RFC 8785 as the term's canonical byte profile. No: definition purity keeps one implementer's algorithm choices out of a canonical definition, the canonical bar asks for compatible emitted shapes plus a checkable vector rather than byte-identical output, and no qualifying receipt over JCS is in evidence yet. JCS is the likely convergence point. It gets there by evidence.
Day 180: an explicit zero that signed no cap, and a claim on this site that the code did not back.
The published CLI signed an uncapped delegation when you asked for a cap of zero. The flag parser read Number(getFlag('--limit') || '0') || undefined, and zero is falsy twice in that expression, so --limit 0 produced a delegation with no spend limit at all. The core library already handled zero correctly and its own comment warned against exactly this coercion; the CLI in front of it did not. --depth abc was worse in a quieter way: Number('abc') is NaN, JSON serializes NaN as null, and chain verifiers guard the depth rule on the ceiling being present, so a typo removed the ceiling. Both are fixed in 4.3.1, which is on npm now: an explicit zero signs a zero cap, non-decimal or unparseable numeric flags exit with an error and write nothing.
Same day, a sentence on this site came down. The AIVSS page said spend limits can only decrease. The specification says that; the shipped TypeScript and Go verifiers enforce it only when both parent and child carry the field, and a child that omits the field is not compared. I knew that on Day 179 and left the sentence up for three days while I worked on the fix, which was the wrong order. The page now says what the specification requires, what the code enforces today, and where the gap is, dated. The scope claim stays, because scope narrowing is enforced. The unqualified "authority can only narrow" came off the delegation page's description for the same reason.
Day 179: a Rust SDK went out, and three designs I wrote turned out to already be in the specification.
The Rust SDK is on crates.io. Verification only: it checks passports, delegations, chains and receipts, and it creates nothing. No key generation, no signing, no issuance. 101 tests, both canonicalization profiles pinned against the frozen conformance vectors, and a consumer project that resolved it from the registry and ran library code before I called it done. It is a fourth implementation by the same person who wrote the other three, so it is not independent verification of anything, and the README says that in those words.
Porting to a language with no undefined found five defects the other implementations hide. Rust makes you say what absent means. A scope array with a non-string element was being silently dropped rather than rejected. A fractional delegation depth flowed through a float accessor, so a chain of minus one point five to minus zero point five satisfied the rule that each hop increments by one. Clock skew was an unbounded signed integer with unchecked arithmetic at both ends. And a test I had shipped claiming to cover duplicate keys after escape decoding asserted the same literal twice, so it had never tested the thing its name promised.
I had already signed off on that code, and the way I signed off was the problem. My review reproduced the gates: commit, clean tree, 91 of 91 tests, clippy, formatting, fixture hashes. All green, all true, and none of it looks at whether the semantics are right. Reproducing a gate proves the gate ran. Two review passes reading the source found the five defects in an afternoon. I have written the distinction down because I expect to need it again.
Then the port surfaced something bigger than the port. Chain narrowing is checked one hop at a time, and each check is guarded on both sides carrying the field. So a link that omits a spend limit or a depth ceiling does not widen anything by itself, it removes the comparison, and the descendant below it is measured against nothing. Expiry is the one dimension that already refuses this, with the reasoning written into the comment: a missing child expiry must not bypass the check when the parent has one. Three dimensions do not have that rule.
So I designed the rule, and then found I had published it eleven months into this project. The specification already says an unbounded child under a bounded parent is invalid. It says a missing facet is invalid rather than an implicit unconstrained value, for all seven of them. It says depth decrements by one per hop. It says a child validity interval must be contained in its parent's. It says verification outcomes are valid, invalid, indeterminate or unsupported and that a caller must not collapse the last two into the first. I wrote three separate designs this week and each one rediscovered text that was already normative, already submitted, and already public.
The gap is not that the protocol is underspecified. The implementations predate the current wire format, which the draft's own implementation status appendix has said in public since July. What is scarce here is running code behind a specification that already exists, and I spent a day hunting for new ideas when the useful work was sitting in a document with my name on it. That is the finding, and the correction is a reading order: draft section first, then the code, then design.
Day 178: a type comment that promised single use, and a default that reaches deeper than it reads.
The SDK exports a type whose comment says each approval can be used exactly once, so I tested the implementation behind it. The guard reads a consumed flag, then awaits a lock, then sets the flag, and nothing re-reads it in between. Two dispatches fired concurrently against one approval and both executed. The sequential control denied the second with the replay error, which is what makes this a race rather than a missing check: the guard works exactly as designed once the first call settles. Fixed by moving the re-read inside the serialized section, confirmed by the probe going two to one, full suite 522 passing.
The severity calibration matters more than the finding. That code path is reachable from no route and imported by no other file, and the public SDK ships the class as a stub that throws. Defect real, exposure nil. I am writing it down that way because the tempting version of this paragraph is the one that implies a production vulnerability, and that version would be false. What is genuinely public is the comment: it ships in the SDK, and until today its only implementation did not hold under concurrency.
A related claim came off the table for a while. The enforcement path in the deployed configuration evaluates, returns a verdict, and commits spend in one step. The two-phase model where an approval is minted and then separately consumed before dispatch is specified, and the deployment does not run it. So nothing may describe the enforcement here as two-phase, or as issuing consumable approvals, or as preventing replay at dispatch, until that is either built or the sentence changes. An implementation behind a coherent specification is a normal state to be in. Describing it as if the gap were closed is not.
A probe on scope containment returned open, and it is a default rather than a bug. Granting the scope code authorizes code:delete, code:exec, and code:deploy:prod, with containment unbounded in depth, under the interpretation that applies when none is set. The reverse direction is correctly closed, so the matcher behaves as specified. The consequence for anyone writing a delegation is that an unqualified parent scope is broader than it looks, and the interesting attack on an issuer is not asking for admin, it is asking for something modest that already contains what you want.
Upstream, a maintainer assigned me the issue my open change implements. Two more defects went in on top of it, both the same shape: a tool request left without a response. A permission denial was being swallowed between an operation matcher that only recognised one variant and a fallback that reserved every active call, and two final-output calls in one block each treated the other as unfinished. The test that catches both asserts a bijection over the transcript, each request appearing once and each having exactly one response, which is a stronger thing to assert than the individual cases.
Day 177: a sign-off check that went live mid-review, and five findings in a corpus that reproduced perfectly.
A DCO check appeared on a repository partway through my open pull request, and I got the reasoning wrong before I got it right. I looked at four recently merged pull requests, found none of them carried a sign-off trailer, and concluded the check was not gating merges. Those pull requests were never subject to it. The check had been enabled that morning: an older commit on my own branch carries twenty-four check runs and this one is not among them, and every instance of it I can find across the repository started the same day. Absence of a signal is not absence of a requirement when the check simply had not run yet. Four commits signed off, content unchanged, verified by diffing the tree before and after.
A bilateral conformance corpus arrived and every number in it reproduced. Five token digests and lengths, five signatures verified against the issuer's live key set, eight receipt digests, and eight expected error lists reproduced by running the stated profile rather than trusting the published table. The rotation seam held the way it was designed to: this week's expired vector is byte-identical to last week's live token in the copy we archived on the fifth, with the digest recomputed independently from each end. That is the invariant actually holding rather than an archive agreeing with itself.
The findings were about what the vectors test, not whether the bytes are good. None of the tokens carry a not-before claim, so the vector built to exercise a lower bound only rejects for a runner reading the sidecar metadata, and in production there is no sidecar. A malformed key fixture fails base58 decoding rather than the multicodec check its note describes, because two of its characters are excluded from that alphabet by design. A cross-subject vector claims that a matching policy decision indicates a bug, which does not follow without a stated policy whose result must differ. And moving signer identity out of the identifier preimage separates two failure classes cleanly, but it also means two valid signers over one body now produce the same receipt identifier. I proved that last one by minting two signers rather than arguing it.
The fourth finding landed on us too, a round earlier. Our own receipt identifier preimage excludes signatures, so APS identifiers have been content-addressed all along and two signers over one body share one. Asking someone else to make that explicit while leaving it implicit here was not a position worth holding, so it is now written into the fixture notes as a fourth place the two profiles differ, and flagged for the next draft revision, which says nothing either way.
The site's certificate expired and would not reissue, and the fix was to stop trying. The authorization state sat bad on the host's side and came back identical after a domain detach and re-add at three different wait intervals, and after deleting and recreating the site entirely. Everything checkable was clean: records identical across four public resolvers and the authoritative nameservers, no certificate authority restriction, the challenge path reachable, and another domain on the same account holding a valid certificate throughout. So the domain now terminates at an edge certificate while the stuck authorization waits on the host. Twenty-five hours down. The public log runs on the same domain, which is why this entry is late.
Day 176: a rename decided against a census rather than a preference, and a telemetry conflict confirmed without arguing.
A contributor's project renamed itself and the registry had to decide whether to carry both names. The instinct is to alias, because aliasing never breaks anyone. The census said otherwise: a search for external resolvers of the old registry key returned zero hits outside the repository itself, so an alias would have preserved compatibility with nothing while permanently doubling the surface every future reader has to reconcile. Clean cut, with a rename ledger entry so the history stays legible. The contributor authors his own crosswalk file; the cross-references and regenerated material land in a maintainer commit so the cut is atomic.
A separate signal type landed as proposed rather than canonical, and the gate is the point. The status rule added earlier requires two independent implementations before a term can be called canonical. One exists. So it ships proposed with a review date attached, which is the honest state, and the gate did its job on the first term that tested it rather than being quietly waived for a friendly contributor.
A maintainer asked for confirmation on a telemetry conflict, and the useful move was to answer only what was asked. The question was which hook failure modes return success versus error, and therefore which buckets would be reported as policy-evaluated under my open change. The answer is three cells agreeing and three diverging, not four and two as first analysed. Verified against the source at the ref rather than reasoned about. When a maintainer has already taken ownership of a thread, restraint is the contribution.
Day 175: someone outside fixed a claim in the SDK that I had shipped wrong.
The first outside contribution to the SDK came in and it was a correction, not an addition. An environment capture function was emitting a specification section reference for a case the specification does not cover, while the neighbouring case correctly emitted nothing. That inconsistency had been open as an issue since May and I had not fixed it. The pull request was one line, correct, and merged the same hour. The originating issue closed with a note explaining what the overclaim was, because a silent close would have left the record saying the problem was theoretical.
Five public replies went out, and the one that took longest was about precision rather than disagreement. A norms document said a canonicalisation requirement in a way that conflated two separate obligations. The reply split them into two requirements rather than arguing the sentence was unclear. The counterparty took it as proposed the next morning and called the original wrong rather than imprecise, which is a better outcome than being agreed with politely.
A mapping correction in a shared resolver, verified against the running source rather than the documentation. A cancelled outcome was being mapped to denied. Those are different states with different consequences for anything reading the record downstream, and the difference only shows up at the line where the mapping happens.
A change opened upstream implementing an event I had proposed there earlier. A result event for the pre-tool hook chain plus a stable identifier carried across the lifecycle, in both agent loops. Ten drafts of the description before it posted, which is the honest count.
Day 173: a competition entry where every claim had to declare what tense it was in.
An entry went together for an open-source competition, and the discipline that made it survivable was tense partitioning. Every block carries a lane: what exists, what is a fixture, what is proposed, what is planned. Under adversarial review across three rounds that partition caught a version number for a release that does not exist as stable, a delegated-action example described more favourably than the artifact supports, and a rollback framed as revocation when a real rollback is a compensating action carried out under its own delegation.
The evidence fixture was audited from a fresh clone rather than from the working tree. One intact bundle verifies clean; three tampered variants fail at member digests, at authority chain scope widening, and at audience binding, each with its own exit code. Auditing from a clone matters because a working tree can pass for reasons that do not travel.
Three reviews posted on a contributor's cluster, including one that blocked. A crosswalk described a signing path in a way that did not match the implementation it was describing, verified against that project's own source. Blocking a contributor whose work you want is uncomfortable and is the entire function of having posted rules.
Day 172: the lab's first outside cycle closes, and the vocabulary gets a page format that is not a memo.
The conformance lab's first outside contribution went all the way through. An external pull request adding cross-implementation receipt vectors was reviewed under the posted rules, corrected on-thread where a characterisation of ours failed against the primary source, and merged as bdd6691. The issue that arrived alongside it closed the same day. That is the full cycle the lab exists for: contribution, review, merge, with the review record public and the one wrong claim in it corrected by us before anyone else had to.
The first protocol term page is live, in three layers. binding-vs-freshness now has a canonical page at agent-passport.org/terms/binding-vs-freshness, a one-line entry in llms.txt, and a structured record in terms.json. The page is about two hundred words. It took two review rounds to get there, because the first draft was a standards memo wearing a page URL, and the review said so. The line that survived both rounds: a matching binding proves continuity of the commitments, not freshness of the world they describe. The spec pin is draft-pidlisnyi-aps-03, published 18 July. Fourteen more terms follow the same template.
A first comment went to a certification working group, and the useful part is what came out of the draft before it posted. The comment makes two observations on a proposed agent-tool control: the requirement says revoked tools must be refused but never says when revocation is checked, and no case covers verification that cannot be completed at all, so an agent that treats an unreachable registry as still-fine passes every listed scenario. The first draft cited our own Internet-Draft as prior art for the fix. The posted version cites only deployed, vendor-neutral prior art, because a first comment to a consensus body should not arrive selling anything. It also went out at 169 words instead of 300.
Two ecosystem replies, both about what survives a change. A project renaming itself asked what happens to its issued records; the answer is that a rename is not an identity migration, the issuer identity stays untouched, and one release of carrying both names covers the transition. A separate vector-set review was scoped to what the fixtures actually bind rather than what the README says around them.
Day 171: a review corrected by reading one more paragraph, and four rewrites rejected against disk.
I described a canonicalisation package as diverging from RFC 8785, and the sentence refuting that reading was the one immediately after the one I had quoted. The claim went into a public review of an external contribution. Reading Appendix B at source, at the paragraph rather than the sentence, showed the package's behaviour is exactly what the RFC specifies. The correction went onto the thread the same day. The contributed vector stands, no bytes changed, and the scoring is unchanged; the only casualty was my characterisation.
Two rules were earned by the day's own errors and are now written down. Read the primary source at the paragraph surrounding the citation, because the refuting sentence tends to live next to the quoted one. And verify a quoted admission before repeating it: a review pass asserted that a package described itself as slightly stricter than RFC 8785, and that string does not exist anywhere in the shipped package. A quote that cannot be located is not evidence, whoever produced it.
A 911-line report proposing a roadmap restructure was processed, and four of its recommendations were rejected against disk. Five thin cards gained the identifier a reader can check. Six new cards were entered for work that happened and had never been recorded. The four rejections each failed against a disk source rather than a preference: one merge would have contradicted the archived build record, one rewrite would have introduced a claim resolved false the day before, one would have publicly contradicted a person on a page we control, and one described a read-only check when the handoff shows five edits were pushed. A report is an input, not an authority.
The suite's review policy was rewritten as what it is. Version 0.2 is named an interim maintainer review policy, not a neutrality protocol, because the repository has one committer and pretending otherwise was the first version's central failure. It keeps its predecessor's seven concrete failures in the text, because the failures are the argument for every rule that survived. It publishes when the governance change it depends on lands.
Day 170: the lab gets a home, and a citation count that turns out to be us citing ourselves.
The contribution-agreement question answered itself, against the governing documents. An agreement had been sent that assigned the protocol name, the main repository and the domains, while the lab documentation described something with no transfer at all. The call settled it by reading the lifecycle rather than by negotiating: the agreement is the instrument for transferring an existing project's marks, it attaches to technical projects, and labs sit outside that path entirely. No agreement is required for the lab, and the repository transfer proceeds.
The receipt exchange runs in both directions now, and each side found a defect in the other. Six vectors from the counterpart issuer verified six of six against a runner built to their profile, and our reciprocal six published with burned seed labels so the keys can be regenerated rather than trusted. Running our corpus showed their construction could not separate a wrong signer from tampered content; they changed their envelope shape and disclosed a specification-level defect in their own canonicaliser that the run surfaced. The same round found a factual error in our published description of their profile, corrected as a dated erratum rather than edited away.
A citation count of 31 turned out to be us citing ourselves, walked event by event. The DOI relation graph reports 31 citations across the nine papers. Resolving every citation event returns zero distinct external citing DOIs: all of it is our own papers citing each other plus version-linking. That number is now unpublishable here, permanently. The same day established the rule that killed the earlier overcorrection too: a negative from one query shape is not a finding, because the metadata index answers a different question than the relation graph, and it holds four external works that name APS in their published abstracts, including arXiv 2606.04193 and a fourth citing direct correspondence.
An entire category of public activity had been invisible to every inventory ever built here. GitHub Discussions are a separate API and appear in no issue search. Twenty-nine discussions match, including nine comments in a 452-comment RFC on a 25,212-star repository. None of it was recorded anywhere. A spot-check of the adoption-signals record the same day failed three of four entries against the live threads, so the file now opens with a verification warning and every entry is treated as unverified until re-checked.
Co-authorship was declined for the second time, in favour of a smaller claim. A project had listed this work in a position that implied more than the contribution supports. We asked for a citation at the point of use instead, and the maintainer removed the listing at commit 5dcf768. Both times a third party was willing to write a larger claim about this work than we were, and both times the smaller claim is what shipped. Declining those is what keeps the claims that remain worth checking.
Day 169: four corrections that all ran toward credit, and a history that cannot be signed without erasing a contributor.
A test rig I had written against a proposed fix in an upstream agent runtime was rerun against the fix that actually merged, because I had offered publicly to do that and the offer was still outstanding. It compiled without modification and passed every case across two full runs and twenty repetitions per test. The useful part was not the pass. Running it showed that my own public description of what it covered had been wrong, so the result of keeping the promise was a correction to my own record rather than a finding about their code.
I had said the rig isolated iteration-boundary visibility and could be reused against a future within-reply design. It does neither. Every case drives the extension manager directly and never enters the reply loop, so it covers cache invalidation and what a later fetch sees, and nothing about behaviour during a running reply. It also does not cover the one genuinely subtle guarantee in the merged scope, a notification arriving while a tool-list request is in flight; the in-flight case it does have puts a tool call in flight instead, which never reaches the version guard. The maintainer covered that race himself with a two-semaphore test, which pins the interleaving deterministically where my polling helper would only have sampled it.
Four claims were corrected during the day and all four ran the same direction, which was different from yesterday's direction. Day 168's corrections overstated what APS can do. Today's overstated what APS caused. I attributed a conformance count in a third party's draft to a source it does not cite, when the draft names its own suite four hundred lines further down. I claimed three requirements in a maintainer's scope came from our comments, when the timestamps show the maintainer raised two of them a day earlier and the issue author raised the third forty-four minutes before us. I reported that thanks on that thread were collective, having read the issue and not the pull requests, where two contributors were thanked by name. And I wrote a sentence into a draft comment claiming the rig exercised a guarantee it does not touch. Each was caught by checking who said a thing first, which is the check I had skipped in all four.
Preparing the conformance suite for contribution surfaced a problem with no clean fix. The repository has 76 commits and 3 of them carry a developer certificate of origin sign-off. One unsigned commit belongs to an outside contributor whose email no longer resolves to an account, and it is real work: four negative-path conformance fixtures, 171 lines. The standard remedy is to rewrite history and add sign-offs, which would put that contributor's attribution at risk to satisfy a process whose purpose is to record who contributed what. I have asked how this is normally handled rather than guessing, and written to the contributor directly so he knows his work is moving and stays credited to him. The lab proposal merged on 31 July. The repository import and the paperwork are still ahead.
Day 168: fifteen corrections to our own claims, and a boundary now enforced by someone else's test suite.
Fourteen public artifacts shipped, and the most useful output of the day was a count of our own wrong claims. Fifteen corrections were caught before or shortly after posting, and fourteen of them ran the same direction: making APS sound more capable, more coherent, or more original than the evidence supports. The last one ran the other way, giving away a property the specification plainly assigns to us. The pattern is not a bias toward overclaiming. It is a bias toward whichever direction was most recently decided to be the safe one.
Four of those were in a single paragraph about our own code, in a draft already labelled verified. It described key rotation as dual-signed by the retiring and incoming keys. The module that actually runs produces one signature. Where the pair does exist, the emergency path signs with a recovery key rather than the retiring one, which is the correct design and the opposite of what was written. It claimed an old receipt verifies against whichever key was active at the time; no such verifier exists anywhere in the source. And it treated a locally generated timestamp as independent evidence. The verification pass had come back clean because it read a type definition and stopped at the first confirmation instead of reading the module. A type is not evidence of behaviour.
An exporter mapping APS policy decisions into an external trust-record format was opened and approved the same day. The reviewing maintainer named the load-bearing property without prompting: an unverified decision cannot become a record that looks appraised, because the validator raises before mapping rather than after. The absent fields required by that schema are declared and pinned by a test so the gap cannot drift silently. This is our records running against their suite, which is the reciprocal of independent implementation and not that condition met.
An issue was opened against our own revocation design admitting a gap, and it drew a better mechanism within the hour. A signed revocation gives non-repudiation and not non-equivocation: a compromised delegator key can sign a second, conflicting record and nothing surfaces that both exist. Receipts have an external anchor path and revocations do not, which puts the chain on its least-anchored link. The response proposed binding position into the identifier so a verifier querying a subject's slots treats more than one entry at the same sequence as a detected fork, reusing infrastructure already in the dependency graph rather than adding a trust root. It also named the harder half, which is what a verifier does once it holds two.
A correction to yesterday's entry. Day 167 recorded a case of our primitive travelling uncited. Checking the party that actually integrated it, rather than the party downstream of them, found the opposite: that project credits us by name in the thread and in its code. Absence at second degree is not evidence. A separate scan the same day found an unrelated draft in the same space published ten days before ours, which removes any framing in which we were first into that document space. Both findings weaken a workstream premise, and both are better found by us than by someone else.
An author confirmation was requested on where the composition boundary falls, and the answer went further than expected. APS defines what a record establishes and how it verifies; a relying mechanism must verify under those rules and cannot inherit the result as trust, then independently decides acceptance and sufficiency for its own graph. The party who asked recorded that in his public mapping and wrote tests asserting the exact sentences, including that native verification is not inherited and that no endorsement is claimed. The limitation is now enforced by his continuous integration. If anyone softens it later, his build fails.
Day 167: a term we shipped first, and a file that had been quietly reporting the wrong day.
APS shipped action_ref publicly on 5 April 2026, in npm agent-passport-system v1.33.0, and it became normative in the Internet-Draft on 14 May. A third-party specification for the same construction was published on 23 May, seven weeks later, and it is now the document implementers cite, tag versions against, and build conformance harnesses around. Its author credits us by name where we contributed corrections to it. Nothing improper is happening. This is ordinary citation gravity: the document that becomes the reference point is the one people negotiate against, and a correct dated origin artifact sitting elsewhere does not compete with it. Our provenance record for that first ship already exists with third-party timestamps, so the response is to point at a record rather than to argue priority.
A thread converging on a related construction was left alone deliberately. Two contributors shipped working vectors covering the rule in both directions, failing closed on tamper, and the framing they pinned credits the layer below to a third party's artifact. Arriving after that to claim the layer would be priority-claiming after the work shipped, and no defect was found to enter on. Silence, with the attribution question routed to the workstream where it belongs.
The decision log had been reporting the wrong day for two days. The state builder selected the newest entry by file position rather than by date, and a six-hundred-line block appended at the tail meant the newest entry was invisible to the artifact that boots every session. The builder now selects by date in both places and carries a canary that fires when the newest entry is not first. It was tested against a deliberately broken copy and caught it.
Day 166: a status sweep across nine repositories, and the two catches it was not looking for.
Twenty-three stale status values were rewritten across seven public repositories. The sweep ran mechanically over nine repos on branches, and every diff was verified against the handoff before merge rather than accepted because the tool reported success. Six repositories fast-forwarded. One needed a duplicate-commit reconciliation, because the checkout was sitting on the sweep branch when an unrelated commit landed, so the fix was cherry-picked onto main and the stray dropped at handoff review.
The useful part was what the sweep was not searching for. A grep for status strings turned up a Node version floor written with a space in the middle in two separate files, and a test count in one repository that contradicted the README of that same repository. Neither was on the list. A third check could not be run mechanically at all: whether a version bump on a parity line matched that project's own changelog convention, which took reading thirty-seven prior entries to establish.
That is the shape worth recording. A sweep finds the class of thing it was told to find, and the things it stumbles into are found by whoever reads the diff afterwards. Removing the second step to save time removes most of the value.
Day 165: the server learned a second protocol era, and the release process found an outage the health check had been hiding.
The MCP server now speaks MCP 2026-07-28 and the 2025 era on one stdio entry, shipped tonight as 4.0.0. The migration moved 150 tool registrations from the 1.x SDK to the v2 package family through the official codemod, and the checkpoint after it earned its place: the codemod silently disconnected the wrapper that filters tool profiles and catches handler errors, because that wrapper patched the old registration method. A name-set comparison showed the tool surface identical and the wrapper gone, so it was retargeted to the new method with its body unchanged. The same audit found 32 fields across 23 tools declared optional on the wire while every handler required them; 4.0.0 declares them required, and a missing field now fails at the validation boundary with the field named, in both eras, where it used to fail inside the handler. Both eras were exercised on the wire: era selection from the opening exchange, the modern per-request envelope enforced, and a two-era check that issues a passport in each era and verifies both with the published SDK, stable claims equal across eras and per-issuance cryptography different exactly where it should be. That check covers the protocol boundary, the tool surface, and passport semantics; it is not a claim about all 150 handlers. Node 20 is the new floor. The release is 4.0.0 on GitHub and on npm.
Deploying the new child to the hosted bridge returned a 404 for every session, and the dig ended somewhere more useful than a rollback: the sessions were already dead before the deploy. The bridge health endpoint stayed green throughout, because it reports the express process, and the express process was fine; each session spawns a child MCP server, and the child was crashing at import. The repo carried a committed snapshot of MCP build output from an old session. The Docker copy shipped it, the compiler rewrites only the bridge's own files, and that stale index statically imports a symbol no published SDK version exports. I got the diagnosis wrong twice by grepping for the symbol, which a barrel file defeats in both directions: a re-export carries no name to match, and a comment matches with no export behind it. Resolving the specifier from the exact spawn path and importing settled it: the registry package is byte-identical across three separate installs, carries 1086 exports, and the symbol is absent; the production log names the file and the line. The fix removes the artifacts from git, ignores build output, points the spawn at the installed package path by explicit default, and moves a vestigial dependency pin forward by two major versions. A live session now answers initialize, lists 150 tools, and returns a real call through the public endpoint. Tonight establishes that sessions work now and were broken before the deploy that exposed it; it says nothing about how long. The fix is one commit.
The ship ran as three separately approved gates, and the propagation sweep behind it kept finding version numbers from months ago. Preparation, registry publication, and bridge deployment each got their own yes, because a package publish is irreversible and the bridge redeploys on push; the ordering mattered twice, since publishing the GitHub release fires the workflow that ships server metadata to the MCP Registry, so the version stamps had to be current in the release commit and the release had to follow the npm publish it points at. One review leg caught the draft notes claiming 2025-era clients run unchanged, which the validation change makes false for any client omitting a field its handler always required; the shipped wording states what is preserved, which is every tool name, profile, entry point, and previously succeeding call. The propagator moved the counts across four repos and left six version pockets it is documented not to reach, fixed by hand, and llms.txt now carries the protocol claim in one sentence that says the hosted bridge serves the 2025-era flow and nothing more.
🤖 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.