Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions AUTH_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ the login components).

A human can hold several credentials by surface: a cookie for the browser, OAuth
tokens for the interactive CLI (`me login`), and a **personal access token** for
headless/SSH/VM use (`me apikey create --self`). All share one identity
headless/SSH/VM use (`me apikey create`, which targets yourself by default).
All share one identity
(the `auth.users` / `core.principal` row). A user PAT carries the user's *full*
authority on the data plane but is barred from minting/revoking credentials — see
flow E. (An agent key is the right choice for sandboxes; a user PAT is for "be me,
Expand Down Expand Up @@ -254,8 +255,8 @@ gated by `requireOwnMember` — the caller's own user or an owned agent):
`space.list`) — but not account management (it owns no agents/keys/spaces and
is never an admin), which the user RPC denies per-method.
- **User PAT** (kind `'u'`, the caller's own principal) — "be me, headless"
(`me apikey create --self`; explicit opt-in, since it's a full-access
credential). Authenticates as
(`me apikey create`, which targets yourself by default; an agent key is the
`--agent` opt-in). Authenticates as
the **user** with their full grants, on **both** the memory RPC and the user
RPC — *except* it cannot mint or revoke keys (`apiKey.create` / `apiKey.delete`
stay session-only). That carve-out keeps a leaked key from minting a sibling
Expand Down
48 changes: 31 additions & 17 deletions docs/cli/me-apikey.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,68 @@
# me apikey

Manage API keys.
Manage API keys — your own **personal access token (PAT)**, or a key for one of your **agents** (`--agent`).

API keys are how **agents** authenticate. Each key belongs to one of your agents and is **global** — not bound to a space. The same key works in any space the agent has been admitted to; the space comes from the `X-Me-Space` header (`--space` / `ME_SPACE`). Keys are formatted `me.<lookupId>.<secret>`.
An API key is a global, per-principal credential — **not** bound to a space. The same key works in any space its principal has been admitted to; the space comes from the `X-Me-Space` header (`--space` / `ME_SPACE`). Keys are formatted `me.<lookupId>.<secret>`.

Humans authenticate with a session (`me login`), not an API key. These commands authenticate with your **session**.
There are two kinds, distinguished only by who they act as:

The CLI never persists API keys. A created key is printed **once** for you to place where the agent runs (typically via the `ME_API_KEY` environment variable). The alias `me apikey revoke` is equivalent to `me apikey delete`.
- **Personal access token** (default) — acts as **you**, for headless/CLI use (a VM, SSH, CI) where your `me login` session isn't available. Full access as you, but it **cannot** manage keys (minting/revoking always needs a session).
- **Agent key** (`--agent <agent>`) — acts as one of your agents, for a dedicated/unattended agent install.

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 key for an agent
- [me apikey list](#me-apikey-list) -- list an agent's keys
- [me apikey create](#me-apikey-create) -- mint a personal access token (or an agent key)
- [me apikey list](#me-apikey-list) -- list your keys (or an agent's)
- [me apikey get](#me-apikey-get) -- show key metadata
- [me apikey delete](#me-apikey-delete) -- delete (revoke) a key

---

## me apikey create

Mint a new API key for one of your agents. The raw key is shown only once — store it securely.
Mint a new API key. With no `--agent`, mints a **personal access token** for yourself; with `--agent`, mints a key for that agent. The raw key is shown only once — store it securely.

```
me apikey create <agent> [name] [--expires <timestamp>]
me apikey create [name] [--agent <agent>] [--expires <timestamp>]
```

| Argument | Required | Description |
|----------|----------|-------------|
| `agent` | yes | Agent id or name. |
| `name` | no | Key name (auto-generated if omitted). |
| `name` | no | Key name (auto-generated as `cli-<date>-<rand>` if omitted). |

| Option | Description |
|--------|-------------|
| `--agent <agent>` | Mint a key for one of your agents (id or name) instead of yourself. |
| `--expires <timestamp>` | Expiration timestamp (ISO 8601). |

Names are unique per principal, so you can't mint two keys with the same name for the same target. The auto-generated default carries a random suffix, making repeated `me apikey create` calls extremely unlikely to collide.

```bash
# A personal access token for yourself (e.g. to use headlessly in a VM)
me apikey create
me apikey create my-laptop # …with a name

# A key for one of your agents
me apikey create --agent claude-code-agent plugin-key
```

---

## me apikey list

List an agent's API keys (metadata only — never the secret). Alias: `me apikey ls`.
List API keys (metadata only — never the secret). With no `--agent`, lists **your own** keys; with `--agent`, lists that agent's keys. Alias: `me apikey ls`.

```
me apikey list <agent>
me apikey list [--agent <agent>]
```

| Argument | Required | Description |
|----------|----------|-------------|
| `agent` | yes | Agent id or name. |
| Option | Description |
|--------|-------------|
| `--agent <agent>` | List one of your agents' keys (id or name) instead of your own. |

Displays a table of keys with ID, name, last-used date, and expiry.
Displays a table of keys with ID, name, created date, and expiry.

---

Expand Down Expand Up @@ -84,5 +98,5 @@ me apikey delete <id> [-y]

## See also

- [`me agent`](me-agent.md) -- create the agents that hold these keys and add them to spaces.
- [`me agent`](me-agent.md) -- create the agents that hold `--agent` keys and add them to spaces.
- [MCP Integration](../mcp-integration.md) -- supply a key to an MCP-connected agent via `--api-key` or `ME_API_KEY`.
2 changes: 1 addition & 1 deletion docs/cli/me-claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Pass `--mcp-only` to skip the plugin and register just the `me` MCP server (no h
| `--server <url>` | Pin a server. Default: use your `me login` server at runtime. |
| `-s, --scope <scope>` | Claude Code config scope: `local`, `user`, or `project`. Default: `user`. |

Credential handling: by default (a personal install) nothing is pinned, so the plugin (and the MCP server) uses your `me login` session, server, and active space, resolved from the OS keychain / `~/.config/me` at runtime — so it follows `me login` / `me space use` and survives re-login. Pass `--server` / `--space` to pin either. Pass `--api-key` (mint one with `me apikey create <agent>` or `--self`) for a **headless** install that can't reach your keychain — since there's no session to fall back to, an api key bakes in a fixed server + space + key together. The space is resolved from `--space`, `ME_SPACE`, or your active space (whichever is set — install errors if none, since a global key has no active space to fall back to at runtime), and `--server` defaults to your resolved server.
Credential handling: by default (a personal install) nothing is pinned, so the plugin (and the MCP server) uses your `me login` session, server, and active space, resolved from the OS keychain / `~/.config/me` at runtime — so it follows `me login` / `me space use` and survives re-login. Pass `--server` / `--space` to pin either. Pass `--api-key` (mint one with `me apikey create` for a personal access token, or `me apikey create --agent <agent>` for an agent) for a **headless** install that can't reach your keychain — since there's no session to fall back to, an api key bakes in a fixed server + space + key together. The space is resolved from `--space`, `ME_SPACE`, or your active space (whichever is set — install errors if none, since a global key has no active space to fall back to at runtime), and `--server` defaults to your resolved server.

The `--scope` flag mirrors `claude plugin install --scope` / `claude mcp add --scope`:

Expand Down
2 changes: 1 addition & 1 deletion docs/cli/me-codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ me codex install [options]
| `--space <slug>` | Pin a space. Default: resolve `ME_SPACE` / active space at runtime. |
| `--server <url>` | Server URL to embed in the MCP config. |

By default only the server URL is baked into the config: at runtime `me mcp` uses your `me login` session (resolved from the OS keychain / `~/.config/me` each run, so it survives re-login) and your active space (set by `me space use` / `ME_SPACE`). Pass `--api-key` (mint one with `me apikey create <agent>`) for a headless agent that cannot reach your keychain; that bakes the key and requires a pinned `--space`.
By default only the server URL is baked into the config: at runtime `me mcp` uses your `me login` session (resolved from the OS keychain / `~/.config/me` each run, so it survives re-login) and your active space (set by `me space use` / `ME_SPACE`). Pass `--api-key` (mint one with `me apikey create --agent <agent>`, or `me apikey create` for a personal access token) for a headless agent that cannot reach your keychain; that bakes the key and requires a pinned `--space`.

For manual MCP client configuration, see [MCP Integration](../mcp-integration.md).

Expand Down
2 changes: 1 addition & 1 deletion docs/cli/me-gemini.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ me gemini install [options]
| `--server <url>` | Server URL to embed in the MCP config. |
| `-s, --scope <scope>` | Gemini CLI config scope: `user` or `project`. Default: `user`. |

By default only the server URL is baked into the config: at runtime `me mcp` uses your `me login` session (resolved from the OS keychain / `~/.config/me` each run, so it survives re-login) and your active space (set by `me space use` / `ME_SPACE`). Pass `--api-key` (mint one with `me apikey create <agent>`) for a headless agent that cannot reach your keychain; that bakes the key and requires a pinned `--space`.
By default only the server URL is baked into the config: at runtime `me mcp` uses your `me login` session (resolved from the OS keychain / `~/.config/me` each run, so it survives re-login) and your active space (set by `me space use` / `ME_SPACE`). Pass `--api-key` (mint one with `me apikey create --agent <agent>`, or `me apikey create` for a personal access token) for a headless agent that cannot reach your keychain; that bakes the key and requires a pinned `--space`.

For manual MCP client configuration, see [MCP Integration](../mcp-integration.md).
2 changes: 1 addition & 1 deletion docs/cli/me-login.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Login also runs the same version compatibility check as `me version` before open

- The session token is stored in your OS keychain when one is available (macOS `security`, Linux `secret-tool`); otherwise it falls back to `~/.config/me/credentials.yaml` (mode 0600). Set `ME_NO_KEYCHAIN=1` to force the file fallback.
- Non-secret settings (default server and per-server active space) live in `~/.config/me/config.yaml`.
- **API keys are for agents, not humans** — `me login` never creates one. Mint agent keys with [`me apikey create`](me-apikey.md#me-apikey-create).
- **Humans authenticate with a session, not an API key** — `me login` never creates a key. For headless/CLI use where a session isn't available you can mint a **personal access token** (acts as you) with [`me apikey create`](me-apikey.md#me-apikey-create); agent keys come from `me apikey create --agent <agent>`.
- Use [`me logout`](me-logout.md) to clear the session; the non-secret config is kept so re-login resumes.

## See also
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/me-opencode.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ me opencode install [options]
| `--space <slug>` | Pin a space. Default: resolve `ME_SPACE` / active space at runtime. |
| `--server <url>` | Server URL to embed in the MCP config. |

By default only the server URL is baked into the config: at runtime `me mcp` uses your `me login` session (resolved from the OS keychain / `~/.config/me` each run, so it survives re-login) and your active space (set by `me space use` / `ME_SPACE`). Pass `--api-key` (mint one with `me apikey create <agent>`) for a headless agent that cannot reach your keychain; that bakes the key and requires a pinned `--space`.
By default only the server URL is baked into the config: at runtime `me mcp` uses your `me login` session (resolved from the OS keychain / `~/.config/me` each run, so it survives re-login) and your active space (set by `me space use` / `ME_SPACE`). Pass `--api-key` (mint one with `me apikey create --agent <agent>`, or `me apikey create` for a personal access token) for a headless agent that cannot reach your keychain; that bakes the key and requires a pinned `--space`.

For manual MCP client configuration, see [MCP Integration](../mcp-integration.md).

Expand Down
2 changes: 1 addition & 1 deletion docs/mcp-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Each `me mcp` instance is locked to a single **space**, carried as the `X-Me-Spa

### Prerequisites

Log in with `me login` and select a space — `me whoami` shows your active space and identity. That session is enough to run the MCP server locally. For an unattended or dedicated-agent install, mint an API key with `me apikey create <agent>` and pass it with `--api-key`.
Log in with `me login` and select a space — `me whoami` shows your active space and identity. That session is enough to run the MCP server locally. For an unattended or dedicated-agent install, mint an API key `me apikey create` for a personal access token (acts as you) or `me apikey create --agent <agent>` for a dedicated agent — and pass it with `--api-key`.

The server defaults to `https://api.memory.build`. Pass `--server <url>` only if you're running a self-hosted server.

Expand Down
43 changes: 43 additions & 0 deletions e2e/cli.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ describe.skipIf(
const key = await meJson<{ id: string; key: string }>([
"apikey",
"create",
"--agent",
agent.id,
]);
expect(key.key).toMatch(/^me\./);
Expand Down Expand Up @@ -697,6 +698,48 @@ describe.skipIf(
expect(denied.code).not.toBe(0);
});

test("7b. personal access tokens: self-default create/list, no same-day collision", async () => {
// `me apikey create` with no agent mints a PAT for the caller. Two unnamed
// PATs minted back-to-back must NOT collide on `unique (member_id, name)` —
// the default name carries a random suffix. (This is the bug TNT-145 fixes.)
const pat1 = await meJson<{ id: string; key: string }>([
"apikey",
"create",
]);
const pat2 = await meJson<{ id: string; key: string }>([
"apikey",
"create",
]);
expect(pat1.key).toMatch(/^me\./);
expect(pat2.key).toMatch(/^me\./);
expect(pat2.id).not.toBe(pat1.id);

// A named PAT is now possible (the old `--self` shape couldn't take a name).
const named = await meJson<{ id: string }>([
"apikey",
"create",
`pat-${rand()}`,
]);

// `me apikey list` (no --agent) lists the caller's OWN keys — all three.
const { apiKeys } = await meJson<{ apiKeys: { id: string }[] }>([
"apikey",
"list",
]);
const ids = new Set(apiKeys.map((k) => k.id));
expect(ids.has(pat1.id)).toBe(true);
expect(ids.has(pat2.id)).toBe(true);
expect(ids.has(named.id)).toBe(true);

// The PAT authenticates as the user themselves (kind "u"), no session.
const patEnv = { ME_API_KEY: pat1.key, ME_SESSION_TOKEN: "" };
const who = await meJson<{ identity: { kind: string } }>(
["whoami"],
patEnv,
);
expect(who.identity.kind).toBe("u");
});

test("8. `me claude import` backfills work that predates the hook", async () => {
// The scenario: a user does a bunch of Claude Code work BEFORE installing
// the capture hook (no hook fires for it), then installs the hook (which
Expand Down
2 changes: 1 addition & 1 deletion packages/claude-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"api_key": {
"type": "string",
"title": "API key (optional)",
"description": "Optional. Leave blank to use your `me login` session — the plugin falls back to it automatically. Set an API key to attribute captures to a dedicated agent instead: create one with `me apikey create` (or have an admin create one with restricted privileges).",
"description": "Optional. Leave blank to use your `me login` session — the plugin falls back to it automatically. Set an API key to attribute captures to a dedicated agent instead: create one with `me apikey create --agent <agent>` (or have an admin create one with restricted privileges).",
"sensitive": true,
"required": false
},
Expand Down
2 changes: 1 addition & 1 deletion packages/claude-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ me agent add claude-code-agent
me access grant claude-code-agent share.projects w

# 3. Mint an API key for that agent
me apikey create claude-code-agent plugin-key
me apikey create --agent claude-code-agent plugin-key
# → prints the raw key once; paste it into the plugin's api_key config
```

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/commands/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ function createAccessMineCommand(): Command {
);
});
} catch (error) {
handleError(error, fmt, { sessionServer: creds.server });
handleError(error, fmt, { creds });
}
});
}
Expand Down
11 changes: 6 additions & 5 deletions packages/cli/commands/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
* me agent — manage your agents (global service accounts).
*
* Agents are owned by you and live across spaces; their lifecycle is on the
* user endpoint. Bringing an agent into the active space and minting its
* (space-bound) api key are space operations — see `me agent add` and
* `me apikey create`.
* user endpoint. Bringing an agent into the active space and minting its api
* key — see `me agent add` and `me apikey create --agent`.
*
* - me agent list: list your agents
* - me agent create <name>: create an agent
Expand Down Expand Up @@ -74,7 +73,7 @@ function createAgentCreateCommand(): Command {
output({ id, name }, fmt, () => {
clack.log.success(`Created agent '${name}' (${id})`);
clack.log.info(
"Add it to a space with 'me agent add', then mint a key with 'me apikey create'.",
`Add it to a space with 'me agent add', then mint a key with 'me apikey create --agent ${name}'.`,
);
Comment on lines 75 to 77
});
} catch (error) {
Expand Down Expand Up @@ -151,7 +150,9 @@ function createAgentAddCommand(): Command {
const result = await memory.principal.add({ principalId: id });
output({ agentId: id, ...result }, fmt, () => {
clack.log.success(`Added agent ${agent} to the space.`);
clack.log.info("Mint a key with 'me apikey create'.");
clack.log.info(
`Mint a key with 'me apikey create --agent ${agent}'.`,
);
Comment on lines 152 to +155
});
} catch (error) {
handleError(error, fmt, { creds, scope: "space" });
Expand Down
Loading