Skip to content

client: carry the non-secret identity the wire has been sending all along - #52

Merged
ualtinok merged 1 commit into
cortexkit:masterfrom
legion-works:feat/client-served-identity
Sep 18, 2026
Merged

ualtinok merged 1 commit into
cortexkit:masterfrom
legion-works:feat/client-served-identity

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

decodeCredential has been dropping five of the eight fields GetResult sends, including the one a consumer needs to verify it is holding the right account's handle.

wire (GetResult)      payload · expires_at_ms · record_version · credential_id
                      project_id · account_id · email · org_name          (8)
client (ServedCredential)   material · recordVersion · expiresAtMs         (3)

Why now

cortexkit/openai-auth has a served-identity fence written and skipped: it compares the vault's account_id against the JWT claim in the served token, which is what catches a handle bound to the wrong ChatGPT account. It cannot run, because the vendored client discards the field. They are holding their main-account enrollment until that branch is armed.

Worth naming the shape of the defect, because it is not "a field is missing". My own OpenCode plugin logs the manifest's credential_id beside the vault's record_version as though the pair corroborated identity. It does not — the vault's credential_id never reaches the consumer, so the plugin is comparing a local value against a local value and rendering it as agreement.

What it does

Five optional fields on ServedCredential, populated in decodeCredential. All five are skip_serializing_if = "Option::is_none" on the server, so absence is the common case and must not throw.

The TSDoc carries forward the distinction the Rust doc comments make, because the client is where a consumer will actually read it: credential_id is a binding-verification value, not a routing key. Account-scoped routing joins on account_id + record_version — the version bumps on every replace, so a re-pointed handle forces the router to re-resolve.

Validation is fail-closed: a non-string where a string is expected throws invalid_response rather than silently becoming undefined. A silent drop would hand the identity fence an absent value and let it pass by default, which inverts the whole point.

CredentialStatus is deliberately untouched — this is the get path only.

Mutations

Two from the implementer, one I ran myself on the validator because it guards the fail-closed property:

isOptionalString -> `return true`
  -> FAIL  ClaustrumClient > rejects non-string served identity metadata
     client.test.ts:523  expected rejects.toMatchObject({ code: 'invalid_response' })
restored byte-identically, 19 pass

Verification

bash scripts/gate.sh exit 0, workspace floor 626 >= origin/master 626 at 539ffd3. Hermetic 223 pass / 0 fail / 871 expectations. Two files, 115 insertions, no deletions; Cargo.lock untouched; two-way revert sweep clean.

One caveat on the gate, stated rather than hidden: the implementer's runs went red twice on manifest_lock members under load [12.93, …] and [15.02, …]reclaim_failure_does_not_fail_acquisition and malformed_diagnostic_owner_fields_are_tolerated_and_evictable_once_stale, in a file this diff does not touch. Mine passed at load ~7, and the isolated suite ran 15/15 five times. That is the class from #51; the two surviving members look different from each other (one pins now_override_ms, one reads system time directly) and I have not established a mechanism. Following up separately rather than guessing at it here.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Exposes the non-secret identity fields GetResult already sends, so cortexkit/openai-auth can arm its served-identity fence that compares the vault's account_id against the served JWT claim.

Behavior changes

  • ServedCredential gains optional credentialId, projectId, accountId, email, and orgName, all populated in decodeCredential.
  • Missing wire fields decode as undefined; a present non-string identity value throws invalid_response rather than silently passing the fence.
  • The TSDoc marks credentialId as a binding-verification value, not a routing key; account-scoped routing joins accountId with recordVersion.
  • CredentialStatus is untouched; only the get path changes.

Caveat

  • scripts/gate.sh went red twice on manifest_lock members (reclaim_failure_does_not_fail_acquisition, malformed_diagnostic_owner_fields_are_tolerated_and_evictable_once_stale) in a file this diff does not touch; mechanism not established.

Written for commit c4ae1d8. Summary will update on new commits.

Review in cubic

Verified: 19 client tests; hermetic suite 223 pass, 0 fail, 871 expectations. Both decoder mutations reached named RED tests and were restored byte-identically.

scripts/gate.sh was not green locally: crates/credentials-module/src/bin/cli_support/opencode_files.rs failed malformed_diagnostic_owner_fields_are_tolerated_and_evictable_once_stale (assertion result.is_ok(), load [12.93, 8.43, 6.32]) and reclaim_failure_does_not_fail_acquisition (assertion quarantine.exists(), load [15.02, 11.22, 7.87]).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants