You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Browse filesBrowse the repository at this point in the historyBrowse files
lxy
committed
feat(mail): add Bot-bound Agent Mail commands
Add Bot- and Space-bound Agent Mail authorization, encrypted mailbox credentials, guarded send and Draft workflows, JMAP polling support, and agent-facing skill guidance. Keep dry-run network-free, bind mailbox credentials to the authorizing Bot token and normalized API origin, and surface ambiguous send outcomes safely.\n\nRefs #128
Copy file name to clipboardExpand all lines: CLAUDE.md
+29-6Lines changed: 29 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -22,14 +22,26 @@
22
22
-**Credential resolution** (see `internal/credential`, `internal/authstore`): a token comes from a stored encrypted profile or, as a fallback, an env var — `OCTO_TOKEN` first, then `OCTO_BOT_TOKEN`. The credential's `Source` records which variable was used, so the envelope's `identity.source` cannot mislead. Stored profiles live in `~/.octo-cli` (override `OCTO_CONFIG_DIR`): metadata in plaintext `config.json`, tokens in AES-256-GCM `credentials.enc`. Manage them with `octo-cli auth`.
23
23
-**Per-domain token gate and mount routing**: a spec may declare which token kinds it accepts and which server mount each kind uses. `drive` accepts all three and routes `uk_*` to `/v1/user/drive/*`, bots to `/v1/bot/drive/*`. An incompatible kind fails locally with `TOKEN_KIND_NOT_ALLOWED` (`validation`, exit 2 — switch credentials, don't re-auth), implemented once in `cmd/service/identity.go` and reused by the hand-written drive composites via `service.MountForOperation`. Generated leaves and composites alike resolve identity **before**`--dry-run` or any local success return, so a refused credential can never have a request described for it or a document link resolved under it.
24
24
-**Lossless uint64 ids**: `drive` file ids are backend uint64s. Inputs are decimal-string flags validated in `[0, 2^64-1]` and sent as JSON integers; responses are emitted as decimal strings. Go's `int` cannot hold the upper half of the range and a `float64` would round above 2^53, so neither is used on this path. The rule holds for *every* output format, not just `json`: the row extraction behind `--format table|csv|ndjson` decodes with `UseNumber` too, so a large integer a spec did not declare as a lossless field still prints the digits the backend sent rather than a rounded float.
25
-
-**Selecting a credential at runtime**: `--bot-id <robot_id>` (env `OCTO_BOT_ID`) is the agent's primary selector — robot ids are self-known; `--profile <name>` selects by friendly name. With exactly one profile, selection is implicit; with **two or more, a selector is required** (ambiguity is a hard error, never a silent guess). Precedence: selector > sole/implicit profile > `OCTO_BOT_TOKEN`. The success envelope's `identity` echoes the active `{profile, robot_id, bot_kind, source}` so misuse is visible.
25
+
-**Selecting a credential at runtime**: `--bot-id <robot_id>` (env `OCTO_BOT_ID`) is the agent's primary selector — robot ids are self-known; `--profile <name>` selects by friendly name. With exactly one profile, selection is implicit; with **two or more, a selector is required** (ambiguity is a hard error, never a silent guess). Precedence: selector > sole/implicit profile > `OCTO_BOT_TOKEN`. An explicit `--bot-id` always selects a stored profile and fails closed when none exists; `OCTO_BOT_ID` may instead label an environment token when the profile store is empty. The success envelope reports that unverified environment value as `identity.robot_id_claimed`; `identity.robot_id` is reserved for a stored or verified binding.
26
26
-**Isolation boundary = OS user**: the encryption key is machine-derived, so the store resists off-machine leakage (commit/backup/sync) but not a same-user process. Isolate mutually-distrusting bots with separate OS users or `OCTO_CONFIG_DIR` values.
27
27
-**Daemon task isolation**: `OCTO_CREDENTIAL_MODE=task` selects the restricted Loop-only policy but cannot protect against a process rewriting its own environment. Daemon task processes must receive an isolated `OCTO_CONFIG_DIR` with no host profiles and the short-lived `OCTO_BOT_TOKEN`.
28
28
- Each Bot has an **owner**; operations are attributed to the Bot identity. For LLM-backed paths (`matter extract`) the bot acts on behalf of its owner — pass `owner_uid` as `creator_uid`.
29
29
-**Search subjects** (`message search` family): a `bf_` token searches as the bot, or as a real person with `--on-behalf-of <uid>` (OBO — requires an active grant); a `uk_` token searches as the real person it belongs to. An `app_` token cannot search — the CLI rejects it locally (`validation`, in `internal/client/search_route.go`) before any request, distinct from a server-side `FORBIDDEN`.
30
30
-`OCTO_SPACE_ID` (or `--space`) supplies space context for platform-scoped bots. Space-scoped bots resolve their space server-side.
31
-
32
-
## Command Structure (11 active domains, 284 operations)
31
+
- Agent Mail authorization is attached to the current Bot and Space.
32
+
The Bot may come from a stored profile or the same runtime-provided
33
+
`OCTO_BOT_TOKEN` used by every other service. A real authorization login
34
+
resolves the authoritative RobotID through `/v1/bot/register`; `--dry-run`
35
+
never performs that lookup. The mailbox token is kept in a dedicated
36
+
encrypted file under `OCTO_CONFIG_DIR`, keyed by RobotID, SpaceID, the
37
+
normalized Octo API origin, and a SHA-256 fingerprint of the authorizing Bot
38
+
token. This prevents a replaced Bot token, another Space, or another gateway
39
+
origin from unlocking the old mailbox credential without storing the Bot
40
+
token again. Changing the API origin therefore requires a Mail authorization
41
+
for that origin. Mail never uses a separate token or base-URL environment
42
+
variable.
43
+
44
+
## Command Structure (12 active domains, 308 operations)
33
45
34
46
Service commands are auto-registered. The hand-written leaves are `schema`, `version`, `api` (generic passthrough), `config`, `auth`, and the cobra-generated `completion`.
35
47
@@ -84,17 +96,28 @@ octo-cli html list | get | publish | versions | rm (octo-doc HTML docs;
octo-cli api <METHOD> <PATH> [--params ...] [--data ...] [--service ...]
90
113
octo-cli config show
91
114
octo-cli completion bash|zsh|fish|powershell
92
115
octo-cli version
93
116
```
94
117
95
-
`octo-cli auth login` stores a bot token (read from a hidden prompt, `--with-token` stdin, or `--token-file` — never argv) under a profile keyed by `--bot-id`/`--profile`. `status`/`list` show metadata only (tokens always masked); `logout` removes a profile.
118
+
`octo-cli auth login` stores a bot token (read from a hidden prompt, `--with-token` stdin, or `--token-file` — never argv) under a profile keyed by `--bot-id`/`--profile`. `update` changes non-secret profile settings such as the API base URL without re-entering or rewriting the token. `status`/`list` show metadata only (tokens always masked); `logout` removes a profile.
96
119
97
-
Bot-type capability and per-command flags are in `docs/octo-cli-design.md`. Agent-facing usage lives under `skills/` (`octo-shared`, `octo-matter` (withheld — see above), `octo-summary` (withheld — see CHANGELOG "Currently withheld" note), `octo-messaging`, `octo-files`, `octo-drive`, `octo-docs`, `octo-html`, `octo-marketplace`) — keep those in sync when command shapes change.
120
+
Bot-type capability and per-command flags are in `docs/octo-cli-design.md`. Agent-facing usage lives under `skills/` (`octo-shared`, `octo-matter` (withheld — see above), `octo-summary` (withheld — see CHANGELOG "Currently withheld" note), `octo-messaging`, `octo-files`, `octo-drive`, `octo-docs`, `octo-html`, `octo-marketplace`, `octo-mail`) — keep those in sync when command shapes change.
98
121
99
122
The hand-written drive leaves (`cmd/drive*.go`) are the exception to "everything is generated": `upload file` / `download file` / `share download` are multi-request transfers, `share create` branches on node type, and `share blob-create` / `share access` / `share download` take an argument shape (positional body field, whole share URL) the engine cannot express. They replace the generated leaf of the same name where one exists, so the spec still documents the endpoint for `octo-cli schema`.
0 commit comments