Skip to content

feat: complete per-user API keys — ready for main (v5.0.0)#25

Merged
bkrabach merged 2 commits into
mainfrom
feat/per-user-api-keys-completion
Jun 22, 2026
Merged

feat: complete per-user API keys — ready for main (v5.0.0)#25
bkrabach merged 2 commits into
mainfrom
feat/per-user-api-keys-completion

Conversation

@colombod

Copy link
Copy Markdown
Collaborator

Complete per-user API keys — ready for main (v5.0.0)

This PR carries @bkrabach's keystore foundation commit (7854776, from #24) plus the completion work required to make the feature mergeable, trustworthy end-to-end, and operable by humans and agents. It supersedes #24.

Please merge preserving history (merge commit or rebase — NOT squash) so Brian's authorship on 7854776 is retained. The branch is intentionally two commits: his foundation + the completion commit.

What the feature does

Replaces the single shared API key with a keystore (api_keys: { sha256_hex(token) -> { id: <contributor> } }) so every graph write carries authenticated, spoof-proof contributor attribution. The server stores only token digests; peers send Authorization: Bearer <raw-token>. Legacy single api_key still authenticates (folds to owner) for back-compat.

Completion changes (on top of #24)

  • Fail-closed auth — empty api_keys: {} is now a hard startup error (only omitted/null disables auth); digests validated as 64 lowercase hex (uppercase normalized); blank contributor ids rejected.
  • Write-once made structuralcreated_by is excluded from node props, so SET n += row.props can never clobber the ON CREATE SET stamp.
  • Edge/relationship provenance (v1) — write-once ON CREATE SET r.created_by; bare endpoint placeholder nodes intentionally left null (avoids cross-session mis-attribution); not indexed in v1.
  • Session-ownership invariant made observable — one contributor per session_id is load-bearing; a conflicting created_by on worker reuse logs ERROR and preserves the bound id (never silently overwrites, never crashes ingest). Documented + test-enforced.
  • Removed the init subcommand (it emitted the legacy single-key format); the Docker auto-bootstrap now emits an api_keys keystore and prints the raw token once. Setup is doc-driven for humans and agents.
  • Docs — new docs/managing-api-keys.md and docs/designs/per-user-api-keys.md; corrected the "per-peer keys (future)" note (it's shipped); updated README, example config, peer-onboarding, service-setup, and the repo AGENTS.md.
  • Version 4.0.2 → 5.0.0 (breaking: init removed, empty api_keys errors).

Note: a pre-existing bug fixed here

The new real (isolated, ephemeral) Neo4j gates surfaced that 7854776's _write_batch signature change (adding created_by) wasn't propagated to two test spies in tests/neo4j/test_flush_lock_ordering.py — they fail with an arity error under Docker-backed CI. Fixed; that suite is green.

Verification

  • Full non-Docker suite: 1409 passed, 2 skipped.
  • Isolated-Neo4j suite (ephemeral container, production untouched): 79 passed, incl. new gates: anti-spoof, node write-once, edge first-asserter-wins, and a real-path crash-recovery test (replaces the prior inline reimplementation) with truncated/garbage-line robustness.
  • ruff + pyright clean.

🤖 Generated with Amplifier

bkrabach and others added 2 commits June 20, 2026 11:15
Replaces the single shared API key with a keystore: 'api_keys' block in
the credentials YAML mapping sha256-hex(token) -> {id: <contributor>}.
The legacy single 'api_key' still works (folds to id "owner") for
back-compat. Auth disabled only when no keys at all are configured
(dev mode preserved). Malformed keystore fails closed (server refuses
to start).

The auth middleware resolves the bearer token to a contributor id
(sha256 + constant lookup; never returns "unknown") and injects it into
the ASGI scope.

POST /events stamps the authenticated id into the event as 'created_by',
OVERWRITING any client-supplied value (anti-spoofing), persisted into
the durable queue line.

The graph write stamps 'created_by' WRITE-ONCE via ON CREATE SET on both
node-write paths (Session inline MERGE and the universal _NODE_MERGE_CYPHER),
never as part of the MERGE key — node identity/uniqueness constraints
unchanged. Adds a non-unique idx_session_created_by index.

Authenticate-and-stamp only (no roles in v1); workspace remains a client
label. Zero client change. Historical nodes keep null created_by.

Verified end-to-end against an isolated instance: a peer's lying
'created_by' is overwritten by the server (anti-spoof), created_by is
immutable across a second writer (write-once), and a bogus token gets 401
with nothing written. 1353 unit tests pass; ruff/pyright clean.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…-once, edge provenance, doc-driven setup

Completes the per-user-API-keys feature (stacks on the keystore foundation) so it
is mergeable and trustworthy end-to-end:

- Auth fail-closed: reject empty `api_keys: {}` (only omitted/null disables auth);
  validate digests are 64 lowercase hex (normalize uppercase); reject blank ids.
- Write-once made structural: `created_by` is excluded from node props so
  `SET n += row.props` can never clobber the `ON CREATE SET` stamp.
- Edge/relationship provenance (v1): write-once `ON CREATE SET r.created_by`;
  bare endpoint placeholder nodes stay null (no cross-session mis-attribution);
  intentionally not indexed in v1.
- Session-ownership invariant (one contributor per session) made observable:
  a conflicting created_by on worker reuse logs ERROR and preserves the bound id
  (never silently overwrites, never crashes ingest); test-enforced.
- Removed the `init` subcommand (it emitted the legacy single-key format); the
  Docker auto-bootstrap now emits an `api_keys` keystore and prints the raw token
  once. Legacy single `api_key` still authenticates (back-compat).
- Documentation: new docs/managing-api-keys.md and docs/designs/per-user-api-keys.md;
  fixed the misleading "per-peer keys (future)" note; updated README, example
  config, peer-onboarding, service-setup, and the repo AGENTS.md.
- Tests: real (isolated, ephemeral) Neo4j gates for anti-spoof, node write-once,
  and edge first-asserter-wins; real-path crash-recovery test (replaces the inline
  reimplementation) with truncated/garbage-line robustness; fixed a pre-existing
  test spy whose arity broke when created_by was added to _write_batch.
- Bump server version 4.0.2 -> 5.0.0 (breaking: init removed, empty api_keys errors).

Verified: full non-Docker suite 1409 passed; isolated-Neo4j suite 79 passed;
ruff + pyright clean.

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@bkrabach bkrabach merged commit ed5b6d7 into main Jun 22, 2026
3 checks passed
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