docs: preserve reviewed NetKingdom session artifacts
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
Reject generated-only commits / generated-commit-guard (push) Waiting to run

This commit is contained in:
codex 2026-11-01 23:25:29 +01:00
parent 24ae04fa80
commit 573f707348
50 changed files with 5107 additions and 0 deletions

View file

@ -0,0 +1,123 @@
---
id: KEY-WP-0016
type: workplan
title: "Authorization-code protocol hardening and token verification bindings"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: authorization-code-protocol-hardening
created: "2026-09-06"
updated: "2026-09-06"
state_hub_workstream_id: "22698171-f283-59a9-9443-183faaeb6287"
---
Closes gap G01 of `history/2026-09-05-011726-scope-intent-assessment.md`, the
assessment's first recommended item. The browser grant validates PKCE, client id
and scopes but does not bind the redirect URI, authenticate confidential clients,
enforce grant-type eligibility, or consume the authorization code atomically.
UserInfo verification is weaker than the caller CLI's contract.
Scope is the local protocol surface only. Upstream provider-token verification
(the Authelia adapter's deliberate no-signature-check trust assumption) is a
separate trust-contract decision and is not addressed here.
## Consume authorization codes atomically
```task
id: KEY-WP-0016-T01
status: done
priority: high
state_hub_task_id: "a5171df0-4905-518d-a52f-7152b980af79"
```
`SessionStore.Get` reads a code and `Delete` removes it in separate lock
acquisitions, with signing in between, so two simultaneous exchanges can both
observe the same session before either deletes it. Replace the token path's
lookup with a single-operation consume that removes the session under the same
lock that reads it, so exactly one concurrent exchange can succeed. A failed
exchange must not leave a replayable code. Cover with a concurrent-reuse test
that asserts exactly one success across parallel requests.
Added `SessionStore.Consume`, which reads and deletes under one lock, and moved
the token path onto it. Verified the defect was real before claiming the fix:
with `Get`/`Delete` restored, 9 of 16 concurrent exchanges succeeded and a
PKCE-failed exchange left the code replayable. Both now fail closed —
`TestConcurrentCodeExchangeSucceedsExactlyOnce`, `TestFailedExchangeConsumesTheCode`.
## Bind redirect URI, grant type and confidential client authentication
```task
id: KEY-WP-0016-T02
status: done
priority: high
state_hub_task_id: "1d0fb108-273f-5c9a-a7c7-7640f19bad9d"
```
On the authorization-code path: require `redirect_uri` and compare it for exact
equality with the value stored at authorization time; reject a client whose
registration does not permit `authorization_code` (an empty `grantTypes` stays an
implicit authorization-code client, matching config validation, but a
`client_credentials`-only client must be refused); and authenticate confidential
clients with a constant-time secret comparison, accepting the same
`client_secret_basic` and form-encoded credentials as the service grant. Public
clients continue to present no secret and must not be able to supply one to
impersonate a confidential registration. Negative tests per condition.
Implemented all three in `token.go`, with `basicClientCredentials` and
`secretsEqual` shared with the service grant (digest-first comparison, so a
wrong-length secret is indistinguishable from a wrong value). Tests:
`TestCodeExchangeRejectsRedirectURIMismatchAndOmission` (omitted, different,
traversal-shaped and trailing-slash variants),
`TestCodeExchangeRejectsServiceOnlyClient`,
`TestCodeExchangeAllowsImplicitAuthorizationCodeClient`,
`TestConfidentialCodeClientRequiresItsSecret` and
`TestPublicClientSecretIsIgnoredAndEmptyConfidentialSecretIsRejected`. The
`keycape login` CLI already sent `redirect_uri`, so no caller change was needed;
the authclient test stub was recording no redirect and is now faithful to real
`/authorize`.
## Harden UserInfo token verification
```task
id: KEY-WP-0016-T03
status: done
priority: high
state_hub_task_id: "336db63a-e4fd-522d-86b6-0bf54a31af1c"
```
UserInfo verifies an RSA signature and expiry but ignores its own `Issuer` field
and does not check the JOSE header algorithm, the issuer claim, or the token
purpose, so an ID token or a foreign-issuer token of the right shape is accepted
where an access token is required. Enforce `alg=RS256` from the header, reject
`none` and any other algorithm, require the configured issuer, and reject tokens
that are not access tokens. Negative tests per condition.
`validateJWT` became `validateAccessToken`, checking the JOSE header before
trusting the signature, requiring the configured issuer (an empty configured
issuer is a misconfiguration, not a skipped check), and requiring the `scope`
claim the token endpoint sets only on access tokens. Purpose is decided
verification-side deliberately: adding a `token_use` claim would change the
issued-token contract that consumers pin exactly. Tests:
`TestUserinfoRejectsUnverifiedTokenShapes` (foreign issuer, missing issuer, ID
token presented as an access token) and `TestUserinfoRejectsNonRS256Algorithms`,
which restates the header algorithm over a genuine RS256 signature so it
isolates the header check.
## Reconcile scope and assessment records
```task
id: KEY-WP-0016-T04
status: done
priority: medium
state_hub_task_id: "90efa6ef-9495-5716-bee2-49d01c9ce05d"
```
Update `SCOPE.md`'s protocol-hardening limit and G01's closure status in the
assessment to state exactly which bindings are now enforced and which remain
open, naming the upstream-trust item as still outstanding. Do not claim complete
profile conformance: G01 closure covers these bindings, not the whole profile.
Updated `SCOPE.md`'s protocol-hardening limit and recorded a partial-closure
status under G01 in the assessment, naming the Authelia upstream-trust item as
still open. G01 is explicitly not fully closed.

View file

@ -0,0 +1,118 @@
---
id: KEY-WP-0017
type: workplan
title: "Reconcile the canonical model and discovery metadata with the runtime"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: canonical-model-and-discovery-conformance
created: "2026-09-07"
updated: "2026-09-07"
state_hub_workstream_id: "0904caad-ad8b-573f-9cdb-b6da09262a18"
---
Closes gap G02 of `history/2026-09-05-011726-scope-intent-assessment.md`.
`spec/canonical-model.yaml` and `src/internal/domain/model.go` both claim to be
the source of truth for the client registration shape, and they disagree: the
spec restricts grants to `authorization_code`, requires redirect URIs for every
client, and omits the service-subject, tenant, audience, lifetime, role and
MFA/handoff fields the runtime actually reads. Discovery advertises a fixed basic
scope and claim list that omits core profile claims and every configured resource
scope.
The point of the work is the executable link, not the edit: reconciling the two
files once leaves them free to drift again the same afternoon.
## Reconcile the client entity with the runtime model
```task
id: KEY-WP-0017-T01
status: done
priority: high
state_hub_task_id: "996f6895-f36b-585e-844d-3e55d02eefda"
```
Bring the `Client` entity in `spec/canonical-model.yaml` in line with
`domain.Client`: add `audience`, `serviceSubject`, `tenant`, `roles`,
`mfaRequired`, `registrationUrl` and `enrollmentUrl`; extend the `grantTypes`
enum to include `client_credentials`; and make `redirectUris` required only for
clients that use the authorization-code grant, matching what config validation
already enforces. Record which fields are deliberately absent from the canonical
model rather than silently omitted — `clientSecret` and `tokenLifetime` are
runtime policy, not identity data, and both are `yaml:"-"` in the Go model.
Added all seven fields, extended the grant enum, and made `redirectUris`
optional at the schema level with the authorization-code condition stated in its
description. `tokenProfile` and `environments` were declared in the spec but read
by nothing; they are kept and marked `runtime: false` rather than deleted, since
removing them would discard intent the check can now hold honest.
## Add an executable conformance check
```task
id: KEY-WP-0017-T02
status: done
priority: high
state_hub_task_id: "77df5043-2b23-56cd-947e-317f7117ac18"
```
Add a test that compares the canonical model's `Client` field set against
`domain.Client`'s YAML tags by reflection and fails on divergence in either
direction: a Go field with no spec entry, or a spec field the runtime does not
read. Fields intentionally excluded must be named in one list the test reads, so
excluding a field is a deliberate, reviewable act rather than an omission. This
is the generation/conformance link the assessment asks for.
`src/internal/domain/conformance_test.go` checks `Client`, `User`, `Group` and
`Membership` by reflection over YAML tags. It earned its place on the first run
by finding drift outside the assessment's list: `User.tenant` was read by the
runtime and undeclared in the spec. `yaml:"-"` fields are excluded by
construction, which is what keeps `clientSecret` and `tokenLifetime` out without
a hand-maintained exception list.
## Reconcile discovery metadata with the profile surface
```task
id: KEY-WP-0017-T03
status: done
priority: medium
state_hub_task_id: "5eb36f9b-a8e0-541e-8f81-310db49b77a0"
```
`claims_supported` omits `tenant`, `principal_type`, `assurance`, `scope` and
`nonce`, which the token endpoint emits on every token, and `scopes_supported` is
a fixed list that omits every configured resource scope. Advertise the core
profile claims and derive the scope list from the registered clients. Keep the
distinction the assessment asks for: an omitted core claim is a contract defect,
while optional discovery metadata is not, so document which is which rather than
treating every omission alike.
`claims_supported` now lists the core profile claims (`tenant`, `principal_type`,
`assurance`, `scope`, `nonce`) alongside the scope-gated human claims and the
optional cached `tenant_roles`. `scopes_supported` is derived from the registered
clients — baseline OIDC scopes first, configured resource scopes sorted after, so
the document is stable regardless of map iteration order — with the baseline
still advertised when no clients are configured.
`TestDiscoveryAdvertisesEveryCoreProfileClaim` states the distinction the
assessment asked for: a missing core claim fails, optional metadata does not.
## State a single source of truth
```task
id: KEY-WP-0017-T04
status: done
priority: medium
state_hub_task_id: "63290e2a-bae1-51c3-9cb9-d49994d804b6"
```
Two files currently claim to be authoritative. State in both that the Go model is
the runtime authority and the canonical model is the reviewed contract the
conformance check holds it to, so the next reader knows which one to edit first.
Update `SCOPE.md` and G02's status in the assessment to say what is now enforced
and what remains — this closes the client-registration and discovery drift, not
schema enforcement in general (G06 is a separate gap).
Both files now state the split: the Go model is the runtime authority, the YAML
the reviewed contract, and the conformance test the link. SCOPE.md and G02's
status say what is enforced and that G06 remains open.

View file

@ -0,0 +1,104 @@
---
id: KEY-WP-0018
type: workplan
title: "Make the LLDAP export report its own completeness"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: export-completeness-evidence
created: "2026-09-07"
updated: "2026-09-07"
state_hub_workstream_id: "82ff99a1-65cc-5e1f-93fc-5bc8911ce77a"
---
Closes gap G05 of `history/2026-09-05-011726-scope-intent-assessment.md`.
`src/internal/migration/lldapexport/exporter.go` discovers groups by walking each
user's memberships, so a group nobody belongs to is absent from the snapshot and
nothing says so. A `LookupGroups` failure is skipped by a `continue` under a
comment claiming it is recorded in the incompatibility report — it is not. The
run then emits `result: "success"` and the CLI reports a clean export. Group
order comes from map iteration, so two exports of an unchanged directory differ.
A second defect falls out of reading the adapter: `LDAPAdapter.LookupGroups`
never populates `Group.Members`, and the exporter builds every membership from
that field. Against a real LLDAP directory the `memberships` block is therefore
always empty, while the fixture-based tests pass because the mock fills it in.
The unit of work is the evidence, not the enumeration: an export that cannot
prove it saw the whole directory must say which one it is.
## Enumerate groups independently of membership
```task
id: KEY-WP-0018-T01
status: done
priority: medium
state_hub_task_id: "abb11f82-06e9-58ee-8be2-6d20491e9524"
```
Add an optional `domain.GroupLister` capability (`ListGroups`) and implement it
on `LDAPAdapter` with a direct group-subtree search that reads `member` and
`uniqueMember`, so `Group.Members` is populated from the directory rather than
left empty. Keep it optional rather than widening `UserRepository`: the OIDC
layer has no use for it, and every existing implementation and test double would
otherwise have to grow a method it never calls.
Added `domain.GroupLister` and `LDAPAdapter.ListGroups`, searching the group
subtree for `groupOfNames`/`groupOfUniqueNames` and reading `member` with
`uniqueMember` as the fallback attribute. Compile-time assertions on the adapter
now state both contracts it satisfies. The adapter tests assert the filter is
*not* a membership filter and that a member-less group survives the mapping,
which is the property the whole task exists for.
## Report or fail on incomplete reads
```task
id: KEY-WP-0018-T02
status: done
priority: medium
state_hub_task_id: "d08f62dd-42e8-5c22-ba50-15b53f7473fd"
```
Record on the result which enumeration actually ran — a complete directory
enumeration or the membership-derived fallback — and treat a failed group
enumeration as fatal rather than as a silent gap. In the fallback path, record
each skipped user in the incompatibility report instead of discarding the error,
and derive memberships from the discovered user/group pairs. Emit `partial`
telemetry, and have the CLI say which enumeration produced the snapshot, so a
degraded export is legible without reading the YAML.
`ExportResult` gained `groupEnumeration` — written unconditionally, so nobody has
to infer completeness from an absent field — and a `Complete()` predicate that
requires both a directory enumeration and an empty report. The fallback path
records its own incompleteness as a report entry before it starts, so the
snapshot says so even when every user lookup succeeds.
The fallback also had to stop deriving memberships from `Group.Members`: the
LLDAP adapter never populated that field, so against a real directory the
`memberships` block was always empty while the fixture-backed tests passed. It
now derives each membership from the user/group pair actually observed.
## Define ordering and test the failure modes
```task
id: KEY-WP-0018-T03
status: done
priority: medium
state_hub_task_id: "3cd47458-9469-5cd9-a0b1-f2894402e1f8"
```
Sort users, groups and memberships on stable keys so an unchanged directory
exports byte-identically. Cover the cases the assessment names and the current
suite does not: an empty group, a group enumeration failure, a per-user lookup
failure in the fallback path, and repeat-run determinism.
Sorted users and groups by ID and memberships by group then user. Four exporter
tests and two adapter tests cover the named cases; the determinism test runs the
export three times over deliberately unsorted input and compares the full
ordering, rather than asserting on a single sorted field.
Recorded in `SCOPE.md` and in G05's status what this does and does not establish:
completeness evidence for the user/group/membership surface, not for credential
migration, which stays under G03.

View file

@ -0,0 +1,155 @@
---
id: KEY-WP-0019
type: workplan
title: "Verify upstream Authelia ID tokens"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: upstream-provider-token-verification
created: "2026-09-07"
updated: "2026-09-07"
state_hub_workstream_id: "87ea18ea-dc60-52ce-baf4-476b46aa06f9"
---
Closes the remaining half of gap G01 in
`history/2026-09-05-011726-scope-intent-assessment.md`. The Authelia adapter
decodes upstream ID-token claims without verifying the signature, issuer,
audience or expiry, justified in a comment by a server-to-server TLS boundary.
**Operator decision (2026-09-07, Bernd):** verify the token itself. The
KeyCape→Authelia hop is to be HTTPS as defence in depth, but KeyCape will not
monitor, check or gate on that — transport enforcement is friction without
protection value here, and a check that must be configured correctly to help is
itself a failure mode. Verification is chosen precisely because it does not
depend on the transport being what we believe it is. No HTTPS validation, opt-in
flag or transport telemetry is to be added.
## Extract a shared RS256/JWKS verifier
```task
id: KEY-WP-0019-T01
status: done
priority: high
state_hub_task_id: "68c1b099-96b4-5065-a4be-8592438a0ca9"
```
`internal/authclient` already contains a strict RS256 verifier — kid required,
no `crit`, RSA-only keys of at least 2048 bits with an odd exponent. The upstream
verification needs the same rules. Duplicating security-critical verification is
how two copies drift and one silently misses a fix, so extract the reusable half
into `internal/jose`: strict JWK-set parsing and signature verification returning
claims. Claim policy stays with each caller, whose audience, nonce and issuer
rules genuinely differ.
Added `internal/jose`: strict JWK-set parsing (RSA signing keys only, at least
2048 bits, odd exponent 3 or greater, no duplicate key ids) and RS256 signature
verification returning claims. All failures collapse to one `ErrVerification`
so a caller cannot leak which check failed back to whoever supplied the token.
`authclient` is not yet migrated onto it — see T05.
## Verify the Authelia ID token
```task
id: KEY-WP-0019-T02
status: done
priority: high
state_hub_task_id: "8105030b-c91f-5a40-a221-4a38c8531e1c"
```
Before trusting any claim from the upstream ID token: verify the RS256 signature
against Authelia's published keys, require the issuer Authelia advertises, require
KeyCape's own client ID in the audience, and reject expired or not-yet-valid
tokens. Fail closed — an unavailable or unparseable key set denies the login
rather than falling back to unverified claims. Resolve Authelia's metadata and
key set from the server-side token base URL so a split-horizon deployment, where
the advertised public URL is not reachable from KeyCape, still verifies; allow
explicit issuer and JWKS overrides for deployments where that inference is wrong.
Refresh the key set once on an unknown key id so provider key rotation does not
require a KeyCape restart. Replace the comment claiming the TLS boundary as
justification.
`idTokenVerifier` in the adapter resolves the issuer and JWKS path from the
provider's discovery document, rebasing the advertised `jwks_uri` path onto the
server-side token base URL so split-horizon deployments resolve, with
`issuer`/`jwksUrl` config overrides where that inference is wrong. Key sets are
cached and refetched once on an unknown key id. `parseIDTokenClaims` remains for
diagnostics but is documented as outside the authentication path.
## Prove the rejections
```task
id: KEY-WP-0019-T03
status: done
priority: high
state_hub_task_id: "163f64dd-aa87-5ce3-b3be-d134fa993eb6"
```
Negative tests per condition: forged signature, `alg: none` and other algorithms,
unknown key id, wrong issuer, missing and wrong audience, expired token, future
`iat`/`nbf`, malformed key set, and an unreachable JWKS endpoint. A positive case
must confirm a genuine token still authenticates, and a rotation case that a new
key id is picked up after one refresh.
Thirteen rejection cases in `verify_test.go` plus the algorithm and rotation
cases. Confirmed they catch the original defect rather than merely passing: with
the unverified parse restored, all fifteen fail. The existing adapter tests now
build genuinely signed tokens against a fixture provider, so they exercise the
real verification path instead of being routed around it.
## Reconcile the records
```task
id: KEY-WP-0019-T04
status: done
priority: medium
state_hub_task_id: "8a208092-7e85-5ad9-a8c4-0dddc66aaaee"
```
Update `SCOPE.md` and G01's status in the assessment to state that upstream
provider tokens are now verified independently of transport, and record the
operator decision and its reasoning so the absence of transport enforcement reads
as a choice rather than an oversight. G01 closes with this; complete profile
conformance is still not claimed.
SCOPE.md and G01's status record the verification and the operator decision,
including why no transport enforcement exists, so its absence reads as a choice.
## Consolidate the caller verifier
```task
id: KEY-WP-0019-T05
status: done
priority: medium
state_hub_task_id: "0dc909fe-1772-5816-9e11-dbe7664a4e1f"
```
Move `internal/authclient`'s inline verification onto `internal/jose` so one
implementation serves both paths. Kept separate from T01/T02 deliberately: the
caller verifier is a tested security path, and destabilising it in the same
change that introduces upstream verification would confuse the evidence for both.
`Client.Verify` now fetches the raw key set and delegates parsing and signature
checking to `internal/jose`, keeping its own claim policy — audience and nonce
bindings are the caller's and cannot live in a shared verifier. Its existing
tests pass unchanged, which is the point: the migration preserved behaviour.
One deliberate strictness increase: a key set containing any malformed RSA
signing key is now refused outright, where the previous code would have used a
good key alongside a bad one. KeyCape's own `/jwks` publishes a single key, so
this affects no current deployment.
Also added direct tests for `internal/jose` (17 cases). It is now the single
verifier behind both paths, so testing it only through its callers would leave
its edges — duplicate key ids, `crit`, even exponents, undersized moduli — to be
covered by accident.
Follow-on evidence for the same task: "existing tests pass unchanged" shows the
move preserved behaviour, not that the behaviour is checked, so the check was
made explicit. Disabling the RSA comparison in `jose.Verify` fails both callers'
suites, which establishes the shared verifier is load-bearing on each path.
Added two caller-side cases the existing tamper case cannot reach, since it
alters signature bytes for a key that is legitimately published: a structurally
valid token signed by an unpublished key under a published kid, which fails only
if key selection is bound to the key set, and an undersized modulus in the
published set, which must deny rather than fall through to the claims.

View file

@ -0,0 +1,107 @@
---
id: KEY-WP-0020
type: workplan
title: "Make the Keycloak transform preserve or name every policy field"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: migration-contract-preservation
created: "2026-09-07"
updated: "2026-09-07"
state_hub_workstream_id: "803845de-98ad-5a5b-89ec-11bd9e0fafec"
---
Closes the first half of gap G03 of
`history/2026-09-05-011726-scope-intent-assessment.md` — semantic preservation.
The second half, proof against a running Keycloak, is G04 and stays open.
`keycape-to-keycloak` calls `Transform`, which passes no clients, so the realm it
writes has an empty `clients` array and nothing says the service-identity
contract was not migrated. `TransformWithClients` exists but no caller reaches
it. Where clients are supplied, `mapClient` always enables the standard flow
regardless of grant types, and drops `Audience`, `ServiceSubject`, `Tenant`,
`Roles`, `TokenLifetime`, `MFARequired`, `SecretRef` and the handoff URLs.
`mapUser` drops the canonical ID, tenant and roles. Realm roles and client scopes
are written as empty containers with a comment saying they "can be extended".
The failure mode is not the missing mapping — it is that a dropped field and an
inapplicable one look identical in the output. An operator diffing the realm JSON
against the KeyCape config has no way to tell what this tool decided not to carry
across. Preserve what Keycloak can express, and name the rest.
## Give the CLI the client registrations
```task
id: KEY-WP-0020-T01
status: done
priority: high
state_hub_task_id: "6f6bcfd7-16c5-5311-9568-ad9aa9c3b60a"
```
Add a `-clients` flag reading the KeyCape config's client registrations and pass
them through `TransformWithClients`. When no client file is given, say so on
stderr and in the report rather than emitting an empty `clients` array that reads
like a realm with no clients.
Added `-clients`, reading registrations through a new `config.Registrations()`
that converts without resolving secrets — migration tooling needs the contract,
never the material, so `ClientSecret` is left empty by construction rather than
by remembering not to use it. An unparseable `tokenLifetime` is an error there,
since silently dropping a per-client lifetime is the defect class this closes.
Run against `config/dev-config.yaml`, all five registrations now reach the realm.
## Preserve what Keycloak can express
```task
id: KEY-WP-0020-T02
status: done
priority: high
state_hub_task_id: "88c0126c-fd73-5178-af1f-a018708ff8fb"
```
Derive `standardFlowEnabled` and `serviceAccountsEnabled` from the declared grant
types instead of hardcoding the standard flow on. Carry the audience, tenant,
service subject, roles and principal type as protocol mappers, per-client
lifetime as the Keycloak client attribute that expresses it, and the handoff URLs
and secret reference as attributes — the reference, never a secret value. Derive
realm roles and client scopes from the users and clients actually present rather
than emitting empty containers. Carry the user's canonical ID, tenant and roles.
Flows now follow the grants: the old mapping set `standardFlowEnabled` on every
client, so migrating a `client_credentials` service registration silently gave it
the browser flow. Audience, tenant, service subject and roles ride as protocol
mappers, since Keycloak has no native concept for the profile's claims and would
otherwise issue tokens the profile rejects. Lifetime, handoff URLs and the secret
*reference* are attributes; a test marshals the realm and fails if a resolved
secret value appears, because a realm import file is not a custody boundary.
Realm roles and client scopes are derived and sorted.
## Name everything not preserved
```task
id: KEY-WP-0020-T03
status: done
priority: high
state_hub_task_id: "5b8127b2-c01c-5d3c-afbf-adbb368941e6"
```
Report each field the transform cannot carry, at the point it is dropped, so the
output distinguishes a deliberate omission from a gap: passwords and MFA
credentials, MFA policy enforcement, and subject continuity — LLDAP's canonical
ID is a DN, and Keycloak will mint its own `sub`, so tokens after a migration
will not carry the same subject unless something downstream maps it. Fail the
realm-level validation when a supplied client's contract is not fully
represented. Test that each unpreserved field appears in the report.
`UnpreservedReport()` is separate from `ValidationReport()`. Folding them
together was the first attempt and it broke an existing test asserting a clean
export reports nothing — correctly so: consistency with the snapshot and
completeness of the migration are different questions, and one list cannot answer
both. Keeping them apart preserves the existing contract and makes the
distinction the point rather than a side effect.
Six tests cover the flows, the mappers and lifetime, the secret-value exclusion,
the derived roles and scopes, every named limit, and the separation of the two
reports. Mutation-checked: restoring the hardcoded `standardFlowEnabled` fails
the flow test. An incomplete transform now emits `partial` telemetry.

View file

@ -0,0 +1,101 @@
---
id: KEY-WP-0021
type: workplan
title: "Make snapshot attribute validation enforce a real rule"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: snapshot-attribute-validation
created: "2026-09-07"
updated: "2026-09-07"
state_hub_workstream_id: "733ad1a5-c4d8-5c2f-8608-c51bf1d5b17e"
---
Closes gap G06 of `history/2026-09-05-011726-scope-intent-assessment.md`:
`checkNoUnknownAttributes` is named for a rule it does not implement. It rejects
blank keys, which is not an allow-list, so a snapshot carrying arbitrary or
malformed LDAP attribute names passes a check whose name says otherwise. A rule
that passes for the wrong reason is worse than an absent one, because a report
listing `no_unknown_attributes: passed` is read as evidence.
One part of the assessment's description needs correcting rather than
implementing: it says group membership validation checks nonempty member IDs
rather than complete referenced identity existence. `checkValidGroupMemberships`
does only check emptiness, but the semantic rule `checkReferencedUsersExist`
already resolves every member against the user set and fails on an unknown one.
The reference constraint exists; it lives in a different rule than the one the
assessment looked at.
## Enforce real attribute constraints
```task
id: KEY-WP-0021-T01
status: done
priority: medium
state_hub_task_id: "e2921983-853c-501b-8dfc-158751dea093"
```
`ldapAttributes` is defined by the canonical model as raw attributes *not covered
by* the model, so an allow-list of permitted names cannot be derived from the
schema — the field exists precisely to carry what the schema does not name. Two
constraints are derivable and worth enforcing:
- the attribute name must be a syntactically valid LDAP attribute descriptor
(RFC 4512: a letter followed by letters, digits or hyphens, or a numeric OID),
since a name that cannot be written to a directory cannot round-trip; and
- it must not shadow a canonical field's documented LDAP mapping (`uid`, `cn`,
`mail`), because two sources for one value is how an export and a directory
come to disagree, and the model already owns those.
Attribute descriptors are case-insensitive in LDAP, so keys differing only by
case are a collision, not two attributes. Keep the blank-key check. Rename the
rule to describe what it actually enforces rather than leaving a name that
overpromises.
Implemented as `raw_attributes_well_formed`, replacing `no_unknown_attributes`
in both `validator.go` and `spec/ldap-schema.yaml`, with the schema entry stating
plainly that this is not an allow-list and why one cannot be derived.
## Prove each rule with an invalid snapshot
```task
id: KEY-WP-0021-T02
status: done
priority: medium
state_hub_task_id: "de4caf0a-39c6-5148-a986-db18a1308505"
```
The assessment asks for invalid-snapshot cases proving each stated rule. Add one
per constraint — malformed descriptor, shadowed canonical mapping, case-only
duplicate, blank key — plus passing cases for legitimate raw attributes and a
valid numeric OID, so the rule is shown to be selective rather than merely
strict.
Eleven malformed-descriptor cases, six canonical-mapping cases across casings, a
case-only duplicate, a message-content case, and five accepted-attribute cases.
Confirmed they test the rule rather than merely passing: making the check return
early fails four of the five tests.
## Reconcile the records
```task
id: KEY-WP-0021-T03
status: done
priority: medium
state_hub_task_id: "8a48c6cc-1e7e-5fd4-abfa-2b893c3d400d"
```
Update `SCOPE.md` and G06's status. State what the rule now enforces and, just as
importantly, what it still does not: this is not schema-derived validation of
every attribute against a directory schema, and the canonical model cannot
provide one for a field defined as everything the model does not cover. Correct
the assessment's reference-constraint premise rather than restating it.
`history/…-scope-intent-assessment.md` was being edited by a concurrent session
when this workplan was written; make this edit only against a clean tree.
Done against a clean tree after the other session committed. G06's status records
both corrections: no allow-list is derivable for a field defined as what the
model does not cover, and the reference constraint was already implemented in
`checkReferencedUsersExist` rather than missing.

View file

