Skip to content

feat(identity): serve the identity operation family - #46

Merged
Knucklessg1 merged 70 commits into
mainfrom
feat/land-identity-operations
Oct 8, 2026
Merged

Knucklessg1 merged 70 commits into
mainfrom
feat/land-identity-operations

Conversation

@Knucklessg1

@Knucklessg1 Knucklessg1 commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Closeout status — 2026-10-08

Draft; not merge-qualified. Reviewed repairs and regenerated artifacts are published at 083672fec0119be65220d6dba654e5473b959fdb, integrating main through f763ff2874359d4723e106077b70312660bd73d3. The exact candidate tree is e3b86afa090328cb24b325f1211a2f8c3e265aef.

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:

  • Normal commit and pre-push hooks: passed, including Ruff, mypy, pinned rustfmt, contract reachability, ownership, persistence, and privacy gates selected by those hooks.
  • Staged complexity, architecture, public-spec checks and 10 artifact-ledger tests: passed.
  • Canonical WASM build/check and contract generation/rebuild/check: passed. Exact alias-bearing source freshness also passed using the verified retained generator and canonical WASM checker after full input equality verification; API-doc freshness passed.
  • The first root-native attempt stopped before compilation because the offline cache lacked a locked dependency. Normal official-registry locked fetch then succeeded; 924 cached archive checksums matched, and the source/lockfile remained unchanged.
  • The single replacement root-native unit used 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.
  • Matching Python codec/contract interop and downstream qualification remain incomplete. Earlier main/PR CI and older native snapshots do not qualify this head.

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::Identity operation, new request/result schemas, a new scope registry (contract/scopes.json), and new IDENTITY_* server error codes. The contract digest moves (contract/receipt.json's contract_digest, method_count 456 → 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 through src/server/dispatch/identity_store.rs and registered in crates/eg-capabilities's security domain.

  • Credential verification uses argon2id at the request boundary (src/server/dispatch/identity_store/stamp.rs, secrets.rs); every plaintext secret is cleared from the op before it is replicated or logged.
  • The identity store and its full RBAC projection share one rbac.redb write-transaction with a hash-chained identity audit trail (IDM-03); RegisterIdentity/RbacAdmin gain the same audit trail and are fenced off the store's own idm: namespace.
  • A pgwire/SQL projection (src/server/sql_catalog_acl/identity_view.rs) exposes the store's relations redacted, visible only to callers holding the exact identity:read/identity:admin scope.
  • crates/eg-capabilities/src/scopes.rs is a new IDM-05 scope registry (contract/scopes.json), the single source of truth agent-utilities generates its session-scope allowlist from.
  • Classified into the native consensus catalog (src/raft/command/native/catalog.rs) alongside RbacAdmin/RbacElevation so 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-560 governed changes (governed_change, dispatch::governed, the GovernedChange method/scopes).
  • IDM-02 multi-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's AccessDecision/AccessReasonCode addition to access_policy.rs (stable graph-access reason codes) — unrelated to the identity operation family.
  • RBAC elevation (EH-404) and the throttle capacity cell (EH-406) were already merged to main independently 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), then python3 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 warnings under --features security, --features cluster (raft), --no-default-features --features server, and --no-default-features --features full,ast-extended — all clean.
  • cargo fmt --check — clean.
  • Targeted cargo test across 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-commit default stage and the manual-stage complexity/dupehound/kiss/jscpd-differential/kiss-census/cccc-census/rust-arch-lint scanners — all green against origin/main.
  • scripts/security/check_secret_history.py --base origin/main and scripts/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 no ruff/pydantic installed — an environment gap, not a regression; every other test in that suite is green.

🤖 Generated with Claude Code

Knucklessg1 and others added 10 commits October 1, 2026 18:42
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>
Knucklessg1 and others added 9 commits October 2, 2026 02:06
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>
Knucklessg1 and others added 9 commits October 4, 2026 14:08
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
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
Knucklessg1 marked this pull request as draft October 8, 2026 09:47
@Knucklessg1

Copy link
Copy Markdown
Member Author

Review of head 083672fec (Claude takeover; Codex closed out).

Verdict: one blocking authority bug, plus a jscpd regression. Not merged.

Blocking: CreateUser can take over a principal registered through RegisterIdentity.

  • CreateUser accepts a caller-chosen principal_id (crates/eg-types/src/identity/requests.rs:34-38). The collision check looks only at the store's own users (store/users.rs:103). import_sql behaves the same way (store/import.rs:182).
  • project_identity_store (crates/eg-core/src/isolation/identity_store_admin.rs:141-150) then deletes the existing AgentIdentity and writes role: AgentRole::Agent with only idm: roles (projection.rs:176).
  • Effect: an identity:admin caller who creates a user named graph-os, a signer-registry id or the System id strips that principal's System role and its non-idm: roles. This is the same replace-not-merge failure that cost graph-os its admin before.
  • Recovery is also locked. try_register_agent_audited refuses the id with IDENTITY_STORE_MANAGED, and RepairSystemIdentity refuses managed principals.
  • Fix: in try_apply_identity, refuse with Collision when next manages an id that already exists in self.agents and is not store-managed. Add a regression test that creates a user with an id already registered through RegisterIdentity (and the System id) and expects a refusal with the agent unchanged.

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: identity_store_tests/tenant_and_audit.rs (x2), identity/tests/auth.rs/modes.rs, tests/recovery.rs, tests/tokens.rs/verdicts.rs (x2), dispatch/identity_store/tests/stale_verdict.rs.

Verified, not blocking

  • No cross-tenant leak. There is one engine-global store routed through __commons__. Note that identity:admin can create roles whose graph_grants cover any tenant, so it is effectively a platform scope.
  • No secret material is returned or logged. Views, sql_dump, audit entries and tracing carry only booleans, secret_ref and one-way handles. stamp_identity clears plaintext before consensus.
  • Existing principals keep their authority. identity_actor keeps only exact identity:* scopes, so wildcard, kg:admin, System and existing signer entries gain no identity ops. The System bootstrap path is preserved. No op re-runs RegisterIdentity; the takeover above is the only implicit role replacement.
  • Generated contract and client artifacts are consistent with their generators.

Open, documented (not blocking for merge):

  • The replicated-tenant OPEN note in test-spec.md remains open. gates (slim server + raft cluster) was skipped on this run.
  • R002 audit fields (tenant, old/new authorization digest, correlation ID) are missing from AuditRecord.
  • Denial samples share the 4096-entry trail with admin mutations, so a burst of denials can evict admin records.
  • try_record_denials writes outside consensus, so per-replica audit chains diverge.
  • tasks.md I-1 through I-6 are still PENDING.

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
Knucklessg1 marked this pull request as ready for review October 8, 2026 18:14
@Knucklessg1
Knucklessg1 merged commit 0035f9f into main Oct 8, 2026
22 checks passed
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)
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.

1 participant