Repository navigation
Phase 6c: authentication — documentation and phase record - #77
Conversation
Review record for the phase 6c stack (#75 → #76 → #77)Five independent reviews, each by a fresh agent with no memory of the previous one, each re-running every gate itself on every tip (4.0.6 all seventeen individually at the code tip and the full
Nothing was skipped. The pattern across the rounds is the point of the record: no round found a red gate or a layering fault; each found the next layer of the credential surface by its own hostile inputs — a Round 0 → fixed in round 1
Round 1 → fixed in round 2
Round 2 → fixed in round 3
Round 3 → fixed in round 4
Round 4 (final) — open, carried into the PR bodies
What the final reviewer verified by experiment, both interpreters
Tips reviewed at the final round: code |
Phase 6c, cut from main at f1fe848 with nothing of phase 6a present, so the steps take their own logger: keyword and AUTH-31 calls phase 3b's Body#replayable? directly. Twenty-five new lib files: auth.rb, the flat AuthResolutionError under error/, and twenty-three under auth/ -- the closed five-member Scheme set with ALL and .of, Requirement, Descriptor and the pure three-tier Resolver (AUTH-1..7); BearerToken, KeyCredential, NamedKeyCredential and PasswordCredential, each redacting in #to_s, #inspect and, for the two Data types, #pretty_print, because pp walks a Data's members and never calls an #inspect override (AUTH-8..10); the never-raising RFC 7235 list parser Challenges.parse over Challenge, the one fold point (AUTH-12, AUTH-13); BasicHandler over pack("m0") and never Base64 (AUTH-14); the challenge-only DigestHandler with MD5, MD5-sess, SHA-256 and SHA-256-sess, qop=auth or legacy, a per-nonce counter in its own BoundedMap and a typed failure for a credential ISO-8859-1 cannot carry (AUTH-15..22, AUTH-24); ChallengeHandlerChain with its proxy-aware header name and the hook adapter (AUTH-23, AUTH-25); the stateless KeyStamper (AUTH-26); BearerStamper with a lock-free hot path and XCUT-12's sanctioned lock-across-fetch (AUTH-34..36); AsyncBearerStamper with AUTH-37's three zones and one single-flight slot; BearerProvider.fetch_async as AUTH-11's never-raising default; and the AUTH pillar Step and AsyncStep at Stages::AUTH, built through .build(stamper:, challenge_hook:, logger:), forking for every drive, reading the cross-origin marker off the cursor and never a header, guarding HTTPS before any fetch or write, replaying a 401 at most once behind the replayability gate and closing the superseded response (AUTH-27..33, AUTH-38). Three earlier files widen in place: BoundedMap gains #update(key), the read-yield-write the nonce counter needs; Instrumentation::Events gains AUTH_REFRESH, the ninth event; lib/dexpace.rb gains the twenty-five-line Phase 6c block. Every file has a sig/ mirror; the strict Steep target is green with no relaxation and no Digest, SecureRandom or Random in any signature. Three existing tests change as pins the code invalidated: the smoke suite's layer table and its preloaded stdlib features, the seam surface's require pin (digest joins the four), and 5b's keys_test.rb (eight events become nine). The surface manifest is regenerated once, 956 to 1059 lines, all 103 rows read against the object model.
Review round 0's R0-3 and R0-4, both in DigestHandler. UnencodableCredentialError always named ISO-8859-1, and its message blamed the challenge for not advertising charset=UTF-8, even when the UTF-8 branch was the one that raised -- a BINARY-tagged credential against a charset=UTF-8 challenge fails "\xE4 from ASCII-8BIT to UTF-8" and was reported as a Latin-1 failure. #materialize now takes the branch's target Encoding and the error names it; the message's reason is keyed by the target (a private REASONS table) so each branch says why its encoding applied. The same branch also refuses a UTF-8-tagged credential carrying an invalid sequence: `encode` to the same encoding passes bytes through unvalidated, so such a value would have been hashed as it was, which is the silently wrong response R10 rejects. #compute took the nonce count before hashing, so a refused attempt consumed an nc and the next response on that nonce went out one higher than the server had seen. The credential is now materialised first -- the one step that can raise -- and the count taken after it, which is the order the design's own authorization_for fence has. The RBS mirrors follow: REASONS declared, the three private signatures that carry the materialised parts and the target updated.
Review round 0's R0-2. Regexp.new, unlike a Regexp literal, returns an unfrozen object, so the parser's eight timeout-compiled patterns were the one set of core patterns not frozen at load: phase 5a's HTTPDate::GRAMMAR freezes and its suite pins the property. The eight now freeze the same way, so the tests branch can pin "a private, frozen Regexp with a per-pattern timeout" on each of them and a removed `timeout:` runs red instead of surviving. Private constants; no surface change.
Review round 1's R1-1 and R1-3.
AsyncStep validated a challenge hook's future-settled value outside the
frame that closes the 401: a hook future fulfilling with a non-request
failed the step's future with the 401 body left open, contradicting the
class comment and AUTH-32. Step#consult's rescue becomes one
closing_on_error(response) frame both runtimes use; AsyncStep overrides
consult to pass a future through and checks the settled value inside the
same frame, so a String, or a future of a future, closes the 401 before
the frame fails the future. replacement! is strict everywhere.
UnencodableCredentialError carried the rescued conversion error as its
cause, whose message names the offending character (U+65E5) or byte
("\xE4") of the secret, and full_message renders a cause on every
supported Ruby (AUTH-8). Both raises in DigestHandler#materialize now
spell cause: nil, and the error carries the value's own encoding as
#source_encoding and in its message instead, so the diagnostic loses
only the character. BasicHandler raised the bare Encoding error for a
BINARY-tagged or invalid UTF-8-tagged field; each field is now
transcoded under its own name and refused as an InvalidArgumentError
naming the field and the two encodings, cause nil.
RBS mirrors follow; the surface manifest gains the one new reader.
Review round 2's R2-1 and R2-4, both in AsyncBearerStamper. The expired-or-missing zone derived the caller's future from the single-flight slot through Future#then, and #then wires the derived future's cancellation back to its source. The source here is the ONE slot every coalesced caller shares, and every arrival until the provider settles, so cancelling one request's future -- which AsyncStep#observe forwards from the step's future -- cancelled every other waiter, and every new request coalesced onto an already-cancelled future until the provider settled (AUTH-37, SEAM-18). R12 prescribes a second #on_settle and a second Completer; the stamper now builds each waiter's future that way, settled FROM the slot's settlement and never wired back to it, so cancelling a waiter detaches that waiter alone and the fetch, the cache and the other waiters are untouched. One private #settle classifies both completers by Future#then's three rules: a cancellation stays a cancellation (a provider that cancels its own fetch cancels the slot and every waiter with its reason), a failure is the same object, a success settles with the block's value; a stamp the outbound header grammar refuses fails that waiter only. The class comment's line beginning `@lock` read to YARD as an unknown tag; reworded. RBS mirror follows; no public surface changes.
Review round 3's R3-1, in both bearer stampers. AUTH-35's validation checked a fetched token for nil, class and expiry and nothing else, so a token whose `Bearer <token>` wire form the outbound header grammar refuses (HTTP-18: a trailing newline read off a file, a CR) was written into the cache. It can never be sent, so no 401 can ever arrive to evict it (AUTH-36), and it stays until it expires -- never, for a token with no expiry: the sync stamper raised HTTP-18's InvalidArgumentError on every later call with the provider never asked again, and the async stamper's fresh zone raised it synchronously out of a method that returns a Future, failing every later request through an AsyncStep with the transport never reached. AUTH-35 wants a misbehaving provider result uncached so a later request retries. The grammar check is now the fourth rejection in BearerStamper#validate and AsyncBearerStamper#invalid, a ProviderError whose message never names the token: nothing is cached, the sync call raises from inside the lock with @token untouched, the async waiters fail with it, the slot clears, and the next call fetches again. Checked where the token arrives, as KeyStamper checks its key where IT arrives (construction), not in BearerToken.build, whose contract is AUTH-9's non-blank rule. A cached token therefore always stamps, so #stamp's fresh and expiring zones cannot raise; #deliver's rescue stays for a request whose own derivation refuses. Comments on both stampers and ProviderError follow; no public surface changes, the RBS mirrors are unchanged.
The reconciled Layers class carried both phase 6a's retry pin and phase 6c's authentication pin beside the phase-3b through phase-5 cases and reached 104 lines, over Metrics/ClassLength's 100, which the honest RuboCop run sees and the nested-worktree rake gate does not. The two phase-6 cases move to a sibling PhaseSixLayers class, the shape phase 4a's reconciliation used for the same collision.
Twenty-nine suites, one per public auth file plus bounded_map_test.rb, the first true mirror of a private constant, plus five that carry no lib mirror and say so in their headers: step_bearer_challenge_test.rb (a second suite over step.rb), pillar_integration_test.rb (one example set over both runtimes), cross_origin_convergence_test.rb (Task 15, guarded on defined?(Dexpace::Redirect::Step) and skipping on this base with a reason naming 6b), matrix_facts_test.rb (Task 1's facts as a standing test, deriving the four Digest expectations rather than transcribing them) and error/auth_resolution_error_test.rb. Eight top-level test-support doubles, one class per file: ChallengeFixtures, FixedCnonce, SequencedTransport (named so as not to collide with the ScriptedTransport phase 6a is writing at the same time), SequencedAsyncTransport, ScriptedBearerProvider, ScriptedAsyncBearerProvider, SpyCursor and AuthFixtures. The AUTH-24 proof is deterministic: bounded_map_test.rb forces the interleaving between the read and the write through the block, and bearer_stamper_test.rb parks sixteen threads on a barrier so the single-flight fetch is counted, never timed. Every "never raises" claim is asserted on the value, identity claims use assert_same, the test helper is named dispatch because run is Minitest::Test#run, and every thread a test starts is joined.
Review round 0's findings, each with the line that now runs red on its revert. R0-1: AsyncStep's post-eviction routing was indistinguishable under the suite, because the real AsyncBearerStamper fetches through #stamp and #stamp_fresh alike once its cache is empty (mutation M45 survived). A new top-level double, SpyBearerStamper, answers "Bearer cached" from #stamp and "Bearer fresh" from #stamp_fresh and records every call, and two BearerTest cases drive a 401-then-200 through it: a successful eviction retries through #stamp_fresh and a failed one through #stamp. M45 now fails on ["Bearer cached", "Bearer fresh"] versus twice cached. R0-2: the parser suite pins each of the eight scanner patterns as a private, frozen Regexp with a non-nil #timeout, as http_date_test.rb pins CFG-31's grammar; a removed `timeout:` or `.freeze` is a red test. The suite crossed Metrics/ClassLength and is split into GrammarTest and LeniencyTest over one shared helper. R0-3: a BINARY-tagged credential under charset=UTF-8 raises naming UTF-8 with the conversion error as cause; a UTF-8-tagged credential with an invalid sequence is refused on its own bytes, cause nil; a Latin-1-tagged one is transcoded and accepted. The error suite asserts the UTF-8 message says the challenge advertised the charset and never that it did not. R0-4: a refused Digest attempt leaves the nonce's counter unset, and the next response on that nonce sends nc=00000001.
Review round 1's R1-1, R1-2, R1-3 and R1-5. R1-2: the AUTH-30 order was asserted by count alone, so a close-after- drive mutation survived on both interpreters. The replay's scripted reply is now a callable that reads the 401's close count AS the second drive reaches the transport, in step_test.rb, step_bearer_challenge_ test.rb and both async_step_test.rb branches; the sync and async close-after-drive mutations run red. R1-1: async_step_test.rb gains the future-fulfilled non-request shape, a String and a future of a future, each closing the 401 and failing the future with the InvalidArgumentError naming the class. R1-3: unencodable_credential_error_test.rb takes the source_encoding keyword and proves the error is raised with no cause inside an in-flight rescue; digest_handler_test.rb asserts nil cause, the source encoding, and that message, detailed_message, inspect, full_message and every each_cause message carry neither U+65E5, the character nor the password; basic_handler_test.rb asserts the typed, causeless refusal of a BINARY-tagged and an invalid UTF-8-tagged field naming no byte. R1-5: the async AUTH-35 test asserts the rejected results cache nothing through #evict_if_matches and the cache slot, so round 1's M51 runs red.
Review round 2's R2-1, R2-2 and R2-3. async_bearer_stamper_test.rb gains a CancellationTest: cancelling one of three coalesced waiters (a #stamp_fresh one among them) leaves the others pending, the provider's fetch unsettled and a new arrival coalescing onto the live slot with one fetch in all, and once the provider settles the survivors stamp the token, the cache holds it and the cancelled one carries its own reason; a provider cancelling its own fetch cancels every waiter AS a cancellation with the provider's reason, caches nothing and frees the slot for a retry; a token the outbound header grammar refuses fails that waiter alone and never raises into the settling thread. async_step_test.rb gains the same isolation through a real async pipeline: one cancelled request, the other two drive with the fresh token. Round 2's tree, the Future#then derivation, a completer wired back to the slot, a cancellation forwarded as a failure, the rescue dropped and a slot left set all run red on 4.0.6 and 3.2.11. digest_handler_test.rb pins the handler's increment to BoundedMap#update deterministically: the store's #[], #set and #put are narrowed in place to raise (the handler is frozen, so the store is not replaced), #update records its key, and two counts on one nonce read 00000001 and 00000002 through two recorded updates. The reviewer's read-then-set counter, which the sixteen-thread race never observed under the GVL, now raises on both interpreters. async_bearer_stamper_test.rb's AUTH-36 case gains the sync suite's exactness pins -- a doubled space, a superstring and the bare token do not evict -- so a substring comparison on the async half runs red as it already did on the sync one.
Review round 3's R3-1. A provider token the outbound header grammar refuses -- a trailing newline, a CR, a non-ASCII byte -- is now AUTH-35's fourth rejection, and the suites pin it where the round found it unobserved: bearer_stamper_test.rb (split into CacheTest, RejectionTest and EvictionTest under Metrics/ClassLength) asserts three such tokens each raise ProviderError with @token nil and no name of the token in the message, then the next call fetches the clean one; async_bearer_stamper_test.rb FailureTest asserts two waiters on one fetch both fail with it, nothing is cached, the slot is free, and the next #stamp returns a future that stamps the clean token -- never a synchronous raise -- and that an unusable BACKGROUND refresh is logged as http.auth.refresh without naming the token and caches nothing, the still-valid token stamped meanwhile. step_bearer_challenge_test.rb and async_step_test.rb carry the same through a real pipeline: one dispatch fails, the transport is never reached, the next two are 200 with one refetch. Round 3's CancellationTest case fed exactly such a token to prove #deliver's rescue; the fourth rejection now pre-empts it before the stamp, so the case becomes a request whose own derivation refuses, which is the raise left for the rescue to keep off the settling thread. Eight mutations run red on 4.0.6 and 3.2.11: the grammar check dropped from either stamper, the sync token cached before validation, the rescue dropped, either message naming the token, the async write made unconditional, and the refusal raised out of the settle block.
The checklist, written at execution time from what was built: 38 own rows, all implemented, nothing deferred, plus the guarded Task 15 row and twelve cross-reference rows; what was built; the matrix facts re-run on 3.2.11, 3.3.12, 3.4.10 and 4.0.6 with the four Digest expectations derived on each; the sixty-one guards run red on 4.0.6 and 3.2.11; the audit groups; twenty-eight departures from the plan's text; the findings routed; postponed work none. The design gains an As-built addendum, rows P6-71 to P6-83, recording where the source reads differently from the design's text: Step.build with no redactor: keyword, the third rendering pp needs, no Replayability module, BearerProvider shipped as a real function, the three Auth errors filed under auth/. docs/knowledge/notes/authentication.md is the corpus's first authentication note: pp gives a Data its own #pretty_print over the members and never calls an #inspect override, so the harvested two-rendering rule is one short. docs/sdk-documentation/auth.md is the as-built page, every fence run verbatim on 4.0.6 and 3.2.11 as one script and no example printing a secret; architecture.md, the core README, README.md and docs/README.md point at it. CLAUDE.md's built-phases paragraph, its counts and its constraints-that-bite list are rewritten from the tree. docs/first-release.md corrects one phrase to what was built: the step takes one stamper and no Scheme => credential table. The roadmap gains the 2026-09-18 status note and one phase-10 inbound bullet for the three replayability spellings phase 6 leaves behind.
The checklist's AUTH-13, AUTH-18, AUTH-21, AUTH-36 and AUTH-37 rows say what the round changed and which guard proves it: the eight scanner patterns pinned frozen with their timeout (R0-2), a refused Digest attempt consuming no nonce count (R0-4), the typed failure naming the branch that raised with a reason worded for it and the UTF-8 branch refusing a credential that is not text under its own tag (R0-3, P6-84), and AsyncStep's post-eviction routing told apart through SpyBearerStamper, the ninth double (R0-1). "What was built" gains the double and records instrumentation/keys.rb as a shared pair for the 6a/6c merge (R0-5); "Guards run red" gains the round's table, guards 62-68, all caught on 4.0.6 and 3.2.11; "Deviations from the plan" gains items 29 and 30. The design's As-built addendum gains P6-84 and a paragraph on the count order, which returns to the object model's own fence and so carries no row. The auth page states both branches raise naming their encoding and adds the BINARY-tagged example, every fence re-run as one script (66 checks, 0 fails on 4.0.6 and 3.2.11). CLAUDE.md's Digest bullet carries the encoding and count-order rules; the roadmap's 6c status note gains the round's record.
The checklist's AUTH-8, AUTH-14, AUTH-21, AUTH-30, AUTH-32 and AUTH-35 rows now say what the repaired tree does: the close-before-replay order read as the second drive reaches the transport, the future-fulfilled non-request closing the 401, both encoding failures raised cause: nil with the value's own encoding carried instead, BasicHandler's typed refusal, and the async stamper caching nothing for a rejected token. A third guard table (69-80) records the twelve mutations run red on 4.0.6 and 3.2.11; deviations 31 and 32 state the two departures; the manifest's second regeneration is noted. The design's As-built addendum gains its seventh differing statement and row P6-85, amending R10's "the rescued conversion error as #cause" and P6-84's parenthetical, plus a round-1 paragraph for the async-step gap P6-78 had promised closed. docs/knowledge/notes/error-handling.md gains an entry narrowing the styleguide's "the original exception object as the cause:" rule for a secret. auth.md's Digest, Basic and async-step prose and examples show #source_encoding, the nil cause and the typed Basic refusal (70 fence checks, 0 fails on 4.0.6 and 3.2.11). CLAUDE.md's Digest bullet carries the rule. The roadmap's 6c note dates its implementer-tip figures, appends the round-0 and round-1 post-repair figures (R1-4), and cites P6-85 and the 1 060-line manifest.
The checklist's AUTH-37 row now states the expired zone as R12's second Completer, settled from the shared fetch and never wired back to it, with the cancellation-isolation clauses the new suites prove; AUTH-24 names the deterministic pin that ties the handler to BoundedMap#update; AUTH-36 the async exactness pins. A fourth guard table (81-87) carries the round's seven mutations run red on 4.0.6 and 3.2.11, deviation 6 and 12 are amended, deviations 33 and 34 record the repair and the two suite pins, and the "Audit groups run" sentence about cause: nil is qualified to the tree after round 1 (R2-5). The design's As-built addendum gains its eighth differing statement, a round-2 paragraph and row P6-86, which narrows P6-79's "cancels the inner future" to the waiter; every P6-71-P6-85 range reads P6-86. auth.md's async-stamper prose says why the waiter is not a Future#then derivation and gains a fence showing one cancelled waiter leaving the others driving (73 fence checks, 0 fails on 4.0.6 and 3.2.11). CLAUDE.md's bearer bullet and the roadmap's 2026-09-18 note carry the same, the note with this round's figures.
Review round 3's R3-1: a provider token the outbound header grammar refuses was cached by both bearer stampers and could never be sent or evicted, and it is now AUTH-35's fourth rejection. The checklist's AUTH-35 row names the fourth rejection and where it is pinned on both stampers and through both pipelines; the AUTH-37 row says an unusable background refresh is logged and cached for nothing, and that #deliver's rescue is re-pinned through a request whose own derivation refuses; the HTTP-18 cross-reference row gains the bearer stampers; a fifth guard table (88-95) records the eight mutations run red on 4.0.6 and 3.2.11; deviation 35 states the departure and the bearer_stamper_test.rb split, deviations 12 and 33 point at it, and the Minitest-conventions row counts nine split suites. The design's As-built addendum gains a ninth differing statement (AUTH-35's validation has a fourth check, the port's), the round-3 paragraph and row P6-87, which amends P6-86's "fails that waiter only". auth.md's bearer section states the rejection on both runtimes with a runnable fence each (77 fence checks, 0 fails, on 4.0.6 and 3.2.11, no secret printed); CLAUDE.md's bearer bullet names it; the roadmap's 6c note gains the round-3 paragraph with the repaired tips' figures. The round's nit, two 73-character body lines in the round-2 docs commit, is deferred to the PR body: rewrapping them would rewrite an earlier fixer's commit.
Phase 6a merged first, so the three 6c branches were rebased from f1fe848 onto main 905523c with the nine files both lanes changed reconciled inside the rebased commits. This commit adds the two records that reconciliation owes: the roadmap's dated paragraph naming the reconciled files, the regenerated manifest count and the old and new tips, and a dated note in the 6c checklist stating that its own count sentences describe its original base while CLAUDE.md carries the combined tree's figures.
13fe732 to
a870f7e
Compare
2039060 to
029a6e7
Compare
Reconciliation onto
|
| Branch | Reviewed (round 4) | Rebased | What changed inside |
|---|---|---|---|
24-phase-6c-authentication (#75) |
430c527 |
5a74e17 |
the rebased five commits, with lib/dexpace.rb (6a's block then 6c's), test/dexpace_test.rb (both layer pins) and the manifest (regenerated: 1,005 + 104 = 1,109 rows) reconciled inside them — plus one new fix: commit: the honest RuboCop run found the reconciled smoke suite's Layers class at 104 lines (Metrics/ClassLength), so the two phase-6 cases moved to a sibling PhaseSixLayers class, the shape phase 4a's reconciliation used |
24-phase-6c-authentication-tests (#76) |
13fe732 |
a870f7e |
rebased only; no conflict, no 6c suite needed a #bundle repair |
24-phase-6c-authentication-docs (#77) |
2039060 |
029a6e7 |
the rebased five commits with CLAUDE.md, the three READMEs, architecture.md and the roadmap reconciled (both lanes' content, counts re-derived from the combined tree: 174 lib/ files beside version.rb, thirteen test-mirror exceptions, thirteen checklists, fourteen pages; both status notes and both phase-10 inbound bullets in merge order) — plus one new docs: commit: the roadmap's dated reconciliation paragraph and a note in the 6c checklist stating its own count sentences describe its original base |
Every file only one lane touched is byte-identical to that lane's reviewed tip (proved over the whole tree: the only paths differing from both main and 2039060 are the nine reconciled files and the checklist note).
Re-proven at the rebased tips: docs tip full rake green on 4.0.6 — 2,588 runs / 67,040 assertions / 1 skip (the guarded end-to-end cross-origin test, 6b's) / 99.98% — with the honest RuboCop clean (484 files), the probe clean and both corpus verifiers OK; tests and code tips full rake on 4.0.6 and the matrix set on 3.2.11 (both), 3.3.12 and 3.4.10 (tests tip) — figures below.
| Tip | Ruby | Run | Result |
|---|---|---|---|
docs 029a6e7 |
4.0.6 | bundle exec rake (all seventeen) |
green — 2,588 runs / 67,040 assertions / 0 failures / 1 skip; 7,171 / 7,172 lines (99.98%); honest RuboCop 484 files, no offenses; probe "no drift found"; verify_knowledge_structure OK (2,166 harvested, 55 notes) |
tests a870f7e |
4.0.6 | bundle exec rake |
green — the same figures |
tests a870f7e |
3.2.11 / 3.3.12 / 3.4.10 | the matrix set | green — 2,588 runs, 1 skip, 99.98% on each row |
code 5a74e17 |
4.0.6 | bundle exec rake |
green — 2,286 runs / 65,267 assertions / 0 failures; 93.01% (above the floor, no layering exception needed); 6 manifests match |
code 5a74e17 |
3.2.11 | the matrix set | green — 92.96% |
Pushed with --force-with-lease on 2026-09-19; the three PRs now carry these tips.
Closes #24. Third and last PR of phase 6c's stack — the documentation and the phase record for the two PRs below it. Targets the tests PR's branch. Its
CLAUDE.md, READMEs,architecture.mdand roadmap text are written for 6c on top ofmain; phase 6a's stack (#72–#74) rewrites the same sentences for 6a and merges first, so this stack's rebase-and-reprove pass re-derives the counts.What lands
12 files, +1,624 / −39, all Markdown.
docs/work/mvp/phase6/phase6c/2026-09-09-phase6c-authentication-checklist.md, written from the build: 38 own rows, all ✅ —AUTH-29's three clauses stated in its row (the stripping clause satisfied by construction and no code; the suppression and HTTPS-guard-skip clauses implemented) — plus the Task 15 row "written, guarded; owned by 6b" and 12 cross-reference rows; the matrix facts on every interpreter; five guard tables (95 guards run red across the rounds); 35 deviations from the plan; the findings routed; postponed work: none.#pretty_print, the username redaction, the BINARY scan, Basic's colon refusal and transcoding,username*,AUTH_REFRESH, the hook future, the observed inner futures, the private replayability gate, theBearerProviderfunctions, the error placement, the keyword builds, the encoding name and the causeless raise, the per-waiter settlement, the fourth bearer rejection. The design's P6-1–P6-7 stand; they collide by number with 6a's (recorded by the roadmap on 2026-09-10) and phase 10 consolidates.docs/knowledge/notes/authentication.md(new —ppgives aDataits own#pretty_printand never calls#inspect, correctingauthentication/f8a5bc6a) and a new entry innotes/error-handling.md(cause: nilfor a secret-carrying cause, narrowingerror-handling/866b8ebe).docs/sdk-documentation/auth.md— new as-built page (the onearchitecture.mdlisted as unwritten), 77 fence checks on 4.0.6 and 3.2.11 with no secret printed;architecture.md, the core README,README.mdanddocs/README.mdpoint at it.docs/first-release.md— one phrase corrected in the AuthDescriptor blocker line (the step takes onestamper:, noScheme => credentialtable); the two entries the design named (that line, the query/cookieapiKeyline) verified present, not re-filed.CLAUDE.md— "Phases 0, 1, 2, 3a, 3b, 4a, 4b, 4c, 5a, 5b, 5c and 6c are built", the opening paragraph gains the authentication layer, 140 → 165 lib files besideversion.rb, eleven test-mirror exceptions (auth/validation.rbjoins,bounded_map.rbleaves — it now has a true mirror), twelve checklists, and the "Constraints that will bite" lines (BoundedMapreachable only by a bare name from a full-nesting body; the bearer hot path lock-free by publication andXCUT-12's one sanctioned lock across a fetch; Basic ispack("m0")and Digest is::Digest).Verification
bundle exec rakeon Ruby 4.0.6 at this tip: exit 0, all seventeen gates green (2,353 / 60,235 / 1 skip, 99.98%, YARD 0 undocumented).ruby .claude/skills/housekeeping/probe.rb: no drift, all eight checks;verify_knowledge_structure.rbOK.docs/product-spec/,docs/sdk-design-ruby/,docs/knowledge/harvested/,docs/deviations.md, every earlier phase's documents, the phase-6 charter, and 6a's and 6b's documents.CLAUDE.mdand the status note state against the tree and ran everyauth.mdfence on both interpreters.Known follow-ups (not blocking)
f1fe848; the rebase ontomainafter 6a merges re-derives them (6a: 149 lib files, thirteen exceptions) and reconciles the two phase-10 inbound bullets (6a's is numbered by ordinal) and the two status notes.a12c758).