@ -0,0 +1,136 @@
---
id: KEY-WP-0022
type: workplan
title: "Make the replacement harness runnable and target conformance externally"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: replacement-harness-and-external-conformance
created: "2026-09-07"
updated: "2026-09-07"
state_hub_workstream_id: "94d74b17-1b2f-57dd-b6c3-50f10a7dccd9"
---
Closes the runnable half of gap G04 of
`history/2026-09-05-011726-scope-intent-assessment.md`. The Scenario B and C
scripts cannot execute: they reference compose files that do not exist, call
binaries at `src/bin/` where the Makefile builds to root `bin/`, pass `--base-dn`
to `lldap-to-ldap` whose flag is `--basedn`, and invoke a hardcoded workstation Go
path from outside the Go module.
The deeper problem is that repairing the shell would not prove anything. Both
scripts set `KEYCAPE_TEST_ISSUER`, but the profile suite builds its own `httptest`
server and never reads it, so the tests pass identically whether or not the
external provider is running. A harness that cannot fail for the reason it exists
is worse than a missing one.
## Repair the harness prerequisites
```task
id: KEY-WP-0022-T01
status: done
priority: high
state_hub_task_id: "1d552328-e75b-57fd-83bc-e469ae46b397"
```
Fix the binary paths, the `--basedn` flag, and the Go invocation so the scripts
run from a clean checkout with `make build`: use the module directory rather than
a workstation-specific Go path, and fail with a clear message when a prerequisite
binary or a required environment value is absent instead of part-way through.
Scenario B rewritten: root `bin/`, `go -C src` so the module root is right,
prerequisite checks before anything starts, and a cleanup trap so a failure does
not leave a stack running. Scenario C repaired in place — `bin/` paths, `--basedn`
for `lldap-to-ldap` (`lldap-export` correctly takes `--base-dn`; the two flags
genuinely differ), and the same module-relative test invocation.
## Add the missing stacks
```task
id: KEY-WP-0022-T02
status: done
priority: high
state_hub_task_id: "06f34e8b-f3d9-53ed-95c5-02e5063774a0"
```
Provide `docker-compose.scenario-b.yml` (Keycloak over the migrated realm) and
`docker-compose.scenario-c.yml` (OpenLDAP seeded from generated LDIF, plus
Keycloak). Validate them with `docker compose config` rather than assuming they
parse. Whether a full scenario passes end to end is a separate claim from whether
the stack definition is valid, and only the second is established here.
Both files added and validated with `docker compose config`. Scenario B was also
started for real; Scenario C's stack has not been run.
## Target conformance at an external issuer
```task
id: KEY-WP-0022-T03
status: done
priority: high
state_hub_task_id: "d837730d-e9d0-5adb-9dda-ffc47661fca3"
```
Add a suite that reads `KEYCAPE_TEST_ISSUER` and exercises that issuer over HTTP:
discovery metadata, the JWKS parsing under the same strict rules the runtime
applies, and — when client credentials are supplied — a real token exchange whose
claims are verified against the published keys. It must skip cleanly when the
variable is unset so ordinary `make test` is unaffected, and it must fail when
pointed at an issuer that does not meet the profile, which is the property the
current harness lacks.
`src/tests/conformance` checks that the issuer advertises itself, the profile
authorization surface (code, authorization_code, S256, RS256), that published
keys parse under `internal/jose` — the same rules the runtime applies, so a
provider publishing keys KeyCape would refuse fails here rather than at first
login — that excluded grants are absent, and, with credentials, that an issued
token verifies against the published keys and carries a matching issuer.
## Prove the suite against a real provider
```task
id: KEY-WP-0022-T04
status: done
priority: high
state_hub_task_id: "cb4d8af7-76a4-50b8-bee5-eb1c52c2f894"
```
Run the new suite against an actual Keycloak instance, not only against KeyCape,
since the point of the scenario is a replacement provider. Record what genuinely
passed and what remains unproven. Do not describe a partially exercised scenario
as a demonstrated live swap.
Run against Keycloak 26.0. Discovery, authorization surface and published-key
checks passed. A `client_credentials` exchange against a service client created
through the admin API returned a token that verified against Keycloak's JWKS via
`internal/jose`, with a matching issuer claim — the end-to-end property a relying
party depends on, proved against a replacement provider rather than against
KeyCape's own handlers.
The suite also failed, correctly. Stock Keycloak advertises `implicit` and
`password` in its discovery document. In Keycloak these are server capabilities,
not per-client toggles, so no realm the transformer emits can remove them: a
migrated Keycloak presents a wider grant surface than KeyCape. Scenario B
therefore reports a real failure today, which is the harness working. KeyCape
itself was not targeted in this run — the dev stack needs key material absent
from the checkout — so the suite is proved against the replacement provider, not
yet against both sides of the swap.
## Reconcile the records
```task
id: KEY-WP-0022-T05
status: done
priority: medium
state_hub_task_id: "2cb63801-8182-5c71-ab8d-9a1c377f418c"
```
Update `SCOPE.md` and G04's status with what the harness now does and what
replacement claims remain unproven — in particular that directory migration, MFA
and unchanged relying-party behaviour against a live Keycloak are not established
by this work. G04 does not fully close here.
SCOPE.md and G04's status record both the working harness and the grant-surface
finding, which substantiates with evidence what SCOPE previously asserted without
it. Scenario C has still never been run end to end.

View file

@ -0,0 +1,136 @@
---
id: KEY-WP-0023
type: workplan
title: "Prove the migration against live directories and fix what that surfaced"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: live-migration-proof
created: "2026-09-08"
updated: "2026-09-08"
state_hub_workstream_id: "9f19bfa0-78d1-5b5c-9b64-e251fa4838d6"
---
Finishes the unproven half of gap G04. KEY-WP-0022 made the harness runnable and
targeted conformance at a live issuer; what remained was directory migration
against a real directory, both sides of the swap measured, and the explicit
limits recorded.
Running it found four defects that no amount of reading would have. That is the
result worth keeping: every one of them passed the full unit suite.
## Run the export against a live LLDAP
```task
id: KEY-WP-0023-T01
status: done
priority: high
state_hub_task_id: "b7b2ea88-8457-5fae-a500-a2d0f75754de"
```
**Defect: every LLDAP user search pointed at a branch that does not exist.**
`Config.userOU()` defaulted to `ou=users`; LLDAP stores users under `ou=people`,
and nothing in this repository set `UserOU` to correct it. `LookupUser`,
`ListUsers` and `ValidatePassword` all derive their search base from it, so all
three silently found nothing against a stock LLDAP — human login included, not
just the export.
Symptom that exposed it: exporting from a live LLDAP returned **zero users while
still emitting a membership referencing `uid=admin`**, a snapshot the repository's
own validator rejects with `referenced_users_exist`. Default corrected to
`ou=people` and pinned by a test; the re-run exports one user and validates
clean.
## Correct the rule I broke in KEY-WP-0021
```task
id: KEY-WP-0023-T02
status: done
priority: high
state_hub_task_id: "2f988626-ed08-52ce-919a-cd45d48e712d"
```
**Defect of my own making.** The LLDAP adapter records `_validation_warning`
inside `LDAPAttributes`. The `raw_attributes_well_formed` rule added in
KEY-WP-0021 rejects underscores as invalid attribute descriptors, so the
exporter's own annotation made its own output fail validation — a false positive
shipped one workplan earlier and caught only by validating a real export.
Keys prefixed with `_` are now recognised as tooling annotations and skipped,
documented as such. A test proves the exemption does not weaken the rule: a
malformed real attribute alongside an annotation still fails.
## Prove the LDIF loads into a real OpenLDAP
```task
id: KEY-WP-0023-T03
status: done
priority: high
state_hub_task_id: "0f503342-0ca8-55ad-b7e4-e2316d4d7bad"
```
**Defect: migrated group memberships were dangling.** `resolveMemberDN` returned
a member that already looked like a DN unchanged, so members kept their *source*
DNs (`uid=admin,ou=people,…`) while `writeUser` wrote entries to the *target*
branch (`ou=users`). Groups named entries the migrated directory does not
contain. Only the naming value is carried across now; branch and naming
attribute come from the target.
**Defect: empty groups could not be loaded at all.** `groupOfNames` makes
`member` a MUST, so OpenLDAP rejected every memberless group with an object class
violation and aborted the load. Since KEY-WP-0018 the export deliberately
preserves empty groups, so this was reachable by any real migration. Empty groups
now reference a placeholder entry the same LDIF creates — an `organizationalRole`,
not a person, so it cannot be mistaken for a migrated user — emitted only when
some group needs it.
**Defect: the compose file could not start.** `bitnami/openldap:2.6` does not
exist in the registry. It passed `docker compose config` in KEY-WP-0022, which is
exactly the gap between a file that parses and a stack that runs. Pinned to
`osixia/openldap:1.5.0`, the image the scenario was actually proved against.
Proof: LLDAP → export → validate → LDIF → `ldapadd` into OpenLDAP 1.5.0, all
seven entries added with no errors, and every `member` value in the loaded
directory resolves to an entry that exists.
## Measure both sides of the swap
```task
id: KEY-WP-0023-T04
status: done
priority: high
state_hub_task_id: "8a7fe95e-d463-5e74-81fa-5fa60a879774"
```
KeyCape was run locally against a generated key and targeted with the
conformance suite: all five checks pass, including the excluded-grant check
Keycloak fails. The Keycloak side was exercised twice — the stock realm in
KEY-WP-0022, and here the *migrated* realm produced from the live export, which
imported cleanly and served discovery. The migrated user arrived carrying
`keycape.canonicalId`.
The divergence is now measured rather than asserted: KeyCape passes 5/5, a
migrated Keycloak passes 4/5 and fails on advertising the excluded `implicit` and
`password` grants.
## Record what is proved and what is not
```task
id: KEY-WP-0023-T05
status: done
priority: medium
state_hub_task_id: "acef6ceb-6dea-56e1-94bc-e5d589ef3b6f"
```
G04 is substantially closed: stacks are reproducible and were run, conformance is
externally targeted and was run against three issuers, and directory migration is
proved end to end into a real directory.
Not closed, and stated as such: no browser login was performed against the
migrated realm, so unchanged relying-party behaviour is not established, and MFA
was not exercised. Credential and MFA migration is a **scope exclusion** rather
than a test gap — SCOPE says it is not supplied, so no harness can demonstrate
it. `keycape-to-keycloak` already reports that subject continuity is not
preserved: the canonical ID survives as an attribute while Keycloak mints its own
`sub`, so relying parties keyed on `sub` will not recognise migrated users.

View file

@ -0,0 +1,62 @@
---
id: KEY-WP-0024
type: workplan
title: "Wire tenant_roles as explicit opt-in configuration"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: tenant-roles-opt-in-wiring
created: "2026-09-08"
updated: "2026-09-08"
state_hub_workstream_id: "d75f52f2-df0d-5a56-8394-892feeec4b4d"
---
Closes gap G07. The tenant-engine client and `TokenHandler.TenantEngine` were
implemented and tested, but `main.go` supplied no client and exposed no
configuration, so the stock executable left the field nil and never emitted
`tenant_roles`. The capability existed at library level only.
Wired rather than documented away as library-only: the claim is cheap, the
adapter already fails open, and leaving a tested capability unreachable from the
binary invites the same gap being rediscovered later.
## Add opt-in configuration and wire the client
```task
id: KEY-WP-0024-T01
status: done
priority: medium
state_hub_task_id: "0e53485b-7e94-5dd8-8e02-1d18f5413a78"
```
Added a `tenantEngine` config block with `baseURL` and an optional `timeout`.
An empty `baseURL` disables the claim, which keeps the stock server's behaviour
exactly as it was — enabling it is a deliberate act.
Validation treats a configured source as one that must work: the URL must be
http/https with a host, and the timeout must parse and fall in (0, 10s], since it
sits on the synchronous token-issuance path. A `timeout` set without a `baseURL`
is rejected rather than ignored — it means someone expected the claim to be on
and it silently would not have been.
## Verify it in the built executable
```task
id: KEY-WP-0024-T02
status: done
priority: medium
state_hub_task_id: "c4279ce0-f6f8-5612-a48a-c1d2fd3da3b6"
```
G07's closure criterion is the built executable, not the wiring, so all three
behaviours were checked by running `bin/keycape` and inspecting real tokens
rather than by reading the code:
- configured against a stub tenant-engine, a `client_credentials` token carries
`tenant_roles: [capability:approve, capability:observe]`;
- with no `tenantEngine` block, the claim is absent;
- configured but with the source down, issuance still succeeds and the claim is
absent — the fail-open behaviour the adapter documents, confirmed end to end.
Nine validation cases cover the accepted and rejected configurations.

View file

@ -0,0 +1,118 @@
---
id: KEY-WP-0025
type: workplan
title: "Give the runtime real readiness, graceful shutdown and stated operational limits"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: runtime-lifecycle-and-readiness
created: "2026-09-08"
updated: "2026-09-08"
state_hub_workstream_id: "70f74bbd-6748-5bbe-b261-a310b6f24e0c"
---
Closes gap G08. `/healthz` returned a constant without probing anything, the
server called `ListenAndServe` with no signal handling, and the operational
limits of in-memory state, startup-loaded keys and local-only logout were spread
across code comments rather than stated anywhere an operator would look.
G08 explicitly does not require shared storage or refresh tokens. The work is
therefore to make readiness real, make restarts survivable for in-flight
requests, and write the limits down honestly.
## Separate readiness from liveness
```task
id: KEY-WP-0025-T01
status: done
priority: medium
state_hub_task_id: "0e0d3349-efaf-546c-a58d-2a9b736278a9"
```
Added `internal/server/readiness` and a `/readyz` endpoint probing LLDAP,
Authelia and privacyIDEA. `/healthz` stays liveness and deliberately probes
nothing: wiring liveness to dependency health means an orchestrator restarts
KeyCape when a dependency blinks, and since a restart also discards every
in-flight login, the reaction is worse than the condition.
Design decisions worth keeping:
- **LLDAP is probed with a bind, not a dial.** A rotated or revoked service
password leaves the port open and every lookup failing — precisely the state
readiness exists to catch, and precisely what a TCP dial would miss.
- **The body names the failing check but never the reason.** The endpoint is
unauthenticated, and upstream error text carries hostnames and sometimes
credentials-in-URLs. Detail goes to the log. A test asserts a probe error
containing a credentialed URL does not reach the response.
- **Results are cached for 2s.** Not an optimisation: without it, anyone able to
reach an unauthenticated endpoint could drive one upstream request per
dependency per call.
- **Probes run concurrently, each bounded at 3s**, so readiness costs the slowest
probe rather than their sum, and a hung dependency makes the endpoint answer
rather than hang alongside it.
## Shut down without dropping requests
```task
id: KEY-WP-0025-T02
status: done
priority: medium
state_hub_task_id: "0bea0780-523a-587e-a157-d429df15307b"
```
`SIGTERM`/`SIGINT` now drain in-flight requests for up to 15 seconds. The grace
period sits under the 30s read/write timeouts so a stuck request cannot outlive
the window an orchestrator allows before `SIGKILL`.
In-memory login state is deliberately not preserved: a browser mid-login must
start again after a restart. That is a property of the single-replica topology,
documented rather than papered over.
## State the operational limits
```task
id: KEY-WP-0025-T03
status: done
priority: medium
state_hub_task_id: "97eba4bb-f8a0-5941-bdc8-6d59b0fa2942"
```
`docs/operations.md` states the supported topology — exactly one replica, because
authorization codes, login sessions and handoffs are process-local and no sticky
configuration makes a second replica safe — and the consequences: restarts drop
in-flight logins while issued tokens keep working, and rolling deploys have a
window where new logins fail.
It also records three limits that are easy to get wrong in operation:
- the key ID is the constant `key-1`, so rotating the key without changing it can
leave consumers caching JWKS by `kid` rejecting freshly issued tokens;
- removing a client registration does not revoke its issued tokens, since there
is no revocation or introspection endpoint — they stay valid until expiry;
- `/logout` clears the local session only, not the Authelia session or any token.
No throughput or resource figures are given: nothing here benchmarks KeyCape, so
any number would be invented.
## Verify in the running server
```task
id: KEY-WP-0025-T04
status: done
priority: medium
state_hub_task_id: "bc64fdc2-0a31-5dab-afd1-5e7b6d11d2b3"
```
Checked against the built executable rather than the handlers, following the
lesson from KEY-WP-0023:
- with every dependency down, `/readyz` returns 503 naming all three as failed
while `/healthz` still returns 200 — the split doing its job;
- starting LLDAP flips its check to `ok` while the others stay failed, so the
probes are measuring what they claim;
- 40 requests issued across a `SIGTERM` all returned 200, and the log shows the
signal handled and shutdown completed.
Seven unit tests cover the reporter, including the disclosure, caching, timeout
and concurrency properties above.

View file

@ -0,0 +1,123 @@
---
id: KEY-WP-0026
type: workplan
title: "Reconcile packaging, bootstrap and migration credential handling"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: packaging-bootstrap-and-credential-handling
created: "2026-09-08"
updated: "2026-09-08"
state_hub_workstream_id: "d2b04d59-41b1-531b-8cab-a980328611b4"
---
Closes gap G09. Five separate defects, only loosely related: the dev stack could
not start from a clean checkout, the image shipped one binary of five, the
publish workflow named a different registry than the one the cluster pulls from,
the exporter took a password on argv, and every migration artifact was written
world-readable.
## Take the bind password off argv
```task
id: KEY-WP-0026-T01
status: done
priority: medium
state_hub_task_id: "510a4603-337f-5ceb-b6b7-b9126d8605f0"
```
`lldap-export` took `--bind-pw`, so the service account password was visible to
any local user through `ps`, and captured by shell history and process
accounting. It now prefers `KEYCAPE_LLDAP_BIND_PW`, accepts `--bind-pw-file`, and
keeps `--bind-pw` working with a warning — deprecated rather than removed,
because existing runbooks use it and silently breaking them would be worse than
the exposure for one more cycle.
Conflicting sources are rejected rather than silently ranked: with two supplied,
an operator cannot tell which bind was attempted. A password file has its
trailing newline trimmed, since editors and heredocs add one and binding with it
fails confusingly. Both migration scripts now pass the password by environment.
## Stop writing migration artifacts world-readable
```task
id: KEY-WP-0026-T02
status: done
priority: medium
state_hub_task_id: "679e21a1-e37b-574b-bc25-b9fd46bdfd16"
```
The canonical export, the generated LDIF and the Keycloak realm were all written
`0644`. None carries credential material, but the snapshot is a directory dump —
every username, display name, email and group membership in the estate — and it
tended to land in `/tmp`. All three are `0600` now.
## Ship every binary in the image
```task
id: KEY-WP-0026-T03
status: done
priority: medium
state_hub_task_id: "5252a13c-cd5d-5686-a7f5-04587d7d9b1d"
```
The image packaged `keycape` alone, so the validator and the three migration
binaries needed a Go toolchain on the host — which defeats shipping an image for
the cutover work they exist to support. All five are built and copied; the issuer
remains the entrypoint and the rest are reachable by overriding it. Verified by
building the image and running each binary inside it.
## Reconcile the publish target
```task
id: KEY-WP-0026-T04
status: done
priority: medium
state_hub_task_id: "45b8c386-41bd-514c-9247-5eba9663d8fb"
```
The workflow published to `92.205.130.254:32166/coulomb/key-cape` while the
cluster runs `forgejo.coulomb.social/coulomb/key-cape` (KEY-WP-0013). Whether
those are one registry behind two names or two registries is not determinable
from this repository, which is exactly the problem: a push could not be assumed
to have deployed the current source. Now defaults to the recorded Forgejo name
and stays overridable through a repository variable.
**Unverified, and needs an operator's eye:** this repository cannot test that the
runner resolves that hostname or that `REGISTRY_USER`/`REGISTRY_TOKEN` are valid
for it. If the next publish fails, set the `REGISTRY` repository variable back to
the address.
## Make the dev stack bootstrappable
```task
id: KEY-WP-0026-T05
status: done
priority: medium
state_hub_task_id: "a750ac2f-cad2-5e7f-a505-2ca6b94f1a5a"
```
`docker-compose.dev.yml` mounts `config/dev-key.pem` and `config/authelia/`,
neither in the checkout — correctly, since one is a private key and the other
carries a password hash. `scripts/bootstrap-dev.sh` generates both locally: an
RSA key created under `umask 077` rather than chmodded afterwards, so it is never
briefly world-readable, plus an Authelia configuration, its OIDC signing key and
a user fixture. Everything it writes is git-ignored, and re-running it leaves
existing material alone.
The fixture password is published in the script deliberately: a reader can see
exactly what it unlocks, which is nothing deployed.
## Reconcile the records
```task
id: KEY-WP-0026-T06
status: done
priority: medium
state_hub_task_id: "cc7c86ae-9c78-5422-8f3c-19441ceeb513"
```
`SCOPE.md` and G09's status record what is fixed and what is not: the registry
change is unverified from here, and a documented external bootstrap remains the
route for anything beyond development.

View file

@ -0,0 +1,130 @@
---
id: KEY-WP-0027
type: workplan
title: "Establish the live state and the rollout gap for G10"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: rollout-readiness-and-live-state
created: "2026-09-08"
updated: "2026-09-08"
state_hub_workstream_id: "2ab60558-ee14-542c-9f19-bb487de8e668"
---
G10 cannot be closed from this repository: it waits on named custody and platform
owners. What was doable was to stop guessing about the live state, verify the
handoffs actually went out, and find out whether main is safe to roll out. The
last question turned up a defect in this repository's own recent work.
## Verify the handoff receipts independently
```task
id: KEY-WP-0027-T01
status: done
priority: medium
state_hub_task_id: "00de2bad-04da-5b58-8cde-aaa144272541"
```
KEY-WP-0009 was reopened once because delivery was claimed without receipts, so
the claim that it has now been sent deserved the same scrutiny rather than being
taken on trust. `GET /messages/?from_agent=key-cape` shows all seven present and
dated 2026-09-08T08:21Z: the four KEY-WP-0009 handoffs (netkingdom,
secrets-engine, ops-warden, railiance-platform) and the three admission requests
(railiance-platform custody, approval-engine human callback, ops-warden token
split). No replies had arrived at the time of writing, which is expected hours
after sending and is not itself a blocker to record.
## Record the live state as observed, not as remembered
```task
id: KEY-WP-0027-T02
status: done
priority: medium
state_hub_task_id: "2d513229-1dbd-500e-9100-c0173be7b4de"
```
Read-only cluster inspection, `sso` namespace, 2026-09-08:
- image `forgejo.coulomb.social/coulomb/key-cape:main-153258b`, predating every
change from KEY-WP-0016 onward;
- environment carries `KEYCAPE_RAPP_QONTO_CLIENT_SECRET` only, so the two
approval clients are still not materialized — KEY-WP-0013-T02 remains correctly
blocked;
- four registered clients: `demo-app`, `netkingdom-bootstrap-console` and
`openbao-admin` (public, authorization_code) and `rapp-qonto-client`
(confidential, client_credentials);
- no `tenantEngine` block, so KEY-WP-0024 is inert on rollout.
**Correction to an earlier claim of mine.** I reported the `ou=people` fix
(KEY-WP-0023) as a production-relevant correction to human login. The deployed
config sets `userOU: "ou=people"` explicitly, so production was never affected.
The fix matters for anyone relying on the default — the dev stack, the scenario
scripts, a fresh deployment — not for this one.
## Find out whether main is safe to roll out
```task
id: KEY-WP-0027-T03
status: done
priority: high
state_hub_task_id: "23c8d04b-c2b7-5347-80a0-6388c1c41c53"
```
**Defect found in KEY-WP-0019, before it shipped.** Upstream verification
discovers the expected issuer server-side from `authelia.tokenBaseURL`. The
deployed Authelia derives its advertised issuer from the request Host, confirmed
directly:
| Host presented | issuer advertised |
| --- | --- |
| `authelia.sso.svc.cluster.local:9091` | `http://authelia.sso.svc.cluster.local:9091` |
| `auth.coulomb.social` | `http://auth.coulomb.social` |
KeyCape fetches over the in-cluster address, so it would expect the first value
while tokens minted for the browser-facing host carry the second. Verification
fails closed, so if they differ **every human login fails after rollout**, and it
presents as a broken login rather than a configuration error.
Which value the ID token actually carries could not be settled from here without
performing a real login against production Authelia, which was not done. The risk
is therefore recorded as a rollout precondition, not as a confirmed breakage.
Two mitigations, both in this repository:
- `docs/operations.md` documents pinning `authelia.issuer` and `authelia.jwksUrl`
explicitly, with the one-line `curl` that reveals the value the provider will
advertise for a given Host. The overrides already existed; nothing said when
they were needed.
- The failure is now diagnosable. The authentication failure event carries a
specific reason — `id_token_issuer_mismatch`, distinct from
`id_token_signature`, `provider_keys_unavailable` and the rest — so an operator
can tell a misconfiguration from an attack. Five cases cover the mapping. The
browser still learns nothing.
## Record what each owner owes
```task
id: KEY-WP-0027-T04
status: done
priority: medium
state_hub_task_id: "7ba84b61-0458-5820-8ded-c6f59d820303"
```
G10's status now names the live state, the outstanding requests with their
receipt ids, and the issuer precondition that must be checked before main is
deployed. The precondition was also sent to railiance-platform as the deployment
owner (message c8b1ad10), with the exact `curl` to settle it, the config keys to
pin, and the `error_type` to look for if it bites — plus a note that the two
approval clients remain unprovisioned, confirmed by inspection rather than
asserted. G10 stays open: nothing here admits custody, registers the human
callback, or reconciles the OpenBao-token boundary, and no repo-local change can.
2026-09-08 live return: the actual signed-token uncertainty in T03 is now
resolved by KEY-WP-0013-T02's admitted probe. The issuer is exactly
`https://auth.coulomb.social`; signature/audience/time/nonce checks passed and
the Job exited 0. The temporary route, policies, service and Job/Pod were removed.
`docs/evidence/2026-09-08-upstream-issuer-live-proof.json` is the source receipt.
The configuration owner still ensures the exact pin before main rollout; no
normal config or Deployment changed during this proof.

View file

@ -0,0 +1,89 @@
---
id: KEY-WP-0028
type: workplan
title: "Reject service-identity fields that a browser client silently ignores"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: browser-client-field-validation
created: "2026-09-09"
updated: "2026-09-09"
state_hub_workstream_id: "6d0cb4fd-17da-5466-8011-5ade1ac46171"
---
`serviceSubject` and `roles` are read only on the `client_credentials` path. On a
browser client they are accepted at startup and then ignored: the subject and
roles come from the directory user. A registration that looks effective and is
not fails later, somewhere else, as a downstream rejection rather than as a
registration defect.
`tokenLifetime` was already rejected this way for non-service clients, so the
rule exists — it was just incomplete.
## Reject the ignored fields
```task
id: KEY-WP-0028-T01
status: done
priority: medium
state_hub_task_id: "e4bce8c7-0e60-55e8-ad32-a3a0c4e46044"
```
Config validation now rejects `serviceSubject` and `roles` on any client that
does not use `client_credentials`, naming where the value actually comes from.
Nothing in `config/dev-config.yaml`, the example fixture or the live deployment
sets either field on a browser client, so this breaks no existing configuration —
checked against all three rather than assumed.
Re-verified against `sso/keycape-config` on 2026-09-09, after a peer raised that
this validation fails closed at startup and a rollout is imminent, so a stray
field would surface as KeyCape failing to boot. The deployed config now holds six
clients: `demo-app`, `netkingdom-bootstrap-console` and `openbao-admin` are
browser clients carrying neither field, while `rapp-qonto-client`,
`secrets-engine-approval` and `approval-engine-operator` carry both legitimately
on the `client_credentials` path. The rule rejects none of them. Kept as errors
rather than downgraded to warnings on that evidence: a warning for a field that
does nothing is the state this change exists to end.
**`tenant` is deliberately excluded from the rule**, and a test pins that. A
browser client may declare a tenant; `humanTenant` resolves it against the
directory and refuses issuance when the two disagree (KEY-WP-0013-T05). An
earlier version of this change rejected `tenant` too, which would have made that
feature unusable.
## Correction
```task
id: KEY-WP-0028-T02
status: done
priority: medium
state_hub_task_id: "5ab951f8-2f9c-522c-b6ea-7f6d572cd618"
```
This work started from KEY-WP-0013-T05's blocker paragraph, which described a
human token as unable to carry `tenant:platform`. That was true when written and
had already been fixed in a concurrent session by the time this task began; the
paragraph was read as current state rather than as a dated record.
Two consequences, both corrected within the session:
- the `tenant` rejection was removed before it was committed, and a test now
asserts a browser client carrying `tenant: tenant:platform` validates cleanly;
- a hub decision (`0145ab57`) recording the tenant question as open and
unimplemented was withdrawn with a rationale pointing at the implementation.
Nothing was built on it.
A third instance followed the same day, outside this task: a message to
railiance-platform (d361bf28) listed the upstream issuer pin as outstanding when
`docs/evidence/2026-09-09-upstream-issuer-pin.json` showed it done ten hours
earlier — `check_before.issuer_matches: false`, then pinned to
`https://auth.coulomb.social`. Corrected in dd827b9f. The concern itself was
sound; only the claim that it was still owed was wrong, and the two are worth
separating rather than letting the correct half excuse the other.
The lesson worth keeping: in a repository where several sessions work at once,
prose ages faster than the code and the receipts it describes. `docs/evidence/`
is the authoritative artifact for anything claiming a live proof here. Read it
before writing "outstanding", "unproven" or "not done" — that check is cheaper
than any of the corrections it would have prevented.

View file

@ -0,0 +1,81 @@
---
id: KEY-WP-0029
type: workplan
title: "Reassess SCOPE against the current revision"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: scope-reassessment
created: "2026-09-09"
updated: "2026-09-09"
state_hub_workstream_id: "cebe226b-5418-5559-bc48-379e843bca78"
state_hub_workstream_id: "cebe226b-5418-5559-bc48-379e843bca78"
---
SCOPE.md declared itself reviewed against `b989de4`, 63 commits behind. Its
limits section had been kept current by appending to bullets, which works until
the header asserts a review date that no longer describes what was reviewed.
## Recheck every claim against source
```task
id: KEY-WP-0029-T01
status: done
priority: medium
state_hub_task_id: "b12dfe7d-f4b6-5c44-89c6-9d3c02435d4c"
```
Routes read from the mux registrations, subcommands from the dispatch in
`main.go` and `internal/authclient/cli.go`, flags from the flag sets, claim
behaviour from `token.go` and `userinfo.go`. Where the existing prose and the
code disagreed, the code won.
Three claims were false and are corrected: caller commands omitted
`verify-client`; the container was described as packaging only `keycape`, untrue
since KEY-WP-0026; and the issuer API row omitted `/readyz`. Capabilities that
had only reached SCOPE piecemeal — the authorization-code bindings, upstream
token verification, readiness and drain, the human tenant rules,
authentication-time assurance, the shared verifier — are now stated in the
capability table rather than only in the limits prose.
## Separate source behaviour from deployed behaviour
```task
id: KEY-WP-0029-T02
status: done
priority: medium
state_hub_task_id: "28ae58c0-9bb9-573f-9cf3-51230d053e3c"
```
The document now says plainly that it describes source, and that the two are
different claims with different evidence: the test suite and harness runs for
one, committed receipts in `docs/evidence/` for the other.
What is proven live is narrower and is stated exactly — the issuer pin, whose
pre-pin state did *not* match, so that risk was real rather than hypothetical;
and the two approval clients verified in an attended owner process. Both receipts
decline real predecessor rotation and observed wall-clock expiry, and SCOPE
declines them too. The deployed image predates this revision, so several
behaviours SCOPE describes are running nowhere yet, two of them fail-closed at
startup.
This is the correction that matters. Four times in two days a statement about
current state was written from a workplan paragraph, a partial `git status`, or
an inference about what was admitted, without reading the authoritative artifact.
A SCOPE that does not distinguish "the code does this" from "this is running"
invites exactly that error from the next reader.
## Record the reassessment
```task
id: KEY-WP-0029-T03
status: done
priority: low
state_hub_task_id: "7ef92d4f-eb56-5208-8b85-ed7bb2656c46"
```
`history/2026-09-09-scope-reassessment.md` records the method, what changed since
2026-09-05, the corrections, and what remains. It supersedes rather than replaces
the 2026-09-05 assessment, which stays as the record of the ten-gap backlog and
its closures; both are linked from SCOPE.

View file

