Skip to content

security

Zachary BENSALEM edited this page Aug 15, 2026 · 1 revision

Security

Purpose

This page describes the Prime Agent trust model: what runs with the user's permissions, how credentials are stored and scoped, where the web server binds and why it is not multi-user, the security policy and input-validation stance, and the optional OS-level sandbox extension.

The authoritative policy statement is SECURITY.md at the repository root, and the README security note at the repo root states the core guarantee.

Trust model

Prime Agent executes model-generated Python and project (shell) commands with the user's permissions. Worker and kernel processes improve lifecycle isolation and recovery, but they are not a security sandbox: a model, a prompt, or a skill can cause arbitrary commands to run as the current user. Review changes and use trusted repositories, instructions, skills, and extensions only.

Concretely:

  • The agent runs tools (IPython, Bash, file edits) in the process or kernel for the session, under the invoking user's account.
  • Worker processes and the Jupyter kernel add isolation between long-running jobs and the daemon/client, and provide recovery, but they do not confine file access or network access.
  • There is no jail or capability-dropping by default.

Credential handling

Credentials are API keys and OAuth tokens for model providers.

Sources and priority

The credential resolver is packages/coding-agent/src/core/auth-storage.ts (AuthStorage). It resolves an API key from, in priority order:

  1. Runtime override (CLI --api-key, in memory only, not persisted).
  2. For Prime Inference: environment variable, then Prime CLI config, then auth.json.
  3. For other providers: auth.json, then environment variable.
  4. Fallback resolver (custom providers from models.json).

AuthStatus.source reports where a credential came from (stored, runtime, environment, prime_cli, fallback, models_json_key, models_json_command, or stale).

auth.json storage

The FileAuthStorageBackend persists credentials to ~/.prime/agent/auth.json (via getAgentDir()). It:

  • Creates the parent directory with mode 0700.
  • Creates the file with mode 0600 and rewrites it as 0600 after every write.
  • Uses proper-lockfile for sync and async locking so concurrent processes cannot race on token refresh.
  • Supports API-key credentials ({ type: "api_key", key }) and OAuth credentials ({ type: "oauth", access, refresh, expires, ... }).
  • Expired OAuth tokens are refreshed under the lock; a refresh failure leaves the credentials in place for a later /login and marks the source stale.

setRuntimeApiKey keeps CLI-supplied keys in memory and never writes them to disk.

Environment variables and --no-env

API keys can come from environment variables via packages/ai/src/env-api-keys.ts (wired through @earendil-works/pi-ai findEnvKeys/getEnvApiKey). The prime-agent.sh --no-env flag starts the agent with ambient environment credentials stripped, so secrets from the shell environment are not exposed to the model session. The DAEMON_CLIENT_ENV_KEYS allowlist (packages/coding-agent/src/modes/daemon/daemon-protocol.ts) limits which client env vars (the HERDR_* set) a client may forward to the daemon, and the daemon re-filters on receipt because the socket peer is untrusted.

OAuth tokens

OAuth tokens are stored in auth.json with type oauth and refreshed under lock when they expire (AuthStorage.refreshOAuthTokenWithLock). Device-code and callback flows are initiated and polled through web/app/src/routes/api/chat/providers/oauth.ts (handleChatProvidersOAuth). markAuthSourceStale fingerprints a credential and tracks stale (expired) sources without exposing the secret values.

Local-only web binding

The web server binds to 127.0.0.1 and uses no tokens, so there is no multi-user authentication: anyone who can reach the port can use the agent in that process. This is intentional for a local single-user tool. web/app/ARCHITECTURE.md and the README both list "no multi-user auth (binds to 127.0.0.1 with no tokens)" as a current limitation. The browser can be pointed at a remote runtime via the optional VITE_FLEET_PI_CHAT_RUNTIME_URL, which changes the trust boundary: when set, the HTTP handlers run against that remote runtime. The prime-agent web command binds to 127.0.0.1:3000 by default; --host/--port override the bind address.

Input validation

The web surface validates every request body with zod schemas in web/protocol/src/chat-protocol.zod.ts. Schemas are per-domain under web/protocol/src/schemas/ and registered with zod-to-openapi. Handlers validate before acting, and userId/userEmail are server-set only (client-supplied values are stripped after validation).

Two renderer-facing security plans address untrusted model output:

  • plans/010-markdown-link-allowlist.md (DONE): the custom markdown a renderer was replaced with a MarkdownLink component that allowlists only http(s) and mailto: schemes in href; javascript:, data:, vbscript:, protocol-relative, and relative links render as inert <span> text instead of navigable anchors. This avoids React 19 script-URL throws and jsx-no-script-url violations.
  • plans/001-sanitize-chart-color-html-sink.md (DONE): OpenUI chart colors are sanitized at the HTML <style> sink so a malicious color cannot close the <style> tag. sanitizeChartColor in web/design/src/components/chart.tsx allows only hex colors, var(--token) references, and rgb/rgba/hsl/hsla without CSS structural characters.

These are examples of the broader rule that dynamic data is sanitized at the trust boundary; the plans avoid adding DOMPurify.

Sandbox extension

packages/coding-agent/examples/extensions/sandbox/index.ts is an example extension that applies OS-level sandboxing to bash commands via @anthropic-ai/sandbox-runtime (sandbox-exec on macOS, bubblewrap on Linux). It replaces the built-in bash tool with a sandboxed BashOperations wrapper:

  • Config merges a default config, a global ~/.prime/agent/extensions/sandbox.json, and a project-local <cwd>/.prime/agent/sandbox.json (project takes precedence).
  • Network allow/deny domain lists, plus filesystem deny-read/allow-write/deny-write lists.
  • Enforced via SandboxManager.wrapWithSandbox on the bash subprocess, with SIGKILL on timeout or abort.
  • Disabled on unsupported platforms and via --no-sandbox or enabled: false.

This is an example of adding a sandbox, not a default guarantee: the sandbox is only active when the extension is installed and enabled.

Related pages

Clone this wiki locally