diff --git a/CLAUDE.md b/CLAUDE.md index 8095d71c..8bb9ab97 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,25 +48,24 @@ Read the relevant docs before starting work on a subsystem. - **Schemas** (three, one database): `auth` (better-auth + its oauth-provider plugin: `users`, `sessions`, `accounts`, `verifications`, `jwks`, `oauth_client`, `oauth_access_token`, `oauth_refresh_token`, `oauth_consent`), `core` (control plane: `principal`, `space`, `principal_space`, `group_member`, `tree_access`, `api_key`), and per-space `me_` (data plane: the single `memory` table). `auth.users.id == core.principal.id` for user principals. - **Memory table** (per space): `content`, `name` (text — optional filename-like leaf slug, unique within `(tree, name)` via a partial unique index where name is not null), `meta` (JSONB), `tree` (ltree), `temporal` (tstzrange), `embedding` (halfvec(1536)). Addressed by immutable `id` (`memory.get`/`delete`) or by `folder/name` path (`memory.getByPath`/`deleteByPath`, split at the final `/`); `deleteTree` removes a subtree. Wire/display paths are canonical leading-slash (`/share/auth`, `~/notes`, root `/`); the leading slash is optional on input. ltree storage and the access-grant shorthand below (`owner@home.`) stay dotted because that is the literal ltree representation. - **Search**: hybrid BM25 + semantic via Reciprocal Rank Fusion, computed in SQL functions. -- **Access**: no RLS. `core.build_tree_access(principalId, spaceId)` produces a `_tree_access` jsonb (rows of `tree_path` + `access`) passed into the space SQL functions (`search_memory`, `get_memory`, …). Three additive levels: **1 = read, 2 = write, 3 = owner**; `owner@root` (the empty ltree path) owns the whole space, and an owner grant at any path delegates access-management within that subtree. Two axes: **structural** authority (`principal_space.admin` — roster mutations, groups, invitations) vs **data** authority (owner@path); an admin may also grant data and can self-grant `owner@root`. Granting normally requires owner@path, with one exception: a member may grant/revoke their **own agents** at any path (`callerOwnsAgent` short-circuits `requireGrantAuthority`) — safe because `agent_tree_access` clamps an agent to `least(agent, owner)` at every path, so an over-grant clamps **down** to the owner's level rather than escalating or vanishing. Service accounts have no owner clamp, so granting them access uses the ordinary owner/admin rule; revocation is also allowed for direct user members of the service account's bound admin group. Endpoint admission is direct `principal_space` membership, so a member may authenticate with zero tree grants; data access is still filtered by `build_tree_access`. -- **Tree conventions**: two reserved roots — per-member `home.` (`~` is input sugar for it; a joining **user** is granted `owner@home.`, and a joining **agent** `owner@home..` — nested under its owner's home so the owner's home grant covers it and the `agent_tree_access` clamp keeps it effective) and the shared `share`. Service accounts have no home grant and no defined `~` home; they get zero tree access until explicitly granted access directly or through a group. A space **creator** gets `admin` + `owner@home` + `owner@share`, **not** `owner@root` — so it sees `share` and its own `~` but not other members' homes (as an admin it can self-grant `owner@root`). **Custom spaces** vary these provisioning defaults via `me space create` flags, resolved in `addSpaceCreator`: `--no-home-grants` sets the space column `space.auto_grant_home = false` (read by `add_principal_to_space`, so **every** join path stops seeding `owner@~` for users/agents) and flips the creator to **god mode** (`admin` + `owner@/`); the default group is controlled by whether `provision_default_group` is called (`--no-default-group`), its name (`--default-group `), and whether it's seeded with grants (`--no-default-group-grants`) — the chosen group is flagged `principal.is_default_group` (one per space, partial-unique; rename/delete-robust; surfaced on the space read as `defaultGroup` and used as the `me space invite` default). There is no `auto_grant_team` column — group grants are just `tree_access` rows. `memory.create`/`batchCreate` **require** an explicit `tree` (callers choose `share` vs `~` deliberately); only the file importers (`me import memories`, the `me_memory_import` MCP tool) default a tree-less record to `share` (`SHARE_NAMESPACE`, canonically defined in `@memory.build/protocol` and re-exported by `@memory.build/database`). +- **Access**: no RLS. `core.build_tree_access(principalId, spaceId)` produces a `_tree_access` jsonb (rows of `tree_path` + `access`) passed into the space SQL functions (`search_memory`, `get_memory`, …). Three additive levels: **1 = read, 2 = write, 3 = owner**; `owner@root` (the empty ltree path) owns the whole space, and an owner grant at any path delegates access-management within that subtree. Two axes: **structural** authority (`principal_space.admin` — roster mutations, groups, invitations) vs **data** authority (owner@path); an admin may also grant data and can self-grant `owner@root`. Granting requires owner@path (or space admin). Service accounts have no owner clamp; revocation of a service-account grant is also allowed for direct user members of the service account's bound admin group. Endpoint admission is direct `principal_space` membership, so a member may authenticate with zero tree grants; data access is still filtered by `build_tree_access`. +- **Tree conventions**: two reserved roots — per-member `home.` (`~` is input sugar for it; a joining **user** is granted `owner@home.`) and the shared `share`. Service accounts have no home grant and no defined `~` home; they get zero tree access until explicitly granted access directly or through a group. A space **creator** gets `admin` + `owner@home` + `owner@share`, **not** `owner@root` — so it sees `share` and its own `~` but not other members' homes (as an admin it can self-grant `owner@root`). **Custom spaces** vary these provisioning defaults via `me space create` flags, resolved in `addSpaceCreator`: `--no-home-grants` sets the space column `space.auto_grant_home = false` (read by `add_principal_to_space`, so **every** join path stops seeding `owner@~` for users) and flips the creator to **god mode** (`admin` + `owner@/`); the default group is controlled by whether `provision_default_group` is called (`--no-default-group`), its name (`--default-group `), and whether it's seeded with grants (`--no-default-group-grants`) — the chosen group is flagged `principal.is_default_group` (one per space, partial-unique; rename/delete-robust; surfaced on the space read as `defaultGroup` and used as the `me space invite` default). There is no `auto_grant_team` column — group grants are just `tree_access` rows. `memory.create`/`batchCreate` **require** an explicit `tree` (callers choose `share` vs `~` deliberately); only the file importers (`me import memories`, the `me_memory_import` MCP tool) default a tree-less record to `share` (`SHARE_NAMESPACE`, canonically defined in `@memory.build/protocol` and re-exported by `@memory.build/database`). - **API**: JSON-RPC 2.0 over HTTP, two endpoints: - - `/api/v1/memory/rpc` — OAuth access token **or** api-key bearer (agent or user PAT) **or** cookie session, + required `X-Me-Space: ` header. Memory data plane (`memory.*`) + space management (`principal.*`, `group.*`, `grant.*`, `invite.*`). - - `/api/v1/user/rpc` — OAuth token, cookie, **or the user's own api key (PAT)**; an **agent** key is barred here (agents can't manage the account), and `apiKey.create`/`delete` are session-only (keys can't mint keys). `whoami`, `agent.*`, `apiKey.*`, `space.*`. - - `/api/v1/auth/*` is owned end-to-end by **better-auth** (social sign-in, OAuth 2.1 authorize/token, sessions/sign-out) — see `AUTH_DESIGN.md`. The **web UI is served at root `/`** (any non-`/api` GET → static assets / SPA fallback), including the `/login` page the CLI authorize flow redirects to. -- **Auth**: humans authenticate via **OAuth (GitHub/Google)** → a **better-auth session** — an httpOnly cookie for the web UI, and for the CLI an OAuth 2.1 authorization-code + PKCE loopback flow (RFC 8252) yielding access/refresh tokens. For **headless** CLI use (a sandboxed harness / no local browser), `me login --device` runs the OAuth 2.0 **Device Authorization Grant** (RFC 8628) via better-auth's `deviceAuthorization` plugin: the CLI polls `/api/v1/auth/device/{code,token}` while the human approves the `user_code` at the web `/device` page; on approval it mints a better-auth **session** (no refresh token — a 7-day sliding session), presented back as an `Authorization: Bearer` token and accepted by the resource-server middleware via the `bearer` plugin (`getSession`). Users headlessly, agents, and service accounts can also use an **api key** (`me..`: user PAT, agent key, or service-account key). Api keys are **global** per-principal credentials, not space-bound: the same key works in any space the principal has been admitted to (the space comes from `X-Me-Space`, gated by `build_tree_access`). Service accounts are space-scoped principals, so their keys are useful only in that service account's space. better-auth owns session + OAuth-token storage (OAuth access/refresh tokens hashed sha256 at rest); api-key secrets are **core** sha256 (compared by equality in SQL), not argon2. Full design: `AUTH_DESIGN.md`. -- **Act as agent (`X-Me-As-Agent`)**: honored on **both** RPC endpoints. A human credential (session / OAuth / user PAT) may send `X-Me-As-Agent: ` to run as one of its **own** agents — the server resolves it against `core.listAgents(caller)` by id or case-insensitive name, rejects ambiguous matches, overwrites the resolved principal to the agent, and authorizes the request as that agent (the **parity invariant**: exactly what the agent's own `ME_API_KEY` could do — memory RPC clamps `treeAccess`/`admin`; user RPC's `AGENT_ALLOWED` allow-list means **management ops fail**). For a bare `--as-agent`/`ME_AS_AGENT` on the CLI, agent mode is **ambient** and always **explicit**: turn it on with `--as-agent ` (a required-value global flag) or `ME_AS_AGENT`, where the literal **`.me` sentinel** sources the id from `.me/config.yaml`'s `agent`, else the global config's `agent` (client-side; never sent). A `.me`/global `agent` alone never activates *this* mode. An **agent api key trumps the header** (the bearer already *is* an agent → header ignored); an unowned/unknown value → 403 `INVALID_AGENT`. The human is recorded as `authenticatedAs` for observability only (never gates authz). Header constant: `AS_AGENT_HEADER` in `@memory.build/protocol/headers`; carried on `ResolvedCredentials.asAgent`. -- **Agent-by-config (harness surfaces)**: `me mcp` and the capture hooks (`me hook`) are the one exception to "activation is always explicit" above — they resolve `resolveHarnessAgent()` UNCONDITIONALLY (project `agent:` → global `agent:` → fatal), as if `--as-agent .me` were passed, because a harness surface has no human caller for "no agent" to safely default to. `me mcp` validates the resolved agent eagerly at startup (one `whoami` round trip) so a dead config fails at launch, not on every tool call. A plain `me` call from a harness's own shell (Claude, opencode, Codex, Gemini CLI) gets the same resolution via an **injected environment contract** (`ME_INJECT_V`/`AI_AGENT`/`ME_AS_AGENT=.me`/`ME_PROJECT_DIR`, see `packages/cli/harness-contract.ts`) that each harness adapter writes into every shell command — Claude via a SessionStart hook appending to `$CLAUDE_ENV_FILE` (`me claude env`), opencode via its plugin's `shell.env` hook, and Codex/Gemini by REWRITING the command string itself (`me codex env-hook` / `me gemini env-hook`, prepending an `export …; ` prefix via `renderExportPrefix` — see `packages/cli/codex/env-hook.ts` / `packages/cli/gemini/env-hook.ts`; both fail open and log an unrecognized payload's STRUCTURE, never its content, via `packages/cli/harness-shape-log.ts`, for a later `me doctor` to surface). `packages/cli/failsafe.ts` + `packages/cli/harness-detect.ts` (wrapping `@vercel/detect-agent`) hard-error a harness-run `me` when that contract is missing (an uninstalled adapter, untrusted Codex hooks, an unintegrated harness), except in an interactive TTY (an IDE integrated terminal — treated as human) or with an explicit `--as-agent`/agent api key. `.me/config.yaml`'s `agent: .user` and the global config's `agent: .user` both mean "run as the user, deliberately"; **only** the committed `.me/config.yaml` rejects `.user` (fatal `ProjectConfigError` — see `PROJECT_USER_SENTINEL` in `project-config.ts`), since that's the one value that would silently *raise* a cloning teammate's privilege. `ensureDefaultAgent()` (`packages/cli/agent/default-agent.ts`) provisions-or-adopts a `coder` agent and writes it as the global fallback at each harness's `install` time (skippable with `--no-default-agent`), so "no agent anywhere" is rare in practice. Codex/Gemini capture hooks and `me doctor` are deferred to a later PR. Full design: `HARNESS_DESIGN.md`. + - `/api/v1/memory/rpc` — OAuth access token **or** api-key bearer (user PAT or service-account key) **or** cookie session, + required `X-Me-Space: ` header. Memory data plane (`memory.*`) + space management (`principal.*`, `group.*`, `grant.*`, `invite.*`). + - `/api/v1/user/rpc` — OAuth token, cookie, **or the user's own api key (PAT)**; a **service-account** key is admitted only for allow-listed reads (`whoami`, `space.list`), and `apiKey.create`/`delete` are session-only (keys can't mint keys). `whoami`, `apiKey.*`, `serviceAccount.*`, `space.*`. + - `/api/v1/auth/*` is owned end-to-end by **better-auth** (social sign-in, OAuth 2.1 authorize/token, sessions/sign-out). The **web UI is served at root `/`** (any non-`/api` GET → static assets / SPA fallback), including the `/login` page the CLI authorize flow redirects to. +- **Auth**: humans authenticate via **OAuth (GitHub/Google)** → a **better-auth session** — an httpOnly cookie for the web UI, and for the CLI an OAuth 2.1 authorization-code + PKCE loopback flow (RFC 8252) yielding access/refresh tokens. For **headless** CLI use (a sandboxed harness / no local browser), `me login --device` runs the OAuth 2.0 **Device Authorization Grant** (RFC 8628) via better-auth's `deviceAuthorization` plugin: the CLI polls `/api/v1/auth/device/{code,token}` while the human approves the `user_code` at the web `/device` page; on approval it mints a better-auth **session** (no refresh token — a 7-day sliding session), presented back as an `Authorization: Bearer` token and accepted by the resource-server middleware via the `bearer` plugin (`getSession`). Users headlessly and service accounts can also use an **api key** (`me..`: user PAT or service-account key). Api keys are **global** per-principal credentials, not space-bound: the same key works in any space the principal has been admitted to (the space comes from `X-Me-Space`, gated by `build_tree_access`). Service accounts are space-scoped principals, so their keys are useful only in that service account's space. better-auth owns session + OAuth-token storage (OAuth access/refresh tokens hashed sha256 at rest); api-key secrets are **core** sha256 (compared by equality in SQL), not argon2. +- **Harness surfaces (`me mcp`, capture hooks, harness shells)**: a plain `me` invocation always runs as the principal represented by the presented credential (session, OAuth token, user PAT, or service-account key). There is no impersonation, no `--as-agent`, no `ME_AS_AGENT`, and no `X-Me-As-Agent`. The one thing a harness still injects into every shell command is the discovery anchor `ME_PROJECT_DIR` — set by Claude's SessionStart hook (`me claude env` → `$CLAUDE_ENV_FILE`), opencode's `shell.env` plugin hook, and Codex/Gemini's command-string rewrites (`me codex env-hook` / `me gemini env-hook`, prepending an `export …; ` prefix via `renderExportPrefix` — see `packages/cli/codex/env-hook.ts` / `packages/cli/gemini/env-hook.ts`; both fail open and log an unrecognized payload's STRUCTURE, never its content, via `packages/cli/harness-shape-log.ts`, for a later `me doctor` to surface). The contract lives in `packages/cli/harness-contract.ts` and consists of two vars: `AI_AGENT` (inert identity metadata, per the `@vercel/detect-agent` convention) and `ME_PROJECT_DIR` (the discovery anchor `me` walks up from). Legacy compat: `X-Me-As-Agent` is silently ignored server-side; a legacy `agent:` field in `.me/config.yaml` or the global `~/.config/me/config.yaml` still validates and is preserved on write, but has no effect. - **Embedding**: Vercel AI SDK; OpenAI `text-embedding-3-small` (1536-dim) in production; Ollama supported for local dev. -- **CLI**: `me` binary — `login`, `logout`, `whoami`, `space`, `group`, `access`, `agent`, `service`, `apikey`, `memory` (+ top-level aliases like `me search`, `me create` — except `import`), `import` (the source group: `memories`/`claude`/`codex`/`opencode`/`granola`/`git`/`docs`/`slab`; `me memory import` and `me import` remain as aliases), `mcp`, `project` (per-project setup: `me project init`), `claude`/`codex`/`gemini`/`opencode`, `serve`, `pack`. +- **CLI**: `me` binary — `login`, `logout`, `whoami`, `space`, `group`, `access`, `service`, `apikey`, `memory` (+ top-level aliases like `me search`, `me create` — except `import`), `import` (the source group: `memories`/`claude`/`codex`/`opencode`/`granola`/`git`/`docs`/`slab`; `me memory import` and `me import` remain as aliases), `mcp`, `project` (per-project setup: `me project init`), `claude`/`codex`/`gemini`/`opencode`, `serve`, `pack`. ## Principals, members, spaces (terminology) -- **Principal** = the union **user | agent | group** (`principal.kind` = `'u'` | `'a'` | `'g'`). The space roster (`principal_space`) holds principals — users, agents, **and groups** (a group is rostered into its space on creation, so `principal_space` is the single source of truth for who/what belongs to a space). `principal.member_id` is a generated column equal to `id` for users/agents (NOT groups). -- **Member** = the **user/agent** sense only — group members and api-key holders. So params split as `principalId` (roster / grants, any kind) vs `memberId` (group membership, api keys; u|a only). The space-roster surface is principal-centric (`principal.*` methods, `SpacePrincipal` type), reserving "member" for u|a. +- **Principal** = the union **user | group | service account** (`principal.kind` = `'u'` | `'g'` | `'s'`). The space roster (`principal_space`) holds principals — users, service accounts, **and groups** (a group is rostered into its space on creation, so `principal_space` is the single source of truth for who/what belongs to a space). `principal.member_id` is a generated column equal to `id` for users/service accounts (NOT groups). +- **Member** = the **user/service-account** sense only — group members and api-key holders. So params split as `principalId` (roster / grants, any kind) vs `memberId` (group membership, api keys; u|s only). The space-roster surface is principal-centric (`principal.*` methods, `SpacePrincipal` type), reserving "member" for u|s. - **Space**: identified by an immutable 12-char `slug` (which is the `me_` schema name and the `X-Me-Space` value) and a renamable `name`. `me space rename` changes only the name. No org / engine / shard concepts. -- **Admin**: `principal_space.admin` is *structural* authority — roster mutations (`principal.add`/`remove`), groups, and invitations (`invite.*`) — distinct from data ownership (owner@path via `tree_access`). Enumerating the **full** roster (`principal.list` — includes groups plus each principal's `admin`/`owner_id`/timestamps) is admin-only; **any member** may `principal.resolve`/`lookup` (a targeted name↔id lookup) and `space.listMembers` (a **minimal** member listing — `id`/`kind`/`name`, u/a/s only, groups excluded). Member-level enumeration is deliberate: any member can already grant a path they own to any other member (`me access grant`) and a group admin (who need not be a space admin) must find who's addable (`me group add`), so discovering fellow members — but not admin metadata or group roster entries — is a member capability. Admin transfers through an admin group (a group whose own `principal_space.admin` is true) to its members who are **also direct space members**; agents are never admins, and service accounts may be made direct space admins but are discouraged and never count toward the last-admin invariant. A group is created non-admin and promoted/demoted via `set_group_is_space_admin` (`group.setIsSpaceAdmin` / `me group set-space-admin`), or created admin directly (`create_group(..., _is_space_admin)` / `me group create --space-admin`); demotion is gated by `enforce_last_admin`. (Distinct from a group *member's* admin flag, `group_member.admin`, which governs the group's own membership; service accounts may hold this flag on ordinary groups, agents cannot exercise it.) A space must always keep ≥1 *effective* admin (a **user** who is a direct admin or a direct member of an admin group — a group with no direct-member users doesn't count) — the `enforce_last_admin` trigger on `principal_space` + `group_member` rejects any remove/demote/group-member-removal that would drop the last one (SQLSTATE `ME001` → `LAST_ADMIN`), but exempts whole-space deletion. -- **Membership is explicit, not transitive**: a user/agent/service-account's space membership is their own `principal_space` row, full stop — group membership alone never confers it. (Groups themselves *are* rostered — a group gets its own `principal_space` row on creation, which is what makes it resolvable/grantable by name — but that is the group's **own** roster entry, independent of conferral: being in a group never makes a *user/agent/service-account* a space member.) A group's grants (and admin, if it's an admin group) are *effective* only for members who **also** hold a `principal_space` row; the gate lives in `member_tree_access`, so `build_tree_access` is empty for a non-member even if group rows were pre-staged. Joining (invite redemption / direct add / service-account creation) is the membership path. An admin can still **pre-stage** a member into a group before they join (`add_group_member` doesn't require membership); the group's grants stay dormant until they join. `remove_principal_from_space` scrubs the member's `group_member` rows along with the membership, and — when the removed principal is a **user** — cascades to the agents that user owns, deprovisioning them **from that space** too (their `principal_space`/`tree_access`/`group_member` rows; the agent `principal` rows and their other spaces are untouched), so an owner leaving never orphans a still-rostered agent. That cascade lives in the DB function, so every caller inherits it: an admin `principal.remove` (`me space remove-member`) and a member's own `me space leave`. Service accounts are space-scoped and deleted through `serviceAccount.delete`, which also deletes their bound admin group. Removal is admin-gated with two self-service exceptions mirroring `principal.add`'s own-agent carve-out — a user removing **themselves** (`me space leave`) and a member removing **their own agent** (`me agent remove`) — both still bounded by the `enforce_last_admin` guard (a sole admin's self-leave → `LAST_ADMIN`). Groups are never removed this way (they leave only via deletion). +- **Admin**: `principal_space.admin` is *structural* authority — roster mutations (`principal.add`/`remove`), groups, and invitations (`invite.*`) — distinct from data ownership (owner@path via `tree_access`). Enumerating the **full** roster (`principal.list` — includes groups plus each principal's `admin`/timestamps) is admin-only; **any member** may `principal.resolve`/`lookup` (a targeted name↔id lookup) and `space.listMembers` (a **minimal** member listing — `id`/`kind`/`name`, u/s only, groups excluded). Member-level enumeration is deliberate: any member can already grant a path they own to any other member (`me access grant`) and a group admin (who need not be a space admin) must find who's addable (`me group add`), so discovering fellow members — but not admin metadata or group roster entries — is a member capability. Admin transfers through an admin group (a group whose own `principal_space.admin` is true) to its members who are **also direct space members**; service accounts may be made direct space admins but are discouraged and never count toward the last-admin invariant. A group is created non-admin and promoted/demoted via `set_group_is_space_admin` (`group.setIsSpaceAdmin` / `me group set-space-admin`), or created admin directly (`create_group(..., _is_space_admin)` / `me group create --space-admin`); demotion is gated by `enforce_last_admin`. (Distinct from a group *member's* admin flag, `group_member.admin`, which governs the group's own membership.) A space must always keep ≥1 *effective* admin (a **user** who is a direct admin or a direct member of an admin group — a group with no direct-member users doesn't count) — the `enforce_last_admin` trigger on `principal_space` + `group_member` rejects any remove/demote/group-member-removal that would drop the last one (SQLSTATE `ME001` → `LAST_ADMIN`), but exempts whole-space deletion. +- **Membership is explicit, not transitive**: a user/service-account's space membership is their own `principal_space` row, full stop — group membership alone never confers it. (Groups themselves *are* rostered — a group gets its own `principal_space` row on creation, which is what makes it resolvable/grantable by name — but that is the group's **own** roster entry, independent of conferral: being in a group never makes a *user/service-account* a space member.) A group's grants (and admin, if it's an admin group) are *effective* only for members who **also** hold a `principal_space` row; the gate lives in `member_tree_access`, so `build_tree_access` is empty for a non-member even if group rows were pre-staged. Joining (invite redemption / direct add / service-account creation) is the membership path. An admin can still **pre-stage** a member into a group before they join (`add_group_member` doesn't require membership); the group's grants stay dormant until they join. `remove_principal_from_space` scrubs the member's `group_member` rows along with the membership. Service accounts are space-scoped and deleted through `serviceAccount.delete`, which also deletes their bound admin group. Removal is admin-gated with one self-service exception — a user removing **themselves** (`me space leave`) — still bounded by the `enforce_last_admin` guard (a sole admin's self-leave → `LAST_ADMIN`). Groups are never removed this way (they leave only via deletion). ## Project Structure @@ -190,11 +189,10 @@ reset when nothing else is using the database. `packages/cli/harness-smoke/*.smoke.ts` launch a real harness binary (`claude` and `opencode` verified live; `gemini` is scaffolded but unverified — see each file's module doc) non-interactively and check that -the injected environment contract -(`ME_INJECT_V`/`AI_AGENT`/`ME_AS_AGENT`/`ME_PROJECT_DIR`) actually lands in a -real shell command's real environment. They exist because `./bun run -check`/`check:full`/CI only exercise the decision logic (what a hook -*should* output) — nothing runs an actual harness end-to-end. +the injected environment contract (`AI_AGENT`/`ME_PROJECT_DIR`) actually +lands in a real shell command's real environment. They exist because +`./bun run check`/`check:full`/CI only exercise the decision logic (what +a hook *should* output) — nothing runs an actual harness end-to-end. **No `codex.smoke.ts`, deliberately**: Codex trusts a hook by the hash of its definition text and gates a new/changed one behind an interactive @@ -224,7 +222,7 @@ you expected to run reports 0 tests exercised. Each test builds a scratch project dir and a scratch `me` binary (shadowing whatever `me` is actually installed on `PATH`, via `writeMeWrapper()` in `_shared.ts`) so it always exercises the CURRENT checkout's code, never a stale global install — and -strips the four contract vars from its own process env before spawning +strips the contract vars from its own process env before spawning (`cleanEnv()`), since this very test suite may itself be running inside a live-injected harness session. @@ -276,10 +274,10 @@ wrap a deliberately overloaded name like `count_tree`. - **Single memory table per space**: all memory lives in `me_.memory`. Complexity comes from conventions in `meta` and `tree`, not schema proliferation. - **Database-native**: PostgreSQL extensions (ltree, pgvector/halfvec, JSONB GIN, tstzrange, BM25) instead of application-layer abstractions. - **Access via `tree_access`, not RLS**: RLS was unperformant. `build_tree_access` produces a `_tree_access` jsonb passed into the space functions; there is no `me.user_id` GUC. Three levels (read/write/owner); an owner grant delegates access-management within its subtree (owner@root = the whole space). -- **Two endpoints, two auth modes**: memory RPC (any bearer — OAuth token or api key — or cookie, + `X-Me-Space`) vs user RPC (OAuth token, cookie, or the user's own PAT; agent keys barred; key mint/revoke session-only). The middleware resolves the credential via `extractBearerToken` (api key → `core.validateApiKey`, else → `verifyOAuthToken`) and falls back to `betterAuth.api.getSession` for the cookie (Origin-CSRF-gated). +- **Two endpoints, two auth modes**: memory RPC (any bearer — OAuth token or api key — or cookie, + `X-Me-Space`) vs user RPC (OAuth token, cookie, or the user's own PAT; service-account keys are admitted only for allow-listed reads; key mint/revoke session-only). The middleware resolves the credential via `extractBearerToken` (api key → `core.validateApiKey`, else → `verifyOAuthToken`) and falls back to `betterAuth.api.getSession` for the cookie (Origin-CSRF-gated). - **Hosted web UI** (same `packages/web` build, two modes): `me serve` proxies `/rpc` locally (browser carries no creds); the API server also serves the UI at root (`https://api.memory.build/` in production) with **httpOnly-cookie** browser login (cookie carries the same session token; Origin-allowlist CSRF gate for cookie creds). Full design + runbook: `DEVELOPMENT.md` → "Hosted web UI" and the `DECISIONS_FOR_REVIEW.md` entry. -- **Principal vs member** terminology (see above): principal = u|a|s|g; member/`memberId` = u|a|s. -- **CLI credentials**: split across `~/.config/me/` — **`config.yaml`** (non-secret: default server + per-server **active space** / the X-Me-Space) and **`credentials.yaml`** (0600, secret session-token *fallback* only). The **session token** lives in the OS keychain when available (macOS `security`, Linux `secret-tool` via libsecret; `ME_NO_KEYCHAIN=1` forces off), else in `credentials.yaml` (empty/absent on keychain hosts); a pre-split `credentials.yaml` is migrated on first read. `me logout` clears the session secret but keeps the non-secret config (so re-login resumes). **Api keys are never persisted** — user PATs, agent keys, and service-account keys only ever come from `ME_API_KEY`/`--api-key`; `apiKey.create` prints the key once for the operator to place where it runs. Env: `ME_SERVER` / `ME_API_KEY` / `ME_SPACE` / `ME_SESSION_TOKEN` / `ME_NO_KEYCHAIN` / `ME_CONFIG_DIR` / `ME_PROJECT_DIR`. A per-project **`.me/config.yaml`** (walk-up from cwd, or `--config-dir`/`ME_CONFIG_DIR` for an exact dir, or `--project-dir`/`ME_PROJECT_DIR` — an ANCHOR that replaces cwd as the walk-up origin, set by harness adapters into every shell command they run) pins `server` + `space` + `agent` (+ optional full no-slug `tree` for integrations); a gitignored `.me/config.local.yaml` overrides it per-field. Below cwd walk-up sits a validated last-resort backstop (today: Claude's `CLAUDE_PROJECT_DIR`, accepted only if it contains `.me/` — it mis-resolves to the main checkout under `claude -w`, so it's demoted below cwd on purpose). Precedence: `--flag > ME_* env > .me (.local > committed) > global config.yaml > default`. Resolution lives in `packages/cli/project-config.ts`, wired into `resolveServer`/`resolveSpace`/`resolveCredentials` (so `me mcp` inherits it) and the capture hooks / `me import git` (which use the `tree` as the project root, no slug appended). **Credential-safety gate**: a `.me`-sourced `server` is only honored if it's in a **trusted list** (`DEFAULT_TRUSTED_SERVERS` = prod + dev, extendable via `server_whitelist` in the global config, auto-extended by `me login --server`) — otherwise a fatal `ProjectConfigError`, so an untrusted repo's `.me` can't redirect a global api key / `ME_SESSION_TOKEN` to an attacker (`--server`/`ME_SERVER`/stored `default_server` are the user's own choice, ungated). See `docs/project-config.md`. +- **Principal vs member** terminology (see above): principal = u|s|g; member/`memberId` = u|s. +- **CLI credentials**: split across `~/.config/me/` — **`config.yaml`** (non-secret: default server + per-server **active space** / the X-Me-Space) and **`credentials.yaml`** (0600, secret session-token *fallback* only). The **session token** lives in the OS keychain when available (macOS `security`, Linux `secret-tool` via libsecret; `ME_NO_KEYCHAIN=1` forces off), else in `credentials.yaml` (empty/absent on keychain hosts); a pre-split `credentials.yaml` is migrated on first read. `me logout` clears the session secret but keeps the non-secret config (so re-login resumes). **Api keys are never persisted** — user PATs and service-account keys only ever come from `ME_API_KEY`/`--api-key`; `apiKey.create` prints the key once for the operator to place where it runs. Env: `ME_SERVER` / `ME_API_KEY` / `ME_SPACE` / `ME_SESSION_TOKEN` / `ME_NO_KEYCHAIN` / `ME_CONFIG_DIR` / `ME_PROJECT_DIR`. A per-project **`.me/config.yaml`** (walk-up from cwd, or `--config-dir`/`ME_CONFIG_DIR` for an exact dir, or `--project-dir`/`ME_PROJECT_DIR` — an ANCHOR that replaces cwd as the walk-up origin, set by harness adapters into every shell command they run) pins `server` + `space` (+ optional full no-slug `tree` for integrations); a gitignored `.me/config.local.yaml` overrides it per-field. A legacy `agent:` field is still accepted and preserved on write for backwards compatibility, but has no effect. Below cwd walk-up sits a validated last-resort backstop (today: Claude's `CLAUDE_PROJECT_DIR`, accepted only if it contains `.me/` — it mis-resolves to the main checkout under `claude -w`, so it's demoted below cwd on purpose). Precedence: `--flag > ME_* env > .me (.local > committed) > global config.yaml > default`. Resolution lives in `packages/cli/project-config.ts`, wired into `resolveServer`/`resolveSpace`/`resolveCredentials` (so `me mcp` inherits it) and the capture hooks / `me import git` (which use the `tree` as the project root, no slug appended). **Credential-safety gate**: a `.me`-sourced `server` is only honored if it's in a **trusted list** (`DEFAULT_TRUSTED_SERVERS` = prod + dev, extendable via `server_whitelist` in the global config, auto-extended by `me login --server`) — otherwise a fatal `ProjectConfigError`, so an untrusted repo's `.me` can't redirect a global api key / `ME_SESSION_TOKEN` to an attacker (`--server`/`ME_SERVER`/stored `default_server` are the user's own choice, ungated). See `docs/project-config.md`. - **Header constants** (`CLIENT_VERSION_HEADER`, `SPACE_HEADER`) live in `@memory.build/protocol/headers`. - **Error reporting**: always import `reportError` from `@memory.build/database/telemetry`, never `@pydantic/logfire-node` directly. The wrapper writes the full error (stack + nested pg `cause`) to **stderr synchronously** *and* forwards to logfire — so a crash that exits before logfire's buffered OTLP exporter flushes (e.g. a failed boot migration) still leaves the cause in `kubectl logs`. (logfire's console processor is span-only, so a bare `reportError` is invisible on a crash.) `info` / `span` / `warning` stay imported directly from logfire. - **MCP compatibility**: all tool parameters are required (nullable for optional). Uses `z.record(z.string(), z.any())` for meta instead of `z.record(z.unknown())` (which crashes the MCP SDK). diff --git a/TNT-244_RELEASE.md b/TNT-244_RELEASE.md new file mode 100644 index 00000000..d92b8d20 --- /dev/null +++ b/TNT-244_RELEASE.md @@ -0,0 +1,153 @@ +# TNT-244 Release Runbook + +TNT-244 removes agent principals, agent API keys, and impersonation from Memory +Engine. Release all committed phases together. Do not deploy an intermediate +commit. + +## Before Release + +1. Confirm normal production backups have completed. +2. Notify the four holders of retired agent API keys. Their keys will stop + working after the migration; direct them to use a user PAT or service-account + key as appropriate. +3. Record the current agent and agent-key inventory: + + ```sql + -- (a) the agents themselves + select p.id, p.name, p.owner_id + from core.principal p + where p.kind = 'a' + order by p.name; + + -- (b) the api keys bound to those agents + select k.id, k.name, k.member_id, k.restricted + from core.api_key k + join core.principal p on p.id = k.member_id + where p.kind = 'a' + order by k.name; + + -- (c) restricted-key declarations that will cascade with the key row + select s.api_key_id, s.space_id, s.space_admin + from core.api_key_space_access s + join core.api_key k on k.id = s.api_key_id + join core.principal p on p.id = k.member_id + where p.kind = 'a'; + + select t.api_key_id, t.space_id, t.tree_path::text, t.access + from core.api_key_tree_access t + join core.api_key k on k.id = t.api_key_id + join core.principal p on p.id = k.member_id + where p.kind = 'a'; + ``` + +4. Record the memory count at every retired agent-home path. These rows must + remain in their existing paths after the migration. For each `me_` + space schema and each `(owner_id, agent_id)` from step 3(a), run a variant + of the following (agent-home paths follow `home..` + with hyphens stripped): + + ```sql + -- Replace , , as needed. + -- `<@` is ltree "descendant of", so this counts the home node itself + -- plus everything beneath it. + select tree::text, count(*) as memory_count + from me_.memory + where tree <@ 'home..'::ltree + group by tree + order by tree; + ``` + + For a scripted sweep across every space, iterate over + `select slug from core.space` and paste the resulting counts into the + release ticket for step 3 of "After Release" to compare against. + +5. Confirm expected user, group, and service-account access with a representative + account in each production space. + +6. **Effective-admin preflight — must return 0.** Application code has never + granted an agent `principal_space.admin`, but the DB does not forbid it. + Migration 018 deletes agent principals in one statement, which fires the + deferred `enforce_last_admin` trigger; if any space's *only* remaining + effective admin were an agent, the trigger would abort the migration with + `LAST_ADMIN`. Verify pre-emptively: + + ```sql + select count(*) + from core.principal_space ps + join core.principal p on p.id = ps.principal_id + where ps.admin and p.kind = 'a'; + ``` + + Any non-zero result means a space would otherwise be left without a user + admin — resolve by promoting a user admin (`me group set-space-admin`, or + a direct roster admin row) before the migration runs. + +## Release + +1. Merge the complete TNT-244 change set. +2. Run the server release as `0.7.0`. **At the `Bump MIN_CLIENT_VERSION?` + prompt, enter `0.7.0`** — do not press Enter to keep the default. This is + what makes older 0.6.x clients incompatible with the new server, matching + step 4's expectation. +3. Run the client release as `0.7.0`. **At the `Bump MIN_SERVER_VERSION?` + prompt, enter `0.7.0`** — again, do not press Enter to keep. +4. Treat the two releases as one maintenance-window deployment. Versioned + `0.6.2` clients are intentionally incompatible with the new server; + clients without a version header remain subject to the existing lenient + behavior. + +**Maintenance-window lock note.** Migration 018 issues +`ALTER TABLE core.principal ALTER COLUMN member_id SET EXPRESSION AS (...)`, +which takes an ACCESS EXCLUSIVE lock on `core.principal` and rewrites the +table (all row values recompute to the same values in practice, so it is +short — but it is a full-table event). This is why the two package releases +are done inside one maintenance window. + +## After Release + +1. Verify no agent principals remain: + + ```sql + select count(*) from core.principal where kind = 'a'; + ``` + +2. Verify no API keys belong to retired agents (and no scoped declarations + linger): + + ```sql + select count(*) + from core.api_key k + join core.principal p on p.id = k.member_id + where p.kind = 'a'; + + -- Both should also be 0 (cascaded from api_key.id): + select count(*) from core.api_key_space_access s + where not exists (select 1 from core.api_key k where k.id = s.api_key_id); + select count(*) from core.api_key_tree_access t + where not exists (select 1 from core.api_key k where k.id = t.api_key_id); + ``` + +3. Recheck the recorded agent-home memory counts and paths (step 4 above). + Each `(tree, count)` pair must match the pre-release capture; no rows + should have moved or been rewritten. + +4. Verify no `core.tree_access` row references a deleted principal (a smoke + check on the cascade — should always be 0): + + ```sql + select count(*) from core.tree_access ta + where not exists (select 1 from core.principal p where p.id = ta.principal_id); + ``` + +5. Verify normal user-session, restricted-PAT, and service-account-key access + with MCP and CLI operations. + +6. Confirm legacy `X-Me-As-Agent` is ignored, and that old agent keys receive + the normal invalid-key error. + +7. Watch error logs and authentication failures during the maintenance window. + +## Rollback + +Do not attempt a schema-only rollback after migration 018 has run. Restore from +the normal production backup if rollback is required. diff --git a/docs/access-control.md b/docs/access-control.md index 141cafcf..fda8e8ea 100644 --- a/docs/access-control.md +++ b/docs/access-control.md @@ -4,24 +4,23 @@ Memory Engine organizes knowledge into **spaces**. Access within a space is gran ## Principals -A **principal** is anything that can be granted access. There are four kinds: +A **principal** is anything that can be granted access. There are three kinds: | Kind | What it is | |------|------------| | **user** (`u`) | A human, authenticated by a session token (OAuth via GitHub or Google). | -| **agent** (`a`) | A non-human agent owned by one user, authenticated by an API key. Its effective access is clamped to its owner's. | | **service account** (`s`) | A space-scoped operational identity for CI/CD, webhooks, and team-owned integrations. It authenticates by API key and has no owner clamp. | -| **group** (`g`) | A named bundle of users, agents, and service accounts. | +| **group** (`g`) | A named bundle of users and service accounts. | -A **member** is the user/agent/service-account sense only — the things that can be put into a group or hold an API key. Group membership does **not** by itself confer space membership: a group's grants (and its admin, if it's an admin group) apply to you only once you have **also** joined the space directly. So an admin can add you to a group before you join — the group's access stays dormant until you do. +A **member** is the user/service-account sense only — the things that can be put into a group or hold an API key. Group membership does **not** by itself confer space membership: a group's grants (and its admin, if it's an admin group) apply to you only once you have **also** joined the space directly. So an admin can add you to a group before you join — the group's access stays dormant until you do. -A group is itself part of its space's roster (it gets a roster entry when created), which is what lets you grant access to it or reference it **by name**. That roster entry is the group's own; it is separate from membership conferral, which still depends on each user/agent/service-account having joined the space directly. +A group is itself part of its space's roster (it gets a roster entry when created), which is what lets you grant access to it or reference it **by name**. That roster entry is the group's own; it is separate from membership conferral, which still depends on each user/service-account having joined the space directly. ### Service accounts A **service account** is a durable operational identity administered by a team, not by one human owner. Create one with [`me service`](cli/me-service.md), then mint a key with `me apikey create --service `. Service accounts are useful for CI/CD jobs, importers, webhooks, and other team-owned integrations. -Each service account has a **bound admin group**. Space admins can manage all service accounts; direct user members of that bound admin group can administer that service account — renaming it, deleting it, and managing its API keys. Users, agents, and service accounts may all be members of the bound group, but only users count as service-account admins. The service account is not automatically added to its own bound admin group, and grants to that group behave like ordinary group grants. +Each service account has a **bound admin group**. Space admins can manage all service accounts; direct user members of that bound admin group can administer that service account — renaming it, deleting it, and managing its API keys. Users and service accounts may be members of the bound group, but only users count as service-account admins. The service account is not automatically added to its own bound admin group, and grants to that group behave like ordinary group grants. Service accounts start with **zero tree access**: no home grant, no default-group membership, and no owner clamp. Grant access to the service account directly, or add it to ordinary groups. A service-account key can use the memory and group/grant authorities the service account actually holds. By default it cannot create invitations; if you explicitly make the service account a space admin, it can use admin-gated invitation operations. It still cannot mint or revoke API keys or delete spaces. @@ -35,7 +34,7 @@ An API key created with [`me apikey create --allow`](cli/me-apikey.md#me-apikey- - `--space-admin ` permits space-admin authority only when the holder is also an effective admin in that space. - Restricted declarations are immutable. Rotate the key: create a replacement, deploy and verify it, then revoke the old key. -Agent keys cannot be restricted; use an agent's regular grants to limit an agent. Restricted user PATs cannot manage the account, including minting, revoking, or inspecting API keys. +Restricted user PATs cannot manage the account, including minting, revoking, or inspecting API keys. ## Spaces @@ -50,7 +49,7 @@ A user can belong to many spaces; each memory lives in exactly one space. There Access splits into two independent axes: -- **Structural authority** — `me space invite`, the roster (`me agent add`, `me service ...`, `me group ...`), and invitations. This is the space **admin** flag. Admin transfers through an **admin group** to its members who are also direct space members. Designate one with `me group create --space-admin` or `me group set-space-admin ` (revoke with `--off`); `me group list` shows which groups are admin groups. Agents are never admins. Service accounts can be made space admins explicitly, but this is discouraged and they do not count toward the last-admin safeguard. +- **Structural authority** — `me space invite`, the roster (`me service ...`, `me group ...`), and invitations. This is the space **admin** flag. Admin transfers through an **admin group** to its members who are also direct space members. Designate one with `me group create --space-admin` or `me group set-space-admin ` (revoke with `--off`); `me group list` shows which groups are admin groups. Service accounts can be made space admins explicitly, but this is discouraged and they do not count toward the last-admin safeguard. - **Data authority** — who can read/write/own memories at a given tree path. This is a **tree-access grant**. A space must always keep at least one *effective* human admin (a user who is a direct admin or a direct member of an admin group). The last-admin safeguard rejects any removal or demotion that would drop it (error code `LAST_ADMIN`). @@ -88,32 +87,24 @@ me access rm-grant bob@example.com /share/work/backend The level argument accepts `r` (read), `w` (write), or `o` (owner). -### Granting your own agents - -Managing a grant normally requires **owner** at the path. The exception is your -own **agents**: an agent's effective access is always clamped to its owner's, so -you can never give an agent more than you hold. Because of that you may grant or -revoke access for an agent you own at **any** path — even one you don't own — -without holding an owner grant there. The clamp keeps it honest: grant the agent -a higher level than you hold and it clamps down to yours; grant it a path you -have no access to and the agent simply gets nothing. This lets you scope an -agent to just the part of a subtree it needs, even on shared trees you don't own. - -Service accounts do **not** use this exception. They have no owner clamp, so granting access to a service account requires the normal authority: space admin or `owner` at the path. Revoking access from a service account is also allowed for space admins and direct user members of that service account's bound admin group. +Managing a grant requires **space admin** or **`owner`** at the path (admin +bypasses the path-owner check; an admin can always self-grant `owner@root`). +Revoking access from a service account is additionally allowed for direct user +members of that service account's bound admin group. ## Reserved tree roots Every space has two conventional roots: - **`/share`** — the shared root. Memories everyone in the space should see go here. This is where the file importers default a tree-less record, and where `me memory create` / `me_memory_create` callers usually place memories. -- **`/home/`** — a per-member private root. The input shortcut **`~`** expands to your own home, so `~/notes` means `/home//notes` and displays back as `~/notes`. An **agent**'s home nests under its owner's — `/home//` — so its owner can see what the agent stores under `~` (an agent's access is capped at its owner's regardless). Service accounts do not get a home grant; put their memories under an explicitly granted path such as `/share/...`. +- **`/home/`** — a user's private root. The input shortcut **`~`** expands to your own home, so `~/notes` means `/home//notes` and displays back as `~/notes`. Service accounts do not get a home grant; put their memories under an explicitly granted path such as `/share/...`. `/` is the canonical path separator (the leading slash is optional on input). Labels must match `[A-Za-z0-9_-]`. ### Default grants - A space **creator** gets `admin` + `owner@home` + `owner@share` — **not** `owner@root`. So the creator sees `share` and their own `~`, but not other members' homes. Because they're an admin, they can self-grant `owner@root` if they need the whole space. -- A **user** who joins a space is granted `owner@home` (their own private root). An **agent** who joins is likewise granted owner over its home — nested under its owner's (`/home//`) — so it's usable immediately and the grant isn't clamped away. Their **shared** access comes from the group they join (the default `team` group — see below), not from a per-invite grant. +- A **user** who joins a space is granted `owner@home` (their own private root). Their **shared** access comes from the group they join (the default `team` group — see below), not from a per-invite grant. - A **service account** starts with no tree grants and is not added to the default group. Grant it access deliberately with `me access grant ` or by adding it to an ordinary group. ### The default `team` group @@ -141,7 +132,7 @@ A **space admin** owns these defaults and can change them at any time: The provisioning defaults above are for a standard space. `me space create` flags let you shape a space's default access up front — e.g. a curated space you write to while others only read, or one where members can read without running up write (embedding) costs: -- `--no-home-grants` — joining users **and** agents get no `owner@~`. Service accounts never get `owner@~`. You (the creator) get **god mode** instead of the standard grants: `admin` + `owner@/` (the whole space). +- `--no-home-grants` — joining users get no `owner@~`. Service accounts never get `owner@~`. You (the creator) get **god mode** instead of the standard grants: `admin` + `owner@/` (the whole space). - `--default-group ` — name the default/invite group (default `team`). - `--no-default-group-grants` — create the default group **grantless** (no `read@/share` + `write@/share/projects`); you grant it by hand. - `--no-default-group` — don't create a default group at all. @@ -183,14 +174,10 @@ me group add backend bob@example.com # Grant the group write access to a subtree (members inherit it) me access grant backend /share/work/backend w -# Add one of your agents to the space and give it write access to share -me agent add ci-bot -me access grant ci-bot /share w - -# Or create a team-owned service account for CI and grant it write access +# Create a team-owned service account for CI and grant it write access me service create deploy-bot --admin ops@example.com me apikey create --service deploy-bot ci-key me access grant deploy-bot /share w ``` -See [`me access`](cli/me-access.md), [`me space`](cli/me-space.md), [`me group`](cli/me-group.md), [`me agent`](cli/me-agent.md), and [`me service`](cli/me-service.md) for full command references. +See [`me access`](cli/me-access.md), [`me space`](cli/me-space.md), [`me group`](cli/me-group.md), and [`me service`](cli/me-service.md) for full command references. diff --git a/docs/cli/me-access.md b/docs/cli/me-access.md index 6115bc83..a1d3308d 100644 --- a/docs/cli/me-access.md +++ b/docs/cli/me-access.md @@ -2,7 +2,7 @@ Manage tree-access grants in the active space. -A grant attaches an access **level** to a principal (user, agent, service account, or group) at a **tree path**. Levels are additive and hierarchical — a grant at `/share/work` also covers everything below it: +A grant attaches an access **level** to a principal (user, service account, or group) at a **tree path**. Levels are additive and hierarchical — a grant at `/share/work` also covers everything below it: | Level | Flag | Capabilities | |-------|------|--------------| @@ -33,7 +33,7 @@ me access grant | Argument | Required | Description | |----------|----------|-------------| -| `principal` | yes | Principal id or name (user email, agent name, service-account name, or group name). | +| `principal` | yes | Principal id or name (user email, service-account name, or group name). | | `path` | yes | Tree path; use an empty string `""` for the space root. | | `level` | yes | Access level: `r` (read), `w` (write), or `o` (owner). | @@ -64,9 +64,9 @@ me access rm-grant List grants in the active space, optionally scoped to one principal and/or a path subtree. Alias: `me access ls`. -Listing the whole space or another principal's grants requires **space admin** or **owner** of the path being listed (an admin can self-grant `owner@root`). Listing your own grants (or an agent you own) is self-service; [`me access mine`](#me-access-mine) is a convenient shortcut. +Listing the whole space or another principal's grants requires **space admin** or **owner** of the path being listed (an admin can self-grant `owner@root`). Listing your own grants is self-service; [`me access mine`](#me-access-mine) is a convenient shortcut. -Pass `--effective` to show the resolved access a member actually executes with instead of raw grant rows. Effective access includes direct grants, inherited group grants, service-account group grants, and agent grants clamped by the owner's access. `--effective` cannot be combined with `--path`. +Pass `--effective` to show the resolved access a member actually executes with instead of raw grant rows. Effective access includes direct grants and inherited group grants. `--effective` cannot be combined with `--path`. ``` me access list [principal] [--path ] [--effective] @@ -87,7 +87,7 @@ me access list [principal] [--path ] [--effective] List the access grants **you** hold in the active space. Available to **any member** (no admin or path-owner rights required). Fresh service accounts may have no grants until one is explicitly added. -Pass `--effective` to show the paths you can actually read, write, or own after group inheritance and agent clamping are applied. +Pass `--effective` to show the paths you can actually read, write, or own after group inheritance is applied. ``` me access mine [--effective] diff --git a/docs/cli/me-agent.md b/docs/cli/me-agent.md deleted file mode 100644 index 3b274727..00000000 --- a/docs/cli/me-agent.md +++ /dev/null @@ -1,139 +0,0 @@ -# me agent - -Manage agents. - -An **agent** is a service account you own — a non-human principal that authenticates with an API key. Agents are **global** (owned by you, names unique per user), independent of any space. Create an agent, add it to the spaces it should work in, then mint it an API key with [`me apikey`](me-apikey.md). - -These commands authenticate with your **session** (`me login`). Lifecycle commands (`create`/`list`/`rename`/`delete`) are global; `spaces` lists one of your agent's space memberships; `add`, `remove`, and `groups` operate on the active space. - -## Commands - -- [me agent list](#me-agent-list) -- list your agents -- [me agent create](#me-agent-create) -- create an agent -- [me agent rename](#me-agent-rename) -- rename an agent -- [me agent delete](#me-agent-delete) -- delete an agent -- [me agent spaces](#me-agent-spaces) -- list the spaces an agent belongs to -- [me agent add](#me-agent-add) -- add an agent to the active space -- [me agent remove](#me-agent-remove) -- remove an agent from the active space -- [me agent groups](#me-agent-groups) -- list an agent's groups in the space - ---- - -## me agent list - -List your agents. Alias: `me agent ls`. - -``` -me agent list -``` - ---- - -## me agent create - -Create an agent (a global service account you own). - -``` -me agent create -``` - -| Argument | Required | Description | -|----------|----------|-------------| -| `name` | yes | Agent name (unique among your agents). | - ---- - -## me agent rename - -Rename an agent. - -``` -me agent rename -``` - -| Argument | Required | Description | -|----------|----------|-------------| -| `agent` | yes | Agent id or name. | -| `new-name` | yes | New name. | - ---- - -## me agent delete - -Delete an agent **globally**. Its API keys are deleted with it, and it leaves every space. Alias: `me agent rm`. To take an agent out of a *single* space while keeping it alive elsewhere, use [`me agent remove`](#me-agent-remove) instead. - -``` -me agent delete -``` - -| Argument | Required | Description | -|----------|----------|-------------| -| `agent` | yes | Agent id or name. | - ---- - -## me agent add - -Add one of your agents to the active space's roster. It joins with owner over its own home — nested under yours (`/home//`), so you can see what it stores under `~`. Grant it shared access (e.g. on `share`) with [`me access`](me-access.md). - -``` -me agent add -``` - -| Argument | Required | Description | -|----------|----------|-------------| -| `agent` | yes | Agent id or name. | - ---- - -## me agent remove - -Remove one of your agents from the active space's roster — the inverse of [`me agent add`](#me-agent-add). Its access grants and group memberships **in this space** are scrubbed; the agent itself, its API keys, and its memberships in other spaces are untouched. Removing your **own** agent is self-service — **no space-admin needed**. - -``` -me agent remove [-y] -``` - -| Argument | Required | Description | -|----------|----------|-------------| -| `agent` | yes | Agent id or name. | - -| Option | Description | -|--------|-------------| -| `-y, --yes` | Skip the confirmation prompt. | - -To remove someone else's agent (as an admin), use [`me space remove-member`](me-space.md#me-space-remove-member). Note that leaving a space with [`me space leave`](me-space.md#me-space-leave) already removes your agents in that space automatically. - ---- - -## me agent spaces - -List the spaces one of your agents belongs to. This uses your logged-in session to verify ownership of the agent; it does not authenticate with the agent's `ME_API_KEY`. - -``` -me agent spaces -``` - -| Argument | Required | Description | -|----------|----------|-------------| -| `agent` | yes | Agent id or name. | - ---- - -## me agent groups - -List the groups an agent belongs to in the active space. - -``` -me agent groups -``` - -| Argument | Required | Description | -|----------|----------|-------------| -| `agent` | yes | Agent id or name. | - -## See also - -- [`me apikey`](me-apikey.md) -- mint, list, and revoke an agent's API keys. -- [`me access`](me-access.md) -- grant the agent access to tree paths. -- [MCP Integration](../mcp-integration.md) -- run an agent against a space over MCP. diff --git a/docs/cli/me-apikey.md b/docs/cli/me-apikey.md index 427643d5..6ad908f8 100644 --- a/docs/cli/me-apikey.md +++ b/docs/cli/me-apikey.md @@ -1,6 +1,6 @@ # me apikey -Manage API keys — your own **personal access token (PAT)**, a key for one of your **agents** (`--agent`), or a key for a **service account** (`--service`). +Manage API keys — your own **personal access token (PAT)** or a key for a **service account** (`--service`). An API key is a global, per-principal credential — **not** inherently bound to a space. An unrestricted key works in any space its principal has been admitted to. A restricted key works only in its declared spaces and is capped to any declared tree grants; the space comes from the `X-Me-Space` header (`--space` / `ME_SPACE`). Keys are formatted `me..`. @@ -9,15 +9,14 @@ current authority, while a restricted key is capped to declared spaces and optional tree grants. Both modes can be held by the following principals: - **Personal access token** (default) — acts as **you**, for headless/CLI use (a VM, SSH, CI) where your `me login` session isn't available. It can be unrestricted or restricted with `--allow`, but it **cannot** manage keys (minting/revoking always needs a session). -- **Agent key** (`--agent `) — acts as one of your agents, for a dedicated/unattended agent install. - **Service-account key** (`--service `) — acts as a space-scoped service account, for CI/CD jobs, webhooks, and other team-owned integrations. Treat it like a production secret. Minting and revoking keys authenticate with your **session** (`me login`); an API key can't mint or revoke keys. The CLI never persists API keys — a created key is printed **once** for you to place where it's used (typically the `ME_API_KEY` environment variable). The alias `me apikey revoke` is equivalent to `me apikey delete`. ## Commands -- [me apikey create](#me-apikey-create) -- mint a personal access token, agent key, or service-account key -- [me apikey list](#me-apikey-list) -- list your keys, an agent's, or a service account's +- [me apikey create](#me-apikey-create) -- mint a personal access token or service-account key +- [me apikey list](#me-apikey-list) -- list your keys or a service account's - [me apikey get](#me-apikey-get) -- show key metadata - [me apikey delete](#me-apikey-delete) -- delete (revoke) a key @@ -25,10 +24,10 @@ Minting and revoking keys authenticate with your **session** (`me login`); an AP ## me apikey create -Mint a new API key. With no target option, mints a **personal access token** for yourself; with `--agent`, mints a key for that agent; with `--service`, mints a key for that service account in the active space. The raw key is shown only once — store it securely. +Mint a new API key. With no target option, mints a **personal access token** for yourself; with `--service`, mints a key for that service account in the active space. The raw key is shown only once — store it securely. ``` -me apikey create [name] [--agent | --service ] [--expires | --ttl ] [--allow ] [--space-admin ] +me apikey create [name] [--service ] [--expires | --ttl ] [--allow ] [--space-admin ] ``` | Argument | Required | Description | @@ -37,7 +36,6 @@ me apikey create [name] [--agent | --service ] [--expires