@ -0,0 +1,73 @@
---
id: KEY-WP-0030
type: workplan
title: "Make the static-registration precondition a checked condition"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: tenant-precondition-guard
created: "2026-09-09"
updated: "2026-09-09"
state_hub_workstream_id: "9ed8f851-080a-5d15-8642-f799c2e3295c"
---
`informed-decision` replied on KEY-WP-0013-T05 and asked for one thing that is
ours to do: the caveat under which a registration-bound human tenant is safe
should be "a condition of the capability, so a future change to registration
policy has to confront it", not reasoning left in a message.
They are right that it was only prose. `docs/tenant-claim-contract.md` states it,
and a separate test asserted `registration_endpoint` is absent — but nothing
connected the two, so a future session adding dynamic registration would see a
test about discovery metadata, not a warning about relabelling users.
## Tie the exclusion to the capability it protects
```task
id: KEY-WP-0030-T01
status: done
priority: medium
state_hub_task_id: "4729d591-39d4-5f33-bef8-db8057090913"
```
`TestRegistrationBoundTenantRequiresStaticRegistration` asserts both halves
together: that a client-declared tenant is issued, and that dynamic client
registration is not advertised. Whichever is removed first, the failure points at
the other.
The failure message carries the reasoning rather than the observation. Adding
dynamic registration while a registration may declare its users' tenant means
anyone able to register a client can relabel the users who log in through it into
a zone of their choosing; the message says that and names the two ways out, drop
the capability or gate it to statically configured registrations.
Verified in both directions rather than assumed: advertising a
`registration_endpoint` fails it with the escalation message, and neutering
`humanTenant` fails it with the message saying to remove the guard along with the
capability it protects.
This is deliberately not a vote on the tenant question. It makes the precondition
of one option checkable; it does not choose between them, and if option 1 lands
the capability and this guard are removed together.
## What stays with the owners
```task
id: KEY-WP-0030-T02
status: done
priority: medium
state_hub_task_id: "32b10327-f55d-5a0e-a4bc-be92929469dc"
```
`informed-decision` prefers option 2 and explicitly declines to treat that as the
answer, flagging it to gate-house on GH-DEC-2026-012 and naming approval-engine's
stake. KeyCape leans the same way, which is a reason for more care rather than
less: the code already implements option 2 (`329e48f`), so the doctrine question
is being decided against a working implementation. That is worth saying out loud
to everyone waiting on it, and was.
Their scope set `[openid, approval:read, approval:approve]` is confirmed and
already published. `client_id` and callback follow at their T07, deliberately not
before the tenant question resolves — registering a client that fails closed at
first use is the failure this exchange existed to avoid.

View file

@ -0,0 +1,81 @@
---
id: KEY-WP-0031
type: workplan
title: "Implement the gate-house tenant ruling"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: gate-house-tenant-ruling
created: "2026-09-10"
updated: "2026-09-10"
state_hub_workstream_id: "2bc4d085-9b10-5160-bc8a-4fccc5d41ba9"
---
gate-house ruled GH-DEC-2026-013 (`gate-house@16c1d46`) on the human-tenant
question KeyCape declined to settle for itself. Directory-sourced is the terminal
state; the registration-bound shape is granted as a declared bounded gap under
three conditions, on the general property that *a transitional shape is
admissible where it fails closed on exactly the case distinguishing it from the
correct resolution*. Ours refuses issuance where registration and directory
disagree, which is that case.
Conditions (a) and (c) needed nothing from this repository — (a) is already the
implemented behaviour, and its strengthening, that a future change preferring the
**directory** is equally void because picking any winner is a silent
cross-tenant assertion, is recorded in the contract. This workplan covers the two
items that were implementation.
## Carry the tenant claim's provenance
```task
id: KEY-WP-0031-T01
status: done
priority: high
state_hub_task_id: "46d42120-eca5-5b7f-8445-2211b3b8eab0"
```
gate-house ruled on GH-DEC-2026-013 while this was in flight. Section 5 is a
finding nobody asked for and is ours to implement: `tenant` is a bare string, so
a consumer cannot tell a zone the directory asserted about the person from one a
registration supplied about the client they came through. approval-engine
exact-matches that string while its contract reads as though it relies on the
first — the check is sound and the property a reader infers from it is absent.
Every token now carries `tenant_source` beside `tenant`: `directory`,
`registration`, or `default`. It is advertised in `claims_supported` and asserted
on both grants at the token level, not only in the resolution function, because
the claim a consumer reads is the thing under obligation.
Three values where the ruling names two, and that is the substantive judgement
here. Labelling an unasserted profile default as `directory` would reproduce the
same defect one level down — a consumer reading an assertion the identity layer
never made. The ruling cites GH-DEC-2026-011 §3 on unknown versus absent for the
case it examined; the same rule applies to our own fallback, so the fallback is
named rather than folded into the strongest neighbouring value.
The agreement case resolves to `directory` deliberately: if a registration
declares the zone the directory also assigned, the directory did assert it, and
reporting the weaker source would understate what is known.
## Condition (b) corrected a message I had just shipped
```task
id: KEY-WP-0031-T02
status: done
priority: high
state_hub_task_id: "b21aeac8-8370-5572-ab00-6e9812fa48e7"
```
The guard committed in `5f516a0` told a future reader that adding dynamic
registration could be resolved by dropping the capability **or gating it to
statically configured registrations**. Condition (b) voids the second: admitting
dynamic registration voids the registration-bound shape that day, and the
directory becomes the only source whatever state the adapter is in. gate-house
strengthened our own "must be revisited" on the grounds that *revisited* implies
the answer might survive review, and it would not.
So the message offered a way out the ruling forbids, in the exact place someone
would read it while making that change. Corrected the same day. Worth recording
rather than quietly editing: the guard was written to force a confrontation, and
a guard that suggests an inadmissible resolution is worse than none.

View file

@ -0,0 +1,75 @@
---
id: KEY-WP-0032
type: workplan
title: "Pin the single route by which a principal becomes human"
domain: infotech
repo: key-cape
status: finished
owner: claude
topic_slug: principal-type-provenance-guard
created: "2026-09-10"
updated: "2026-09-10"
state_hub_workstream_id: "150b892c-f112-5fb9-b0f0-8072a52b85d5"
---
GH-DEC-2026-016 §5 applies A-16 to what makes a principal *human*: if `human` is
reachable by two routes — asserted by the identity layer about the person, or
supplied by a registration about the client they came through — the record must
say which, and a human-in-the-loop control must not be discharged on the
registration-supplied one. Refusing a service principal while accepting an
unverified assertion of humanity moves the defect rather than closing it.
gate-house said explicitly this was not a request. It is built because the
property it protects is true *today* and is the kind a plausible change erases
quietly.
## Pin the property, not the behaviour
```task
id: KEY-WP-0032-T01
status: done
priority: medium
state_hub_task_id: "2f42a72e-6292-5c02-9223-835db17b0a63"
```
`principal_type` has exactly one route to `human`: a string literal on the
authorization-code path, reached only after an upstream login resolved to a
directory user. No configuration field can assert it — verified against source
rather than assumed, and it is why the tenant provenance work has no counterpart
here and why A-16 does not yet bite.
Asserting the *behaviour* would not have protected that. A test checking "a human
token says human" passes just as happily when the value starts coming from a
registration. So the guard parses the package and requires every assignment to
`principal_type` to be a string literal, with the literal set exactly
`{human, service}` — one route each. `claims["principal_type"] =
client.PrincipalType` would look like a feature in review and fails the build
instead, with a message saying that if it is deliberate the claim must carry its
provenance the way `tenant_source` does.
Both assignment shapes are covered. The browser path assigns into a map; the
service path uses a key-value pair inside a map literal. Checking only
assignments found one of two routes and passed — a guard proving less than it
claimed, caught because the expected literal set was pinned rather than merely
counted.
Verified by mutation: replacing the literal with a function of the client
registration fails it at the exact line, and restoring it passes.
## What this deliberately does not do
```task
id: KEY-WP-0032-T02
status: done
priority: medium
state_hub_task_id: "13199970-75d1-5c38-aa8c-27d6e38dcf65"
```
No `principal_type_source` claim was added. There is one route, so a provenance
marker would encode a distinction that does not exist and invite consumers to
branch on it — the opposite of the tenant case, where two routes existed and the
claim could not say which had been taken.
The guard is the honest response to "not a request today": it makes the absence
of a second route a checked fact rather than a remembered one, and hands the
decision to whoever creates the second route, at the moment they create it.

View file

@ -0,0 +1,93 @@
---
id: KEY-WP-0033
type: workplan
title: "Preserve fresh-user authentication for the Vergabe company handoff"
domain: infotech
repo: key-cape
status: finished
flavor: implementation
owner: codex
topic_slug: netkingdom
created: "2026-09-12"
updated: "2026-09-25"
related: [VERGABE-WP-0019, NK-WP-0037, USER-WP-0025]
state_hub_workstream_id: "144dd430-1a09-51e1-9aad-e799ee86c338"
---
## Forward login freshness to the actual authentication provider
```task
id: KEY-WP-0033-T01
status: done
priority: high
state_hub_task_id: "fe05486b-f079-503b-ade1-fca65bf64b34"
```
KeyCape parsed prompt=login and max_age but dropped them before Authelia.
Forward these through the provider-neutral AuthRequest and the Authelia adapter.
Absent values retain ordinary SSO behavior. Handler and adapter regressions
cover forced login and zero/nonzero maximum age. Full Go suite passes after
correcting the existing example-count regression: the example file already
contains four service clients and the admitted human approver client.
## Publish, preflight and prove the fresh recipient boundary
```task
id: KEY-WP-0033-T02
status: done
priority: high
state_hub_task_id: "7b58f08c-6063-554a-8700-a1edbc805ca4"
```
Publish the exact source and validate current live config before replacing the
single issuer instance. The live image is dcebd46/digest 7ff54c54; current main
also contains startup validation and tenant provenance changes documented in
docs/operations.md. Preserve all unrelated client, credential and MFA policy
configuration. NK-WP-0037 adds the exact public Vergabe callback with no tenant
assertion or MFA downgrade. Confirm prompt=login reaches Authelia through the
live redirect, wrong callbacks and missing PKCE fail, and the actual recipient
uses their own identity. Provider restart invalidates pending in-memory logins;
USER-WP-0025-T03 still owns full provider sign-out coordination.
2026-09-12 release prepared: source 8d4336e, Forgejo image run 45 succeeded,
digest sha256:5f10f36a5da23ce1aaf3df9b84a8ff98d7926f34ceaa19e63bd3356adb68e01a.
The exact client-registration and deployment server dry runs pass. The runtime
is unchanged. Await the attended window required by docs/operations.md; the
complete packet is railiance-apps/docs/vergabe-demo-company-sso-rollout.md.
2026-09-12 attended rollout executed after explicit operator approval. KeyCape
and password setup are Ready on the prepared digests; exact public client
registration was CAS-applied (config resourceVersion 60123977) with unrelated
config bytes/Secret data preserved. Existing portal and product client both
pass fresh-login forwarding, wrong-callback and missing-PKCE checks (6 checks).
Vergabe Helm revision 2 is Ready; identity migration completed, both PVCs remain,
and requests remain 60m CPU/256Mi memory. Eleven live product checks pass:
company welcome, anonymous gate, no-store, secure scoped CSRF, POST/CSRF-only
login start, native issuer redirect, private company/media protection and
invalid callback/confirmation rejection. Initial readback showed zero accounts,
identity mappings and staff accounts. Native invited-user sign-in/MFA and
confirmation are now requested from the operator; no user credential was used
by the agent. Recovery and two-user acceptance remain their existing tasks.
Evidence: railiance-apps/docs/evidence/2026-09-12-demo-company-sso-live.md.
2026-09-23 regression from the attended recipient sign-in (net-kingdom message
ee4808e2, NK-WP-0037-T02): Authelia 4.38 registers the authorization request
only after the login, so forwarding prompt=login fails every real login ("auth_time
happened before the authorization request was registered"). The 2026-09-12
forwarding checks were redirect-only and never completed a login. Fix (owner
option a): KeyCape no longer sends prompt to Authelia. prompt=login and
max_age=0 travel as max_age=10, a shorter max_age is raised to 10, a longer one
passes through. KeyCape then enforces the exact requirement itself against the
verified upstream auth_time: prompt=login needs an authentication no earlier
than the KeyCape request, max_age needs one within that age, and a missing
auth_time fails closed (event `stale_upstream_authentication`). Known edge: a
user whose Authelia login is under 10s old at request time is not re-prompted
and is refused rather than admitted. Also fixed the stale example-count test left
by 651625c/1620ce2 (6 service + 1 human). Full Go suite passes.
The image was already the live issuer
(`sha256:8dc35801`, Forgejo run 69, source `11ce29a`). No second rollout.
2026-09-24 23:46 UTC the founder completed the Vergabe journey: one
`token_exchange_error`, then `auth_success` and `token_issued` for
`vergabe-demo-company`. No `stale_upstream_authentication`. Receipt:
`docs/evidence/2026-09-24-fresh-login-and-account-switch.md`.

View file

@ -0,0 +1,64 @@
---
id: KEY-WP-0034
type: workplan
title: "Browser authentication recovery and confirmed shared sign-out"
domain: infotech
repo: key-cape
status: finished
flavor: implementation
owner: codex
topic_slug: key-cape
created: "2026-09-12"
updated: "2026-09-25"
state_hub_workstream_id: "49b0cd1d-fb9d-5351-99eb-9be9143a092d"
---
The operator reports a dead-end authentication error after using an account
outside the product tenant. Recent issuer telemetry indicates token exchange
failure; tenant rejection and provider failure must not be conflated.
## Implement and validate recovery
```task
id: KEY-WP-0034-T01
status: done
priority: high
state_hub_task_id: "ba285335-4ead-5478-8fb7-89a2ec1ba695"
```
Route failed browser login to the public account recovery surface without codes,
state or unverified identity. Show verified portal identity, tenant memberships,
and recorded workload memberships; preserve operator/customer separation.
Provide CSRF-protected portal logout and confirmed shared provider sign-out with
fixed owner-configured return locations. No automatic reauthentication loops,
MFA downgrade, global JWT revocation claim or inferred workload entitlements.
## Publish and verify the recovery flow
```task
id: KEY-WP-0034-T02
status: done
priority: high
state_hub_task_id: "4c836ddf-2eb8-528a-b63f-8499f3a6cbf2"
```
Publish immutable images, update canonical runtime pins, verify anonymous
recovery and sign-out confirmation live, and record actual account switching
only after browser evidence. Existing application sessions may outlive provider
logout. Related: USER-WP-0025-T03 and VERGABE-WP-0019-T06.
Source verification: Full Go suite passed; final OIDC/config tests passed after recovery expansion. Immutable publication and live checks are in progress.
### Live recovery rollout — 2026-09-12
Recovery is deployed in KeyCape 4d8b8fe, User Engine e54b6ee and Vergabe c067993
(Helm revision 3). All three are Ready. Six provider checks, eleven product
checks and six fresh anonymous Chromium checks pass, including actual provider
logout POST and return to the portal without test overrides. Detailed receipt: railiance-apps/docs/evidence/2026-09-12-account-recovery-live.md.
2026-09-24 23:42–23:45 UTC the founder completed the account switch. The issuer
recorded one `mfa_failed` for `user-engine-portal`, then three separate
`auth_start` → `auth_success` → `token_issued` sequences. A silent reuse of
the previous portal session would not have produced those new successes.
Provider logout still does not revoke already issued application JWTs.
Receipt: `docs/evidence/2026-09-24-fresh-login-and-account-switch.md`.

View file

@ -0,0 +1,112 @@
---
id: KEY-WP-0035
type: workplan
title: "Opt-in MFA for demo-company login"
domain: infotech
repo: key-cape
status: finished
owner: codex
topic_slug: infotech
created: "2026-09-13"
updated: "2026-09-14"
state_hub_workstream_id: "b27601d8-d0a5-5c79-a1ff-c2a4cedafa74"
---
Requested behavior: password-only login before OTP activation; require OTP after
activation. Do not lower assurance of unrelated applications.
## Implement enrollment-dependent browser policy
```task
id: KEY-WP-0035-T01
status: done
priority: high
state_hub_task_id: "c1ed06c2-c689-53d1-afc7-e10d79e9e94f"
```
Add mfaOptional, preserve explicit AAL2 and existing client policy, fail closed on
provider lookup errors and malformed/incomplete responses. Preserve the setting
in config/runtime/canonical model and flag manual migration requirements.
Validation: `go test ./...` and `git diff --check` passed on 2026-09-13. See docs/optional-mfa.md.
## Restore authoritative factor lookup and enable the reviewed client
```task
id: KEY-WP-0035-T02
status: done
priority: high
state_hub_task_id: "b066a273-f91c-50ee-b497-a39eace03b3a"
```
Factor lookup restored on 2026-09-13 through the new dedicated service lane
RPF-WP-0040 / CCR-2026-0023. Native ESO delivery, scoped provider user lookup,
renewal and mounted replacement passed. KeyCape now uses adminTokenFile; other
configuration and current MFA policy were preserved. Historical NK-WP-0033
resolver lanes remain separate. The remaining T02 work is the exact scoped
client migration and no-factor/enrolled/error/step-up acceptance before enabling
optional policy. See railiance-platform/docs/evidence/2026-09-13-keycape-factor-custody.md.
## Verify optional enrollment and account management access
```task
id: KEY-WP-0035-T03
status: done
priority: high
state_hub_task_id: "93ce2e97-6c5a-58a7-8f3c-1c4f0eb6f1b5"
```
Verify provider self-service login, possession-confirmed activation, cancellation
and removal/recovery. Resolve shared portal assurance scope before surfacing the
verified OTP setup link. Actual user login acceptance remains open under
KEY-WP-0034 and VERGABE-WP-0019; this work does not finish either workplan.
## Support bounded provider credential renewal without issuer restart
```task
id: KEY-WP-0035-T04
status: done
priority: high
state_hub_task_id: "494f554d-6542-5797-804f-6df4a05dcadb"
```
Supports platform journey P05 and USER-WP-0030-T03. Add an exclusive mounted adminTokenFile credential source, fresh reads for lookup and validation, no stale fallback, bounded input, sanitized failures and atomic replacement acceptance. Preserve inline configuration compatibility. Require provider success and true validation value before AAL2; bound default request time and reject credential-bearing redirects. Test renewal, rejection, recovery and policy regressions. This implements the consumer delivery contract; owner credential issuance/custody and live P04/P05/P06 acceptance remain T02/T03.
All Go regression/conformance suites pass, including five new renewal/validation tests with invalid-source subcases. Added exact-commit authentication acceptance CI. Provider-mounted credential delivery and effective optional policy remain gated separately.
Consumer source 632b1f1 deployed and Ready 1/1; CI, four provider HTTP checks and twelve live browser checks passed. See docs/credential-renewal-release-2026-09-13.md. T02/T03 retain actual credential delivery and live recovery/policy acceptance.
2026-09-13 custody activation supersedes the earlier owner-handoff gate: the new factor service is live and renewable. T02/T03 now track effective policy/onboarding acceptance, not missing credential ownership.
## Require confirmed enrollment and actual OTP evidence
```task
id: KEY-WP-0035-T05
status: done
priority: high
state_hub_task_id: "94f579d1-a7af-564c-ab0c-ae76fce343aa"
```
Live provider source inspection found active tokens can remain in verification
state; live policy inspection found password passthrough enabled for users without
factors. Distinguish pending enrollment from completed enrollment and require
TOTP/HOTP serial/type evidence before AAL2. Cover pending/confirmed/cancelled,
existing-factor plus pending enrollment, missing/unknown state, incomplete pages,
password passthrough and static-password token rejection. All Go suites pass
locally; source 113f3a6 was published, CI acceptance/build passed, and the guarded issuer replacement is live. See docs/enrollment-assurance-release-2026-09-13.md.
Provider recovery testing additionally proved that withdrawing reader policy
returns HTTP 200 with an empty list. Added and deployed a same-realm disabled,
unassigned visibility probe before no-factor decisions, with missing visibility
failing closed. Native scope proof passed. Isolated installed-provider tests
passed possession confirmation/cancellation, permission withdrawal/recovery and
provider-issued JWT expiry; these are now included in CI. T02/T03 remain the
actual optional-client and self-service browser activation gates.
2026-09-14: T02/T03 completed through USER-WP-0033 P06. Installed-provider
native and five browser checks passed, including confirmed activation,
cancellation, old-session MFA and denied active-factor replacement. Portal
guards, provider restrictions and both scoped optional-client policies are live;
PVC readback confirms effective state. Full release evidence and rollback:
`../user-engine/docs/evidence/2026-09-13-p06-authentication-policy.md`.
Residual actual-user and workload sign-in acceptance remains live under
KEY-WP-0034, USER-WP-0028 and VERGABE-WP-0019 before this closure.

View file

@ -0,0 +1,83 @@
---
id: KEY-WP-0036
type: workplan
title: "Audited platform administrator lost-factor recovery"
domain: infotech
repo: key-cape
status: finished
owner: codex
topic_slug: infotech
created: "2026-09-13"
updated: "2026-09-13"
state_hub_workstream_id: "f9ba00a6-5758-5677-9a49-da1e35875f7a"
---
Implements the provider part of P04 under USER-WP-0030-T03. The existing factor
read credential is deliberately not elevated to recovery authority. Recovery
now has a private signed-OIDC service and platform-only portal browser flow.
## Implement bounded and audited provider recovery
```task
id: KEY-WP-0036-T01
status: done
priority: high
state_hub_task_id: "f3287544-8412-5b1f-9d16-9f7b9e895ceb"
```
`scripts/factor_recovery.py` previews exact ownership and shared-identity scope,
requires an operator verification attestation and matching metadata version,
disables one factor with a row lock, requires pre-mutation audit durability,
reads back state, records completion and reconciles retries. Reject changed or
replaced factors and reference conflicts. Nine isolated unit tests pass, including replacement between disable and readback.
The installed-provider Job provider-recovery-contract-02 passed real database
and audit persistence, disable/readback and replay assertions in an isolated
fixture. Acceptance CI includes both suites. No real user's factor was changed.
## Validate the attended owner entry point
```task
id: KEY-WP-0036-T02
status: cancel
priority: high
state_hub_task_id: "2d59da13-782e-5aeb-8957-69e41a15ddb1"
```
railiance-platform/scripts/keycape_factor_recovery.py derives actor from an
OpenBao platform-admin entity, rejects root/workload substitutes, checks cluster
identity, and emits only a private metadata receipt. Runbook:
railiance-platform/docs/keycape-factor-recovery.md. Offline owner tests pass.
Superseded: routine recovery now independently verifies the platform operator's
signed KeyCape session through the private service. The extra OpenBao/Kubernetes
operator front door is not part of the supported P04 journey; its native acceptance
is cancelled rather than falsely marked passed. No credential handoff is pending.
## Integrate platform support recovery with the browser journey
```task
id: KEY-WP-0036-T03
status: done
priority: high
state_hub_task_id: "f7248d5e-6f68-5637-ada5-2527436f5f9e"
```
Implement an authenticated bounded recovery service and platform-only portal
flow with fresh assurance, server-derived actor, CSRF, ownership-verification
process, exact target/scope confirmation, audit/reference readback and safe
retry. Never grant Kubernetes access to the portal. Test unauthorized/tenant
admin denial, stale confirmation, provider failure, retry and replacement OTP
enrollment using disposable identities. Mandatory-AAL2 access is restored only
after possession of a replacement factor is proven. Keep P04 incomplete until
browser success, failure and recovery paths pass; CLI tests alone do not close it.
Completion evidence: user-engine/docs/evidence/2026-09-13-p04-recovery.md.
KeyCape 63b3070 and portal c298cd9 are released. Fifteen recovery tests, 219 portal
regressions with PostgreSQL and 19 Chromium checks pass. Installed-provider
provider-p04-contract-03 verifies signature/role/assurance denials, confirmation,
multi-owner refusal, ownership locking, audited mutation/retry/reference lookup and replacement OTP possession/login.
Production health, anonymous/invalid-token denials, signing-key connectivity,
rollout and new-enrollment policy readback pass. Positive mutation used disposable
fixtures; no real account was changed. Provider and portal implement the same
supported recovery contract; no outstanding P04 implementation remains.
Broader policy/self-service and notification operations remain live under
USER-WP-0030-T03 and KEY-WP-0035, not residuals of this bounded recovery plan.

View file

@ -0,0 +1,135 @@
---
id: KEY-WP-0037
type: workplan
title: "Secrets Engine OpenBao live registration and protected custody"
domain: infotech
repo: key-cape
status: finished
owner: codex
topic_slug: netkingdom
created: "2026-10-04"
updated: "2026-10-07"
state_hub_workstream_id: "62800ecf-064b-5546-a1e4-4521b5932973"
---
KeyCape's accepted provider contract (KEY-WP-0009) does not establish actual
registration or custody. This plan names those missing owner gates for
RPF-WP-0035-T02. The founder authorized workers to attend the review requests,
not live registration, credential generation, rotation, fetching or deployment.
## Establish the exact registration and deployment authority
```task
id: KEY-WP-0037-T01
status: done
priority: high
state_hub_task_id: "0409f656-65fb-5ee2-a934-a687a3e139f1"
```
Reviewed KeyCape base 672e1faf4b3401ff7bf68bb0b29f8dc50ca51ac6:
`config/service-clients.example.yaml`, `docs/openbao-service-auth-contract.md`,
`src/internal/config/config.go`, `src/cmd/keycape/clients_test.go`.
The exact service registration remains accepted: secrets-engine-openbao,
service:secrets-engine, tenant:platform, role secrets-engine, only openbao:login,
client_credentials, confidential, tokenLifetime 15m and issuer
https://kc.coulomb.social. Audience defaults to clientId, with no resource
audience override. See the owner packet for concrete deployment/custody proposals.
NetKingdom owns the live sso/keycape deployment and config, not this example.
Existing approved approval-client rollout demonstrates the owner procedure;
it does not approve or provision this different service client.
## Agree and prepare the separately approved protected custody operation
```task
id: KEY-WP-0037-T02
status: done
priority: high
state_hub_task_id: "ac798fb4-ed77-5e45-83ab-f56eea92be34"
```
Platform owns the authoritative OpenBao client-secret custody and least-privilege
issuer-side delivery; secrets-engine owns its explicitly selected mode-0600
client-side file outside every worktree. The proposal in
`docs/secrets-engine-openbao-owner-packet.md` provides candidate coordinates,
not a live custody claim or allocated CCR. Complete the platform CCR(s), delivery
policy/roles, two target bindings, exact attended child and rollback before
seeking the founder's separate concrete approval. Values stay out of Hub/Git/chat,
argv and logs. Existing verifier/requester values must not be reused.
**Done 2026-10-05.** The founder resolved `632a8893-0d6a-4591-a50b-40bbdb798f32`.
The custody boundary is approved: platform owns OpenBao client-secret custody
and issuer-side delivery, and secrets-engine owns the mode-0600 file outside
every worktree. The owner packet is still coordinates. This approval does not
register the client, generate a secret, or deploy.
## Prepare and approve the deployment-owned registration operation
```task
id: KEY-WP-0037-T03
status: done
priority: high
needs_human: true
decision_id: caafe4be-4f68-47b4-9567-316d47b94992
blocking_reason: "Custody boundary 632a8893 is approved. Registration merge and rollout still need an exact reviewed revision and a separate live go. Opening caafe4be does not deploy."
state_hub_task_id: "52f03e39-d05b-5da5-85df-e28c059430e4"
```
Coordinate source registration merge and the single new env secretKeyRef with
NetKingdom's `sso-mfa/k8s/keycape/deployment.yaml` and owner rollout implementation.
An exact reviewed source revision, image digest, config preservation test,
synthetic issuance/denial verification and founder live go are required.
Use an idempotent exact-match merge: absence permits adding only this client;
matching client permits noop; duplicates or drift fail closed. Preserve signing
key, unrelated clients, existing policy and confidential config bytes.
No deployment changes are authorized by this source plan.
**Done 2026-10-07 under live go `caafe4be`.**
- Custody (decision `2ab717e1`): KV v1 written with cas=0.
- ESO amendment `1fdd993b`; the sso Secret is synced.
- NK-WP-0046 (`1fbb8db`): env binding, then the exact-match config add
(rv 71471711 to 71655453, unrelated bytes preserved), then a restart on image
6f79a2af.
**Prepared 2026-10-06 (source only).** NetKingdom NK-WP-0046 (`a1e7302`) holds
the exact-match single-client merge helper, its rollback and the proposed env
patch. secrets-engine SECRETS-WP-0012 (`a3897c2`) names the consumer file.
Platform CCR-2026-0033 (`df24a77`, still proposed) holds the KV path
`platform/workloads/keycape/secrets-engine-openbao`, the sso-only ESO and the
attended custody script; its approval is RPF-WP-0035-T09. The live order is
custody run, then issuer Secret, env patch, this merge and a restart. Nothing
is live. The founder keeps the deployed image (NK-WP-0046-T02, 2026-10-06).
Revised the same day: the value lives only in OpenBao (ops-mason MASON-WP-0009)
and has no consumer file. The custody approvals A1-A3 are decision
`2ab717e1-7881-491e-aed4-28946550c289`.
## Verify actual issuance and publish non-secret owner receipt
```task
id: KEY-WP-0037-T04
status: done
priority: high
depends_on:
- KEY-WP-0037-T03
blocking_reason: "Live issuance proof waits on the registration operation (T03)."
state_hub_task_id: "40b80bcd-243d-5bf8-ba18-b992e3c5f8f7"
```
After authorized registration and custody, verify issuer/JWKS signature, subject,
audience, principal_type, tenant, roles, scope and 900-second lifetime; refuse
wrong credential/excess scope and preserve existing human login. Record only
booleans and identities, immutable image/source coordinates and custody binding
metadata. Hand off to RPF-WP-0035-T02 for isolated OpenBao metadata provisioning,
claim/expiry/use-limit denial and self-revocation proof. Never report an example
registration, discovery metadata or source test as live acceptance.
**Done 2026-10-07.** `keycape verify-client` passed all 10 checks:
- issuer, JWKS signature and claims (`service:secrets-engine`, tenant:platform,
role secrets-engine);
- no excess scope;
- `openbao:admin` and a wrong secret refused.
The founder's KeyCape login after the restart also proved the CCR-2026-0034
operator reader (net-kingdom-admins, aal2). Receipt:
`docs/evidence/2026-10-07-secrets-engine-openbao-issuance.json`. The 900s lifetime
was not measured. Handed to RPF-WP-0035-T02 for the OpenBao JWT role and login
proof.

View file

@ -0,0 +1,212 @@
---
id: KEY-WP-0038
type: workplan
title: "Forgejo as a NetKingdom OIDC client, so the forge can close"
domain: infotech
repo: key-cape
status: active
flavor: implementation
owner: claude-code
topic_slug: railiance
created: "2026-10-08"
updated: "2026-10-16"
depends_on: []
related_repos:
- prj-forgejo-org-refactor
- railiance-apps
- railiance-platform
state_hub_workstream_id: "098e3e2c-a138-560e-b2d3-3a15eb49d8a9"
---
# KEY-WP-0038 - Forgejo as a NetKingdom OIDC client, so the forge can close
## Why
`forgejo.coulomb.social` is closing: private by default, no anonymous access
(prj-forgejo-org-refactor ORGREF-WP-0002, decision
`decisions/forge-visibility-policy.md`). The founder's condition is that Forgejo
sign-in runs through NetKingdom **before** anything closes, so that every human,
agent and service reader has a governed identity rather than an ad-hoc local
account. This plan is ORGREF-WP-0002-T04. Its Forgejo-side counterpart is
railiance-apps `RAPPS-WP-0016` (ORGREF-WP-0002-T05), which owns the Forgejo
Helm release.
## Boundaries
- Registration follows the existing human browser-client pattern
(`docs/approval-engine-provisioning-request.yaml`, `human_registration`) and the
client fields validated in KEY-WP-0028. Redirect URIs are exact, never
wildcards.
- The client secret goes to OpenBao through railiance-platform custody. No secret
value in git, State Hub, logs or chat.
- Founder guardrail for the whole closure: tighten gradually, never lock anyone
out, never risk assets. Nothing in this plan changes who can read the forge
today. It only makes the NetKingdom path available.
## T01 — Define what Forgejo receives
```task
id: KEY-WP-0038-T01
status: done
priority: high
state_hub_task_id: "5f019d13-1fa3-5b88-a27a-75a3339ca96e"
```
Write the claim contract Forgejo consumes, as a non-secret request packet under
`docs/`:
- issuer `https://kc.coulomb.social`; confidential client `forgejo`; authorization
code with PKCE; scopes `openid profile email groups`
- redirect URI
`https://forgejo.coulomb.social/user/oauth2/netkingdom/callback`. The
authentication-source name `netkingdom` must match railiance-apps'
configuration exactly.
- subject and username: canonical directory subject, plus a stable
`preferred_username` that Forgejo can match to the existing local accounts
(founder and agent accounts) for linking
- `groups` naming the Forgejo organisations and teams to map to (for example
`forge:coulomb:owners`, `forge:coulomb:readers`), and `principal_type` so that
human and service principals stay distinguishable
- MFA: forge administrators and owners require assurance with a fresh second
factor; plain readers follow the client-aware MFA policy (KEY-WP-0008)
Acceptance: the founder accepts the packet. The group names are agreed with
railiance-apps.
2026-10-08: packet drafted at `docs/forgejo-oidc-client-request.yaml`. It covers a
confidential client `forgejo` with PKCE and `mfaRequired: true`, the exact
callback, scopes, a custody path, the claims as emitted at this revision, new
LLDAP groups `forge-admins`, `forge-coulomb-owners` and `forge-coulomb-readers`
with a required-claim gate, no automatic account creation, and linking of local
accounts. It also proposes that agents and services use scoped tokens bound to
service identities, not browser sign-in.
2026-10-08: accepted by the founder (in session). railiance-apps' RAPPS-WP-0016
uses the same authentication-source name `netkingdom` and the same group names,
so the agreement condition holds. Before T02, the LLDAP groups `forge-admins`,
`forge-coulomb-owners` and `forge-coulomb-readers` must exist, with the founder
in the first two.
## T02 — Register the client and hand over its secret
```task
id: KEY-WP-0038-T02
status: wait
depends_on: [KEY-WP-0038-T01, NK-WP-0048-T06]
blocking_reason: "Registration runs as the first KnownSecurityCase of type oidc-browser-relying-party (NK-WP-0048-T06), not as a hand-built rollout."
priority: high
state_hub_task_id: "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30"
```
**Changed 2026-10-08 (founder direction):** no case-specific helper script.
Registration is the first case of the SecurityCaseType `oidc-browser-relying-party`
(net-kingdom NK-WP-0048, canon `security-case-types_v0.1`). The parameters
accepted in T01 become the case; ops-mason materializes it (MASON-WP-0010) and
ops-warden follows through (WARDEN-WP-0044). The registration capability is
net-kingdom's (`net-kingdom.keycape-client-registration`); key-cape's part is its
field rules and reviewing the type's composition.
The original intent below still describes what must result.
Register `forgejo` from the packet. Mint the client secret straight into custody
(OpenBao path `platform/workloads/keycape/forgejo`, field `CLIENT_SECRET`, derived
by the case type since net-kingdom 074b3a0; custody owner railiance-platform,
consumers KeyCape via ESO and Forgejo). Record a receipt with no
secret value.
Acceptance: discovery metadata lists the client's redirect URI. The secret is
readable by Forgejo's custody path only.
2026-10-10: key-cape reviewed the type's composition
(`docs/oidc-browser-relying-party-type-review.md`). net-kingdom fixed the findings
in `61ab6bc`, a 1.0 patch: redirects with `?` or `#` refused, the groups scope
required, and descriptions corrected. A new 1.1 (`e2174ce`, founder go) adds the
`consumer-enforces-groups` verification. For Forgejo, that is the required-claim
setting railiance-apps configures. `security-cases/forgejo-netkingdom-oidc.yaml`
is re-pinned to @1.1, with parameters unchanged. KeyCape added `keycape
check-config` (`c528daa`), which register-client runs before the restart. T02 still
waits on NK-WP-0048-T06, which needs both executors' light paths (MASON-WP-0010-T03,
WARDEN-WP-0044-T03).
### Exact offline checker owner review — 2026-10-16
KeyCape owner review under KEY-WP-0038-T02 endorses only the offline config
validation behavior of source `3c71226a1a7b8a2bdb710e9c497e1a1f7f8a3987`,
`src` tree `f0010b9cab17dbc3a787628a29657d98b900320f`, compiled artifact
`sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5`.
The exact source/archive, 108 source files, 9876 cached Go 1.23.0 toolchain
files and all ten selected module trees matched the candidate closure. Fourteen
actual artifact checks and the cmd/config/policy Go tests passed using only
synthetic inputs. The check dispatches before private-key loading, upstream
adapter construction or server startup and uses placeholder client secrets.
[Owner review](../docs/offline-checker-owner-review-2026-10-16.md) records the
exact evidence and limits: non-secret portal/logout URL environment overlays,
optional/nonempty env-name assertion and policy-store assertion, permissive YAML
loading, and no key/custody/readiness or authentication proof. It does not endorse
the caller's inherited environment, executable storage/custody, the NetKingdom
descriptor/type, Warden admission or any browser/native identity. Parent review
identified caller environment inheritance; NetKingdom owns its correction and
regression checks. T02 remains `wait`; T03 and all live acceptance gates remain
unchanged. No live config, credentials, cluster objects or paid calls were used.
## T03 — Prove the claims for each kind of identity
```task
id: KEY-WP-0038-T03
status: wait
depends_on: [KEY-WP-0038-T02]
blocking_reason: "Claims are verified against the registered client."
priority: high
state_hub_task_id: "40ddaa29-7245-508c-ad48-ddc063e25115"
```
Before Forgejo uses the client, verify the issued ID token and UserInfo for one
human (the founder, with MFA), one agent identity and one service identity:
subject, `preferred_username`, `groups`, `principal_type`, assurance. Record the
claims, minus tokens, as evidence.
Decide with railiance-apps whether agents and services sign in to Forgejo at
all, or receive scoped Forgejo tokens bound to a NetKingdom service identity
(ORGREF-WP-0002-T06). Record the decision.
Acceptance: evidence for all three principals; the agent and service decision is
recorded. Then hand over to RAPPS-WP-0016.
## T04 — Implement the `keycape-client` entry point
```task
id: KEY-WP-0038-T04
status: cancel
priority: high
state_hub_task_id: "583cf1f1-16dc-515f-83bc-1efc45024a89"
```
**Cancelled 2026-10-08: moved to net-kingdom NK-WP-0048-T09 (done).** The live
registration machinery (contained kubectl, cluster and issuer pins) already lives
in net-kingdom `sso-mfa/k8s/keycape/`, where KeyCape is deployed; a copy here
would drift. The entry point is `net-kingdom/sso-mfa/k8s/keycape/keycape-client.py`
and the declaration is `net-kingdom.keycape-client-registration`; key-cape's draft
declaration is removed. key-cape still owns the client field rules (canonical
model) and their enforcement at authorization time.
`capabilities/playbooks/key-cape.oidc-client-registration.yaml` (contract PASS,
`maturity: planned`) declares client registration as a playbook capability, so
that the SecurityCaseType `oidc-browser-relying-party` (net-kingdom NK-WP-0048)
can compose it. Implement the entry point it names, `tools/keycape-client/`:
- `keycape-client register --client-file <rendered-client.yaml>` with modes
`inspect`, `dry-run`, `apply` and `remove`; adds, updates or removes exactly one
client in the live registry; idempotent; never prints or writes secret values;
a confidential client's secret is read from its custody reference
- one implementation replacing the per-client helpers in net-kingdom
`sso-mfa/k8s/keycape/` (register-coulomb-social, portal-client-rollout,
vergabe-client-rollout, secrets-engine-openbao-client-rollout), keeping their
safeguards (byte-preserving merge, compare-and-swap, cluster pin, safe receipts)
and their tests as regression cases
- client field validation by the KEY-WP-0028 rules
- then set the declaration's `maturity` to `implemented`
Longer term, loading clients from a non-secret registry with secrets referenced
from custody would remove the type's only attended step (see the type README,
"Attended steps").

