Skip to content

Latest commit

 

History

History
316 lines (247 loc) · 13 KB

users.md

File metadata and controls

316 lines (247 loc) · 13 KB

Multi-user attribution

Status: Introduced in v0.8; this page documents the current shipped contract.

ai-memory is single-tenant data with optional multi-user attribution. Every authenticated request sees the same wiki pages — there is no per-page RBAC, no per-user data scoping, no group permissions. What multi-user mode adds is who-did-this: every write attributes to a named user, audit-log rows carry that identity, and the web UI can show "Last edited by Alice Smith" instead of the anonymous default. Every /admin/* endpoint stays root-only once at least one user row exists, including read-only status/search/read-page helpers.

If you run ai-memory alone, you can skip this page — your install keeps working unchanged.

When to enable it

You probably want multi-user mode when:

  • More than one human shares a single ai-memory server (a household, a small team's homelab).
  • You want the audit log to record who made each write (e.g. to trace Codex writes vs Claude Code writes vs hand-rolled CLI calls).
  • You're planning to use the admission webhook chain — webhooks receive the actor identity in their payload.

You probably don't need it when:

  • You're the sole human user of your install. Single-user mode (no user rows) remains compatible whether or not init has generated [auth].token_pepper.
  • You need permissions / access control. v1 of ai-memory does not implement RBAC by design (see design-decisions.md §13). Attribution records who did a write; it does not gate whether they could.

The four resolution rungs

Every HTTP request is resolved to one of four authentication tiers:

Rung Trigger What the request gets
0 — Anonymous No [auth].bearer_token set. Allowed, no identity. Same as pre-multi-user defaults.
1 — Root Bearer matches [auth].bearer_token. Allowed as root. When [auth].root_username is set, writes attribute to that name; otherwise attribution stays anonymous.
2 — DB user Bearer doesn't match root, matches a users.token_hash row (via SHA-256 of token + [auth].token_pepper). Allowed as that user for normal read/write APIs. All /admin/* endpoints are root-only in multi-user mode. The audit log records the username/email/name.
3 — 401 Bearer present but matches nothing. Rejected. Closes the bypass — unknown bearers can't slip through as anonymous.

The rungs are sticky: a request is matched at the lowest tier that applies, never escalates. Root token always wins over any users-row collision; the two namespaces are intentionally distinct.

Implementation contract

Request identity and authorization are separate:

  • ActorContext carries who made the request and is used for attribution, frontmatter, audit payloads, and active-project keys.
  • AuthLevel carries what auth tier the middleware resolved.
  • AuthLevel::authorize(Capability::...) is the shared permission check for admin routes, user-management routes, normal read/write surfaces, and the admission-chain skip header.

Handlers should not compare usernames, infer root from ActorContext, or add ad hoc root-only branches. PRs that touch auth behavior should cover root, DB-user, and anonymous callers, including the single-user compatibility mode where [auth].token_pepper is absent.

Quick start

Prerequisite: a fresh ai-memory init. Pre-v0.8 installs need the migration step below before any of these commands work.

1. Set the root identity

Edit your config.toml (typically <data_dir>/config.toml or /etc/ai-memory/config.toml) and uncomment the root_* lines in the [auth] block:

[auth]
bearer_token = "<your-existing-token-or-a-fresh-one>"
token_pepper = "<auto-generated-by-ai-memory-init>"

root_username = "boss"            # required for root attribution
root_email    = "boss@example.com" # optional, surfaced in UIs
root_name     = "Boss"             # optional, surfaced in UIs

token_pepper was auto-generated by ai-memory init; do not change it after adding users — rotating the pepper invalidates every existing token. The pepper is what makes a stolen users table useless to an offline attacker; an attacker with both the DB and the config has tokens at their disposal anyway, so the pepper's job is closed by the file-permission boundary.

init creates the pepper before any users exist. Until the first user is added, operational admin endpoints retain single-user compatibility; creating that first user switches them to root-only immediately, without a restart. Expired user rows still keep admin mode root-only. If a database has users but either the pepper or static root bearer is missing or blank, serve refuses startup. Restore both original secrets from configuration backup (or set the root bearer from the secret manager) rather than removing users; the root token is required to administer the existing users.

2. Add another user

Each ai-memory user add issues one token, printed exactly once. Only its SHA-256 digest is kept in the DB.

$ AI_MEMORY_AUTH_TOKEN=<root-token> \
  ai-memory user add --username alice --email alice@home --name "Alice Smith"

✓ created user 'alice'
  name:  Alice Smith
  email: alice@home
  id:    01935a82-6f7a-7d22-b8c0-...

Store this token now — it will NOT be shown again. Only its
SHA-256 digest is kept in the DB.

mYi3pq...<43-chars>...wKp2Ze

stderr carries the human chrome, stdout carries the bare token so you can pipe it (> ~/.config/ai-memory/alice.token).

3. List users

$ AI_MEMORY_AUTH_TOKEN=<root-token> ai-memory user list

USERNAME  NAME         EMAIL             STATUS
alice     Alice Smith  alice@home        active
bob       -            bob@home          active
carol     -            -                 expired

The list never surfaces tokens — only their hashes are in the DB.

4. Disable a token (without losing attribution history)

ai-memory user expire <username> stamps token_expired_at = now() on the row. The user's bearer stops authenticating immediately, but the row stays put so historical author_id references in audit_log and pages keep resolving to their real names.

$ ai-memory user expire alice
Expire token for user 'alice'? Their token stops authenticating immediately. (y/N) y
✓ expired token for user 'alice'

Pass --yes to skip the prompt (CI / scripts).

To re-enable later: ai-memory user revive alice.

5. Rotate a leaked / lost token

$ ai-memory user rotate-token alice
Rotate token for user 'alice'? Any existing client using the old token will start getting 401 immediately. (y/N) y
✓ rotated token for user 'alice'

Store this token now — it will NOT be shown again.

XGqsBp...<43-chars>...zRm0Vt

Rotation implicitly revives an expired token — you can recover an offboarded user without first running revive.

Backward compatibility

If you're upgrading from a pre-v0.8 ai-memory:

  • No action is required. Your existing [auth].bearer_token-only setup continues to authenticate exactly as before. The auth middleware just stamps an anonymous ActorContext and your audit log records the same shape it did before.
  • The users table is added by migration V14 and stays empty until you actively run ai-memory user add. SQL queries against it return no rows; the rest of the schema is unchanged.
  • Multi-user mode requires [auth].token_pepper. Without it, the user-management endpoints return 503 with a clear multi-user not enabled message. Existing installs never trip this because they never call user add.
  • /admin/* endpoints are open to the configured bearer token in single-user mode, matching historical behavior. Creating the first user row immediately makes every admin endpoint root-only; DB-user tokens receive 403 and anonymous requests receive 401. Merely configuring [auth].token_pepper does not activate that boundary.

Migrating an existing single-user install

ai-memory init is idempotent and won't overwrite a config it finds. To populate token_pepper without losing your current config:

  1. Back up the existing config (cp config.toml config.toml.bak).

  2. Generate a pepper: ai-memory generate-auth-token 32 — this prints a hex string of the same shape init would have generated.

  3. Add the [auth] block to your config.toml:

    [auth]
    # ... your existing settings (bearer_token, etc.) ...
    token_pepper = "<paste-the-generated-pepper-here>"
    root_username = "boss"     # optional; enables root-token attribution
    root_email    = "boss@..." # optional
    root_name     = "Boss"     # optional
  4. Restart ai-memory serve. The new fields are picked up; existing behaviour is unchanged.

You can defer steps 3-4 indefinitely — bearer_token alone keeps working as it always has.

How tokens are stored

  • 32 bytes of OS CSPRNG, URL-safe-base64-encoded → 43-character string.
  • DB column users.token_hash stores SHA-256(token || ":" || token_pepper), never the plaintext.
  • The per-server token_pepper makes a DB-only theft (e.g. a copied SQLite file) useless to an offline attacker: the search space for the unpeppered hash is (token, pepper) jointly.
  • Constant-time comparison (subtle::ConstantTimeEq) on the hash side-steps timing attacks against the lookup path.

We deliberately don't use argon2id here even though it would be the textbook choice. Tokens are 256-bit CSPRNG, so brute force is infeasible regardless of hash strength; argon2id's per-hash salt would force O(N) scans on every auth request, where SHA-256 + UNIQUE index gives us the O(1) lookup the hot path needs. See crates/ai-memory-store/src/users.rs for the full rationale.

Where attribution shows up

Surface Status
Auth middleware injects Extension<ActorContext> on every request ✓ P1.3
All /admin/* routes gate on Extension<AuthLevel>::Root in multi-user mode ✓ P1.4
ai-memory user add/list/expire/revive/rotate-token CLI ✓ P1.5
pages.author_id populated, frontmatter last_modified_by block ✓ P1.6
/api/v1 page responses include author: { username, name?, email? } ✓ P1.7
ETag invalidation on author change (so caches refresh attribution) ✓ P1.7
install-hooks --as-user <name> metadata + flag validation ✓ P1.8
Web UI shows author on the page view ✓ shipped
Attributed mutation audit rows carry audit_log.author_id ✓ shipped

Commit ids for each milestone are recorded in CHANGELOG.md.

Wiring agent hooks to a specific user

After ai-memory user add prints a user's token, point that user's agent install at it via install-hooks:

$ ai-memory user add --username alice --email alice@home --name "Alice Smith"
✓ created user 'alice'
  name:  Alice Smith
  email: alice@home
  ...

XGq...<43-chars>...zRm    # the token, stdout only

$ ai-memory install-hooks --apply --agent claude-code \
    --as-user alice --auth-token XGq...<43-chars>...zRm
[ai-memory] hooks installing for user: alice
✓ staged 5 hook script(s) → ...

--as-user is metadata only: it labels the install for the operator's records and prints a confirmation line so you can verify which identity the next session's writes will attribute to. The actual token wired into the hook env block is whatever you pass via --auth-token. Mismatching the two (e.g. --as-user alice --auth-token <bob's token>) is permitted at the CLI layer; the server will resolve to bob at runtime. The flag is there to keep the operator honest, not to enforce.

Without --as-user, hooks install the same way they always have — the bearer authenticates, attribution flows from the token's owner (root user or DB user) at write time.

Limitations

  • No per-page RBAC. Every authenticated user sees every page in the workspace. All /admin/* endpoints are still root-only in multi-user mode. If you need data isolation, run separate ai-memory servers (per-user data dirs) and front them with a reverse proxy.
  • One token per user. Rotation issues a new token and invalidates the old in the same transaction. There's no notion of multiple device-bound tokens per user.
  • Root token is single. [auth].bearer_token is the admin token for every /admin/* endpoint. DB users created with user add are normal users, not additional admins.
  • OIDC is request authentication, not page authorization. Native hooks and thin-client CLI commands can send a per-developer OIDC bearer for an external OIDC-aware gateway/bridge. Native ai-memory server auth still uses static root bearer / DB-user tokens, and /admin/* stays root-only unless a gateway translates accepted OIDC auth into upstream auth that ai-memory accepts. ai-memory still has one shared wiki per server and no per-page RBAC. The Keycloak/OIDC sid claim is also not an ai-memory agent session id; session auto-scope needs the lifecycle-hook session id or explicit workspace + project / scopes.