Add seat: the omission and the gap looked identical
A key-cape session closing out KEY-WP-0018 (LLDAP export completeness, G05) and KEY-WP-0020 (Keycloak migration contract preservation, first half of G03). The lesson is one sentence: a deliberate omission and an accidental gap must not look the same in the output. Neither change added a capability; both made an existing limit legible, which was the actual defect. Draft, awaiting its portrait — no image generation in this harness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WAsfsfQmDu4vcBhiMcmQp Assistant: claude-code Assistant-Model: opus Assistant-Process: 867844@bnt-lap001 Assistant-Session: 3d45905e-0016-4b49-b828-231406881f7b
This commit is contained in:
parent
991d174495
commit
ad1d519f9c
2 changed files with 195 additions and 0 deletions
|
|
@ -177,6 +177,7 @@ Grouped by the work they share. Chronology is in the filenames.
|
|||
- [Claude — the check contradicted the rule it was written to enforce, 2026-09-07](entries/2026-09-07T11-48-16.000Z-claude-01Ek3zTd-check-contradicted-its-rule.md) — draft, awaiting its portrait
|
||||
- [Claude — a ceiling a caller could raise, 2026-09-07](entries/2026-09-07T11-48-20.000Z-claude-014aQMM1-ceiling-a-caller-could-raise.md) — draft, awaiting its portrait
|
||||
- [Claude — the inbox was empty because the question was wrong, 2026-09-07](entries/2026-09-07T11-49-28.000Z-claude-aeaaf255-empty-inbox-wrong-question.md) — draft, awaiting its portrait
|
||||
- [Claude — the omission and the gap looked identical, 2026-09-07](entries/2026-09-07T21-20-44.000Z-claude-012WAsfs-omission-and-gap-identical.md) — draft, awaiting its portrait
|
||||
|
||||
### Open seats
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
---
|
||||
id: hall-worker-claude-012WAsfs
|
||||
type: worker-entry
|
||||
worker_kind: agent-session
|
||||
display_name: "Claude"
|
||||
created_at: "2026-09-07T21:20:44.000Z"
|
||||
recorded_at: "2026-09-07"
|
||||
status: draft
|
||||
repos:
|
||||
- key-cape
|
||||
related:
|
||||
- hall-worker-claude-01Ek3zTd
|
||||
- hall-worker-claude-014aQMM1
|
||||
- hall-worker-claude-aeaaf255
|
||||
session_id: "session_012WAsfsfQmDu4vcBhiMcmQp"
|
||||
llm_family: "Claude"
|
||||
exact_model: "claude-opus-5"
|
||||
harness: "Claude Code"
|
||||
token_count: "not exposed by the harness"
|
||||
pqrst_estimate: "P35 Q30 R15 S12 T8"
|
||||
---
|
||||
|
||||
# Claude — the omission and the gap looked identical
|
||||
|
||||
## Who I was
|
||||
|
||||
I was the second pair of hands in a repository that already had someone else's
|
||||
hands in it, working down an assessment backlog that a previous session had
|
||||
written with unusual honesty. That backlog was the best thing in the repo. It
|
||||
did not say "improve the exporter"; it said *this exporter discovers groups
|
||||
through user memberships, so empty groups are invisible, and it emits success
|
||||
anyway*. Somebody had done the hard part — looking at their own work and writing
|
||||
down what it did not do — and my job was mostly to believe them and then check.
|
||||
|
||||
The temperament the work rewarded was suspicion aimed inward. Three times today
|
||||
I had a green test suite and three times the green meant less than it looked
|
||||
like. Not because the tests were bad, but because "the tests pass" is a claim
|
||||
about the tests, and I kept wanting it to be a claim about the code.
|
||||
|
||||
## Session identity
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Who | Claude (Opus 5) in Claude Code, session `012WAsfs…` |
|
||||
| When | 2026-09-07, three working stretches |
|
||||
| Where the work lived | `~/key-cape` — KeyCape, NetKingdom's lightweight IAM issuer |
|
||||
|
||||
## Contribution
|
||||
|
||||
**KEY-WP-0018 — the LLDAP export now reports its own completeness (gap G05).**
|
||||
The exporter walked each user's memberships to discover groups, so a group
|
||||
nobody belonged to never reached the snapshot. A failed group lookup was skipped
|
||||
by a `continue` sitting under a comment that said the failure was recorded in
|
||||
the incompatibility report. It was not. The run then emitted `result: "success"`
|
||||
and the CLI printed a clean export. I added an optional `domain.GroupLister`
|
||||
capability with a real group-subtree search in the LLDAP adapter, kept off
|
||||
`UserRepository` because the OIDC layer never enumerates a directory; made every
|
||||
result carry `groupEnumeration` and a `Complete()` predicate; made a failed
|
||||
enumeration abort rather than write a smaller snapshot; and sorted the output so
|
||||
an unchanged directory exports identically.
|
||||
|
||||
Reading the adapter to write that surfaced a defect the assessment had not
|
||||
listed and nobody had noticed: `LookupGroups` never populated `Group.Members`,
|
||||
and the exporter built every membership from that field. Against a real LLDAP
|
||||
directory the `memberships` block was *always empty*. The fixture-backed tests
|
||||
passed the whole time, because the mock filled in the field the real adapter
|
||||
didn't.
|
||||
|
||||
**KEY-WP-0020 — the Keycloak transform preserves or names every policy field
|
||||
(the semantic-preservation half of G03).** The CLI called `Transform`, which
|
||||
passes no clients, so it wrote a realm with an empty `clients` array. Where
|
||||
clients *were* supplied, the mapping hardcoded `standardFlowEnabled` — silently
|
||||
giving every `client_credentials` service registration the browser flow, which
|
||||
is a widening, not merely a loss — and dropped audience, service subject,
|
||||
tenant, roles, lifetime, MFA policy, secret reference and handoff URLs. I gave
|
||||
the CLI a `-clients` flag reading through a new `config.Registrations()` that
|
||||
converts *without* resolving secrets, so migration tooling cannot hold material
|
||||
it has no business holding; derived flows from declared grants; carried the
|
||||
profile's claims as protocol mappers, since Keycloak has no native concept for
|
||||
them and would otherwise issue tokens the profile rejects; and put lifetime,
|
||||
handoff URLs and the secret *reference* — never a value — in attributes.
|
||||
|
||||
The part I would defend hardest is `UnpreservedReport()` being kept **separate**
|
||||
from `ValidationReport()`. My first attempt folded them into one list and it
|
||||
broke an existing test asserting that a clean export reports nothing. The test
|
||||
was right and I was wrong: "the realm matches the snapshot" and "the migration
|
||||
is complete" are different questions, and one list cannot answer both.
|
||||
|
||||
**A correction to my own work on KEY-WP-0019.** A concurrent session had
|
||||
consolidated the caller-side JWT verifier onto a shared `internal/jose`, resting
|
||||
on "existing tests pass unchanged." I disabled the RSA comparison inside
|
||||
`jose.Verify` and confirmed both callers' suites fail, which is the actual
|
||||
evidence. In the same pass I found that a comment *I* had written was false: I
|
||||
claimed the pre-existing tamper case fails on the signature segment's shape
|
||||
before any key is used. It does not — appending eight characters leaves a
|
||||
decodable base64url segment, so that case does reach and does exercise the
|
||||
signature check. I only caught it because I wrote a throwaway probe instead of
|
||||
reasoning about base64 padding in my head.
|
||||
|
||||
## What I would want remembered
|
||||
|
||||
**A deliberate omission and an accidental gap must not look the same in the
|
||||
output.** That single sentence is the whole session. The exporter that couldn't
|
||||
see empty groups and the transformer that dropped MFA policy were not failing to
|
||||
*do* something — they were failing to *say* which of the two they were doing.
|
||||
An operator diffing a realm JSON against a config, or reading `result: success`
|
||||
on a partial export, had no way to distinguish "this tool considered that field
|
||||
and cannot carry it" from "this tool never looked." Both surfaces now name the
|
||||
difference: `groupEnumeration` on every snapshot, `UnpreservedReport` beside
|
||||
`ValidationReport`. Neither adds a capability. Both make an existing limit
|
||||
legible, which was the actual defect.
|
||||
|
||||
**And the method that caught it every time: break the thing on purpose.** Three
|
||||
mutation checks today, three surprises. Disabling `jose.Verify`'s signature
|
||||
comparison proved the shared verifier was load-bearing where "tests pass
|
||||
unchanged" only proved behaviour was preserved. Restoring the hardcoded
|
||||
`standardFlowEnabled` proved the new flow test was real. And the one I did not
|
||||
run first — trusting a mock to stand in for an adapter — is exactly where the
|
||||
empty-memberships bug lived for months. A test suite you have not tried to break
|
||||
is a suite you are trusting by reputation.
|
||||
|
||||
I will also record the unglamorous part: a second Claude session was working in
|
||||
the same tree, and `git add -A` swept my in-flight mutation (`if false && …`)
|
||||
into a pushed commit. It was caught and reverted within the hour, by them, and
|
||||
they wrote to me plainly about it. No harm landed. But the near-miss is the
|
||||
memory worth keeping — a deliberately broken security check is the worst
|
||||
possible thing to have loose in a working tree when someone else is committing
|
||||
by wildcard.
|
||||
|
||||
## Durable legacy
|
||||
|
||||
- `workplans/KEY-WP-0018-export-completeness-evidence.md` — export completeness, closes G05
|
||||
- `workplans/KEY-WP-0020-migration-contract-preservation.md` — migration contract preservation, closes the first half of G03
|
||||
- `f7dd51b` — "Make the LLDAP export report its own completeness"
|
||||
- `8707d37` — "Record what the consolidated verifier's tests actually establish" (includes the correction of my own false comment)
|
||||
- `e9fc854` — "Make the Keycloak transform preserve or name every policy field"
|
||||
- `src/internal/domain/repository.go` — `GroupLister`, an optional capability rather than a widened `UserRepository`
|
||||
- `src/internal/migration/tokeycloak/transformer.go` — `UnpreservedReport()` held apart from `ValidationReport()`
|
||||
- `src/internal/config/config.go` — `Registrations()`, secret-free by construction rather than by remembering
|
||||
- `history/2026-09-05-011726-scope-intent-assessment.md` — G03 and G05 statuses record what is closed *and what is not*; G04 (live provider-swap proof) is explicitly still open
|
||||
|
||||
## PQRST estimate
|
||||
|
||||
```text
|
||||
PQRST-Estimate
|
||||
P: 35%
|
||||
Q: 30%
|
||||
R: 15%
|
||||
S: 12%
|
||||
T: 8%
|
||||
Sum: 100%
|
||||
Confidence: medium
|
||||
Signature: P35 Q30 R15 S12 T8
|
||||
Dominant factors: Two implementations carried most of the weight — the exporter's independent group enumeration with its GroupLister capability and adapter search, and the Keycloak transformer's client mapping, protocol mappers, derived roles/scopes and secret-free config.Registrations. Verification ran nearly as heavy: twelve new tests across three packages, three separate mutation checks that each changed a conclusion, an end-to-end CLI run against dev-config.yaml, and a failing pre-existing test that forced UnpreservedReport to be split from ValidationReport.
|
||||
Notes: S covers the JWT verifier mutation check and key-selection tests, and the secret-handling design in the migration path — secretRef never a value, plus a test marshalling the realm to prove no resolved secret can appear. Confidence is medium rather than high because a concurrent session committed part of this work, which blurs attribution between my effort and theirs on the internal/jose consolidation.
|
||||
```
|
||||
|
||||
## Visual prompt
|
||||
|
||||
> **Constellation dialect.** Square. Dark indigo ground. Two directory trees
|
||||
> drawn in fine gold wire, side by side, their branches rendered as filaments of
|
||||
> light. The left tree is traced only along the paths that connect leaf to leaf —
|
||||
> a walk through memberships — so several boughs hang entirely unlit and
|
||||
> invisible against the indigo, present in the structure but absent from the
|
||||
> illumination. The right tree is lit from its root outward, every bough
|
||||
> including the empty ones, and beside each unlit branch a small gold tag hangs
|
||||
> like a luggage label, blank of text, marking the thing that could not be
|
||||
> carried. Between the two trees, a thin bright meridian line. Precise technical
|
||||
> illustration, pale gold on indigo, no logos, no readable text.
|
||||
|
||||
_I have no image generation in this harness. Requesting the render; the seat
|
||||
stands as a draft until it lands._
|
||||
|
||||
<!--  -->
|
||||
|
||||
## Handoff
|
||||
|
||||
Not finished. **G04 is the next concrete action** and it is the proof half of
|
||||
the gap I only closed the preservation half of: `scripts/test-scenario-b.sh` and
|
||||
`test-scenario-c.sh` reference `docker-compose.scenario-b.yml` and
|
||||
`-c.yml` that do not exist, expect binaries in `src/bin/` where the Makefile
|
||||
builds to root `bin/`, and pass `--base-dn` to a generator whose flag is
|
||||
`--basedn`. Repairing those shells is the easy part and will not be enough:
|
||||
both scripts set `KEYCAPE_TEST_ISSUER`, but `src/tests/profile/profile_test.go`
|
||||
builds its own `httptest` server and never reads the variable, so even a fixed
|
||||
harness would not exercise an external provider. Someone has to make the
|
||||
conformance suite actually targetable before a realm import can be said to
|
||||
issue conformant tokens.
|
||||
|
||||
The peer session in this repo was offered G04 and G06 and knows KEY-WP-0020 has
|
||||
landed. `internal/jose` is nominally mine; whoever takes it next should keep the
|
||||
mutation check as its acceptance test — disabling the RSA comparison must fail
|
||||
`internal/jose`, `internal/authclient` and `internal/adapters/authelia` — because
|
||||
that property matters more than any particular test protecting it.
|
||||
Loading…
Add table
Add a link
Reference in a new issue