View file

@ -0,0 +1,368 @@
---
id: KEY-WP-0039
type: workplan
title: "Negative-check tokens for the secrets-engine OpenBao JWT role"
domain: infotech
repo: key-cape
status: active
flavor: implementation
owner: claude-code
topic_slug: netkingdom
created: "2026-10-10"
updated: "2026-10-17"
depends_on: []
related_repos:
- railiance-platform
- secrets-engine
state_hub_workstream_id: "09c3bddf-6058-5603-8488-cccdb7be4f86"
---
# KEY-WP-0039 - Negative-check tokens for the secrets-engine OpenBao JWT role
## Why
CCR-2026-0038 (railiance-platform RPF-WP-0035-T02) requires the OpenBao role
`secrets-engine-openbao` on `auth/keycape-services` to refuse JWTs with a wrong
`aud`, `sub`, `tenant`, `principal_type`, `roles` or `scope`, a coding-agent JWT
and an expired JWT. The positive login proof passed on 2026-10-10 (secrets-engine
`docs/evidence/2026-10-10-keycape-openbao-probe.json`). railiance-platform asked
whether KeyCape can issue the negative tokens without touching the
`secrets-engine` registration.
KeyCape signs only what a registration declares. It has no way to mint a token
with arbitrary claims, and adding one would create a forging path in the issuer.
So the negative tokens must come from real, separate registrations that differ
from `secrets-engine` in exactly one property each.
## Boundaries
- The `secrets-engine` registration is not changed.
- Probe clients are confidential `client_credentials` clients with the minimum
60-second lifetime, scopes that grant nothing, and secrets minted into custody
like any other client. They are removed after the proof.
- The probe script prints only accept or refuse booleans per case, never tokens or
secrets. It runs attended.
## T01 — Define the probe registrations
```task
id: KEY-WP-0039-T01
status: done
priority: medium
state_hub_task_id: "4d06b059-3768-53bb-83a4-a9a71c0850b8"
```
Write a non-secret request packet under `docs/` with one probe client per claim
mismatch, each identical to `secrets-engine` except for:
- `aud`: a different access-token resource audience
- `sub`: a different service subject
- `tenant`: a tenant other than `platform`
- `roles`: a role other than `secrets-engine`
- `scope`: a scope outside the role's bound scope
Two cases need no probe client. A service token always carries
`principal_type: service` (`src/internal/server/oidc/token.go`), so the wrong
`principal_type` case uses a human access token from an attended login. The
coding-agent case uses the existing `codex-railiance-platform` registration (role
`coding-agent`, `docs/openbao-service-auth-contract.md`) if it is registered live;
T01 confirms that.
The expired-JWT case reuses any probe client: request a token, wait past its
60-second lifetime, then present it.
Acceptance: railiance-platform agrees the packet covers every refusal that
CCR-2026-0038 names.
2026-10-10: railiance-platform agreed that the case list covers every
CCR-2026-0038 claim negative. The remaining CCR checks (capabilities-self deny, no
provider fallback) are theirs and secrets-engine's. Packet written at
`docs/openbao-negative-probe-clients.yaml`. It departs from the agreed list in two
ways, both sent back for re-agreement:
- **Expired case.** A token from another probe already fails a claim, so its
refusal does not show that expiry was checked. It needs a sixth client,
`openbao-probe-expiry`, identical to `secrets-engine-openbao` in every claim. That
makes it a second credential able to log in as secrets-engine for the session.
OpenBao's `expiration_leeway` (150s by default) also extends the wait.
- **Audience pin.** A service token's `aud` is its `clientId` unless `audience` is
set. Every probe except the aud probe therefore pins
`audience: secrets-engine-openbao`, so it fails only on its own claim.
Condition (a), no OpenBao custody, holds. The values still have to reach KeyCape's
environment, because KeyCape resolves `secretRef` at startup. The packet uses a
transient Kubernetes Secret that is deleted in the same session.
## T02 — Register the probe clients, run the attended probe, then remove them
```task
id: KEY-WP-0039-T02
status: wait
depends_on: [KEY-WP-0039-T01, KEY-WP-0038-T02, KEY-WP-0038-T03]
blocking_reason: "Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; original secrets-engine-openbao registration go remains resolved; probe/coding-client custody and attended readiness are not established."
priority: medium
state_hub_task_id: "03da4c38-1dac-53b1-8b0b-e697df48e9d3"
```
2026-10-11: requalified — live registration go caafe4be was already resolved; owner authorization present. See the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md.
Write the attended script, which prints booleans only.
2026-10-10: script written at `scripts/openbao_negative_probe.py`, with tests in
`scripts/test_openbao_negative_probe.py`. It meets condition (b): one refused
boolean per case plus the refusal class (`claim_mismatch`, `expired`,
`not_service_principal`), and no token, secret, claim value or OpenBao error text.
A transport failure or an unexpected response is reported as `inconclusive`, never
as a refusal. A token that OpenBao accepts is revoked at once and fails the run.
Still waiting: railiance-platform's re-agreement on the two departures above, and
an attended registry session with net-kingdom. Register the clients
through the normal registration path, hand the run to railiance-platform, record
a value-free receipt in `docs/evidence/`, and deregister the clients afterwards.
Acceptance: every case is refused, the receipt is committed, and the probe clients
are gone.
2026-10-11 source preparation: T01 remains progress because the exact expiry
client and audience-pin amendments require platform-owner re-agreement; the
prior agreement covered the earlier case list, not this revised packet. The
prepared runner revision adds a valid expiry positive control and immediate
revoke, exact decoded claim-isolation preflight (not signature evidence),
semantic refusal classes, bounded expiry TTL and redirect refusal. Seventeen
local tests pass; original-source semantic regressions demonstrate two false
successes. See `docs/openbao-negative-probe-session-proposal.md` for exact owner
returns, live authority, effective leeway, custody boundaries and cleanup. No
live token, registry or OpenBao action occurred. T02 continues to wait for owner
re-agreement plus separate attended live authorization. MASON-WP-0009-T05
operator/ESO capability and nonmember checks remain independent acceptance.
2026-10-11: Platform independently re-agreed the final amended design (session proposal SHA25623f13d2a5a4204f749f35c5d44ef39d213838cd2fce1c36fb4775254ef1c7f09). Original conditional review and narrow confirmation are retained in railiance-platform/docs/reviews/2026-10-11-keycape-openbao-negative-session-owner-review*. Source acceptance T01 is complete; actual registry/leeway/readiness, founder live go, negative/cleanup and separate capability/no-fallback obligations remain T02 and owner plans.
2026-10-17 Custodian coordinator finalization review (not a KeyCape formal owner
acknowledgment): the [infrastructure assessment](../../the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-infra-finalization-r1/ASSESSMENT.md)
confirms all 14 executable/test/producer-decision pins and three Platform source
pins still match. Source reviews remain complete: T01/T03 done, T02 wait. Existing
source acceptance remains valid. Resolved registration go caafe4be covers only
the original secrets-engine-openbao client (KEY-WP-0037-T03, deployed 6f79a2af,
NK-WP-0046 exact-match helper); it does not authorize new probe clients or
coding-client custody, and no renewed original-client approval is requested.
The standing attended rule remains separate, for reviewed commands within
existing scope; no new protocol or blanket approval request is introduced. The
remaining sequence is KEY-WP-0038-T02/T03 first-case registration/claims proof,
admission of the existing coding-client proposal with sanitized owner readiness
returns, then one attended negative/revoke/cleanup receipt committed with probe
clients removed. No native acceptance or credential/registration action is
inferred from this coordinator review.
## KeyCape issuer-source compatibility inspection for lifecycle v2
```task
id: KEY-WP-0039-T03
status: done
priority: medium
state_hub_task_id: "54d37ad8-2352-51c2-bb2e-af6070dbbd73"
```
2026-10-11: Completed KeyCape's own issuer-source inspection for
KEY-WP-0039-T02/attended-lifecycle-v2, exact manifest SHA256
6fc0fe47f99a97c11c6e7538f5ea02054335df7d46055d3f84bed4e194aeaaed.
Read AGENTS.md, governing INTENT.md, current client-credentials claim source,
registration fragments, native caller-authentication commands and frozen v2
issuer transport/probe wiring. Seventeen unchanged runner tests pass locally.
This task completes only issuer-source inspection; the resulting narrow
source acknowledgment is in docs/reviews/2026-10-11-key39-lifecycle-v2-keycape-source-ack.json (retained frozen prior acknowledgment).
T01 stays done and T02 stays open for actual native registration, token input,
refusal and cleanup proof. No Secret, token, credential or native environment was accessed; no owner
checkout was changed during this source inspection. Owner source files were
read for the bounded issuer compatibility check.
Current token.go:306-309 returns HTTP 401 invalid_profile_usage with
feature=client_id for a removed confidential client; wrong-secret
feature=Authorization cannot count as removed-registration evidence. The
proposal and packet comments are narrowly corrected against that current
issuer contract. Prior T01/source re-agreement history is preserved; this source
acknowledgment supplies no NetKingdom runtime-owner review or founder live go.
The private token-producer handoff remains a real execution gap under T02.
Existing keycape login/service-token commands require a new private output
file; login also needs a concrete admitted public PKCE client/callback and
attended browser/MFA. The lifecycle expects an in-memory private stdin object
containing human_token and coding_agent_token, but no reviewed file-to-process
or process-to-process producer, admitted human client/callback, custody-injected
coding-agent source, native placement or producer cleanup receipt has been
provided. No unattended token producer or admitted secret route is invented.
Resolve those exact owner contracts before founder native approval; existing
static registration is not live token custody or producer admission.
2026-10-11 same T03 source-inspection refinement for lifecycle V4: completed
KeyCape issuer-facing inspection before the new formal acknowledgment in
docs/reviews/2026-10-11-key39-lifecycle-v4-keycape-source-ack.json. Exact V4
manifest SHA256 d079f85c47d5c2635e1ee633ea4d1346b1737ce2be9ca273f22a38e620f418d8,
executor b4687c2d07819c43b45d0cddcad0488b62eb49175b2a06a43f7806a0838d3a4d.
NetKingdom's independent V4 source review is approved, receipt SHA256
4f4498203a31bd46dbc3715c0df1cbb9eee63bed5b2c1baf430ff3c4a37c13cb.
The V2/V3 unsuccessful reviews and earlier KeyCape acknowledgment remain
retained as one correction trial; they are not extra scored successes.
V4 retains the unchanged issuer runner and six-client claim design, and fixes
finite/nonboolean windows, true total request deadlines, late-known OpenBao
session private revoke/refusal and guarded bounded final Secret deletion.
The issuer-facing acknowledgement covers only stopping late token exchanges,
claim source/shape and preserving token cleanup failure/unknown outcomes; it
does not replace NetKingdom runtime or Platform backend acceptance. Tests: 41
supplied lifecycle, 17 unchanged runner and 17 independent owner checks pass
locally. No native tokens, credentials, issuer mutations or live cleanup were
performed. T01 stays done; T02 stays open and the genuine private token-producer
handoff remains its next source prerequisite before actual founder native go.
2026-10-11 same T03 source-inspection refinement for lifecycle V5: completed
KeyCape's own current issuer-source/claim/cleanup-contract inspection before
any new formal acknowledgment. Current token.go:306-309 still distinguishes
removed-client invalid_profile_usage feature=client_id from wrong-secret
feature=Authorization. The six-client design, audience source and expiry
positive control remain unchanged, and the original runner's 17 tests pass.
Exact V5 manifest SHA256
d1f997624db46693a7126a6eb6435f6b3a353d5bd67dcc8a539683e4b7147285;
executor 3bcceebc845b17492040c76223d77088257c547a2b890198e7b5c2b04a0cd360.
V5 has 46 passing supplied lifecycle checks. It adds a single exceptional
response-close abort under a fixed20ms cap clipped to nonresettable aggregate
cleanup, and to positive original request time remaining, with expired aggregate
zero calls, fixed private errors and truthful known-session revoke/unknown
outcomes. This inspection covers issuer-facing token lifetime and claim
interfaces only; NetKingdom and Platform independently own their runtime and
backend review decisions. Previous V2/V3 Net source refusals and Platform V4
exceptional-close refusal remain retained; KeyCape's V2/V4 acknowledgments are
historical narrow source inspections, never aggregate native approval.
T01 remains done, T02 open. No native actions or credentials occurred. The real
private token-producer/client/callback/coding-agent custody/cleanup handoff gap
stays the next prerequisite; no unattended admission is inferred.
Exact independent V5 source decisions were then verified: NetKingdom approved
receipt SHA256 32a62bd1ca1bd7311965d016d4d6a6e4d9177f3423c63add436d9d309865eacc
(46 supplied +17 runner +21 independent checks); Platform approved receipt
SHA256 ba872dc53061b1dcca9dea1e8897597c394c1f9365e0fc49ee6f2095f57f11e0
(46 supplied +5 independent +17 previously verified runner checks). Those are
foreign owners' own source decisions, not KeyCape runtime/backend reviews or
founder native authority. Formal KeyCape bounded issuer acknowledgment follows
in docs/reviews/2026-10-11-key39-lifecycle-v5-keycape-source-ack.json.
2026-10-11 T02 continuing source preparation: private producer decision packet
`KEY-WP-0039-T02/private-producer-handoff-v1` stages supported existing CLI
flows, selected existing netkingdom-bootstrap-console literal-loopback human
client subject to owner purpose/MFA/listener admission, and the existing
codex-railiance-platform source identity subject to actual live/custody/consumer
admission. No existing coding-agent delivery receipt was found; AppRole tokens
do not substitute for issuer JWTs. A bounded pipe-only adapter for the current
CLI's two caller-owned private files has 18 passing synthetic checks, with
identity-guarded explicit file consumption and fixed value-free failure output.
Decoded claim shape does not prove signatures or authenticated admission.
See docs/openbao-negative-token-producer-decision.md and .json for exact owner
choices and missing receipts. The exact trusted workstation-to-node launcher,
private-file allowance and railiance01 Bao route/public-health guard still need
source review and founder disposition before any token/temporary credential/
mutation/restart. No registration, custody, login, native action or credential
request occurred. V5 and failed review histories retained; T01 stays done and
T02 open. No new work record or native acceptance is inferred.
2026-10-11 same T02 source-preparation correction for
KEY-WP-0039-T02/private-producer-handoff-v2: NetKingdom's exact V1 consumer
refusal remains recorded (no child-ready gate, stdio alias, non-atomic unlink).
V2 has source-pinned private launch/ready handshake compatible with exact
Net V9 manifest84542ddc: fresh nonce/source/envelope/health/window marker before
ANY caller-token file load/consume/write; separate local data/control pipes
reject kernel identity aliases of stdin/stdout/stderr. Adapter read/write budget
only clips downward after readiness. 31 distinct producer/launcher synthetic
checks and 74 compatible consumer supplied checks pass. No SSH/native/login or
credential access was performed. Stat/unlink remains non-atomic and requires
continuously exclusive sameUID custody; the retained race test is a custody
violation counterexample, not a claim of repaired filesystem atomicity. Receipt
keeps atomic delete/copy cleanup unverified and possible copies true. Exact
trusted launcher/directSSH profile/private tmpfs allowance, selected human
client/MFA purpose reuse and coding-agent registration/custody/delivery remain
unadmitted owner choices. Native node route, role/leeway/window/founder decision
and actual negative/cleanup acceptance remain pending. T01done and T02open; no
new task or plan, and V1/V5-V8/current failure histories are not overwritten.
2026-10-11 same T02 producer-preparation correction V3: Platform V2 pair
source refusal RPF-KEY39-PAIR-V2-F01 is retained. Private write uncertainty
now propagates into sticky remote consumer uncertainty before launcher early
return; local SSH termination never proves remote no-effects/recovery. Actual
full-payload then interrupted confirmation and4096-byte partialpipe failures
are retained regressions. 33 distinct source checks pass. Exact Net V9 source
bytes unchanged; new source reviews required, not implicit repin. Native
client/MFA/coding-agent custody/transport/tmpfsfile and actualroute/session
authority remain unadmitted. T01done/T02open; no new task/plan or native action.
2026-10-11 same T02 producer-preparation V4: exact V3 Net consumer refusal
NET-PRODUCER-V3-F01 retained. Actual CLI contract acquisition now bounded
nofollow/nonblocking regular stable reads and one ORIGINAL5-second preparation
budget across contract/source/envelope/SSHspawn/ready/file/delivery phases, not
a renewed launcher allowance. 40 own source tests plus9 retained paired
wrapper/child handshake regressions pass with synthetic/no native transport.
V3stickyremoteunknown/full+partialdelivery regressions retained; Net V9 exact
bytes unchanged. Prior issuer/custody/transport/file/route/readiness-Argo/role/
maintenanceexception/founder choices remain unadmitted. No SSH/network/Secrets/
credentials/native/owner checkout changes; no task/plan duplication. T01done
and T02open; source correction is not native acceptance.
2026-10-11 T02 source-design disposition checkpoint: founder accepted exact
source memo `infd-20261011-key39-source-design-r1`, review
`review-62626f54-0212-4684-9b45-0a7222d61957` version1 at `2026-10-11T17:19:01Z`.
Disposition `disp-870f9c1b-0444-4216-ae7f-f5463d590e36`, approval
`ca118afc-d746-4454-a5b9-9b387b120cd9`; authenticated person subject
`uid=platform-root,ou=people,dc=netkingdom,dc=local`. Exact canonical public review URL:
`https://decisions.coulomb.social/review?memo_id=review-62626f54-0212-4684-9b45-0a7222d61957`.
Root's authenticated confirmed readback is
`the-custodian/docs/evidence/2026-10-10-security-consolidation/key39-source-design-publication/founder-source-disposition-readback-r1.json`,
SHA256 `453213194d605336e9c424976f1ac27691def39e225026f0a99628d1cfc97d1d`; observed
`2026-10-11T17:30:47.080081+00:00`. Source commands SHA256
`36e20813596f3ddbb86e01fd0b838867ece2f4916c5880305cf12cd61b567f89` and source inventory SHA256
`aea196db9e513900d5e7165455d6e9c3dc68749a606dc219ec340ac595642cc0` bind the accepted source direction.
Acceptance covers recommended source preparation for existing bootstrap-console
human-client purpose reuse, returning existing coding-agent custody admission or
preparing a narrow owner proposal, proposed founder directSSH source profile,
and conditional exclusive-owner tmpfs files. It does not establish actual
registration/MFA/listener, coding-agent custody/delivery, transport/file admission,
node-local Bao route, issuer/OpenBao role/leeway/maintenance readiness or fresh
attended native go. The published choice documents remain historical exact
pending proposals; this appended receipt records their later source disposition.
T02 remains wait on external owner custody/admission and separate attended
execution authorization. Source acceptance permits further source preparation;
actual registry, token exchange, negative probe, revoke and cleanup remain pending. T01 and T03
remain done. No native or credential action is authorized by this source receipt.
See docs/proposals/2026-10-11-key39-next-execution-prerequisites.json for exact
owner returns and safe metadata evidence needed before preparing a runnable
attended execution proposal.
2026-10-11 ordered-goal qualification (T02 only): the final source runner already
exists and its 17 local semantic tests pass. No new helper or source protocol is
needed to repeat accepted preparation. Native execution follows the first
Forgejo SecurityCase registration and claims proof (KEY-WP-0038-T02/T03);
only those task metadata were checked, not wait-task bodies. NetKingdom owns the
registry/env/restart session, KeyCape owns issuance/isolation, and Platform owns
leeway/refusal/capability/no-fallback/cleanup proof. The live registration go is
already recorded resolved; do not ask again. Attended human authentication and
owner-held existing coding-agent custody go on the single attended checklist.
No credentials, live registry/API changes or KEY-WP-0040 recovery work occurred.
2026-10-17 Platform owner returns (source-prepared, not admitted):
railiance-platform `docs/proposals/2026-10-17-key39-t02-owner-returns.md`
(commit `0a9617a`). The coding client `codex-railiance-platform` has no
admission, custody or delivery record. The existing narrow proposal
`docs/proposals/2026-10-11-key39-coding-client-custody-proposal.json` (`9460f6f`)
remains the one to admit, so the coding-agent case stays unrunnable until owners
admit it. The role source (`openbao/auth/secrets-engine-jwt-role.json`) matches
the packet's claim design. It declares no `expiration_leeway`, so the expiry
probe must assume OpenBao's 150s default until an attended sanitized read
confirms the live value. The node-local Bao origin is not declared in source and
also needs that attended read. T02 stays wait.

View file

