AGENTS.md@cli · diff
git:20260827.d25b5b4 to git:20261008.8517a4a
3 added, 1 removed. Audit A to A.
# CLI Package — Agent Instructions
## Purpose
The `cli/` package (`@vellumai/cli`) manages the **lifecycle of Vellum assistant instances** — creating, starting, stopping, connecting to, and deleting them. Commands here operate on or across instances and typically require specifying which assistant to target.
This contrasts with `assistant/src/cli/`, where commands are scoped to a **single running assistant** and operate on its local state (config, memory, contacts, etc.).
## Scope
Commands here operate on or across **assistant instances** — creating, starting, stopping, connecting to, and deleting them. They require specifying which assistant to target and work without an assistant process running.
For commands scoped to a **single running assistant's** local state (config, memory, contacts), see `assistant/src/cli/AGENTS.md`.
Examples: `hatch`, `wake`, `sleep`, `retire`, `ps`, `ssh` belong here. `config`, `memory` belong in `assistant/src/cli/`. Contact ACL writes belong on `gateway contacts` (via `vellum exec --service gateway`), not on `assistant contacts` or `vellum gateway`.
## Assistant targeting convention
New or modified commands that act on a specific assistant should accept an assistant display name or ID as an argument. Exact assistant ID matches must win over display-name matches. Unique display-name matches may resolve to the matching assistant ID, but ambiguous display names must fail with an error that lists the matching IDs.
Use the shared helpers from `lib/assistant-config` instead of hand-rolled lookup:
- `lookupAssistantByIdentifier()` for commands that require an explicit target and need custom error handling.
- `resolveTargetAssistant()` for commands that may fall back to the active assistant or sole lockfile entry.
- `formatAssistantReference()` for user-facing output that should include both display name and ID when they differ.
Use `parseAssistantTargetArg()` from `lib/assistant-target-args` when parsing command arguments that may contain an unquoted multi-word display name. Do not store raw display names in `activeAssistant`; persist the resolved `assistantId`.
New or modified destructive lifecycle commands must be explicit and safe. A command that deletes, retires, archives, or removes assistant state must print the resolved assistant identity before acting and require an interactive confirmation, with a documented `--yes` bypass only for automation or higher-level clients that already own confirmation. Do not expose destructive lifecycle actions as `vellum client` slash commands.
## Conventions
- Commands are standalone exported functions in `src/commands/`.
- Each command manually parses `process.argv.slice(3)` (no framework — keep it lightweight).
- Register new commands in the `commands` object in `src/index.ts` and add a help line.
- User-facing output uses `console.log`/`console.error` directly (no shared logger).
## Help Text Standards
Every command must have high-quality `--help` output. Follow the same standards as `assistant/src/cli/AGENTS.md` § Help Text Standards, adapted for this package's manual argv parsing (no Commander.js).
### Requirements
1. **Each command**: Include a concise one-liner description in the help output,
followed by an explanation of arguments/options with their formats and
constraints.
2. **Include examples**: Show 2-3 concrete invocations with realistic values.
3. **Write for machines**: Be precise about formats, constraints, and side effects.
AI agents parse help text to decide which command to run and how. Avoid vague
language — say exactly what the command does and where state is stored.
## Boundary: No integration-specific references
The CLI is a generic lifecycle manager. It must **never** contain references to specific skills, integrations, or features (e.g. "Meet", "Slack", "Telegram"). Environment variables, volume mounts, and device passthroughs defined here must use generic names (e.g. `VELLUM_AVATAR_DEVICE`, not `VELLUM_MEET_AVATAR_DEVICE`). The skill that uses a resource decides how to interpret it — the CLI just passes it through.
Cross-package imports into `skills/` are forbidden. The CLI is distributed as an npm package; anything outside `cli/` is not included in the tarball and will fail to resolve at runtime.
Workspace packages that `cli/src` never imports may still be listed in `package.json` `dependencies` and `bundledDependencies` when a bundled dependency depends on them (for example `@vellumai/avatar-manifest`, pulled in by `@vellumai/local-mode`); `bundledDependencies` only nests a `file:` dep when it is a direct dependency of the CLI.
## Boundary: No `.vellum/` directory access
The CLI must **never** read from or write to the `.vellum/` directory (e.g. `~/.vellum/protected/`, `<instanceDir>/.vellum/`). That directory structure is an **assistant daemon / gateway implementation detail**. The CLI's job is to spawn those processes and pass configuration via environment variables — not to reach into their internal storage.
- For example, the signing key used for JWT auth between the daemon and gateway is persisted in the lockfile (`resources.signingKey`) so that client actor tokens survive daemon/gateway restarts. On first start (or when the key is missing), the CLI generates a new key via `generateLocalSigningKey()` in `lib/local.ts`, saves it to the lockfile entry, and passes it to both `startLocalDaemon` and `startGateway` as the `ACTOR_TOKEN_SIGNING_KEY` env var. The CLI does **not** read or write to the `.vellum/` directory for signing keys — it uses the lockfile instead.
+ The local signing key is host-owned state in `resources.signingKey`. The CLI injects it into both services as `ACTOR_TOKEN_SIGNING_KEY`. Wake, local upgrade and recovery must use `resolveExistingLocalSigningKey()`; ordinary lifecycle operations never generate replacement signing keys. Explicit `wake --repair-guardian` may replace an unresolvable key after warning that clients must authenticate again; stop both services and persist the host key before restarting. Renderer updates cannot write resources or secrets.
+
+ **Read-only compatibility exception:** the shared resolver checks the existing instance-scoped `.vellum/protected/actor-token-signing-key` and `.vellum/workspace/deprecated/actor-token-signing-key` as migration sources. When no host key exists, matching legacy copies can backfill the host entry before startup. A valid host key is authoritative over legacy files. Missing, unreadable, malformed or conflicting migration material stops ordinary operations without rotation. Do not add new writes to those legacy locations or duplicate resolution logic in lifecycle commands.
**Exception: `~/.vellum/device.json`.** That file is the machine-wide shared device-identity file, co-owned by the Electron main process, the host-mode assistant, and the CLI (see `packages/electron-desktop/src/device-id.ts`). The boundary rule covers daemon/gateway-internal state (e.g. `~/.vellum/protected/`, instance dirs), not this file.
## Process liveness
Use `resolveProcessState()` from `lib/process.ts` when checking whether a daemon or gateway should be (re)started. It combines PID existence with an HTTP `/healthz` probe, a readiness grace period, and a [`isVellumProcess()`](https://man7.org/linux/man-pages/man1/ps.1.html) guard against PID reuse — see the function's JSDoc for the full flow.
Reserve `isProcessAlive()` for teardown paths (`sleep`, `retire`) where you need to kill a process regardless of its health.
## Docker Volume Management
The CLI creates and manages six per-instance Docker volumes with strict per-service access boundaries (least-privilege at the container level).
| Volume | Mount path | Access | Contents |
| ------------------------------------------- | -------------------- | ----------------------------------- | -------------------------------------------------------------------------------- |
| **Workspace** (`<name>-workspace`) | `/workspace` | Assistant: rw, Gateway: rw, CES: ro | `config.json`, conversations, apps, skills, db, logs, `.backups/`, `.backup.key` |
| **Gateway security** (`<name>-gateway-sec`) | `/gateway-security` | Gateway only | `trust.json`, `actor-token-signing-key`, capability-token secrets |
| **CES security** (`<name>-ces-sec`) | `/ces-security` | CES only | `keys.enc`, `store.key` |
| **Socket** (`<name>-socket`) | `/run/ces-bootstrap` | Assistant + CES | CES bootstrap socket for initial handshake |
| **Gateway IPC** (`<name>-gateway-ipc`) | `/run/gateway-ipc` | Assistant + Gateway | `gateway.sock` (assistant → gateway) |
| **Assistant IPC** (`<name>-assistant-ipc`) | `/run/assistant-ipc` | Assistant + Gateway | `assistant.sock` (gateway → assistant) |
The assistant container's root (`/`) holds per-container ephemeral and persistent state: package installs (`~/.bun`), `device.json`, embed-worker PID files.
**Lifecycle**:
- `hatch` creates the six volumes.
- `retire` removes all of them.
**Mount rules**: each container receives only the volumes it needs. The assistant never mounts `gateway-security` or `ces-security`. The gateway never mounts `ces-security`. The CES mounts the workspace volume as read-only.
**Container security posture**: the assistant container runs as a non-root user (UID 1001) with no elevated capabilities — `--privileged`, `--cap-add`, and `--security-opt` overrides are not used; the host Docker socket is not bind-mounted; default Docker seccomp and AppArmor profiles remain active. Do not add elevated capabilities without a concrete runtime requirement.
**Backup paths in Docker mode**: backups land on the workspace volume (`VELLUM_BACKUP_DIR` defaults to `/workspace/.backups/`, key at `VELLUM_BACKUP_KEY_PATH` defaults to `/workspace/.backup.key`), so workspace-volume destruction loses both data and backups.