Repository navigation
feat(identity): serve the identity operation family - #46
Merged
Merged
Conversation
Lands the engine-owned identity store (users, credentials, sessions, tokens, MFA, API keys, roles, groups, identity providers, links, audit and throttle) as a new Method::Identity operation dispatched through src/server/dispatch/identity_store.rs and crates/eg-capabilities's security domain. Credential verification uses argon2id at the request boundary; the identity store and its RBAC projection share one rbac.redb write-transaction with a hash-chained audit trail; a pgwire / SQL projection exposes the store's relations redacted to callers holding the exact identity:read or identity:admin scope; RegisterIdentity and RbacAdmin gain the same audit trail and are fenced off the store's own idm: namespace. This is a wire-contract change: it adds the Identity method, its request/result schemas, new identity:* and other previously-unregistered scopes to the IDM-05 scope registry (crates/eg-capabilities/src/scopes.rs, new), and new IDENTITY_* server error codes. The contract digest moves; see the follow-up regeneration commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Regenerated with cargo run -p eg-capabilities --features "contract canonical-ledger contract-schema" --bin gen_contract, then scripts/gen_api_docs.py --write. Adds the Identity method's request/ result schemas and generated Python client (send_identity), the new contract/scopes.json and epistemic_graph/contract/scopes.json scope registry (IDM-05) artifacts, and moves the contract digest and method count (456 -> 457). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
epistemic_graph/generated/models.py now renders the identity contract's UserStatus/TokenPurpose enums, whose wire tags (password_change_required, password_reset) trip bandit's B105 hardcoded-password heuristic. The file is machine-rendered (RF-RULING-003) and never hand-edited, so the fix is excluding the generated tree from this hook, matching the existing jscpd/generated-artifact precedent rather than editing generated output or the wire tag names. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Method::Identity mutates the rbac.redb policy image exactly like RbacAdmin/RbacElevation, but was missing from raft::command::native::catalog's native_method_catalog! inventory, so a clustered (feature "raft") build had no bounded native command to replicate it through and the persisted-mutation-contract gate's cluster mutation inventory came up short by one. Classified it into the existing Identity native-consensus domain alongside RbacAdmin/RbacElevation, and added a round-trip test for it beside RbacAdmin's. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…c_elevation Shares the real duplication the dupehound/jscpd gates found rather than suppressing it: - crate::contract::bounded_control_free_text is the one bounded/control- free text check identity::text::bounded and rbac_elevation's bounded_text now both call. - identity::store::import's import_group_role/import_user_role share required_known_role for the role lookup both repeated. - identity::store::mfa's with_second_factor_gate is the one pending- session/throttle gate verify_totp and webauthn::verify_webauthn now both call through, replacing each one's inline copy. - eg-core's identity_store_tests::reports_role is the one "reports" role fixture identity_bootstrap_tests now reuses instead of rebuilding it. - identity/tests/access.rs's bind_alice_as_reader is the one alice+reader setup its two tests now share. - src/server/dispatch's test_support::verified_as lets elevation/tests.rs share the verified-context builder it used to duplicate locally (its own "user:"-prefixed principal convention is preserved via the new explicit agent parameter). Also records three reviewed dupehound findings in .config/dupehound-distinct.toml that are not real clones (the same class already documented there): two enum-to-&str / newtype-delegation accessors over unrelated types, and one view-struct field-by-field constructor over an unrelated record, each with no shared concept to extract. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…wo DTO fields eg_types::test_support::identity::bootstrap_local_op is the one Local-mode bootstrap request (root administrator, no password) that identity::tests::modes's own precondition test and the root crate's sql_catalog_acl::identity_view tests now both build through, instead of each repeating the same InitializeRequest literal (test_support is exactly the existing cfg(test)-does-not-cross-a-crate-boundary mechanism this repository already uses for cross-crate fixtures). UserView and WebauthnCredential each had their trailing field reordered (a redacted view's field declarations otherwise ran contiguously identical to its stored record's, and a request's to its own stored counterpart): harmless for named-field serde structs, and it clears the last two jscpd pairs that are not real logic duplication -- the same "no shared concept, an invented common type would be the wrong direction" class already on record for the function-level dupehound findings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… cyclomatic cap The 11-way &&-chain was 11 branches with no match to discount, so it was real backlog under the whole-tree complexity census (cyclomatic 11 > the 10 cap), not exhaustive-dispatch residual. Restated as an array of the same is_empty() checks reduced through Iterator::all, same behavior, cyclomatic within cap. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replaces internal program-tracking labels (IDM-01..19, EH-404, EH-406, EH-560, RF-RULING-003) added by this change with plain descriptions a public reader can resolve without the source design document: "the identity store", "the scope registry", "role elevations", "governed changes", "generated artifacts are machine-rendered from the contract", and so on. No label was replaced with another label, and no behavior changed. Where a public requirement ID already applies, the comment still names the thing in plain terms and does not add a private code in its place. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Doc-comment text feeds the contract digest (method descriptions, module docs). Regenerated with cargo run -p eg-capabilities --features "contract canonical-ledger contract-schema" --bin gen_contract (verified with -- --check), then scripts/gen_api_docs.py --write. Method count is unchanged (457); only the digest moves, since no method, schema or scope changed -- only description text. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The two gate-configuration edits are withdrawn: the reviewed-distinct register and the bandit exclusion are back to what the default branch carries, and the findings they covered are resolved in the code. - The refusal vocabulary is declared through the shared closed-codes form (one definition of the enum, its wire tokens and their enumeration). - The redacted principal view takes its record apart field by field, so a field added to the record is a compile error until someone decides whether a reader may see it. - The bounded-text check is one function taking the caller's refusal. - The family delegation of an op's static facts is one exhaustive list. - Two wire tags that read as credential variables in the generated Python enums are renamed: credential_change_required, credential_reset. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ted against The boundary verifies a candidate against a snapshot of the store, off the engine's write lock. The verdict named only the principal, so an administrator's password reset landing while the derivation ran did not stop the old password's verdict from opening a session, or from changing the password, afterwards. A credential now carries a generation that moves on every replacement and whenever the principal's sessions are revoked. The verdict names the generation it was computed against; apply honors it only while that is still the principal's credential and otherwise refuses with IDENTITY_STALE_CREDENTIAL, changing nothing. A rehash of the same password keeps the generation. A record written before the counter existed reads as generation 0. The served tests hold one derivation after its verdict is computed, replace the credential through the served boundary, and release it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ever enrolled A session that owed its first enrolment could issue itself recovery codes and complete with one, so a group's required second factor was met by the password alone. Codes are now issued only to the principal of a completed session who holds a confirmed factor, and spent only by a principal who holds one. Recovery of a lost factor with a previously issued code is unchanged, and codes that outlive their factor complete nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Administrator, reader and broker authority was taken from the scopes on the verified token alone, so a disabled, deprovisioned or demoted administrator could keep administering -- and re-enable itself -- with a token minted before the change. One function now answers whether an actor holds a scope: the token must carry it, and for a principal the store owns the principal must be active and its current roles must still resolve to it. Every op family, the administrator override on a single credential and the identity SQL relations ask that function, at the boundary and again at apply. A principal the store does not own (the broker, a provisioner, first-run setup) has no record there and keeps its token's authority. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
RegisterIdentity is refused for a principal the identity store manages and for a role in the store's namespace, but declared only the first, so the served boundary replaced the second with INTERNAL. Both are declared; the test sends each case through the signed dispatch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An internally tagged enum that wraps another puts both tags on one wire object. The Python model renderer emitted one class per outer variant holding the outer tag alone, which refused every identity request. Such a variant now renders as a nested union whose member classes carry both tags and the member's own fields. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d codecs Regenerated with the repository generators from the sources in the preceding commits: gen_contract (58 artifacts), gen_api_docs, and build_method_codec_wasm for the Go and JavaScript clients, with the two codec pins in the generated-artifact ledger moved to the new module digest. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The new recovery, revocation and SQL-relation tests each spelled out the same group and user requests. They now use one helper for the group that requires a second factor and one test-support op for a human administrator. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…afts No client could commit a ChangeEnvelope: its MutationBatch needs a scope identity, version expectation and admission envelope (AuthorityContext) that only engine-internal compilers mint, and the Python ChangeEnvelopeClient still emitted the retired flat v2 mutation fields, so changes.apply / apply_batch failed for every caller. Add ApplyChangeEnvelopeDraft / ApplyChangeEnvelopeDrafts. A caller submits the envelope rows plus a mutation draft (batch id, operations, outbox, placement epoch, optional expected graph version and fencing token). After the request is authorized (ingest:write on the request graph) the request boundary compiles each draft under the verified context -- tenant, caller, request id, attempt nonce, idempotency key; an absent expected version binds the authoritative graph version -- refuses any operation whose declared surface/domain differs from the engine's, and the request then continues as ApplyChangeEnvelope(s), so material preflight, consensus, the commit kernel and CDC see only the governed form. Batch drafts replay under position-qualified keys. ChangeEnvelopeClient now emits the draft. Contract, generated client, method codec and API docs regenerated; ledger snapshots and census pins updated; one Python end-to-end test of changes.apply / apply_batch against the session engine. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lope mirror Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…draft # Conflicts: # .config/generated-artifacts.toml # clients/go/eg_method_codec.wasm # clients/js/eg_method_codec.wasm # contract/receipt.json # crates/eg-capabilities/generated/catalog_digest.rs # docs/status.md # epistemic_graph/contract/receipt.json
fix: reject nonstandard constants in decision body JSON
ci: preserve running main qualification
test: verify sync client teardown by owned resources
test: prove nested generated request fields are strict
fix: detect Unicode home paths in privacy scans
…-task10 fix: reject coerced certificate incumbent assignments
docs: publish specification delivery dashboard
docs: describe observed specification history
…inding-01a10522 Keep local process proofs bound to current authority and recipient
Integrate identity operations with main through PR63. Bind replacement password policy verdicts to checked history, preserve transactional tenant grants and audit persistence, enforce factor throttling and secret binding, and withhold ambiguous managed auto-binding during replicated creation. Regenerate and verify canonical client artifacts. Root-native qualification remains incomplete after the bounded build timeout; retain this work for further review without claiming merge readiness.
Knucklessg1
marked this pull request as draft
October 8, 2026 09:47
Member
Author
|
Review of head Verdict: one blocking authority bug, plus a jscpd regression. Not merged. Blocking:
Scanner regression: "Scanner + architecture quality" passes on main (run 37734422245) and fails here on jscpd only. There are 7 new duplicate pairs, all in new identity tests: Verified, not blocking
Open, documented (not blocking for merge):
|
The identity store could start managing a principal id that already held an identity registered outside it (RegisterIdentity, the System bootstrap, a signer). The projection then replaced that identity with plain-agent idm: roles, and neither RegisterIdentity nor RepairSystemIdentity could restore it. try_apply_identity now refuses such an op with IDENTITY_COLLISION. Also extract shared identity test helpers to clear the 7 new jscpd pairs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Knucklessg1
marked this pull request as ready for review
October 8, 2026 18:14
Knucklessg1
added a commit
that referenced
this pull request
Oct 8, 2026
…ntext-producer feat(identity): compose request-context authority on credential resolution (stacked on #46)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closeout status — 2026-10-08
Draft; not merge-qualified. Reviewed repairs and regenerated artifacts are published at
083672fec0119be65220d6dba654e5473b959fdb, integrating main throughf763ff2874359d4723e106077b70312660bd73d3. The exact candidate tree ise3b86afa090328cb24b325f1211a2f8c3e265aef.The repairs bind replacement-password policy verdicts to the checked credential history, preserve transactional tenant grants and audit persistence, enforce MFA throttling and secret binding, and withhold managed tenant auto-binding when replicated fingerprint authority cannot identify the original subject. Legitimate fingerprint-shaped self IDs also require explicit Identity grants. The inherited unmanaged fallback and broader clustered provisioning/ordering gap remain documented and open.
Validation for this candidate:
server,security,query,raft,redb, offline, one Cargo job, a four-CPU/24-GiB limit, and a 45-minute cap. It timed out during compilation at 09:44:02 UTC. Zero native tests executed: the three alias regressions, two password-race regressions, SQL identity, and Raft filters remain unqualified.No main merge or deployment was performed. No compiler remains from the bounded unit. Complete source patch and raw receipts are retained in the task workspace; no follow-on build or repair is scheduled.
The validation above supersedes historical validation statements below for the current head.
Summary
This is a wire-contract change: it adds a new
Method::Identityoperation, new request/result schemas, a new scope registry (contract/scopes.json), and newIDENTITY_*server error codes. The contract digest moves (contract/receipt.json'scontract_digest,method_count456 → 457).Lands the engine-owned identity store (
EG-TYPED-PACKS-R085): users, credentials, sessions, tokens, MFA, API keys, roles, groups, identity providers, links, audit and throttle, dispatched throughsrc/server/dispatch/identity_store.rsand registered incrates/eg-capabilities's security domain.src/server/dispatch/identity_store/stamp.rs,secrets.rs); every plaintext secret is cleared from the op before it is replicated or logged.rbac.redbwrite-transaction with a hash-chained identity audit trail (IDM-03);RegisterIdentity/RbacAdmingain the same audit trail and are fenced off the store's ownidm:namespace.src/server/sql_catalog_acl/identity_view.rs) exposes the store's relations redacted, visible only to callers holding the exactidentity:read/identity:adminscope.crates/eg-capabilities/src/scopes.rsis a new IDM-05 scope registry (contract/scopes.json), the single source of truth agent-utilities generates its session-scope allowlist from.src/raft/command/native/catalog.rs) alongsideRbacAdmin/RbacElevationso a clustered (raft) build replicates it correctly.What was left on the source branch (
feat/t7-eg-auth-reason-20260925)Deliberately excluded from this slice (separate requirements, not needed for identity to compile or serve):
EH-560governed changes (governed_change,dispatch::governed, theGovernedChangemethod/scopes).IDM-02multi-issuer OIDC trust (src/server/oidc/trust.rs) — the identity store's own identity-provider CRUD is self-contained and does not depend on it.EG-TYPED-PACKS-R087'sAccessDecision/AccessReasonCodeaddition toaccess_policy.rs(stable graph-access reason codes) — unrelated to the identity operation family.EH-404) and the throttle capacity cell (EH-406) were already merged tomainindependently before this branch was rebased; nothing further was needed from those commits.Regeneration
Regenerated with
cargo run -p eg-capabilities --features "contract canonical-ledger contract-schema" --bin gen_contract(verified with-- --check), thenpython3 scripts/gen_api_docs.py --write. No generated artifact was hand-edited.Gates run locally (R820 for Rust, this host for scanners)
cargo check/clippy --all-targets -D warningsunder--features security,--features cluster(raft),--no-default-features --features server, and--no-default-features --features full,ast-extended— all clean.cargo fmt --check— clean.cargo testacross the touched modules (identity types, eg-core isolation, dispatch identity_store, elevation, sql_catalog_acl, access, mutation/mutation_batch, audit, raft native catalog, eg-capabilities contract/scopes) — all green.pre-commitdefault stage and the manual-stage complexity/dupehound/kiss/jscpd-differential/kiss-census/cccc-census/rust-arch-lint scanners — all green againstorigin/main.scripts/security/check_secret_history.py --base origin/mainandscripts/security_sanitizer.py— clean.Two eg-capabilities Python-interop tests (
generated_python_is_ruff_clean_and_formatter_stable,generated_source_ingestion_imports_and_executes_with_rust_digest_parity) fail on the Rust build host used for this lane because it has noruff/pydanticinstalled — an environment gap, not a regression; every other test in that suite is green.🤖 Generated with Claude Code