@ -0,0 +1,162 @@
---
id: KEY-WP-0040
type: workplan
title: "Fix findings from the first attended MFA run (factor recovery preview, OTP prompt help)"
domain: infotech
repo: key-cape
status: finished
owner: claude-code
topic_slug: netkingdom
created: "2026-10-11"
updated: "2026-10-17"
flavor: implementation
depends_on: []
state_hub_workstream_id: "a67f9d60-9b8f-5111-9fe5-9f64e631b1c3"
---
From the attended run of 2026-10-11 (user-engine `docs/evidence/2026-10-11-mfa-attended-u05-u08.md`).
U05–U07 passed. U08 is blocked by T01.
## Recovery preview rejects valid input (F6)
```task
id: KEY-WP-0040-T01
status: done
priority: high
state_hub_task_id: "b385602d-714a-5fdb-b40b-bd870420f28e"
```
Operator `platform-root` (holds platform-operator, fresh AAL2) previewed login `tegwick`,
reference `U08-2026-10-11` at `/platform/factor-recovery`. Every attempt returned `invalid_request`
("Enter a valid login and support reference."). Both inputs match `[A-Za-z0-9_.@:/-]{1,150}`.
The `factor-recovery` container in `mfa/privacyidea` logged nothing.
Candidate causes in `scripts/recovery_service.py` / `scripts/factor_recovery.py`:
- the actor `sub` (KeyCape `user.ID`) failing `_recover`'s character rule;
- a privacyIDEA serial failing it;
- the request body differing from what is validated, e.g. form encoding through the portal.
Done when:
- the failing check is identified and fixed with a regression test;
- the sidecar logs the failing check (field name and error code, never values);
- a preview for `tegwick` lists the active TOTP (serial suffix …E1D) without applying anything.
Then hand back to user-engine, which re-runs U08 (USER-WP-0028-T03). The fix and roll-out
of the sidecar are blue lane under the build-mode change rule.
2026-10-12 source repair: LLDAP adapter returns entry.DN as user.ID and the token
issuer emits that exact value as sub. Recovery incorrectly applied the login
regex to actor, rejecting comma/equal characters. Two DN-shaped subject tests
reproduce invalid_request against the old source. The repair validates the
bounded printable ASCII actor separately, preserves exact ticket/audit binding,
and keeps target/serial/realm/reference validation strict. Validation diagnostics
emit only closed field names and error codes to stderr; provider logging stays
disabled. Twenty recovery tests pass, including identity switching, malformed
input, provider field failures, replay and mutation/audit guards. Go OIDC and
LLDAP suites pass. No live factor was read or changed. T01 stays progress until
the deployed authenticated preview succeeds and user-engine can rerun U08.
## Link the OTP prompt to lost-authenticator help (F3)
```task
id: KEY-WP-0040-T02
status: done
priority: medium
state_hub_task_id: "3ad155dd-15ef-5522-bdf9-c78832728553"
```
The OTP input offers no way out for a user whose code is rejected or whose device is lost. Add
"Code rejected or lost your authenticator?" linking to
`https://users.coulomb.social/security` (the lost-authenticator section). The same link serves users
who have no factor yet (the dead end noted in `mfa-attended-run.md`).
2026-10-12: source link added to the MFA challenge with noreferrer, preserving
challenge state and requiring no query/token in the destination. Go OIDC tests
pass. Runtime publication and attended journey verification remain open; T02
is not represented as deployed.
2026-10-12 blue-lane runtime repair completed: immutable ConfigMap
factor-recovery-01119187d90d7b76 replaced only the recovery-code volume through
UID/resourceVersion CAS. Server dry-run proved no other pod-template change.
privacyIDEA generation 11 is Ready; recovery health returns 200 and anonymous
recovery is denied (403). All Go tests pass in the actual checkout. An early
ready=false observation was retained while the rollout completed; no apply was
replayed. Abort owner: key-cape; rollback restores prior immutable ConfigMap
factor-recovery-79e2964c112f3688 with the same CAS helper. Receipt and exact
proposal: the-custodian/docs/evidence/2026-10-12-netkingdom-security/.
T01 remains progress for the founder's authenticated tegwick preview and U08.
T02 is a local source edit only; its source/image publication remains pending.
2026-10-14: the MFA challenge render test now requires the lost-authenticator
link after the OTP form, with rel="noreferrer" and no request state in the URL.
T02 stays progress until that page is published and an attended journey sees it.
## Current publication and attended acceptance — 2026-10-16
The source-only publication statements above describe the October 12–14 state.
They are superseded for T02: the October 15 founder-run publication recorded in
`docs/operations.md` (Current image) and KEY-WP-0041-T02 delivered image
`sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010`
from source `8c6c071`. That source includes the OTP help link commit `205a1c0`.
The founder verified the image-only diff, server dry-run, Recreate rollout,
Ready pod with zero restarts and discovery HTTP200. No additional image rollout
is needed to make the existing T02 link available. This reconciliation makes
no new live observation and does not infer attended acceptance from publication.
T01 still needs the actual authenticated preview; T02 still needs the actual
OTP-page observation. Both remain progress. Resume the existing owner procedure
`user-engine/docs/mfa-attended-run.md`, corrected by its October 11 evidence
`user-engine/docs/evidence/2026-10-11-mfa-attended-u05-u08.md`:
1. In the real user-engine-portal OTP challenge, observe **Code rejected or lost
your authenticator?**, follow it to `https://users.coulomb.social/security`,
and confirm the rejected-code/lost-device guidance is reachable. Preserve no
challenge URL, request state, cookie or OTP value in evidence. Record time,
client, link text/destination and outcome. This supplies T02 acceptance.
2. With platform-root's own fresh AAL2 browser session, open the existing
`/platform/factor-recovery` form. Preview `tegwick` using the existing support
reference `U08-2026-10-11`; inspect any existing reference before changing it.
Confirm the current factor metadata and shared-identity warning. The October
11 suffix was …E1D; a changed factor is a reason to reconcile current state,
never to force the old selection. Record only time, preview outcome and serial
suffix. This preview alone makes no factor change and supplies the remaining
T01 preview evidence.
3. Only in the separately attended recovery window, after the operator verifies
the person and exact current scope, continue the existing U08 confirmation,
audited disable, reference readback, replacement enrollment and fresh login
procedure. Preserve the original reference and safe recovery record id.
Neither the published bug fix nor a passing preview closes U08. Stop on
stale metadata, replaced factor or failed audit/readback; do not reset or
re-enroll merely to make an acceptance check pass.
No authenticated preview, real-factor mutation or attended page observation was
performed during this source reconciliation. No passwords, codes, token values,
provider keys or production Secret objects were requested or read.
## T02 attended acceptance — 2026-10-17
The founder reported in session that the real user-engine-portal OTP challenge
shows **Code rejected or lost your authenticator?**, that it leads to
`https://users.coulomb.social/security`, and that the lost-device guidance is
reachable there ("T02 is fine"). This is the founder's own observation, on the
deployed image `cdec9dd7` from source `8c6c071`. No exact time or client was
recorded, and no challenge URL, request state, cookie or OTP value was preserved.
T02 is done. T01 still waits for the authenticated preview (step 2 above).
## T01 attended preview acceptance — 2026-10-17
The founder, signed in as platform-root with its own fresh OTP (AAL2), opened
`/platform/factor-recovery` and previewed login `tegwick` with the existing
reference `U08-2026-10-11`. The founder reported that the preview listed
tegwick's TOTP. The `invalid_request` failure from October 11 (F6) did not recur.
No confirm or disable action was taken, and no factor was changed. When asked
afterwards, the founder confirmed that the listed TOTP's serial suffix is …E1D,
the same active factor as on October 11. The exact time was not recorded.
All three Done-when conditions now hold: the DN-shaped actor defect was fixed
with regression tests (2026-10-12); the sidecar logs only closed field names and
error codes; and the deployed preview for tegwick lists the active TOTP …E1D
without applying anything. That third condition was met only after the suffix
was confirmed, not before. T01 and the workplan are done. U08 itself, meaning
the attended disable, re-enrolment and fresh login, belongs to user-engine
(USER-WP-0028-T03) and is handed back to it. This workplan does not close U08.

View file

@ -0,0 +1,63 @@
---
id: KEY-WP-0041
type: workplan
title: "Sign-in diagnosis page and auth log uid (NK-WP-0051-T02)"
domain: infotech
repo: key-cape
status: finished
owner: claude-code
topic_slug: netkingdom
created: "2026-10-14"
updated: "2026-10-15"
flavor: implementation
depends_on: []
state_hub_workstream_id: "092c7590-3700-524d-bbe0-a934e063be6b"
---
Demand: NetKingdom `NK-WP-0051-T02`. The field names (`uid`, `groups`,
`assurance`, `client_id`) and the never-show/never-log list come only from
`net-kingdom/docs/sign-in-diagnosis.md`; this workplan does not restate them.
## Opt-in pre-redirect page and auth log fields
```task
id: KEY-WP-0041-T01
status: done
priority: high
state_hub_task_id: "5947be07-bc84-5672-b807-608070a39d10"
```
- Client registration flag `signInDiagnosis` (default false) in the YAML
config, `domain.Client` and `spec/canonical-model.yaml`.
- With the flag on, `/authorize/callback` renders a page before the redirect
with `uid`, sorted `groups`, `assurance` and `client_id`, one continue link
to the unchanged redirect (code + state), and a sign-out hint. html/template
escaping, `no-store`, `Referrer-Policy: no-referrer`, restrictive CSP.
- `assurance` is `aal2` only when this request verified a one-time code; a
reused aal2 session is `aal1` (contract wording, stricter than the session
satisfied `MFAVerified`).
- The `auth_success` log line gains `uid` and `assurance` beside `client_id`.
- Enabled for `openbao-admin` in `config/dev-config.yaml`; the live flag is set
in `sso/keycape-config` (see `docs/operations.md`).
- Tests: `src/internal/server/oidc/signin_diagnosis_test.go`.
## Deploy the image and record its digest
```task
id: KEY-WP-0041-T02
status: done
priority: high
state_hub_task_id: "bd5db26c-b71c-5291-bdc3-7c0f718651db"
```
Build and push with `make push IMAGE_TAG=<sha>`, set `signInDiagnosis: true`
on the OpenBao browser client in `sso/keycape-config`, roll the issuer under
the usual live-change approval, and send the digest to net-kingdom for the
evidence table in `docs/sign-in-diagnosis.md`. Not done by the implementing
session: deployment needs operator approval.
2026-10-15: image `sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010`
(source `8c6c071`) is live; see `docs/operations.md`. 2026-10-15: `signInDiagnosis: true` set on `openbao-admin` with
net-kingdom `openbao-signin-diagnosis-rollout.py` (rv 71655453 → 75372162,
unrelated bytes preserved) and the issuer restarted; pod ready, discovery 200.
Accepted 2026-10-15: an attended OpenBao login showed the page and completed.

View file

@ -0,0 +1 @@
WARN: git commit failed for writeback: Generated-only commits rejected: staged

View file

@ -0,0 +1,123 @@
{
"repo_slug": "key-cape",
"repo_path": "/home/worsch/key-cape",
"source_revision": "d803e9282a47b49daf930fb905312fa5104c0ace",
"source_revision_reason": null,
"issues": [
{
"severity": "WARN",
"check_id": "C-24",
"message": "Repo classification advisory: capability_tag 'security' is not a recommended family tag (allowed, check for synonym)",
"file_path": "",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-11",
"message": "Task 'ADHOC-2026-09-27-T02' has no state_hub_task_id",
"file_path": "workplans/ADHOC-2026-09-27.md#ADHOC-2026-09-27-T02",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": true
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'USER-WP-0020' is not linked to State Hub",
"file_path": "workplans/KEY-WP-0007-user-engine-portal-oidc-client.md",
"db_id": "",
"file_value": "USER-WP-0020",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'NK-WP-0025' is not linked to State Hub",
"file_path": "workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md",
"db_id": "",
"file_value": "NK-WP-0025",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'NK-WP-0030' is not linked to State Hub",
"file_path": "workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md",
"db_id": "",
"file_value": "NK-WP-0030",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'RAILIANCE-WP-0027' is not linked to State Hub",
"file_path": "workplans/KEY-WP-0010-openbao-operator-loopback-callback.md",
"db_id": "",
"file_value": "RAILIANCE-WP-0027",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-41",
"message": "Task wait-qualifier drift 'KEY-WP-0039-T02': file={'needs_human': False, 'blocking_reason': 'Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; original secrets-engine-openbao registration go remains resolved; probe/coding-client custody and attended readiness are not established.', 'decision_id': None, 'blocked_on': None, 'wait_until': None} db={'needs_human': False, 'blocking_reason': 'Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; registration authorization already resolved.', 'decision_id': None, 'blocked_on': None, 'wait_until': None} (file wins)",
"file_path": "workplans/KEY-WP-0039-openbao-negative-probe-tokens.md#KEY-WP-0039-T02",
"db_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3",
"file_value": "{\"blocked_on\": null, \"blocking_reason\": \"Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; original secrets-engine-openbao registration go remains resolved; probe/coding-client custody and attended readiness are not established.\", \"decision_id\": null, \"needs_human\": false, \"wait_until\": null}",
"db_value": "{\"blocked_on\": null, \"blocking_reason\": \"Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; registration authorization already resolved.\", \"decision_id\": null, \"needs_human\": false, \"wait_until\": null}",
"fixable": true
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'keycape-implementation@retired-20260826' (id=2c9caad8\u2026, status=archived) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "2c9caad8-2ced-492d-9d63-376387b4b9b0",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-28",
"message": "2 unread inbox message(s) older than 3 day(s): the-custodian:23172cb2, the-custodian:51e9eace",
"file_path": "",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-28",
"message": "2 unread message(s) lack thread_id for supersession tracking: the-custodian:23172cb2, the-custodian:51e9eace",
"file_path": "",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": false
}
],
"fixes_applied": [
"bindings: synced 44/44",
"C-17 cleared: pushed 1 backlogged commit(s)",
"C-11 skipped: task 'ADHOC-2026-09-27-T02' in finished workstream \u2014 not created",
"C-41 fixed: task 03da4c38\u2026 wait qualifiers synced",
"brief: .custodian-brief.md updated",
"push: pushed"
],
"summary": {
"fail": 0,
"automation_error": 0,
"warn": 9,
"info": 1
},
"result": "warn"
}

View file

@ -0,0 +1,54 @@
{
"commit": "d803e9282a47b49daf930fb905312fa5104c0ace",
"receiver_status": "applied",
"acknowledge_retirements": false,
"instance_role": "primary",
"instance_label": "railiance01",
"workplan": {
"id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
"status": "active",
"record_id": "KEY-WP-0039"
},
"task_bindings": [
{
"record_id": "KEY-WP-0039-T01",
"state_hub_task_id": "4d06b059-3768-53bb-83a4-a9a71c0850b8",
"status": "done",
"workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
"depends_on_source": []
},
{
"record_id": "KEY-WP-0039-T02",
"state_hub_task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3",
"status": "wait",
"workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
"depends_on_source": [
"KEY-WP-0039-T01",
"KEY-WP-0038-T02",
"KEY-WP-0038-T03"
]
},
{
"record_id": "KEY-WP-0039-T03",
"state_hub_task_id": "54d37ad8-2352-51c2-bb2e-af6070dbbd73",
"status": "done",
"workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
"depends_on_source": []
}
],
"all_other_task_fields_unchanged": true,
"only_task_field_delta": {
"task": "KEY-WP-0039-T02",
"field": "blocking_reason",
"old": "Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; registration authorization already resolved.",
"new": "Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; original secrets-engine-openbao registration go remains resolved; probe/coding-client custody and attended readiness are not established."
},
"needs_human": false,
"primary_task_blocking_reason": "Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; original secrets-engine-openbao registration go remains resolved; probe/coding-client custody and attended readiness are not established.",
"frontmatter_unchanged": true,
"only_committed_path": "workplans/KEY-WP-0039-openbao-negative-probe-tokens.md",
"remote_forge_matches": true,
"owner_worktree_and_index_clean": true,
"normal_sync_refusal": "unrelated grandfathered ADHOC-2026-09-27-T02 UUID; not modified",
"native_acceptance_inferred": false
}

View file

@ -0,0 +1,70 @@
{
"scope": "only authorized KEY39 T02 blocking_reason suffix correction; all other task fields/frontmatter unchanged; no native action",
"commit": "d803e9282a47b49daf930fb905312fa5104c0ace",
"files": {
"KEY39.preimage.md": "sha256:618600946f6dd4128206db7ab23da9cc96e22cb84e0fd301b1b2eacd5c9bdba4",
"KEY39.proposed.md": "sha256:517f9526d574ce3faafad18837e0039db8f3f7990eac3e34a1678cdd7307a9e1",
"apply.py": "sha256:1c4997372c3c43e78ffcce533e33b0c38ececaf252cd4bf62db69cf43f11967a",
"baseline.json": "sha256:7415b840f97b217a29004cd9b3a4a0737dbc31c9c03b821d326442bc793c95ab",
"canonical-receiver.json": "sha256:31a225513ccee3266fd59476950875c78bcbe57d13558b20940a096e95536e80",
"canonical-reconcile.py": "sha256:dca4dcfc37db7f68e3059fe99b910ff7f207e2ef3c4b69f926ebaf46317d078a",
"commit.json": "sha256:9e534c5ab7bd5ebb728774c58d363eaa52bcbb3f561118df75ad07eef9699974",
"consistency-backup/.custodian-brief.md": "sha256:96d84539b3a92786bff37fcc70161adec4648b9547287e52e6b817bc080a080a",
"consistency-backup/WORK-RECORDS.md": "sha256:431842547bd82750f284f8c1c9f519892f1bb1c1a684b9b1e38adc6fdfdc299d",
"consistency-backup/originals.json": "sha256:0d7851a532b1b451b20eb00a1eb98727b004da073173263251cf3fae40a34e52",
"consistency-backup/workplans/ADHOC-2026-09-05.md": "sha256:76123d22d0c1efc51212f8914e386f078b3c6d908959af6978ae73874e492b04",
"consistency-backup/workplans/ADHOC-2026-09-07.md": "sha256:c335ebfe91dad291277e8f17034ac8f785b3bc767f281f205c446c08f03d7978",
"consistency-backup/workplans/ADHOC-2026-09-27.md": "sha256:2794d1940fc3c5e4a2df57de9d4b742fa6aa8602b67f1d3177c0d96bc5aaf314",
"consistency-backup/workplans/KEY-WP-0001-keycape-implementation.md": "sha256:29a2bf4892b7185e28e9126a3d8311d0fed34a876f416a3e75c605915fde0969",
"consistency-backup/workplans/KEY-WP-0002-container-image-gitea.md": "sha256:76fee187185b30219bc257ccc9b587661df4489574a2bb57703a365a5d803cec",
"consistency-backup/workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md": "sha256:5e949c696e587540d305b682eb1f8390ec544bbec6a566be8d04e73fb1b254ca",
"consistency-backup/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md": "sha256:260ba7d3116db3bc5a72e5cd8b9984137e7e372c8128d21073614dbd948c10c2",
"consistency-backup/workplans/KEY-WP-0005-iam-profile-core-claims.md": "sha256:c6702b644cdd046c0d3bba8ba8837050826e4dc3d855bd73327ae94218ba5b57",
"consistency-backup/workplans/KEY-WP-0006-client-credentials-service-tokens.md": "sha256:b3227849978c95cad89e02afb74bf2217b6188ae65c5b0af6a21703d65477701",
"consistency-backup/workplans/KEY-WP-0007-user-engine-portal-oidc-client.md": "sha256:5408ff6dfeb58a8a7756b48fe4f54f7c7b98d7940cb9aaa5c7864f1247e58150",
"consistency-backup/workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md": "sha256:bfc9b2118f0712e92a86bbb0f14a2857fafc3a42a3c14c0e2535002aaa57a2f5",
"consistency-backup/workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md": "sha256:d1daac9815930104a392ea96c14922e34f947720a31d1b0ac45a770997e0f476",
"consistency-backup/workplans/KEY-WP-0010-openbao-operator-loopback-callback.md": "sha256:41d28a16bef9313b776c5c6305316a9e7d4f5528b7fb7427b8384a0ed0ad35c4",
"consistency-backup/workplans/KEY-WP-0011-live-secret-exposure-recovery.md": "sha256:3c5458180fe81a04e357f8db737e4287f79640b5ab02682bcadc595b73f846cd",
"consistency-backup/workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md": "sha256:68d34d75c914e7d2d8177000e800d6372cccda2b850c03ab85da42b90a66b91c",
"consistency-backup/workplans/KEY-WP-0013-approval-engine-resource-audience.md": "sha256:55c88bf4ed9a456db59b319bbaf960dfe6f4147437e339f950e0583d7582057a",
"consistency-backup/workplans/KEY-WP-0014-native-credential-lane-handoff.md": "sha256:22aac43ae5039aebd11f9f0aa57a882ad009ad6ed119ec08eb85b32a5af48cd7",
"consistency-backup/workplans/KEY-WP-0015-scope-intent-assessment.md": "sha256:60b9f2265eff880d916c3ee8dba36aec9984ec310804de776cebf89cc98fdb95",
"consistency-backup/workplans/KEY-WP-0016-authorization-code-protocol-hardening.md": "sha256:ed48128a1cb51cbe3d154727bafd5a5ffb3b5b618c20649b2a0e7db154cf3a0c",
"consistency-backup/workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md": "sha256:8700e0e5aa07dcd7f651e792cca12a0330b7ee9d2f6064b4cc3fcf4c7d509340",
"consistency-backup/workplans/KEY-WP-0018-export-completeness-evidence.md": "sha256:15026b708d116f803b0b2fa43f30a4dbced76e4d8137b5fcb5b4275e593d7c7b",
"consistency-backup/workplans/KEY-WP-0019-upstream-provider-token-verification.md": "sha256:0d2d8b5bda1e0d333935812d8d3749cb7b0c5523a3987d1b26185f9f6d0f0fa2",
"consistency-backup/workplans/KEY-WP-0020-migration-contract-preservation.md": "sha256:cc842d15bf42ddc4d0f4056bc19b3176cab2c5f3c1a2fe6d648e5dbd077be8b3",
"consistency-backup/workplans/KEY-WP-0021-snapshot-attribute-validation.md": "sha256:b603248693073a9b9b25b4d0a4147d8d88c16a07dc9936e48fa1f3c76fe8bab6",
"consistency-backup/workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md": "sha256:8c135e84be81de31b4949f52ac28dccd404f2e4c3eaa576d733bd50160427a53",
"consistency-backup/workplans/KEY-WP-0023-live-migration-proof.md": "sha256:e581cd11f3905805b5e8ba8e2ede85092577c7d086e1761f2a2d033fdead4e47",
"consistency-backup/workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md": "sha256:af7842fa97263bfd46a4b5ae84c5c1ecddb06ab9623586e0292dcc93099fea35",
"consistency-backup/workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md": "sha256:0addb34e6d85e376c6a5db7afba597b6f268afa98b86c8c60add9be174dd1b26",
"consistency-backup/workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md": "sha256:c1bc0de47511ae49102009a172c84d9d5d64e829a18c8d40564fe817935563b3",
"consistency-backup/workplans/KEY-WP-0027-rollout-readiness-and-live-state.md": "sha256:f559eae61d433d451f80ea575254bb14a47f2485267b412e42d524b434bb1b59",
"consistency-backup/workplans/KEY-WP-0028-browser-client-field-validation.md": "sha256:fbd51422fa5e6449000e4520e6639f6135c7393165a5ae02b1e92892edba0480",
"consistency-backup/workplans/KEY-WP-0029-scope-reassessment.md": "sha256:3241f3b94db9cad5e3770cf1fedc5c211288357e4f69b5ba9da57065094bdf0e",
"consistency-backup/workplans/KEY-WP-0030-tenant-precondition-guard.md": "sha256:c94c3afc7fb4270dfc18f3fe8a32946c524e4a6ba2d02a3d8fda69c9b8441af0",
"consistency-backup/workplans/KEY-WP-0031-gate-house-tenant-ruling.md": "sha256:0abb135367f0366aa071769c4bd76fdeb321d810ec75ad3b58d718734f78382b",
"consistency-backup/workplans/KEY-WP-0032-principal-type-provenance-guard.md": "sha256:6a5cd483a40b9477227b02156c42b3469094dac145b33727b336118898aa782a",
"consistency-backup/workplans/KEY-WP-0033-vergabe-fresh-login.md": "sha256:c01bb51cf2dcad06ea846040dddef09845041fa7271cd7015e5a34a49e60bb8f",
"consistency-backup/workplans/KEY-WP-0034-account-recovery.md": "sha256:7db9adcd418ccabfbce672aec283249a6e374212492f0349e8c47a568f130eda",
"consistency-backup/workplans/KEY-WP-0035-optional-mfa.md": "sha256:c60ea8d0c195686c0d2317f657aad2de27c9b9432d311627c0e1d06124cabb49",
"consistency-backup/workplans/KEY-WP-0036-platform-factor-recovery.md": "sha256:5ceb08fe5789b4f02550bb04fcaf8eeb25a43ac3f506dfc62d60a513bcd0013f",
"consistency-backup/workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md": "sha256:3a90a0c460fc439570820b96d1ddc3b7f8646c557ca3a8aa27c6d546bb1a845a",
"consistency-backup/workplans/KEY-WP-0038-forgejo-oidc-client.md": "sha256:dc8d88d462799e7e6e44da759a973d52e44102a271969a5daaa9d30ec015b064",
"consistency-backup/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md": "sha256:517f9526d574ce3faafad18837e0039db8f3f7990eac3e34a1678cdd7307a9e1",
"consistency-backup/workplans/KEY-WP-0040-mfa-attended-run-findings.md": "sha256:98b23d8810bff047067169232213cb313ecc14ff79751224842723afe808fbd2",
"consistency-backup/workplans/KEY-WP-0041-sign-in-diagnosis-page.md": "sha256:7e44f46acd6b3942c1852b49e0d64b205dfd7d9bb378aed5241b1bf22d7ce88a",
"consistency.stderr.txt": "sha256:f4a6aa1609a13600a0b97cbb08bbc03b3e247b619ff37912d37f7e109ce798ba",
"consistency.stdout.json": "sha256:a77c1056e9cbf8fb5ecced6ef316b7bde9646d258452f875575c4e039fea7709",
"final-verification.json": "sha256:43e0489c538788921be2f9ea2dec89451730a46ea6ace5b5d35b6e9d812db7a6",
"normal-publication.json": "sha256:724f185f729f8224fc85a257cb9c564d77585e81f672e11eb2c77f4de56c20ce",
"normal-sync.stderr.txt": "sha256:832bd22063c46072d0d91381f7d1356be933b4d09ded52fa19282da13c2855b1",
"normal-sync.stdout.json": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"one-path.patch": "sha256:3dd74d482fe512683731cde8b57d3e3259f0fb523272ee88e6838a17ceb35611",
"progress-receipt.json": "sha256:ae2bc1eb3c069753e269bb176b0660ba732a3ab966853a5d6a0d69613b1bf835",
"progress-request.json": "sha256:df686f30b8ca8704745bd3317a87611a292409719f0e5a996d267fb087da63f7",
"sync.py": "sha256:f9714d9ddd0b47e9e359edabc2b3f7edf84c2c9b4d55952215f2df65750415d0"
}
}

View file

@ -0,0 +1,34 @@
{
"published_candidate_commit": "d803e9282a47b49daf930fb905312fa5104c0ace",
"normal_commands": [
{
"command": [
"statehub",
"fix-consistency",
"--repo",
"key-cape",
"--json"
],
"exit_code": 0,
"label": "consistency"
},
{
"command": [
"/home/worsch/repo-manager/.venv/bin/rmgr",
"sync",
"--path",
"/home/worsch/key-cape",
"--push"
],
"exit_code": 1,
"label": "normal-sync"
}
],
"unrelated_writeback_restored": [
".custodian-brief.md"
],
"all_original_index_worktree_fields_preserved": true,
"one_path_commit": true,
"normal_hooks": "changed-staged-blobs scanner8.30.1 clean",
"native_or_credential_actions": 0
}

View file

@ -0,0 +1,13 @@
Traceback (most recent call last):
File "/home/worsch/repo-manager/.venv/bin/rmgr", line 8, in <module>
sys.exit(main())
^^^^^^
File "/home/worsch/repo-manager/src/repo_manager/cli.py", line 853, in main
result = sync_repository_projection(
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/worsch/repo-manager/src/repo_manager/projection_sync.py", line 98, in sync_repository_projection
assignments = ensure_missing_work_record_identifiers(repo_root)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/worsch/repo-manager/src/repo_manager/identifiers.py", line 962, in ensure_missing_work_record_identifiers
raise ValueError(
ValueError: missing UUIDs on grandfathered or noncanonical records require an explicit identity decision: [{'kind': 'task', 'record_id': 'ADHOC-2026-09-27-T02', 'path': 'workplans/ADHOC-2026-09-27.md'}]

View file

@ -0,0 +1,13 @@
diff --git a/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md b/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md
index b22aa6a..4398517 100644
--- a/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md
+++ b/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md
@@ -99,7 +99,7 @@ transient Kubernetes Secret that is deleted in the same session.
id: KEY-WP-0039-T02
status: wait
depends_on: [KEY-WP-0039-T01, KEY-WP-0038-T02, KEY-WP-0038-T03]
-blocking_reason: "Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; registration authorization already resolved."
+blocking_reason: "Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; original secrets-engine-openbao registration go remains resolved; probe/coding-client custody and attended readiness are not established."
priority: medium
state_hub_task_id: "03da4c38-1dac-53b1-8b0b-e697df48e9d3"
```

View file

@ -0,0 +1,14 @@
{
"workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
"id": "777d28ea-7ebc-4f2a-814f-adad63d72dd5",
"topic_id": null,
"task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3",
"decision_id": null,
"event_type": "note",
"summary": "Published KEY39 current narrative approval-scope clarification in commit 26ec9bb9b195ad377448570d6b161fd682311a0b, then the separately authorized T02 blocking_reason suffix correction in final commit d803e9282a47b49daf930fb905312fa5104c0ace. Decision caafe4be remains valid only for original secrets-engine-openbao registration under KEY-WP-0037-T03 / deployed 6f79a2af / NK-WP-0046 exact helper; it does not authorize new probe clients or coding-client custody. Standing attended rule remains separate within reviewed existing scope; no renewed original-client approval or new protocol requested. Both ordinary one-file commits passed normal hooks, consistency passed, generated brief writeback restored, unrelated historical ADHOC UUID gap preserved. Canonical primary receiving applied each exact published commit with retirement acknowledgment false. Primary task 03da4c38-1dac-53b1-8b0b-e697df48e9d3 blocking_reason equals the entire corrected source value and needs_human remains false. T01 done, T02 wait, T03 done; UUIDs, dependencies, priorities and frontmatter unchanged. No native, credential or registration action. New work stopped after source/metadata verification.",
"detail": null,
"author": "codex",
"session_id": null,
"created_at": "2026-10-17T11:42:42.743840Z",
"workstream_id": "09c3bddf-6058-5603-8488-cccdb7be4f86"
}

View file

@ -0,0 +1,7 @@
{
"summary": "Published KEY39 current narrative approval-scope clarification in commit 26ec9bb9b195ad377448570d6b161fd682311a0b, then the separately authorized T02 blocking_reason suffix correction in final commit d803e9282a47b49daf930fb905312fa5104c0ace. Decision caafe4be remains valid only for original secrets-engine-openbao registration under KEY-WP-0037-T03 / deployed 6f79a2af / NK-WP-0046 exact helper; it does not authorize new probe clients or coding-client custody. Standing attended rule remains separate within reviewed existing scope; no renewed original-client approval or new protocol requested. Both ordinary one-file commits passed normal hooks, consistency passed, generated brief writeback restored, unrelated historical ADHOC UUID gap preserved. Canonical primary receiving applied each exact published commit with retirement acknowledgment false. Primary task 03da4c38-1dac-53b1-8b0b-e697df48e9d3 blocking_reason equals the entire corrected source value and needs_human remains false. T01 done, T02 wait, T03 done; UUIDs, dependencies, priorities and frontmatter unchanged. No native, credential or registration action. New work stopped after source/metadata verification.",
"event_type": "note",
"author": "codex",
"workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
"task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3"
}

View file

@ -0,0 +1,36 @@
from pathlib import Path
import subprocess,json,hashlib,stat
root=Path('/home/worsch/key-cape');out=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-finalization-blocking-reason-r1');backup=out/'consistency-backup';backup.mkdir()
def git(*args):return subprocess.check_output(['git','-C',str(root),*args]).decode()
head=git('rev-parse','HEAD').strip();assert head==json.loads((out/'commit.json').read_text())['commit']
assert git('diff-tree','--no-commit-id','--name-only','-r',head).splitlines()==['workplans/KEY-WP-0039-openbao-negative-probe-tokens.md']
assert not git('status','--porcelain=v1')
paths=sorted({p.relative_to(root).as_posix() for p in (root/'workplans').rglob('*.md')}|{n for n in ('.custodian-brief.md','WORK-RECORDS.md') if (root/n).exists()})
original={}
for rel in paths:
p=root/rel;assert p.is_file() and not p.is_symlink()
dest=backup/rel;dest.parent.mkdir(parents=True,exist_ok=True);dest.write_bytes(p.read_bytes())
original[rel]={'sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'mode':stat.S_IMODE(p.stat().st_mode),'index':git('ls-files','--stage','--',rel)}
(backup/'originals.json').write_text(json.dumps(original,indent=2)+'\n')
results=[];changed=[]
try:
for label,argv in [('consistency',['statehub','fix-consistency','--repo','key-cape','--json']),('normal-sync',['/home/worsch/repo-manager/.venv/bin/rmgr','sync','--path',str(root),'--push'])]:
cp=subprocess.run(argv,cwd=root,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=240)
(out/(label+'.stdout.json')).write_bytes(cp.stdout);(out/(label+'.stderr.txt')).write_bytes(cp.stderr)
results.append({'command':argv,'exit_code':cp.returncode,'label':label})
finally:
assert git('rev-parse','HEAD').strip()==head,'unexpected commit; stop for review'
for rel,b in original.items():
p=root/rel
if not p.exists() or hashlib.sha256(p.read_bytes()).hexdigest()!=b['sha256']:
changed.append(rel);p.write_bytes((backup/rel).read_bytes());p.chmod(b['mode'])
current=git('ls-files','--stage','--',rel)
if current!=b['index']:
assert b['index'];mode,oid,stage,path=b['index'].split();assert stage=='0'
git('update-index','--cacheinfo',mode,oid,path)
assert git('ls-files','--stage','--',rel)==b['index']
assert hashlib.sha256(p.read_bytes()).hexdigest()==b['sha256']
assert not git('status','--porcelain=v1'),git('status','--porcelain=v1')
receipt={'published_candidate_commit':head,'normal_commands':results,'unrelated_writeback_restored':changed,'all_original_index_worktree_fields_preserved':True,'one_path_commit':True,'normal_hooks':'changed-staged-blobs scanner8.30.1 clean','native_or_credential_actions':0}
(out/'normal-publication.json').write_text(json.dumps(receipt,indent=2)+'\n')
print(json.dumps(receipt))

View file

@ -0,0 +1,14 @@
{
"workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
"id": "32796349-c902-456c-97e6-e4de38467aac",
"topic_id": null,
"task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3",
"decision_id": null,
"event_type": "note",
"summary": "Reviewed KEY-WP-0039 and infrastructure owner reviews for finalization. Revalidated17 executable/producer/Platform source pins plus exact source-only infrastructure approval and seven KEY38/KEY39 source/Hub task bindings. T01/T03 complete; T02 remains wait for KEY38 first-case claims proof, existing coding-client custody admission/readiness returns, and actual attended negative/expiry/revoke/client-removal receipt. Kept parent active and existing approvals intact; no native or credential action. Evidence: docs/changes/netkingdom-target-2026-10-17/key39-finalization-current-r2/close-verification-r3.json.",
"detail": null,
"author": "codex",
"session_id": null,
"created_at": "2026-10-17T21:58:46.203045Z",
"workstream_id": "09c3bddf-6058-5603-8488-cccdb7be4f86"
}

View file

@ -0,0 +1,58 @@
{
"observed_at": "2026-10-17T21:58:43.188615+00:00",
"review_files_and_source_pins_verified": 18,
"plans": {
"KEY-WP-0039": {
"source_sha256": "sha256:517f9526d574ce3faafad18837e0039db8f3f7990eac3e34a1678cdd7307a9e1",
"parent_status": "active",
"tasks": [
{
"record_id": "KEY-WP-0039-T01",
"uuid": "4d06b059-3768-53bb-83a4-a9a71c0850b8",
"source_status": "done"
},
{
"record_id": "KEY-WP-0039-T02",
"uuid": "03da4c38-1dac-53b1-8b0b-e697df48e9d3",
"source_status": "wait"
},
{
"record_id": "KEY-WP-0039-T03",
"uuid": "54d37ad8-2352-51c2-bb2e-af6070dbbd73",
"source_status": "done"
}
]
},
"KEY-WP-0038": {
"source_sha256": "sha256:dc8d88d462799e7e6e44da759a973d52e44102a271969a5daaa9d30ec015b064",
"parent_status": "active",
"tasks": [
{
"record_id": "KEY-WP-0038-T01",
"uuid": "5f019d13-1fa3-5b88-a27a-75a3339ca96e",
"source_status": "done"
},
{
"record_id": "KEY-WP-0038-T02",
"uuid": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30",
"source_status": "wait"
},
{
"record_id": "KEY-WP-0038-T03",
"uuid": "40ddaa29-7245-508c-ad48-ddc063e25115",
"source_status": "wait"
},
{
"record_id": "KEY-WP-0038-T04",
"uuid": "583cf1f1-16dc-515f-83bc-1efc45024a89",
"source_status": "cancel"
}
]
}
},
"disposition": "Source work complete; implementation remains active with native T02 wait. No new approval requested; existing source acceptance preserved.",
"native_actions": false,
"credentials_accessed": false,
"task_status_changes": false,
"assessment_reference": "../key39-infra-finalization-r1/ASSESSMENT.md"
}

View file

@ -0,0 +1,351 @@
---
id: KEY-WP-0039
type: workplan
title: "Negative-check tokens for the secrets-engine OpenBao JWT role"
domain: infotech
repo: key-cape
status: active
flavor: implementation
owner: claude-code
topic_slug: netkingdom
created: "2026-10-10"
updated: "2026-10-17"
depends_on: []
related_repos:
- railiance-platform
- secrets-engine
state_hub_workstream_id: "09c3bddf-6058-5603-8488-cccdb7be4f86"
---
# KEY-WP-0039 - Negative-check tokens for the secrets-engine OpenBao JWT role
## Why
CCR-2026-0038 (railiance-platform RPF-WP-0035-T02) requires the OpenBao role
`secrets-engine-openbao` on `auth/keycape-services` to refuse JWTs with a wrong
`aud`, `sub`, `tenant`, `principal_type`, `roles` or `scope`, a coding-agent JWT
and an expired JWT. The positive login proof passed on 2026-10-10 (secrets-engine
`docs/evidence/2026-10-10-keycape-openbao-probe.json`). railiance-platform asked
whether KeyCape can issue the negative tokens without touching the
`secrets-engine` registration.
KeyCape signs only what a registration declares. It has no way to mint a token
with arbitrary claims, and adding one would create a forging path in the issuer.
So the negative tokens must come from real, separate registrations that differ
from `secrets-engine` in exactly one property each.
## Boundaries
- The `secrets-engine` registration is not changed.
- Probe clients are confidential `client_credentials` clients with the minimum
60-second lifetime, scopes that grant nothing, and secrets minted into custody
like any other client. They are removed after the proof.
- The probe script prints only accept or refuse booleans per case, never tokens or
secrets. It runs attended.
## T01 — Define the probe registrations
```task
id: KEY-WP-0039-T01
status: done
priority: medium
state_hub_task_id: "4d06b059-3768-53bb-83a4-a9a71c0850b8"
```
Write a non-secret request packet under `docs/` with one probe client per claim
mismatch, each identical to `secrets-engine` except for:
- `aud`: a different access-token resource audience
- `sub`: a different service subject
- `tenant`: a tenant other than `platform`
- `roles`: a role other than `secrets-engine`
- `scope`: a scope outside the role's bound scope
Two cases need no probe client. A service token always carries
`principal_type: service` (`src/internal/server/oidc/token.go`), so the wrong
`principal_type` case uses a human access token from an attended login. The
coding-agent case uses the existing `codex-railiance-platform` registration (role
`coding-agent`, `docs/openbao-service-auth-contract.md`) if it is registered live;
T01 confirms that.
The expired-JWT case reuses any probe client: request a token, wait past its
60-second lifetime, then present it.
Acceptance: railiance-platform agrees the packet covers every refusal that
CCR-2026-0038 names.
2026-10-10: railiance-platform agreed that the case list covers every
CCR-2026-0038 claim negative. The remaining CCR checks (capabilities-self deny, no
provider fallback) are theirs and secrets-engine's. Packet written at
`docs/openbao-negative-probe-clients.yaml`. It departs from the agreed list in two
ways, both sent back for re-agreement:
- **Expired case.** A token from another probe already fails a claim, so its
refusal does not show that expiry was checked. It needs a sixth client,
`openbao-probe-expiry`, identical to `secrets-engine-openbao` in every claim. That
makes it a second credential able to log in as secrets-engine for the session.
OpenBao's `expiration_leeway` (150s by default) also extends the wait.
- **Audience pin.** A service token's `aud` is its `clientId` unless `audience` is
set. Every probe except the aud probe therefore pins
`audience: secrets-engine-openbao`, so it fails only on its own claim.
Condition (a), no OpenBao custody, holds. The values still have to reach KeyCape's
environment, because KeyCape resolves `secretRef` at startup. The packet uses a
transient Kubernetes Secret that is deleted in the same session.
## T02 — Register the probe clients, run the attended probe, then remove them
```task
id: KEY-WP-0039-T02
status: wait
depends_on: [KEY-WP-0039-T01, KEY-WP-0038-T02, KEY-WP-0038-T03]
blocking_reason: "Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; registration authorization already resolved."
priority: medium
state_hub_task_id: "03da4c38-1dac-53b1-8b0b-e697df48e9d3"
```
2026-10-11: requalified — live registration go caafe4be was already resolved; owner authorization present. See the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md.
Write the attended script, which prints booleans only.
2026-10-10: script written at `scripts/openbao_negative_probe.py`, with tests in
`scripts/test_openbao_negative_probe.py`. It meets condition (b): one refused
boolean per case plus the refusal class (`claim_mismatch`, `expired`,
`not_service_principal`), and no token, secret, claim value or OpenBao error text.
A transport failure or an unexpected response is reported as `inconclusive`, never
as a refusal. A token that OpenBao accepts is revoked at once and fails the run.
Still waiting: railiance-platform's re-agreement on the two departures above, and
an attended registry session with net-kingdom. Register the clients
through the normal registration path, hand the run to railiance-platform, record
a value-free receipt in `docs/evidence/`, and deregister the clients afterwards.
Acceptance: every case is refused, the receipt is committed, and the probe clients
are gone.
2026-10-11 source preparation: T01 remains progress because the exact expiry
client and audience-pin amendments require platform-owner re-agreement; the
prior agreement covered the earlier case list, not this revised packet. The
prepared runner revision adds a valid expiry positive control and immediate
revoke, exact decoded claim-isolation preflight (not signature evidence),
semantic refusal classes, bounded expiry TTL and redirect refusal. Seventeen
local tests pass; original-source semantic regressions demonstrate two false
successes. See `docs/openbao-negative-probe-session-proposal.md` for exact owner
returns, live authority, effective leeway, custody boundaries and cleanup. No
live token, registry or OpenBao action occurred. T02 continues to wait for owner
re-agreement plus separate attended live authorization. MASON-WP-0009-T05
operator/ESO capability and nonmember checks remain independent acceptance.
2026-10-11: Platform independently re-agreed the final amended design (session proposal SHA25623f13d2a5a4204f749f35c5d44ef39d213838cd2fce1c36fb4775254ef1c7f09). Original conditional review and narrow confirmation are retained in railiance-platform/docs/reviews/2026-10-11-keycape-openbao-negative-session-owner-review*. Source acceptance T01 is complete; actual registry/leeway/readiness, founder live go, negative/cleanup and separate capability/no-fallback obligations remain T02 and owner plans.
## KeyCape issuer-source compatibility inspection for lifecycle v2
```task
id: KEY-WP-0039-T03
status: done
priority: medium
state_hub_task_id: "54d37ad8-2352-51c2-bb2e-af6070dbbd73"
```
2026-10-11: Completed KeyCape's own issuer-source inspection for
KEY-WP-0039-T02/attended-lifecycle-v2, exact manifest SHA256
6fc0fe47f99a97c11c6e7538f5ea02054335df7d46055d3f84bed4e194aeaaed.
Read AGENTS.md, governing INTENT.md, current client-credentials claim source,
registration fragments, native caller-authentication commands and frozen v2
issuer transport/probe wiring. Seventeen unchanged runner tests pass locally.
This task completes only issuer-source inspection; the resulting narrow
source acknowledgment is in docs/reviews/2026-10-11-key39-lifecycle-v2-keycape-source-ack.json (retained frozen prior acknowledgment).
T01 stays done and T02 stays open for actual native registration, token input,
refusal and cleanup proof. No Secret, token, credential or native environment was accessed; no owner
checkout was changed during this source inspection. Owner source files were
read for the bounded issuer compatibility check.
Current token.go:306-309 returns HTTP 401 invalid_profile_usage with
feature=client_id for a removed confidential client; wrong-secret
feature=Authorization cannot count as removed-registration evidence. The
proposal and packet comments are narrowly corrected against that current
issuer contract. Prior T01/source re-agreement history is preserved; this source
acknowledgment supplies no NetKingdom runtime-owner review or founder live go.
The private token-producer handoff remains a real execution gap under T02.
Existing keycape login/service-token commands require a new private output
file; login also needs a concrete admitted public PKCE client/callback and
attended browser/MFA. The lifecycle expects an in-memory private stdin object
containing human_token and coding_agent_token, but no reviewed file-to-process
or process-to-process producer, admitted human client/callback, custody-injected
coding-agent source, native placement or producer cleanup receipt has been
provided. No unattended token producer or admitted secret route is invented.
Resolve those exact owner contracts before founder native approval; existing
static registration is not live token custody or producer admission.
2026-10-11 same T03 source-inspection refinement for lifecycle V4: completed
KeyCape issuer-facing inspection before the new formal acknowledgment in
docs/reviews/2026-10-11-key39-lifecycle-v4-keycape-source-ack.json. Exact V4
manifest SHA256 d079f85c47d5c2635e1ee633ea4d1346b1737ce2be9ca273f22a38e620f418d8,
executor b4687c2d07819c43b45d0cddcad0488b62eb49175b2a06a43f7806a0838d3a4d.
NetKingdom's independent V4 source review is approved, receipt SHA256
4f4498203a31bd46dbc3715c0df1cbb9eee63bed5b2c1baf430ff3c4a37c13cb.
The V2/V3 unsuccessful reviews and earlier KeyCape acknowledgment remain
retained as one correction trial; they are not extra scored successes.
V4 retains the unchanged issuer runner and six-client claim design, and fixes
finite/nonboolean windows, true total request deadlines, late-known OpenBao
session private revoke/refusal and guarded bounded final Secret deletion.
The issuer-facing acknowledgement covers only stopping late token exchanges,
claim source/shape and preserving token cleanup failure/unknown outcomes; it
does not replace NetKingdom runtime or Platform backend acceptance. Tests: 41
supplied lifecycle, 17 unchanged runner and 17 independent owner checks pass
locally. No native tokens, credentials, issuer mutations or live cleanup were
performed. T01 stays done; T02 stays open and the genuine private token-producer
handoff remains its next source prerequisite before actual founder native go.
2026-10-11 same T03 source-inspection refinement for lifecycle V5: completed
KeyCape's own current issuer-source/claim/cleanup-contract inspection before
any new formal acknowledgment. Current token.go:306-309 still distinguishes
removed-client invalid_profile_usage feature=client_id from wrong-secret
feature=Authorization. The six-client design, audience source and expiry
positive control remain unchanged, and the original runner's 17 tests pass.
Exact V5 manifest SHA256
d1f997624db46693a7126a6eb6435f6b3a353d5bd67dcc8a539683e4b7147285;
executor 3bcceebc845b17492040c76223d77088257c547a2b890198e7b5c2b04a0cd360.
V5 has 46 passing supplied lifecycle checks. It adds a single exceptional
response-close abort under a fixed20ms cap clipped to nonresettable aggregate
cleanup, and to positive original request time remaining, with expired aggregate
zero calls, fixed private errors and truthful known-session revoke/unknown
outcomes. This inspection covers issuer-facing token lifetime and claim
interfaces only; NetKingdom and Platform independently own their runtime and
backend review decisions. Previous V2/V3 Net source refusals and Platform V4
exceptional-close refusal remain retained; KeyCape's V2/V4 acknowledgments are
historical narrow source inspections, never aggregate native approval.
T01 remains done, T02 open. No native actions or credentials occurred. The real
private token-producer/client/callback/coding-agent custody/cleanup handoff gap
stays the next prerequisite; no unattended admission is inferred.
Exact independent V5 source decisions were then verified: NetKingdom approved
receipt SHA256 32a62bd1ca1bd7311965d016d4d6a6e4d9177f3423c63add436d9d309865eacc
(46 supplied +17 runner +21 independent checks); Platform approved receipt
SHA256 ba872dc53061b1dcca9dea1e8897597c394c1f9365e0fc49ee6f2095f57f11e0
(46 supplied +5 independent +17 previously verified runner checks). Those are
foreign owners' own source decisions, not KeyCape runtime/backend reviews or
founder native authority. Formal KeyCape bounded issuer acknowledgment follows
in docs/reviews/2026-10-11-key39-lifecycle-v5-keycape-source-ack.json.
2026-10-11 T02 continuing source preparation: private producer decision packet
`KEY-WP-0039-T02/private-producer-handoff-v1` stages supported existing CLI
flows, selected existing netkingdom-bootstrap-console literal-loopback human
client subject to owner purpose/MFA/listener admission, and the existing
codex-railiance-platform source identity subject to actual live/custody/consumer
admission. No existing coding-agent delivery receipt was found; AppRole tokens
do not substitute for issuer JWTs. A bounded pipe-only adapter for the current
CLI's two caller-owned private files has 18 passing synthetic checks, with
identity-guarded explicit file consumption and fixed value-free failure output.
Decoded claim shape does not prove signatures or authenticated admission.
See docs/openbao-negative-token-producer-decision.md and .json for exact owner
choices and missing receipts. The exact trusted workstation-to-node launcher,
private-file allowance and railiance01 Bao route/public-health guard still need
source review and founder disposition before any token/temporary credential/
mutation/restart. No registration, custody, login, native action or credential
request occurred. V5 and failed review histories retained; T01 stays done and
T02 open. No new work record or native acceptance is inferred.
2026-10-11 same T02 source-preparation correction for
KEY-WP-0039-T02/private-producer-handoff-v2: NetKingdom's exact V1 consumer
refusal remains recorded (no child-ready gate, stdio alias, non-atomic unlink).
V2 has source-pinned private launch/ready handshake compatible with exact
Net V9 manifest84542ddc: fresh nonce/source/envelope/health/window marker before
ANY caller-token file load/consume/write; separate local data/control pipes
reject kernel identity aliases of stdin/stdout/stderr. Adapter read/write budget
only clips downward after readiness. 31 distinct producer/launcher synthetic
checks and 74 compatible consumer supplied checks pass. No SSH/native/login or
credential access was performed. Stat/unlink remains non-atomic and requires
continuously exclusive sameUID custody; the retained race test is a custody
violation counterexample, not a claim of repaired filesystem atomicity. Receipt
keeps atomic delete/copy cleanup unverified and possible copies true. Exact
trusted launcher/directSSH profile/private tmpfs allowance, selected human
client/MFA purpose reuse and coding-agent registration/custody/delivery remain
unadmitted owner choices. Native node route, role/leeway/window/founder decision
and actual negative/cleanup acceptance remain pending. T01done and T02open; no
new task or plan, and V1/V5-V8/current failure histories are not overwritten.
2026-10-11 same T02 producer-preparation correction V3: Platform V2 pair
source refusal RPF-KEY39-PAIR-V2-F01 is retained. Private write uncertainty
now propagates into sticky remote consumer uncertainty before launcher early
return; local SSH termination never proves remote no-effects/recovery. Actual
full-payload then interrupted confirmation and4096-byte partialpipe failures
are retained regressions. 33 distinct source checks pass. Exact Net V9 source
bytes unchanged; new source reviews required, not implicit repin. Native
client/MFA/coding-agent custody/transport/tmpfsfile and actualroute/session
authority remain unadmitted. T01done/T02open; no new task/plan or native action.
2026-10-11 same T02 producer-preparation V4: exact V3 Net consumer refusal
NET-PRODUCER-V3-F01 retained. Actual CLI contract acquisition now bounded
nofollow/nonblocking regular stable reads and one ORIGINAL5-second preparation
budget across contract/source/envelope/SSHspawn/ready/file/delivery phases, not
a renewed launcher allowance. 40 own source tests plus9 retained paired
wrapper/child handshake regressions pass with synthetic/no native transport.
V3stickyremoteunknown/full+partialdelivery regressions retained; Net V9 exact
bytes unchanged. Prior issuer/custody/transport/file/route/readiness-Argo/role/
maintenanceexception/founder choices remain unadmitted. No SSH/network/Secrets/
credentials/native/owner checkout changes; no task/plan duplication. T01done
and T02open; source correction is not native acceptance.
2026-10-11 T02 source-design disposition checkpoint: founder accepted exact
source memo `infd-20261011-key39-source-design-r1`, review
`review-62626f54-0212-4684-9b45-0a7222d61957` version1 at `2026-10-11T17:19:01Z`.
Disposition `disp-870f9c1b-0444-4216-ae7f-f5463d590e36`, approval
`ca118afc-d746-4454-a5b9-9b387b120cd9`; authenticated person subject
`uid=platform-root,ou=people,dc=netkingdom,dc=local`. Exact canonical public review URL:
`https://decisions.coulomb.social/review?memo_id=review-62626f54-0212-4684-9b45-0a7222d61957`.
Root's authenticated confirmed readback is
`the-custodian/docs/evidence/2026-10-10-security-consolidation/key39-source-design-publication/founder-source-disposition-readback-r1.json`,
SHA256 `453213194d605336e9c424976f1ac27691def39e225026f0a99628d1cfc97d1d`; observed
`2026-10-11T17:30:47.080081+00:00`. Source commands SHA256
`36e20813596f3ddbb86e01fd0b838867ece2f4916c5880305cf12cd61b567f89` and source inventory SHA256
`aea196db9e513900d5e7165455d6e9c3dc68749a606dc219ec340ac595642cc0` bind the accepted source direction.
Acceptance covers recommended source preparation for existing bootstrap-console
human-client purpose reuse, returning existing coding-agent custody admission or
preparing a narrow owner proposal, proposed founder directSSH source profile,
and conditional exclusive-owner tmpfs files. It does not establish actual
registration/MFA/listener, coding-agent custody/delivery, transport/file admission,
node-local Bao route, issuer/OpenBao role/leeway/maintenance readiness or fresh
attended native go. The published choice documents remain historical exact
pending proposals; this appended receipt records their later source disposition.
T02 remains wait on external owner custody/admission and separate attended
execution authorization. Source acceptance permits further source preparation;
actual registry, token exchange, negative probe, revoke and cleanup remain pending. T01 and T03
remain done. No native or credential action is authorized by this source receipt.
See docs/proposals/2026-10-11-key39-next-execution-prerequisites.json for exact
owner returns and safe metadata evidence needed before preparing a runnable
attended execution proposal.
2026-10-11 ordered-goal qualification (T02 only): the final source runner already
exists and its 17 local semantic tests pass. No new helper or source protocol is
needed to repeat accepted preparation. Native execution follows the first
Forgejo SecurityCase registration and claims proof (KEY-WP-0038-T02/T03);
only those task metadata were checked, not wait-task bodies. NetKingdom owns the
registry/env/restart session, KeyCape owns issuance/isolation, and Platform owns
leeway/refusal/capability/no-fallback/cleanup proof. The live registration go is
already recorded resolved; do not ask again. Attended human authentication and
owner-held existing coding-agent custody go on the single attended checklist.
No credentials, live registry/API changes or KEY-WP-0040 recovery work occurred.
2026-10-17 Platform owner returns (source-prepared, not admitted):
railiance-platform `docs/proposals/2026-10-17-key39-t02-owner-returns.md`
(commit `0a9617a`). The coding client `codex-railiance-platform` has no
admission, custody or delivery record. The existing narrow proposal
`docs/proposals/2026-10-11-key39-coding-client-custody-proposal.json` (`9460f6f`)
remains the one to admit, so the coding-agent case stays unrunnable until owners
admit it. The role source (`openbao/auth/secrets-engine-jwt-role.json`) matches
the packet's claim design. It declares no `expiration_leeway`, so the expiry
probe must assume OpenBao's 150s default until an attended sanitized read
confirms the live value. The node-local Bao origin is not declared in source and
also needs that attended read. T02 stays wait.

View file

@ -0,0 +1,364 @@
---
id: KEY-WP-0039
type: workplan
title: "Negative-check tokens for the secrets-engine OpenBao JWT role"
domain: infotech
repo: key-cape
status: active
flavor: implementation
owner: claude-code
topic_slug: netkingdom
created: "2026-10-10"
updated: "2026-10-17"
depends_on: []
related_repos:
- railiance-platform
- secrets-engine
state_hub_workstream_id: "09c3bddf-6058-5603-8488-cccdb7be4f86"
---
# KEY-WP-0039 - Negative-check tokens for the secrets-engine OpenBao JWT role
## Why
CCR-2026-0038 (railiance-platform RPF-WP-0035-T02) requires the OpenBao role
`secrets-engine-openbao` on `auth/keycape-services` to refuse JWTs with a wrong
`aud`, `sub`, `tenant`, `principal_type`, `roles` or `scope`, a coding-agent JWT
and an expired JWT. The positive login proof passed on 2026-10-10 (secrets-engine
`docs/evidence/2026-10-10-keycape-openbao-probe.json`). railiance-platform asked
whether KeyCape can issue the negative tokens without touching the
`secrets-engine` registration.
KeyCape signs only what a registration declares. It has no way to mint a token
with arbitrary claims, and adding one would create a forging path in the issuer.
So the negative tokens must come from real, separate registrations that differ
from `secrets-engine` in exactly one property each.
## Boundaries
- The `secrets-engine` registration is not changed.
- Probe clients are confidential `client_credentials` clients with the minimum
60-second lifetime, scopes that grant nothing, and secrets minted into custody
like any other client. They are removed after the proof.
- The probe script prints only accept or refuse booleans per case, never tokens or
secrets. It runs attended.
## T01 — Define the probe registrations
```task
id: KEY-WP-0039-T01
status: done
priority: medium
state_hub_task_id: "4d06b059-3768-53bb-83a4-a9a71c0850b8"
```
Write a non-secret request packet under `docs/` with one probe client per claim
mismatch, each identical to `secrets-engine` except for:
- `aud`: a different access-token resource audience
- `sub`: a different service subject
- `tenant`: a tenant other than `platform`
- `roles`: a role other than `secrets-engine`
- `scope`: a scope outside the role's bound scope
Two cases need no probe client. A service token always carries
`principal_type: service` (`src/internal/server/oidc/token.go`), so the wrong
`principal_type` case uses a human access token from an attended login. The
coding-agent case uses the existing `codex-railiance-platform` registration (role
`coding-agent`, `docs/openbao-service-auth-contract.md`) if it is registered live;
T01 confirms that.
The expired-JWT case reuses any probe client: request a token, wait past its
60-second lifetime, then present it.
Acceptance: railiance-platform agrees the packet covers every refusal that
CCR-2026-0038 names.
2026-10-10: railiance-platform agreed that the case list covers every
CCR-2026-0038 claim negative. The remaining CCR checks (capabilities-self deny, no
provider fallback) are theirs and secrets-engine's. Packet written at
`docs/openbao-negative-probe-clients.yaml`. It departs from the agreed list in two
ways, both sent back for re-agreement:
- **Expired case.** A token from another probe already fails a claim, so its
refusal does not show that expiry was checked. It needs a sixth client,
`openbao-probe-expiry`, identical to `secrets-engine-openbao` in every claim. That
makes it a second credential able to log in as secrets-engine for the session.
OpenBao's `expiration_leeway` (150s by default) also extends the wait.
- **Audience pin.** A service token's `aud` is its `clientId` unless `audience` is
set. Every probe except the aud probe therefore pins
`audience: secrets-engine-openbao`, so it fails only on its own claim.
Condition (a), no OpenBao custody, holds. The values still have to reach KeyCape's
environment, because KeyCape resolves `secretRef` at startup. The packet uses a
transient Kubernetes Secret that is deleted in the same session.
## T02 — Register the probe clients, run the attended probe, then remove them
```task
id: KEY-WP-0039-T02
status: wait
depends_on: [KEY-WP-0039-T01, KEY-WP-0038-T02, KEY-WP-0038-T03]
blocking_reason: "Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; registration authorization already resolved."
priority: medium
state_hub_task_id: "03da4c38-1dac-53b1-8b0b-e697df48e9d3"
```
2026-10-11: requalified — live registration go caafe4be was already resolved; owner authorization present. See the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md.
Write the attended script, which prints booleans only.
2026-10-10: script written at `scripts/openbao_negative_probe.py`, with tests in
`scripts/test_openbao_negative_probe.py`. It meets condition (b): one refused
boolean per case plus the refusal class (`claim_mismatch`, `expired`,
`not_service_principal`), and no token, secret, claim value or OpenBao error text.
A transport failure or an unexpected response is reported as `inconclusive`, never
as a refusal. A token that OpenBao accepts is revoked at once and fails the run.
Still waiting: railiance-platform's re-agreement on the two departures above, and
an attended registry session with net-kingdom. Register the clients
through the normal registration path, hand the run to railiance-platform, record
a value-free receipt in `docs/evidence/`, and deregister the clients afterwards.
Acceptance: every case is refused, the receipt is committed, and the probe clients
are gone.
2026-10-11 source preparation: T01 remains progress because the exact expiry
client and audience-pin amendments require platform-owner re-agreement; the
prior agreement covered the earlier case list, not this revised packet. The
prepared runner revision adds a valid expiry positive control and immediate
revoke, exact decoded claim-isolation preflight (not signature evidence),
semantic refusal classes, bounded expiry TTL and redirect refusal. Seventeen
local tests pass; original-source semantic regressions demonstrate two false
successes. See `docs/openbao-negative-probe-session-proposal.md` for exact owner
returns, live authority, effective leeway, custody boundaries and cleanup. No
live token, registry or OpenBao action occurred. T02 continues to wait for owner
re-agreement plus separate attended live authorization. MASON-WP-0009-T05
operator/ESO capability and nonmember checks remain independent acceptance.
2026-10-11: Platform independently re-agreed the final amended design (session proposal SHA25623f13d2a5a4204f749f35c5d44ef39d213838cd2fce1c36fb4775254ef1c7f09). Original conditional review and narrow confirmation are retained in railiance-platform/docs/reviews/2026-10-11-keycape-openbao-negative-session-owner-review*. Source acceptance T01 is complete; actual registry/leeway/readiness, founder live go, negative/cleanup and separate capability/no-fallback obligations remain T02 and owner plans.
2026-10-17 Custodian coordinator finalization review (not a KeyCape formal owner
acknowledgment): the [infrastructure assessment](../../the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-infra-finalization-r1/ASSESSMENT.md)
confirms all 14 executable/test/producer-decision pins and three Platform source
pins still match. Source reviews remain complete: T01/T03 done, T02 wait. Existing
source acceptance, resolved registration go caafe4be and the standing attended
rule remain valid; no new protocol or blanket approval request is needed. The
remaining sequence is KEY-WP-0038-T02/T03 first-case registration/claims proof,
admission of the existing coding-client proposal with sanitized owner readiness
returns, then one attended negative/revoke/cleanup receipt committed with probe
clients removed. No native acceptance or credential/registration action is
inferred from this coordinator review.
## KeyCape issuer-source compatibility inspection for lifecycle v2
```task
id: KEY-WP-0039-T03
status: done
priority: medium
state_hub_task_id: "54d37ad8-2352-51c2-bb2e-af6070dbbd73"
```
2026-10-11: Completed KeyCape's own issuer-source inspection for
KEY-WP-0039-T02/attended-lifecycle-v2, exact manifest SHA256
6fc0fe47f99a97c11c6e7538f5ea02054335df7d46055d3f84bed4e194aeaaed.
Read AGENTS.md, governing INTENT.md, current client-credentials claim source,
registration fragments, native caller-authentication commands and frozen v2
issuer transport/probe wiring. Seventeen unchanged runner tests pass locally.
This task completes only issuer-source inspection; the resulting narrow
source acknowledgment is in docs/reviews/2026-10-11-key39-lifecycle-v2-keycape-source-ack.json (retained frozen prior acknowledgment).
T01 stays done and T02 stays open for actual native registration, token input,
refusal and cleanup proof. No Secret, token, credential or native environment was accessed; no owner
checkout was changed during this source inspection. Owner source files were
read for the bounded issuer compatibility check.
Current token.go:306-309 returns HTTP 401 invalid_profile_usage with
feature=client_id for a removed confidential client; wrong-secret
feature=Authorization cannot count as removed-registration evidence. The
proposal and packet comments are narrowly corrected against that current
issuer contract. Prior T01/source re-agreement history is preserved; this source
acknowledgment supplies no NetKingdom runtime-owner review or founder live go.
The private token-producer handoff remains a real execution gap under T02.
Existing keycape login/service-token commands require a new private output
file; login also needs a concrete admitted public PKCE client/callback and
attended browser/MFA. The lifecycle expects an in-memory private stdin object
containing human_token and coding_agent_token, but no reviewed file-to-process
or process-to-process producer, admitted human client/callback, custody-injected
coding-agent source, native placement or producer cleanup receipt has been
provided. No unattended token producer or admitted secret route is invented.
Resolve those exact owner contracts before founder native approval; existing
static registration is not live token custody or producer admission.
2026-10-11 same T03 source-inspection refinement for lifecycle V4: completed
KeyCape issuer-facing inspection before the new formal acknowledgment in
docs/reviews/2026-10-11-key39-lifecycle-v4-keycape-source-ack.json. Exact V4
manifest SHA256 d079f85c47d5c2635e1ee633ea4d1346b1737ce2be9ca273f22a38e620f418d8,
executor b4687c2d07819c43b45d0cddcad0488b62eb49175b2a06a43f7806a0838d3a4d.
NetKingdom's independent V4 source review is approved, receipt SHA256
4f4498203a31bd46dbc3715c0df1cbb9eee63bed5b2c1baf430ff3c4a37c13cb.
The V2/V3 unsuccessful reviews and earlier KeyCape acknowledgment remain
retained as one correction trial; they are not extra scored successes.
V4 retains the unchanged issuer runner and six-client claim design, and fixes
finite/nonboolean windows, true total request deadlines, late-known OpenBao
session private revoke/refusal and guarded bounded final Secret deletion.
The issuer-facing acknowledgement covers only stopping late token exchanges,
claim source/shape and preserving token cleanup failure/unknown outcomes; it
does not replace NetKingdom runtime or Platform backend acceptance. Tests: 41
supplied lifecycle, 17 unchanged runner and 17 independent owner checks pass
locally. No native tokens, credentials, issuer mutations or live cleanup were
performed. T01 stays done; T02 stays open and the genuine private token-producer
handoff remains its next source prerequisite before actual founder native go.
2026-10-11 same T03 source-inspection refinement for lifecycle V5: completed
KeyCape's own current issuer-source/claim/cleanup-contract inspection before
any new formal acknowledgment. Current token.go:306-309 still distinguishes
removed-client invalid_profile_usage feature=client_id from wrong-secret
feature=Authorization. The six-client design, audience source and expiry
positive control remain unchanged, and the original runner's 17 tests pass.
Exact V5 manifest SHA256
d1f997624db46693a7126a6eb6435f6b3a353d5bd67dcc8a539683e4b7147285;
executor 3bcceebc845b17492040c76223d77088257c547a2b890198e7b5c2b04a0cd360.
V5 has 46 passing supplied lifecycle checks. It adds a single exceptional
response-close abort under a fixed20ms cap clipped to nonresettable aggregate
cleanup, and to positive original request time remaining, with expired aggregate
zero calls, fixed private errors and truthful known-session revoke/unknown
outcomes. This inspection covers issuer-facing token lifetime and claim
interfaces only; NetKingdom and Platform independently own their runtime and
backend review decisions. Previous V2/V3 Net source refusals and Platform V4
exceptional-close refusal remain retained; KeyCape's V2/V4 acknowledgments are
historical narrow source inspections, never aggregate native approval.
T01 remains done, T02 open. No native actions or credentials occurred. The real
private token-producer/client/callback/coding-agent custody/cleanup handoff gap
stays the next prerequisite; no unattended admission is inferred.
Exact independent V5 source decisions were then verified: NetKingdom approved
receipt SHA256 32a62bd1ca1bd7311965d016d4d6a6e4d9177f3423c63add436d9d309865eacc
(46 supplied +17 runner +21 independent checks); Platform approved receipt
SHA256 ba872dc53061b1dcca9dea1e8897597c394c1f9365e0fc49ee6f2095f57f11e0
(46 supplied +5 independent +17 previously verified runner checks). Those are
foreign owners' own source decisions, not KeyCape runtime/backend reviews or
founder native authority. Formal KeyCape bounded issuer acknowledgment follows
in docs/reviews/2026-10-11-key39-lifecycle-v5-keycape-source-ack.json.
2026-10-11 T02 continuing source preparation: private producer decision packet
`KEY-WP-0039-T02/private-producer-handoff-v1` stages supported existing CLI
flows, selected existing netkingdom-bootstrap-console literal-loopback human
client subject to owner purpose/MFA/listener admission, and the existing
codex-railiance-platform source identity subject to actual live/custody/consumer
admission. No existing coding-agent delivery receipt was found; AppRole tokens
do not substitute for issuer JWTs. A bounded pipe-only adapter for the current
CLI's two caller-owned private files has 18 passing synthetic checks, with
identity-guarded explicit file consumption and fixed value-free failure output.
Decoded claim shape does not prove signatures or authenticated admission.
See docs/openbao-negative-token-producer-decision.md and .json for exact owner
choices and missing receipts. The exact trusted workstation-to-node launcher,
private-file allowance and railiance01 Bao route/public-health guard still need
source review and founder disposition before any token/temporary credential/
mutation/restart. No registration, custody, login, native action or credential
request occurred. V5 and failed review histories retained; T01 stays done and
T02 open. No new work record or native acceptance is inferred.
2026-10-11 same T02 source-preparation correction for
KEY-WP-0039-T02/private-producer-handoff-v2: NetKingdom's exact V1 consumer
refusal remains recorded (no child-ready gate, stdio alias, non-atomic unlink).
V2 has source-pinned private launch/ready handshake compatible with exact
Net V9 manifest84542ddc: fresh nonce/source/envelope/health/window marker before
ANY caller-token file load/consume/write; separate local data/control pipes
reject kernel identity aliases of stdin/stdout/stderr. Adapter read/write budget
only clips downward after readiness. 31 distinct producer/launcher synthetic
checks and 74 compatible consumer supplied checks pass. No SSH/native/login or
credential access was performed. Stat/unlink remains non-atomic and requires
continuously exclusive sameUID custody; the retained race test is a custody
violation counterexample, not a claim of repaired filesystem atomicity. Receipt
keeps atomic delete/copy cleanup unverified and possible copies true. Exact
trusted launcher/directSSH profile/private tmpfs allowance, selected human
client/MFA purpose reuse and coding-agent registration/custody/delivery remain
unadmitted owner choices. Native node route, role/leeway/window/founder decision
and actual negative/cleanup acceptance remain pending. T01done and T02open; no
new task or plan, and V1/V5-V8/current failure histories are not overwritten.
2026-10-11 same T02 producer-preparation correction V3: Platform V2 pair
source refusal RPF-KEY39-PAIR-V2-F01 is retained. Private write uncertainty
now propagates into sticky remote consumer uncertainty before launcher early
return; local SSH termination never proves remote no-effects/recovery. Actual
full-payload then interrupted confirmation and4096-byte partialpipe failures
are retained regressions. 33 distinct source checks pass. Exact Net V9 source
bytes unchanged; new source reviews required, not implicit repin. Native
client/MFA/coding-agent custody/transport/tmpfsfile and actualroute/session
authority remain unadmitted. T01done/T02open; no new task/plan or native action.
2026-10-11 same T02 producer-preparation V4: exact V3 Net consumer refusal
NET-PRODUCER-V3-F01 retained. Actual CLI contract acquisition now bounded
nofollow/nonblocking regular stable reads and one ORIGINAL5-second preparation
budget across contract/source/envelope/SSHspawn/ready/file/delivery phases, not
a renewed launcher allowance. 40 own source tests plus9 retained paired
wrapper/child handshake regressions pass with synthetic/no native transport.
V3stickyremoteunknown/full+partialdelivery regressions retained; Net V9 exact
bytes unchanged. Prior issuer/custody/transport/file/route/readiness-Argo/role/
maintenanceexception/founder choices remain unadmitted. No SSH/network/Secrets/
credentials/native/owner checkout changes; no task/plan duplication. T01done
and T02open; source correction is not native acceptance.
2026-10-11 T02 source-design disposition checkpoint: founder accepted exact
source memo `infd-20261011-key39-source-design-r1`, review
`review-62626f54-0212-4684-9b45-0a7222d61957` version1 at `2026-10-11T17:19:01Z`.
Disposition `disp-870f9c1b-0444-4216-ae7f-f5463d590e36`, approval
`ca118afc-d746-4454-a5b9-9b387b120cd9`; authenticated person subject
`uid=platform-root,ou=people,dc=netkingdom,dc=local`. Exact canonical public review URL:
`https://decisions.coulomb.social/review?memo_id=review-62626f54-0212-4684-9b45-0a7222d61957`.
Root's authenticated confirmed readback is
`the-custodian/docs/evidence/2026-10-10-security-consolidation/key39-source-design-publication/founder-source-disposition-readback-r1.json`,
SHA256 `453213194d605336e9c424976f1ac27691def39e225026f0a99628d1cfc97d1d`; observed
`2026-10-11T17:30:47.080081+00:00`. Source commands SHA256
`36e20813596f3ddbb86e01fd0b838867ece2f4916c5880305cf12cd61b567f89` and source inventory SHA256
`aea196db9e513900d5e7165455d6e9c3dc68749a606dc219ec340ac595642cc0` bind the accepted source direction.
Acceptance covers recommended source preparation for existing bootstrap-console
human-client purpose reuse, returning existing coding-agent custody admission or
preparing a narrow owner proposal, proposed founder directSSH source profile,
and conditional exclusive-owner tmpfs files. It does not establish actual
registration/MFA/listener, coding-agent custody/delivery, transport/file admission,
node-local Bao route, issuer/OpenBao role/leeway/maintenance readiness or fresh
attended native go. The published choice documents remain historical exact
pending proposals; this appended receipt records their later source disposition.
T02 remains wait on external owner custody/admission and separate attended
execution authorization. Source acceptance permits further source preparation;
actual registry, token exchange, negative probe, revoke and cleanup remain pending. T01 and T03
remain done. No native or credential action is authorized by this source receipt.
See docs/proposals/2026-10-11-key39-next-execution-prerequisites.json for exact
owner returns and safe metadata evidence needed before preparing a runnable
attended execution proposal.
2026-10-11 ordered-goal qualification (T02 only): the final source runner already
exists and its 17 local semantic tests pass. No new helper or source protocol is
needed to repeat accepted preparation. Native execution follows the first
Forgejo SecurityCase registration and claims proof (KEY-WP-0038-T02/T03);
only those task metadata were checked, not wait-task bodies. NetKingdom owns the
registry/env/restart session, KeyCape owns issuance/isolation, and Platform owns
leeway/refusal/capability/no-fallback/cleanup proof. The live registration go is
already recorded resolved; do not ask again. Attended human authentication and
owner-held existing coding-agent custody go on the single attended checklist.
No credentials, live registry/API changes or KEY-WP-0040 recovery work occurred.
2026-10-17 Platform owner returns (source-prepared, not admitted):
railiance-platform `docs/proposals/2026-10-17-key39-t02-owner-returns.md`
(commit `0a9617a`). The coding client `codex-railiance-platform` has no
admission, custody or delivery record. The existing narrow proposal
`docs/proposals/2026-10-11-key39-coding-client-custody-proposal.json` (`9460f6f`)
remains the one to admit, so the coding-agent case stays unrunnable until owners
admit it. The role source (`openbao/auth/secrets-engine-jwt-role.json`) matches
the packet's claim design. It declares no `expiration_leeway`, so the expiry
probe must assume OpenBao's 150s default until an attended sanitized read
confirms the live value. The node-local Bao origin is not declared in source and
also needs that attended read. T02 stays wait.

View file

@ -0,0 +1,36 @@
from pathlib import Path
import hashlib,json,re,subprocess,yaml
root=Path('/home/worsch/key-cape');out=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1');out.mkdir()
rel='workplans/KEY-WP-0039-openbao-negative-probe-tokens.md';path=root/rel
def git(*args):return subprocess.check_output(['git','-C',str(root),*args]).decode()
assert git('rev-parse','HEAD').strip()=='e76d4130f31a02554048f30bf838043cea7a5b80'
assert not git('status','--porcelain=v1')
assert not git('diff','--cached','--name-only')
before=path.read_text();anchor='## KeyCape issuer-source compatibility inspection for lifecycle v2'
assert before.count(anchor)==1
paragraph='''2026-10-17 Custodian coordinator finalization review (not a KeyCape formal owner
acknowledgment): the [infrastructure assessment](../../the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-infra-finalization-r1/ASSESSMENT.md)
confirms all 14 executable/test/producer-decision pins and three Platform source
pins still match. Source reviews remain complete: T01/T03 done, T02 wait. Existing
source acceptance, resolved registration go caafe4be and the standing attended
rule remain valid; no new protocol or blanket approval request is needed. The
remaining sequence is KEY-WP-0038-T02/T03 first-case registration/claims proof,
admission of the existing coding-client proposal with sanitized owner readiness
returns, then one attended negative/revoke/cleanup receipt committed with probe
clients removed. No native acceptance or credential/registration action is
inferred from this coordinator review.
'''
after=before.replace(anchor,paragraph+anchor)
blocks=lambda s:re.findall(r'^```task\n.*?^```',s,re.M|re.S)
assert blocks(before)==blocks(after)
assert before.split('---',2)[1]==after.split('---',2)[1]
(out/'KEY39.preimage.md').write_text(before);(out/'KEY39.proposed.md').write_text(after)
baseline={'base_commit':git('rev-parse','HEAD').strip(),'branch':git('branch','--show-current').strip(),'status':git('status','--porcelain=v1'),'index':git('ls-files','--stage'),'path':rel,'before_sha256':hashlib.sha256(before.encode()).hexdigest(),'after_sha256':hashlib.sha256(after.encode()).hexdigest(),'frontmatter':yaml.safe_load(before.split('---',2)[1]),'task_blocks':[yaml.safe_load(t.split('\n',1)[1].rsplit('```',1)[0]) for t in blocks(before)]}
(out/'baseline.json').write_text(json.dumps(baseline,indent=2)+'\n')
path.write_text(after)
assert git('diff','--name-only').splitlines()==[rel]
(out/'one-path.patch').write_text(git('diff','--',rel))
print(json.dumps({'status':'one T02 narrative paragraph applied','task_blocks_unchanged':True,'frontmatter_unchanged':True,'base':baseline['base_commit'],'task_bindings':baseline['task_blocks']},indent=2))

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,173 @@
{
"schema": "state-hub.repository-projection-reconcile.v1",
"instance_role": "primary",
"instance_label": "railiance01",
"expected_commit": "40de616e078906e04c692edf7cb7a8bfbc07b5ea",
"derived_commit": "40de616e078906e04c692edf7cb7a8bfbc07b5ea",
"outcome": {
"schema": "state-hub.projection-reset.v1",
"repo_slug": "key-cape",
"commit": "40de616e078906e04c692edf7cb7a8bfbc07b5ea",
"status": "applied",
"counts": {
"created": 0,
"updated": 44,
"retired": 0,
"refused": 0,
"released": 0,
"created_tasks": 0,
"updated_tasks": 0,
"cancelled_tasks": 0,
"created_edges": 0,
"removed_edges": 0,
"unresolved_edges": 1,
"external_edges": 8
},
"created": [],
"updated": [
"ADHOC-2026-09-05",
"ADHOC-2026-09-07",
"ADHOC-2026-09-27",
"KEY-WP-0001",
"KEY-WP-0002",
"KEY-WP-0003",
"KEY-WP-0004",
"KEY-WP-0005",
"KEY-WP-0006",
"KEY-WP-0007",
"KEY-WP-0008",
"KEY-WP-0009",
"KEY-WP-0010",
"KEY-WP-0011",
"KEY-WP-0012",
"KEY-WP-0013",
"KEY-WP-0014",
"KEY-WP-0015",
"KEY-WP-0016",
"KEY-WP-0017",
"KEY-WP-0018",
"KEY-WP-0019",
"KEY-WP-0020",
"KEY-WP-0021",
"KEY-WP-0022",
"KEY-WP-0023",
"KEY-WP-0024",
"KEY-WP-0025",
"KEY-WP-0026",
"KEY-WP-0027",
"KEY-WP-0028",
"KEY-WP-0029",
"KEY-WP-0030",
"KEY-WP-0031",
"KEY-WP-0032",
"KEY-WP-0033",
"KEY-WP-0034",
"KEY-WP-0035",
"KEY-WP-0036",
"KEY-WP-0037",
"KEY-WP-0038",
"KEY-WP-0039",
"KEY-WP-0040",
"KEY-WP-0041"
],
"retired": [],
"released": [],
"created_tasks": [],
"updated_tasks": [],
"cancelled_tasks": [],
"created_edges": [],
"removed_edges": [],
"unresolved_edges": [
{
"workplan": "KEY-WP-0010",
"from": "KEY-WP-0010",
"target": "RAILIANCE-WP-0027",
"reason": "missing"
}
],
"external_edges": [
{
"workplan": "KEY-WP-0038",
"from": "KEY-WP-0038-T03",
"target": "KEY-WP-0038-T02",
"relationship_type": "starts_after",
"from_workplan_id": "098e3e2c-a138-560e-b2d3-3a15eb49d8a9",
"from_task_id": "40ddaa29-7245-508c-ad48-ddc063e25115",
"to_workplan_id": null,
"to_task_id": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30"
},
{
"workplan": "KEY-WP-0038",
"from": "KEY-WP-0038-T02",
"target": "KEY-WP-0038-T01",
"relationship_type": "starts_after",
"from_workplan_id": "098e3e2c-a138-560e-b2d3-3a15eb49d8a9",
"from_task_id": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30",
"to_workplan_id": null,
"to_task_id": "5f019d13-1fa3-5b88-a27a-75a3339ca96e"
},
{
"workplan": "KEY-WP-0038",
"from": "KEY-WP-0038-T02",
"target": "NK-WP-0048-T06",
"relationship_type": "starts_after",
"from_workplan_id": "098e3e2c-a138-560e-b2d3-3a15eb49d8a9",
"from_task_id": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30",
"to_workplan_id": null,
"to_task_id": "d9b26bcf-a7c6-59a2-92a0-f913d76a203c"
},
{
"workplan": "KEY-WP-0039",
"from": "KEY-WP-0039-T02",
"target": "KEY-WP-0038-T03",
"relationship_type": "starts_after",
"from_workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
"from_task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3",
"to_workplan_id": null,
"to_task_id": "40ddaa29-7245-508c-ad48-ddc063e25115"
},
{
"workplan": "KEY-WP-0039",
"from": "KEY-WP-0039-T02",
"target": "KEY-WP-0039-T01",
"relationship_type": "starts_after",
"from_workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
"from_task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3",
"to_workplan_id": null,
"to_task_id": "4d06b059-3768-53bb-83a4-a9a71c0850b8"
},
{
"workplan": "KEY-WP-0039",
"from": "KEY-WP-0039-T02",
"target": "KEY-WP-0038-T02",
"relationship_type": "starts_after",
"from_workplan_id": "09c3bddf-6058-5603-8488-cccdb7be4f86",
"from_task_id": "03da4c38-1dac-53b1-8b0b-e697df48e9d3",
"to_workplan_id": null,
"to_task_id": "ddcfb482-8f33-5c81-9ce8-84be8fa5dc30"
},
{
"workplan": "KEY-WP-0007",
"from": "KEY-WP-0007",
"target": "KEY-WP-0005",
"relationship_type": "blocks",
"from_workplan_id": "24db4178-705c-5b09-b7db-ee7a345d71a0",
"from_task_id": null,
"to_workplan_id": "711a8303-69aa-5db6-9043-92530c6dc81b",
"to_task_id": null
},
{
"workplan": "KEY-WP-0009",
"from": "KEY-WP-0009",
"target": "KEY-WP-0006",
"relationship_type": "blocks",
"from_workplan_id": "c1a9b1cc-2ff0-566a-b544-1a2ef967fc0d",
"from_task_id": null,
"to_workplan_id": "d8d20d8f-61e0-54e2-9a88-a3958f0b6f2d",
"to_task_id": null
}
],
"refused": [],
"notes": []
}
}

View file

@ -0,0 +1,32 @@
from pathlib import Path
import hashlib,json,subprocess,urllib.request,urllib.error,yaml,re
root=Path('/home/worsch/key-cape');out=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/key39-finalization-owner-followthrough-r1');base='http://127.0.0.1:8000';head='40de616e078906e04c692edf7cb7a8bfbc07b5ea'
def git(*args):return subprocess.check_output(['git','-C',str(root),*args]).decode()
assert git('rev-parse','HEAD').strip()==head
assert git('ls-remote','origin','refs/heads/main').split()[0]==head
assert not git('status','--porcelain=v1')
def get(path):
with urllib.request.urlopen(base+path,timeout=30) as response:return json.load(response)
health=get('/state/health');assert (health['instance_role'],health['instance_label'])==('primary','railiance01')
request_path='/repos/key-cape/work-record-projection/reconcile';body={'expected_commit':head,'acknowledge_retirements':False}
digest=hashlib.sha256(json.dumps({'method':'POST','path':request_path,'body':body},sort_keys=True,separators=(',',':')).encode()).hexdigest()
request=urllib.request.Request(base+request_path,data=json.dumps(body).encode(),method='POST',headers={'Content-Type':'application/json','Idempotency-Key':'rmgr-projection:v2:'+digest,'X-StateHub-Source-Agent':'custodian-codex'})
try:
with urllib.request.urlopen(request,timeout=120) as response:receipt=json.load(response)
except urllib.error.HTTPError as error:
raw=error.read();(out/'canonical-receiver-refusal.json').write_bytes(raw);raise
(out/'canonical-receiver.json').write_text(json.dumps(receipt,indent=2)+'\n')
assert receipt['expected_commit']==receipt['derived_commit']==head
assert (receipt['instance_role'],receipt['instance_label'])==('primary','railiance01')
assert receipt['outcome']['status'] in ('applied','noop'),receipt['outcome']['status']
snapshot=get('/repos/key-cape/work-record-projection/snapshot');baseline=json.loads((out/'baseline.json').read_text())
planid=baseline['frontmatter']['state_hub_workstream_id'];plan=next(w for w in snapshot['workplans'] if w['id']==planid)
rows=[]
for block in baseline['task_blocks']:
task=next(t for t in snapshot['tasks'] if t['id']==block['state_hub_task_id'])
assert task['status']==block['status'];rows.append({'record_id':block['id'],'state_hub_task_id':task['id'],'status':task['status'],'workplan_id':task.get('workplan_id'),'depends_on_source':block.get('depends_on',[])})
current=(root/baseline['path']).read_text();assert current==(out/'KEY39.proposed.md').read_text()
assert re.findall(r'^```task\n.*?^```',current,re.M|re.S)==re.findall(r'^```task\n.*?^```',(out/'KEY39.preimage.md').read_text(),re.M|re.S)
verification={'commit':head,'receiver_status':receipt['outcome']['status'],'acknowledge_retirements':False,'instance_role':receipt['instance_role'],'instance_label':receipt['instance_label'],'workplan':{'id':planid,'status':plan['status'],'record_id':'KEY-WP-0039'},'task_bindings':rows,'all_three_source_blocks_unchanged':True,'frontmatter_unchanged':True,'only_committed_path':baseline['path'],'remote_forge_matches':True,'owner_worktree_and_index_clean':not git('status','--porcelain=v1'),'normal_sync_refusal':'unrelated grandfathered ADHOC-2026-09-27-T02 UUID; not modified','native_acceptance_inferred':False}
assert plan['status']==baseline['frontmatter']['status']
(out/'final-verification.json').write_text(json.dumps(verification,indent=2)+'\n');print(json.dumps(verification,indent=2))

View file

@ -0,0 +1,37 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — key-cape
**Domain:** infotech
**Last synced:** 2026-10-17 02:16 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### Negative-check tokens for the secrets-engine OpenBao JWT role
Progress: 2/3 done | workplan_id: `09c3bddf-6058-5603-8488-cccdb7be4f86`
**Open tasks:**
- ! T01 — Define the probe registrations `03da4c38`
*(wait: Ordered identity gate: Forgejo first SecurityCase registration/claims proof, then NetKingdom registry session and attended human/coding-agent custody; registration authorization already resolved.)*
### Forgejo as a NetKingdom OIDC client, so the forge can close
Progress: 2/4 done | workplan_id: `098e3e2c-a138-560e-b2d3-3a15eb49d8a9`
**Open tasks:**
- ! T01 — Define what Forgejo receives `ddcfb482`
*(wait: Registration runs as the first KnownSecurityCase of type oidc-browser-relying-party (NK-WP-0048-T06), not as a hand-built rollout.)*
- ! T02 — Register the client and hand over its secret `40ddaa29`
*(wait: Claims are verified against the registered client.)*
## Inbox Hygiene
**Stale unread:** 2 message(s) older than 3 day(s) — triage at session start.
**Missing thread_id:** 2 unread message(s) lack supersession chains.
---
## MCP Orientation (when available)
If the state-hub MCP server is reachable, call:
`get_domain_summary("infotech")`
This provides richer cross-domain context.
If the MCP call fails, use this file as your orientation source.

View file

@ -0,0 +1,224 @@
# Work Records — key-cape
> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record
> stage 3). Do not edit by hand — edit the source file/block listed for
> each record and re-run fix-consistency to refresh this index. Archived
> workplans are omitted; closed decisions/intakes/engagements stay listed
> so recently-resolved work is still visible. [auto]
| Kind | ID | Status | Lane | Source |
| --- | --- | --- | --- | --- |
| workplan | ADHOC-2026-09-05 | finished | — | workplans/ADHOC-2026-09-05.md |
| workplan | ADHOC-2026-09-07 | finished | — | workplans/ADHOC-2026-09-07.md |
| workplan | ADHOC-2026-09-27 | finished | — | workplans/ADHOC-2026-09-27.md |
| workplan | KEY-WP-0001 | finished | — | workplans/KEY-WP-0001-keycape-implementation.md |
| workplan | KEY-WP-0002 | finished | — | workplans/KEY-WP-0002-container-image-gitea.md |
| workplan | KEY-WP-0003 | finished | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md |
| workplan | KEY-WP-0004 | finished | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
| workplan | KEY-WP-0005 | finished | — | workplans/KEY-WP-0005-iam-profile-core-claims.md |
| workplan | KEY-WP-0006 | finished | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md |
| workplan | KEY-WP-0007 | finished | — | workplans/KEY-WP-0007-user-engine-portal-oidc-client.md |
| workplan | KEY-WP-0008 | finished | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md |
| workplan | KEY-WP-0009 | finished | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md |
| workplan | KEY-WP-0010 | finished | — | workplans/KEY-WP-0010-openbao-operator-loopback-callback.md |
| workplan | KEY-WP-0011 | finished | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md |
| workplan | KEY-WP-0012 | finished | — | workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md |
| workplan | KEY-WP-0013 | finished | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
| workplan | KEY-WP-0014 | finished | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md |
| workplan | KEY-WP-0015 | finished | — | workplans/KEY-WP-0015-scope-intent-assessment.md |
| workplan | KEY-WP-0016 | finished | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md |
| workplan | KEY-WP-0017 | finished | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md |
| workplan | KEY-WP-0018 | finished | — | workplans/KEY-WP-0018-export-completeness-evidence.md |
| workplan | KEY-WP-0019 | finished | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
| workplan | KEY-WP-0020 | finished | — | workplans/KEY-WP-0020-migration-contract-preservation.md |
| workplan | KEY-WP-0021 | finished | — | workplans/KEY-WP-0021-snapshot-attribute-validation.md |
| workplan | KEY-WP-0022 | finished | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md |
| workplan | KEY-WP-0023 | finished | — | workplans/KEY-WP-0023-live-migration-proof.md |
| workplan | KEY-WP-0024 | finished | — | workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md |
| workplan | KEY-WP-0025 | finished | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md |
| workplan | KEY-WP-0026 | finished | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
| workplan | KEY-WP-0027 | finished | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md |
| workplan | KEY-WP-0028 | finished | — | workplans/KEY-WP-0028-browser-client-field-validation.md |
| workplan | KEY-WP-0029 | finished | — | workplans/KEY-WP-0029-scope-reassessment.md |
| workplan | KEY-WP-0030 | finished | — | workplans/KEY-WP-0030-tenant-precondition-guard.md |
| workplan | KEY-WP-0031 | finished | — | workplans/KEY-WP-0031-gate-house-tenant-ruling.md |
| workplan | KEY-WP-0032 | finished | — | workplans/KEY-WP-0032-principal-type-provenance-guard.md |
| workplan | KEY-WP-0033 | finished | — | workplans/KEY-WP-0033-vergabe-fresh-login.md |
| workplan | KEY-WP-0034 | finished | — | workplans/KEY-WP-0034-account-recovery.md |
| workplan | KEY-WP-0035 | finished | — | workplans/KEY-WP-0035-optional-mfa.md |
| workplan | KEY-WP-0036 | finished | — | workplans/KEY-WP-0036-platform-factor-recovery.md |
| workplan | KEY-WP-0037 | finished | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md |
| workplan | KEY-WP-0038 | active | — | workplans/KEY-WP-0038-forgejo-oidc-client.md |
| workplan | KEY-WP-0039 | active | — | workplans/KEY-WP-0039-openbao-negative-probe-tokens.md |
| workplan | KEY-WP-0040 | finished | — | workplans/KEY-WP-0040-mfa-attended-run-findings.md |
| workplan | KEY-WP-0041 | finished | — | workplans/KEY-WP-0041-sign-in-diagnosis-page.md |
| task | ADHOC-2026-09-05-T01 | done | — | workplans/ADHOC-2026-09-05.md |
| task | ADHOC-2026-09-07-T01 | done | — | workplans/ADHOC-2026-09-07.md |
| task | ADHOC-2026-09-27-T01 | done | — | workplans/ADHOC-2026-09-27.md |
| task | ADHOC-2026-09-27-T02 | done | — | workplans/ADHOC-2026-09-27.md |
| task | KEY-WP-0001-T01 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T02 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T03 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T04 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T05 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T06 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T07 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T08 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T09 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T10 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T11 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T12 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T13 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T14 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T15 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T16 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T17 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T18 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T19 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T20 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T21 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T22 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0001-T23 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
| task | KEY-WP-0002-T01 | done | — | workplans/KEY-WP-0002-container-image-gitea.md |
| task | KEY-WP-0002-T02 | done | — | workplans/KEY-WP-0002-container-image-gitea.md |
| task | KEY-WP-0002-T03 | done | — | workplans/KEY-WP-0002-container-image-gitea.md |
| task | KEY-WP-0002-T04 | done | — | workplans/KEY-WP-0002-container-image-gitea.md |
| task | KEY-WP-0002-T05 | done | — | workplans/KEY-WP-0002-container-image-gitea.md |
| task | KEY-WP-0002-T06 | done | — | workplans/KEY-WP-0002-container-image-gitea.md |
| task | KEY-WP-0003-T01 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md |
| task | KEY-WP-0003-T02 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md |
| task | KEY-WP-0003-T03 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md |
| task | KEY-WP-0003-T04 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md |
| task | KEY-WP-0003-T05 | done | — | workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md |
| task | KEY-WP-0004-T01 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
| task | KEY-WP-0004-T02 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
| task | KEY-WP-0004-T03 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
| task | KEY-WP-0004-T04 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
| task | KEY-WP-0004-T05 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
| task | KEY-WP-0004-T06 | cancel | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
| task | KEY-WP-0004-T07 | done | — | workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md |
| task | KEY-WP-0005-T01 | done | — | workplans/KEY-WP-0005-iam-profile-core-claims.md |
| task | KEY-WP-0005-T02 | done | — | workplans/KEY-WP-0005-iam-profile-core-claims.md |
| task | KEY-WP-0005-T03 | done | — | workplans/KEY-WP-0005-iam-profile-core-claims.md |
| task | KEY-WP-0006-T01 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md |
| task | KEY-WP-0006-T02 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md |
| task | KEY-WP-0006-T03 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md |
| task | KEY-WP-0006-T04 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md |
| task | KEY-WP-0006-T05 | done | — | workplans/KEY-WP-0006-client-credentials-service-tokens.md |
| task | KEY-WP-0007-T01 | done | — | workplans/KEY-WP-0007-user-engine-portal-oidc-client.md |
| task | KEY-WP-0007-T02 | done | — | workplans/KEY-WP-0007-user-engine-portal-oidc-client.md |
| task | KEY-WP-0007-T03 | done | — | workplans/KEY-WP-0007-user-engine-portal-oidc-client.md |
| task | KEY-WP-0008-T01 | done | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md |
| task | KEY-WP-0008-T02 | done | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md |
| task | KEY-WP-0008-T03 | done | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md |
| task | KEY-WP-0008-T04 | done | — | workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md |
| task | KEY-WP-0009-T01 | done | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md |
| task | KEY-WP-0009-T02 | done | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md |
| task | KEY-WP-0009-T03 | done | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md |
| task | KEY-WP-0009-T04 | done | — | workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md |
| task | KEY-WP-0010-T01 | done | — | workplans/KEY-WP-0010-openbao-operator-loopback-callback.md |
| task | KEY-WP-0011-T01 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md |
| task | KEY-WP-0011-T02 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md |
| task | KEY-WP-0011-T03 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md |
| task | KEY-WP-0012-T01 | done | — | workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md |
| task | KEY-WP-0012-T02 | done | — | workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md |
| task | KEY-WP-0012-T03 | done | — | workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md |
| task | KEY-WP-0013-T01 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
| task | KEY-WP-0013-T02 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
| task | KEY-WP-0013-T03 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
| task | KEY-WP-0013-T04 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
| task | KEY-WP-0013-T05 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
| task | KEY-WP-0013-T06 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
| task | KEY-WP-0013-T07 | done | — | workplans/KEY-WP-0013-approval-engine-resource-audience.md |
| task | KEY-WP-0014-T01 | done | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md |
| task | KEY-WP-0014-T02 | done | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md |
| task | KEY-WP-0014-T03 | done | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md |
| task | KEY-WP-0014-T04 | done | — | workplans/KEY-WP-0014-native-credential-lane-handoff.md |
| task | KEY-WP-0015-T01 | done | — | workplans/KEY-WP-0015-scope-intent-assessment.md |
| task | KEY-WP-0016-T01 | done | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md |
| task | KEY-WP-0016-T02 | done | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md |
| task | KEY-WP-0016-T03 | done | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md |
| task | KEY-WP-0016-T04 | done | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md |
| task | KEY-WP-0017-T01 | done | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md |
| task | KEY-WP-0017-T02 | done | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md |
| task | KEY-WP-0017-T03 | done | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md |
| task | KEY-WP-0017-T04 | done | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md |
| task | KEY-WP-0018-T01 | done | — | workplans/KEY-WP-0018-export-completeness-evidence.md |
| task | KEY-WP-0018-T02 | done | — | workplans/KEY-WP-0018-export-completeness-evidence.md |
| task | KEY-WP-0018-T03 | done | — | workplans/KEY-WP-0018-export-completeness-evidence.md |
| task | KEY-WP-0019-T01 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
| task | KEY-WP-0019-T02 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
| task | KEY-WP-0019-T03 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
| task | KEY-WP-0019-T04 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
| task | KEY-WP-0019-T05 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
| task | KEY-WP-0020-T01 | done | — | workplans/KEY-WP-0020-migration-contract-preservation.md |
| task | KEY-WP-0020-T02 | done | — | workplans/KEY-WP-0020-migration-contract-preservation.md |
| task | KEY-WP-0020-T03 | done | — | workplans/KEY-WP-0020-migration-contract-preservation.md |
| task | KEY-WP-0021-T01 | done | — | workplans/KEY-WP-0021-snapshot-attribute-validation.md |
| task | KEY-WP-0021-T02 | done | — | workplans/KEY-WP-0021-snapshot-attribute-validation.md |
| task | KEY-WP-0021-T03 | done | — | workplans/KEY-WP-0021-snapshot-attribute-validation.md |
| task | KEY-WP-0022-T01 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md |
| task | KEY-WP-0022-T02 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md |
| task | KEY-WP-0022-T03 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md |
| task | KEY-WP-0022-T04 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md |
| task | KEY-WP-0022-T05 | done | — | workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md |
| task | KEY-WP-0023-T01 | done | — | workplans/KEY-WP-0023-live-migration-proof.md |
| task | KEY-WP-0023-T02 | done | — | workplans/KEY-WP-0023-live-migration-proof.md |
| task | KEY-WP-0023-T03 | done | — | workplans/KEY-WP-0023-live-migration-proof.md |
| task | KEY-WP-0023-T04 | done | — | workplans/KEY-WP-0023-live-migration-proof.md |
| task | KEY-WP-0023-T05 | done | — | workplans/KEY-WP-0023-live-migration-proof.md |
| task | KEY-WP-0024-T01 | done | — | workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md |
| task | KEY-WP-0024-T02 | done | — | workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md |
| task | KEY-WP-0025-T01 | done | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md |
| task | KEY-WP-0025-T02 | done | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md |
| task | KEY-WP-0025-T03 | done | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md |
| task | KEY-WP-0025-T04 | done | — | workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md |
| task | KEY-WP-0026-T01 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
| task | KEY-WP-0026-T02 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
| task | KEY-WP-0026-T03 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
| task | KEY-WP-0026-T04 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
| task | KEY-WP-0026-T05 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
| task | KEY-WP-0026-T06 | done | — | workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md |
| task | KEY-WP-0027-T01 | done | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md |
| task | KEY-WP-0027-T02 | done | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md |
| task | KEY-WP-0027-T03 | done | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md |
| task | KEY-WP-0027-T04 | done | — | workplans/KEY-WP-0027-rollout-readiness-and-live-state.md |
| task | KEY-WP-0028-T01 | done | — | workplans/KEY-WP-0028-browser-client-field-validation.md |
| task | KEY-WP-0028-T02 | done | — | workplans/KEY-WP-0028-browser-client-field-validation.md |
| task | KEY-WP-0029-T01 | done | — | workplans/KEY-WP-0029-scope-reassessment.md |
| task | KEY-WP-0029-T02 | done | — | workplans/KEY-WP-0029-scope-reassessment.md |
| task | KEY-WP-0029-T03 | done | — | workplans/KEY-WP-0029-scope-reassessment.md |
| task | KEY-WP-0030-T01 | done | — | workplans/KEY-WP-0030-tenant-precondition-guard.md |
| task | KEY-WP-0030-T02 | done | — | workplans/KEY-WP-0030-tenant-precondition-guard.md |
| task | KEY-WP-0031-T01 | done | — | workplans/KEY-WP-0031-gate-house-tenant-ruling.md |
| task | KEY-WP-0031-T02 | done | — | workplans/KEY-WP-0031-gate-house-tenant-ruling.md |
| task | KEY-WP-0032-T01 | done | — | workplans/KEY-WP-0032-principal-type-provenance-guard.md |
| task | KEY-WP-0032-T02 | done | — | workplans/KEY-WP-0032-principal-type-provenance-guard.md |
| task | KEY-WP-0033-T01 | done | — | workplans/KEY-WP-0033-vergabe-fresh-login.md |
| task | KEY-WP-0033-T02 | done | — | workplans/KEY-WP-0033-vergabe-fresh-login.md |
| task | KEY-WP-0034-T01 | done | — | workplans/KEY-WP-0034-account-recovery.md |
| task | KEY-WP-0034-T02 | done | — | workplans/KEY-WP-0034-account-recovery.md |
| task | KEY-WP-0035-T01 | done | — | workplans/KEY-WP-0035-optional-mfa.md |
| task | KEY-WP-0035-T02 | done | — | workplans/KEY-WP-0035-optional-mfa.md |
| task | KEY-WP-0035-T03 | done | — | workplans/KEY-WP-0035-optional-mfa.md |
| task | KEY-WP-0035-T04 | done | — | workplans/KEY-WP-0035-optional-mfa.md |
| task | KEY-WP-0035-T05 | done | — | workplans/KEY-WP-0035-optional-mfa.md |
| task | KEY-WP-0036-T01 | done | — | workplans/KEY-WP-0036-platform-factor-recovery.md |
| task | KEY-WP-0036-T02 | cancel | — | workplans/KEY-WP-0036-platform-factor-recovery.md |
| task | KEY-WP-0036-T03 | done | — | workplans/KEY-WP-0036-platform-factor-recovery.md |
| task | KEY-WP-0037-T01 | done | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md |
| task | KEY-WP-0037-T02 | done | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md |
| task | KEY-WP-0037-T03 | done | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md |
| task | KEY-WP-0037-T04 | done | — | workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md |
| task | KEY-WP-0038-T01 | done | — | workplans/KEY-WP-0038-forgejo-oidc-client.md |
| task | KEY-WP-0038-T02 | wait | — | workplans/KEY-WP-0038-forgejo-oidc-client.md |
| task | KEY-WP-0038-T03 | wait | — | workplans/KEY-WP-0038-forgejo-oidc-client.md |
| task | KEY-WP-0038-T04 | cancel | — | workplans/KEY-WP-0038-forgejo-oidc-client.md |
| task | KEY-WP-0039-T01 | done | — | workplans/KEY-WP-0039-openbao-negative-probe-tokens.md |
| task | KEY-WP-0039-T02 | wait | — | workplans/KEY-WP-0039-openbao-negative-probe-tokens.md |
| task | KEY-WP-0039-T03 | done | — | workplans/KEY-WP-0039-openbao-negative-probe-tokens.md |
| task | KEY-WP-0040-T01 | done | — | workplans/KEY-WP-0040-mfa-attended-run-findings.md |
| task | KEY-WP-0040-T02 | done | — | workplans/KEY-WP-0040-mfa-attended-run-findings.md |
| task | KEY-WP-0041-T01 | done | — | workplans/KEY-WP-0041-sign-in-diagnosis-page.md |
| task | KEY-WP-0041-T02 | done | — | workplans/KEY-WP-0041-sign-in-diagnosis-page.md |
| intake | KEY-IN-0001 | closed | — | intakes/intakes.md |

View file

@ -0,0 +1,232 @@
{
".custodian-brief.md": {
"sha256": "96d84539b3a92786bff37fcc70161adec4648b9547287e52e6b817bc080a080a",
"mode": 436,
"index": "100644 d3b36c3bffbeb922769a2a788e35b4113fccaa3f 0\t.custodian-brief.md\n"
},
"WORK-RECORDS.md": {
"sha256": "431842547bd82750f284f8c1c9f519892f1bb1c1a684b9b1e38adc6fdfdc299d",
"mode": 420,
"index": "100644 d86b29bbc480acfa6d12d5052039578d7d4e380c 0\tWORK-RECORDS.md\n"
},
"workplans/ADHOC-2026-09-05.md": {
"sha256": "76123d22d0c1efc51212f8914e386f078b3c6d908959af6978ae73874e492b04",
"mode": 420,
"index": "100644 f7e78675df62c9b48e29f882fa99298d6d8ce7f1 0\tworkplans/ADHOC-2026-09-05.md\n"
},
"workplans/ADHOC-2026-09-07.md": {
"sha256": "c335ebfe91dad291277e8f17034ac8f785b3bc767f281f205c446c08f03d7978",
"mode": 420,
"index": "100644 17401073c053c9710e6e680243bbd313b08d1fc1 0\tworkplans/ADHOC-2026-09-07.md\n"
},
"workplans/ADHOC-2026-09-27.md": {
"sha256": "2794d1940fc3c5e4a2df57de9d4b742fa6aa8602b67f1d3177c0d96bc5aaf314",
"mode": 420,
"index": "100644 0b755547aa7d5589f0898df2f102a45b6aa24838 0\tworkplans/ADHOC-2026-09-27.md\n"
},
"workplans/KEY-WP-0001-keycape-implementation.md": {
"sha256": "29a2bf4892b7185e28e9126a3d8311d0fed34a876f416a3e75c605915fde0969",
"mode": 420,
"index": "100644 74334411cd056e7c7253e84f1c4b40c42eb13de3 0\tworkplans/KEY-WP-0001-keycape-implementation.md\n"
},
"workplans/KEY-WP-0002-container-image-gitea.md": {
"sha256": "76fee187185b30219bc257ccc9b587661df4489574a2bb57703a365a5d803cec",
"mode": 420,
"index": "100644 418f477c0ede13d7c003d744abf234347f5d5e31 0\tworkplans/KEY-WP-0002-container-image-gitea.md\n"
},
"workplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md": {
"sha256": "5e949c696e587540d305b682eb1f8390ec544bbec6a566be8d04e73fb1b254ca",
"mode": 420,
"index": "100644 9896c67d78f0efdd03325535eb41295250be3a19 0\tworkplans/KEY-WP-0003-bootstrap-console-oidc-mfa-login.md\n"
},
"workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md": {
"sha256": "260ba7d3116db3bc5a72e5cd8b9984137e7e372c8128d21073614dbd948c10c2",
"mode": 420,
"index": "100644 a50e910558ee43073c92cb27865a1aceac990dd5 0\tworkplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md\n"
},
"workplans/KEY-WP-0005-iam-profile-core-claims.md": {
"sha256": "c6702b644cdd046c0d3bba8ba8837050826e4dc3d855bd73327ae94218ba5b57",
"mode": 420,
"index": "100644 648d2405699dcf25b3121df6c46d871597100d7b 0\tworkplans/KEY-WP-0005-iam-profile-core-claims.md\n"
},
"workplans/KEY-WP-0006-client-credentials-service-tokens.md": {
"sha256": "b3227849978c95cad89e02afb74bf2217b6188ae65c5b0af6a21703d65477701",
"mode": 420,
"index": "100644 632c4c2cad39cbde55c2ae1f2ba5ecf2f445b1a0 0\tworkplans/KEY-WP-0006-client-credentials-service-tokens.md\n"
},
"workplans/KEY-WP-0007-user-engine-portal-oidc-client.md": {
"sha256": "5408ff6dfeb58a8a7756b48fe4f54f7c7b98d7940cb9aaa5c7864f1247e58150",
"mode": 420,
"index": "100644 f1b04e1a61b398426c23d8d9ca655b3937d7bd10 0\tworkplans/KEY-WP-0007-user-engine-portal-oidc-client.md\n"
},
"workplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md": {
"sha256": "bfc9b2118f0712e92a86bbb0f14a2857fafc3a42a3c14c0e2535002aaa57a2f5",
"mode": 420,
"index": "100644 438ab5480de3bd66d75b6ec7e09639d1b93e3e5f 0\tworkplans/KEY-WP-0008-registration-handoff-and-client-mfa-policy.md\n"
},
"workplans/KEY-WP-0009-provider-capabilities-and-service-identities.md": {
"sha256": "d1daac9815930104a392ea96c14922e34f947720a31d1b0ac45a770997e0f476",
"mode": 420,
"index": "100644 c9e585db65424c7c4e3b5adf37a68de2bdfbdc1c 0\tworkplans/KEY-WP-0009-provider-capabilities-and-service-identities.md\n"
},
"workplans/KEY-WP-0010-openbao-operator-loopback-callback.md": {
"sha256": "41d28a16bef9313b776c5c6305316a9e7d4f5528b7fb7427b8384a0ed0ad35c4",
"mode": 420,
"index": "100644 81614eeb8c98205d6e5eb3201da10e36b76a21e9 0\tworkplans/KEY-WP-0010-openbao-operator-loopback-callback.md\n"
},
"workplans/KEY-WP-0011-live-secret-exposure-recovery.md": {
"sha256": "3c5458180fe81a04e357f8db737e4287f79640b5ab02682bcadc595b73f846cd",
"mode": 420,
"index": "100644 f7b906f552f6e3279895a90afbe216b6e78ee5ff 0\tworkplans/KEY-WP-0011-live-secret-exposure-recovery.md\n"
},
"workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md": {
"sha256": "68d34d75c914e7d2d8177000e800d6372cccda2b850c03ab85da42b90a66b91c",
"mode": 420,
"index": "100644 0f3deb626e5de9a9f898754ef81d3f2ee5282792 0\tworkplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md\n"
},
"workplans/KEY-WP-0013-approval-engine-resource-audience.md": {
"sha256": "55c88bf4ed9a456db59b319bbaf960dfe6f4147437e339f950e0583d7582057a",
"mode": 420,
"index": "100644 7d77aee3caf3d79fe6d6c1369249c6d94e0ae123 0\tworkplans/KEY-WP-0013-approval-engine-resource-audience.md\n"
},
"workplans/KEY-WP-0014-native-credential-lane-handoff.md": {
"sha256": "22aac43ae5039aebd11f9f0aa57a882ad009ad6ed119ec08eb85b32a5af48cd7",
"mode": 420,
"index": "100644 bacf04c07f59125a90c519d3c306a7414cb9a1a3 0\tworkplans/KEY-WP-0014-native-credential-lane-handoff.md\n"
},
"workplans/KEY-WP-0015-scope-intent-assessment.md": {
"sha256": "60b9f2265eff880d916c3ee8dba36aec9984ec310804de776cebf89cc98fdb95",
"mode": 420,
"index": "100644 82b952a216346ffb7f5cab413623fe4a88f7700e 0\tworkplans/KEY-WP-0015-scope-intent-assessment.md\n"
},
"workplans/KEY-WP-0016-authorization-code-protocol-hardening.md": {
"sha256": "ed48128a1cb51cbe3d154727bafd5a5ffb3b5b618c20649b2a0e7db154cf3a0c",
"mode": 420,
"index": "100644 52839bd0f83d97656c8ad97a8ffc338d9019239a 0\tworkplans/KEY-WP-0016-authorization-code-protocol-hardening.md\n"
},
"workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md": {
"sha256": "8700e0e5aa07dcd7f651e792cca12a0330b7ee9d2f6064b4cc3fcf4c7d509340",
"mode": 420,
"index": "100644 00eb03971aae75724afbf7b8a4946ce6b7eda3af 0\tworkplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md\n"
},
"workplans/KEY-WP-0018-export-completeness-evidence.md": {
"sha256": "15026b708d116f803b0b2fa43f30a4dbced76e4d8137b5fcb5b4275e593d7c7b",
"mode": 420,
"index": "100644 325fc483d62dbad53b92b986de56112168a92b4a 0\tworkplans/KEY-WP-0018-export-completeness-evidence.md\n"
},
"workplans/KEY-WP-0019-upstream-provider-token-verification.md": {
"sha256": "0d2d8b5bda1e0d333935812d8d3749cb7b0c5523a3987d1b26185f9f6d0f0fa2",
"mode": 420,
"index": "100644 dfeb92296f46d4ce2ac7df1fedcc61e9ccc5d6b3 0\tworkplans/KEY-WP-0019-upstream-provider-token-verification.md\n"
},
"workplans/KEY-WP-0020-migration-contract-preservation.md": {
"sha256": "cc842d15bf42ddc4d0f4056bc19b3176cab2c5f3c1a2fe6d648e5dbd077be8b3",
"mode": 420,
"index": "100644 7b6f3be976293469dee2180c10e16f313876d640 0\tworkplans/KEY-WP-0020-migration-contract-preservation.md\n"
},
"workplans/KEY-WP-0021-snapshot-attribute-validation.md": {
"sha256": "b603248693073a9b9b25b4d0a4147d8d88c16a07dc9936e48fa1f3c76fe8bab6",
"mode": 420,
"index": "100644 1f6efeb32a6eb8fcbc52f4d3b591e2fc6b9e006d 0\tworkplans/KEY-WP-0021-snapshot-attribute-validation.md\n"
},
"workplans/KEY-WP-0022-replacement-harness-and-external-conformance.md": {
"sha256": "8c135e84be81de31b4949f52ac28dccd404f2e4c3eaa576d733bd50160427a53",
"mode": 420,
"index": "100644 7c69629ecd20d0158aa1332d9d1442b796f6a7af 0\tworkplans/KEY-WP-0022-replacement-harness-and-external-conformance.md\n"
},
"workplans/KEY-WP-0023-live-migration-proof.md": {
"sha256": "e581cd11f3905805b5e8ba8e2ede85092577c7d086e1761f2a2d033fdead4e47",
"mode": 420,
"index": "100644 3e7c83cb12e7fc31e682cf3c27f4c4767ded2905 0\tworkplans/KEY-WP-0023-live-migration-proof.md\n"
},
"workplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md": {
"sha256": "af7842fa97263bfd46a4b5ae84c5c1ecddb06ab9623586e0292dcc93099fea35",
"mode": 420,
"index": "100644 baa0bbd195a92708f2befd027b938fcd1812ced7 0\tworkplans/KEY-WP-0024-tenant-roles-opt-in-wiring.md\n"
},
"workplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md": {
"sha256": "0addb34e6d85e376c6a5db7afba597b6f268afa98b86c8c60add9be174dd1b26",
"mode": 420,
"index": "100644 a53e646761178748e8c4b00dcde888698447a948 0\tworkplans/KEY-WP-0025-runtime-lifecycle-and-readiness.md\n"
},
"workplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md": {
"sha256": "c1bc0de47511ae49102009a172c84d9d5d64e829a18c8d40564fe817935563b3",
"mode": 420,
"index": "100644 1f96a2ac3ac939c4e6e0f66172477cf73db04e51 0\tworkplans/KEY-WP-0026-packaging-bootstrap-and-credential-handling.md\n"
},
"workplans/KEY-WP-0027-rollout-readiness-and-live-state.md": {
"sha256": "f559eae61d433d451f80ea575254bb14a47f2485267b412e42d524b434bb1b59",
"mode": 420,
"index": "100644 57e01b2db033847cc786504b8795e37112d988a7 0\tworkplans/KEY-WP-0027-rollout-readiness-and-live-state.md\n"
},
"workplans/KEY-WP-0028-browser-client-field-validation.md": {
"sha256": "fbd51422fa5e6449000e4520e6639f6135c7393165a5ae02b1e92892edba0480",
"mode": 420,
"index": "100644 46b0672fea90cd7cb672f7d119706c240474f8ca 0\tworkplans/KEY-WP-0028-browser-client-field-validation.md\n"
},
"workplans/KEY-WP-0029-scope-reassessment.md": {
"sha256": "3241f3b94db9cad5e3770cf1fedc5c211288357e4f69b5ba9da57065094bdf0e",
"mode": 420,
"index": "100644 392f97750f6cb8b12868a6fd016c7d396b58477e 0\tworkplans/KEY-WP-0029-scope-reassessment.md\n"
},
"workplans/KEY-WP-0030-tenant-precondition-guard.md": {
"sha256": "c94c3afc7fb4270dfc18f3fe8a32946c524e4a6ba2d02a3d8fda69c9b8441af0",
"mode": 420,
"index": "100644 0f74e0d5a037eec64b11b57c67b02057a3b0bfef 0\tworkplans/KEY-WP-0030-tenant-precondition-guard.md\n"
},
"workplans/KEY-WP-0031-gate-house-tenant-ruling.md": {
"sha256": "0abb135367f0366aa071769c4bd76fdeb321d810ec75ad3b58d718734f78382b",
"mode": 420,
"index": "100644 d2ede9d494927976ccb9ba6a5aeaf1ed21c30837 0\tworkplans/KEY-WP-0031-gate-house-tenant-ruling.md\n"
},
"workplans/KEY-WP-0032-principal-type-provenance-guard.md": {
"sha256": "6a5cd483a40b9477227b02156c42b3469094dac145b33727b336118898aa782a",
"mode": 420,
"index": "100644 a04f52f817a51efc1d0d0ecb99efacea8dc53a17 0\tworkplans/KEY-WP-0032-principal-type-provenance-guard.md\n"
},
"workplans/KEY-WP-0033-vergabe-fresh-login.md": {
"sha256": "c01bb51cf2dcad06ea846040dddef09845041fa7271cd7015e5a34a49e60bb8f",
"mode": 420,
"index": "100644 5b478f953890339aec796c11a536af0ec3238789 0\tworkplans/KEY-WP-0033-vergabe-fresh-login.md\n"
},
"workplans/KEY-WP-0034-account-recovery.md": {
"sha256": "7db9adcd418ccabfbce672aec283249a6e374212492f0349e8c47a568f130eda",
"mode": 420,
"index": "100644 eb58f99bc26e3c3ac33230dfe432c8b1fabe47ce 0\tworkplans/KEY-WP-0034-account-recovery.md\n"
},
"workplans/KEY-WP-0035-optional-mfa.md": {
"sha256": "c60ea8d0c195686c0d2317f657aad2de27c9b9432d311627c0e1d06124cabb49",
"mode": 420,
"index": "100644 29eb711f656c91808e91f3981a9168c3a0ebc253 0\tworkplans/KEY-WP-0035-optional-mfa.md\n"
},
"workplans/KEY-WP-0036-platform-factor-recovery.md": {
"sha256": "5ceb08fe5789b4f02550bb04fcaf8eeb25a43ac3f506dfc62d60a513bcd0013f",
"mode": 420,
"index": "100644 fd296bc05a6e565b0ef6e88a9ae3daa7c9f81786 0\tworkplans/KEY-WP-0036-platform-factor-recovery.md\n"
},
"workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md": {
"sha256": "3a90a0c460fc439570820b96d1ddc3b7f8646c557ca3a8aa27c6d546bb1a845a",
"mode": 420,
"index": "100644 ad7fed77a27aa8487693ad6fcf342502f418427a 0\tworkplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md\n"
},
"workplans/KEY-WP-0038-forgejo-oidc-client.md": {
"sha256": "dc8d88d462799e7e6e44da759a973d52e44102a271969a5daaa9d30ec015b064",
"mode": 420,
"index": "100644 8cd5bc66f7b0115eec7813d6eec994a1bb618668 0\tworkplans/KEY-WP-0038-forgejo-oidc-client.md\n"
},
"workplans/KEY-WP-0039-openbao-negative-probe-tokens.md": {
"sha256": "c576db4b97ea5de0a50e17f251f8d5c57a744ded557bede963e48608bf3a6ca2",
"mode": 420,
"index": "100644 fd0fc46e49164312208afcc2c53549973b56e1fd 0\tworkplans/KEY-WP-0039-openbao-negative-probe-tokens.md\n"
},
"workplans/KEY-WP-0040-mfa-attended-run-findings.md": {
"sha256": "98b23d8810bff047067169232213cb313ecc14ff79751224842723afe808fbd2",
"mode": 420,
"index": "100644 8e8730c8e7733b13d6a9f42689a9d8e407769e81 0\tworkplans/KEY-WP-0040-mfa-attended-run-findings.md\n"
},
"workplans/KEY-WP-0041-sign-in-diagnosis-page.md": {
"sha256": "7e44f46acd6b3942c1852b49e0d64b205dfd7d9bb378aed5241b1bf22d7ce88a",
"mode": 420,
"index": "100644 56fd05819e3acd37b8e6cb468ec3d62307f1ae7a 0\tworkplans/KEY-WP-0041-sign-in-diagnosis-page.md\n"
}
}

View file

@ -0,0 +1,27 @@
---
id: ADHOC-2026-09-05
type: workplan
title: "Repository review and layer declaration"
domain: infotech
repo: key-cape
status: finished
owner: codex
created: "2026-09-05"
updated: "2026-09-05"
state_hub_workstream_id: "6f5a12f0-5dac-5e02-bbd9-9ab2263264ba"
---
## Resolve declaration intake and normalize repository records
```task
id: ADHOC-2026-09-05-T01
status: done
priority: low
state_hub_task_id: "54c89abe-2539-550d-bc3a-649af8d4580a"
```
Reviewed KEY-IN-0001 against local security-layer-model_v0.4. Declared Tooling
and the authentication/authorization boundary in INTENT.md and closed the intake.
Normalized two legacy completed workplan frontmatter statuses to finished.
Ignored the generated .repo-manager workstation cache. Existing task statuses
already conform to the State Hub task lifecycle canon.

View file

@ -0,0 +1,36 @@
---
id: ADHOC-2026-09-07
type: workplan
title: "Document the KEY-WP-0016 authorization-code bindings for consumers"
domain: infotech
repo: key-cape
status: finished
owner: claude
created: "2026-09-07"
updated: "2026-09-07"
state_hub_workstream_id: "c2fdc5cc-89be-561d-b832-f0b3019ea1c7"
---
## Publish the consumer-facing rollout note
```task
id: ADHOC-2026-09-07-T01
status: done
priority: medium
state_hub_task_id: "2187cef2-07c0-5ac1-89ca-3bb25a160fa4"
```
KEY-WP-0016 changed `/token` and `/userinfo` behaviour without a consumer-facing
note; no document in `docs/` mentioned `redirect_uri` at all, so the change would
have reached a deployment silently.
Published `docs/authorization-code-bindings.md` stating what an exchange must
send, who is affected and how to roll out. Verified the blast radius rather than
assuming it: all five browser registrations in `config/dev-config.yaml`
(`openbao-admin`, `demo-app`, `netkingdom-bootstrap-console`,
`user-engine-portal`, `coulomb-social`) are `clientType: public` with
`grantTypes: ["authorization_code"]`, so the grant-type and confidential-client
bindings cannot affect them and only the `redirect_uri` requirement can. Named
the realistic failure — a client that sends `redirect_uri` to `/authorize` but
omits it at `/token` — and said plainly that it has not been observed against a
live consumer. Referenced the note from SCOPE.md.