Skip to content

CLI

pal is a single static Go binary that boots the local stack, scaffolds and runs agents, gates CI on evals, drives sandbox VMs, sets up the personal agent and installs the data plane in your cloud. It is for anyone who works from a terminal. Just looking? pal demo needs Docker and no API key. Building your own? pal dev boots the stack and pal onboard walks you through a first agent. Personal agent users start with pal setup.

Area Commands
Get started demo, onboard, setup, doctor, dev
Build init, agents (incl. agents dev), deploy, hooks
Ship safely freeze, promote, approve, reject, rollback, canary, promotions
Govern an agent agents policy, agents identity, agents audit
Run and observe runs, logs, test, eval, debug, support
Sandbox VMs run, vm, sandbox
Cloud infra
Account login, whoami, logout

Source lives at packages/cli/cmd/pal. The command below builds it under the name pal, so every command on this site works as written.

Terminal window
# Build from the repo
cd packages/cli && go build -o "$(go env GOPATH)/bin/pal" ./cmd/pal
pal --version

Starts the self-contained demo (kiosk mode): Docker only, no API key. Each visitor gets a private workspace with sample agents that is deleted after 30 minutes; answers are recorded replays from a local fixture server. Run it from inside a PAL checkout. It builds and starts Postgres, Redis, the control plane, the dashboard and the replay server as the compose project pal-demo, waits for /healthz and the demo page (up to 5 minutes), then opens http://localhost:3001/kiosk.

Flags: --no-open (don’t open the browser), --down (stop and delete all demo data). Host ports default to 8080 and 3001; override with KIOSK_API_PORT and KIOSK_WEB_PORT. See Demo.

Terminal window
pal demo # start (first run builds images)
pal demo --no-open # start without opening the browser
pal demo --down # stop and delete all demo data

First run. On a terminal it is a conversation with PAL’s onboarding agent (the same one as the dashboard): say what you want, and it gets a working key (typed at a hidden prompt, never into the chat), designs the agent, creates it on your go-ahead and runs it. --steps uses the step-by-step wizard instead (health, sign-in, key test, design, confirm, streamed first run); it also takes over, labelled, when the agent can’t reach the model. With --yes or a non-terminal stdin it never prompts (CI). Flags: --provider (openai|anthropic), --api-key, --description, --steps. See Quickstart.

pal setup (PAL, the personal agent) works the same way: a conversation by default, --steps for the wizard, and consent only ever from your own keypress.

Terminal window
pal onboard # conversation
pal onboard --steps # step-by-step wizard
pal onboard --yes --provider anthropic --api-key "$KEY" \
--description "Triage support tickets" # CI

Agentic first run for the personal agent: the terminal twin of the dashboard’s /pal/onboarding. The control plane owns the state and returns the step to resume, so the CLI, the dashboard and the server agree. Steps are welcome, channels, voice, trust, profile and done (your first Brief). Each step advances only on a real result: a live bridge check, a saved setting read back, your explicit confirmation. Consent is never given non-interactively.

Flags: --yes / -y never prompts; it reports where setup stands and does not change consent. --apply writes the setup into this Mac’s bridge config: confirmed facts and relationships into the owner profile, and quiet hours and the voice setting into bridge.env. Both files are backed up first. Bridge addresses come from PAL_BRIDGE_URL, PAL_IMESSAGE_BRIDGE_URL and PAL_DASHBOARD_URL, and tokens from PAL_BRIDGE_TOKEN and PAL_IMESSAGE_BRIDGE_TOKEN. See Set up PAL.

Terminal window
pal setup # interactive, resumes where you left off
pal setup --apply # also write the result into the local bridge config
pal setup --yes # report progress without prompting

Runs readiness checks against the local stack, one pass/fail line each. Exits non-zero if any hard check fails. Run it before you report a bug. The Sandbox runtime section, which never fails the run, reports whether the API uses a real scheduler or the stub, whether the scheduler and runtime manager are reachable, and whether KVM and Firecracker are present. Each failing check prints its fix.

Terminal window
pal doctor

Boots the local stack: Postgres, Redis and MinIO (Docker), the control-plane API (host Go process), the Next.js dashboard, and the WhatsApp and iMessage bridges (iMessage is skipped off macOS). Logs interleave in one terminal. Flags: --infra-only, --with-whatsapp=false, --with-imessage=false, --no-open, --dashboard-port (3001), --api-port (8080).

Terminal window
pal dev # start everything
pal dev down # stop infra containers (--volumes also deletes data)
pal dev logs # tail all infra logs
pal dev logs postgres # tail one service

Scaffold a new agent directory with agent.yaml, src/index.ts, package.json and tsconfig.json. Without --template an interactive picker appears. Templates: basic (default), research, tool-use, connector, personal, scheduled, approval.

Terminal window
pal init my-agent
pal init my-agent --template research

Manage agents. Aliased as agent.

Terminal window
pal agents create --name my-agent --description "Triage tickets" [--label team=support]
pal agents list
pal agents get my-agent
pal agents delete my-agent [--force]
# Development inner loop — watches the agent dir and re-publishes on change
pal agents dev my-agent [--dir ./my-agent] [--input '{"prompt":"hi"}'] [--eval] [--yes]

agents dev (also pal agent dev) is the agent development inner loop: it watches the local directory for changes to agent.yaml or code files, re-publishes the agent version on each change, runs a smoke input (--input), and streams the run events. --eval also runs the agent’s eval suite after each publish.

Every registered agent is a principal with a signed identity and a least-privilege policy. Background: Agent policy and identity.

Terminal window
pal agents policy get <name> # -o json for the raw document
pal agents policy set <name> -f policy.json # replace; if the file has a stale "version" the server answers 409
pal agents policy propose <name> [--apply] # least-privilege proposal with a rationale per field
pal agents identity show <name>
pal agents identity rotate <name> # new key generation; old tokens stop verifying
pal agents identity revoke <name> [--reason "..."] # the agent's runs fail admission
pal agents audit <name> [--event policy_denied] [--limit 50] # also verifies the hash chain

propose saves nothing unless you pass --apply. With --apply it writes the proposal, reads it back and reports “verified” only if the read-back equals the proposal; otherwise it exits non-zero.

Manage runs: create, list, inspect, cancel.

Terminal window
pal runs create --agent my-agent --input '{"topic":"AI"}' [--stream]
pal runs list [--agent my-agent] [--status succeeded]
pal runs get <run-id>
pal runs cancel <run-id> [--reason "..."]

Stream a run’s journal events. For dev-stack containers use pal dev logs.

Terminal window
pal logs <run-id> [-f] # journal events; -f keeps the stream open

The debug agent root-causes a failed run, an incident or a failing eval case and proposes fixes; the support agent answers questions from your own data and the docs. See Debugging and support. A proposal is applied only when you answer y to it on a terminal; --json and non-terminal runs never apply anything.

Terminal window
pal debug run <run-id> [-q "why did this start failing?"] [--json]
pal debug incident <incident-id>
pal debug eval <agent> <case>
pal support "why are my runs getting 402?" [--run <run-id>] [--json]
pal support escalate "the canary keeps rolling back" [--run <id> | --incident <id>]

Run an agent’s eval suite and compare against the branch baseline. Exits non-zero with a regression summary when the score drops below the baseline. Wire it into CI to gate merges. --set-baseline pins the run as the branch baseline. --rehearse replays past failed or low-score production runs instead of a suite (--window, default 7d). Runs execute asynchronously on the control plane, so each case waits for its run to finish; --timeout (default 2m) bounds that wait per case, and a failed or timed-out run scores as a failed case with its error.

Terminal window
pal test --agent my-agent --suite factual-accuracy
pal test --agent my-agent --suite factual-accuracy --against last-green
pal test --agent my-agent --rehearse --window 24h

Ship an agent version through sandbox, staging and prod, with deterministic gates and a human approval for prod. The full flow, gates and audit trail are in Promotion.

Terminal window
pal freeze <agent> [--label v7] # snapshot the working copy as an immutable version
pal promote <agent> --to sandbox|staging|prod [--version V] [--canary N] \
[--max-error-rate 0.10] [--max-cost-ratio 1.5] [--min-eval-score S] [--min-runs 20] \
[--reason "..."] [--run-eval <suite-id>] [--require-approval] [--json]
pal approve <promotion-id> [--note "..."] # human owner/admin only
pal reject <promotion-id> [--note "..."]
pal rollback <agent> [--env prod] [--note "..."] # idempotent
pal canary evaluate <agent> # run the canary guards now
pal promotions list [--status pending_approval] [--agent A]
pal promotions envs <agent> # what each environment serves
pal promotions audit [--agent A] # append-only decision trail
pal promote contract # JSON tool contract for the onboarding agent

promote, approve, reject and rollback exit non-zero when the request is blocked, so CI and scripts cannot treat “blocked” as success. --run-eval retries once after running the named suite hermetically against the target environment.

Hermetic, LLM-judged evals run on the control plane with mocked tools (Evaluations). pal test is the older runner that executes a suite through real runs.

Terminal window
pal eval run --agent support --suite core [--samples 3] [--set-baseline] [--branch main] [--case NAME] [--max-cost 1.00] [--judge-model reasoning-large] [--json]
pal eval generate --agent support [--goal "..."] [--max-cases 12] [--out draft.json] [--approve [-y]]
pal eval approve --file draft.json
pal eval calibrate --agent support [--rubric R] [--limit 20]

run exits non-zero when any case fails or the run regressed against the branch baseline. generate never saves a suite without --approve or a later eval approve.

Promote an agent to sandbox, run its workload in a real microVM and record the evidence.

Terminal window
pal sandbox run <agent> --spec agent.yaml [--exec "/bin/sh -c uname"] [--input '{...}'] [--keep] [--wait 2m] [--version V] [--json]

It exits non-zero if the VM never ran or the command failed, and refuses a stub scheduler because nothing would have run. --exec is execve-style: no shell unless you pass /bin/sh -c .... The VM is terminated unless --keep.

Schedule and manage headless sandbox VMs. pal run takes a spec file; vm (aliases vms, runtime) manages what it started.

Terminal window
# Schedule a headless agent from a spec file
pal run agent.yaml [--input '{...}'] [--follow] [--idle-timeout 600]
# VM subcommands
pal vm list [--state running] [--limit 50]
pal vm get <vm-id>
pal vm logs <vm-id> # SSE log stream from the harness
pal vm stop <vm-id> [--grace 30]
pal vm pause <vm-id> # snapshot + stop, frees the quota slot
pal vm resume <vm-id> # restore under the same vm id
pal vm fork <vm-id> --count 3 # N copies; the server caps N (100 by default, PAL_MAX_FORK_COUNT, ceiling 1000)
pal vm exec <vm-id> -- ls /tmp # exec into a running VM (-t/-i for a PTY)
pal vm ls <vm-id> [path] # list /workspace
pal vm cat <vm-id> <path> # print a file (1 MiB cap)
pal vm put <vm-id> ./f.txt /workspace/f.txt # write a file (1 MiB cap)
pal vm quota get # current quota + today's usage
pal vm quota set --max-concurrent 10 --max-cost 50 # owner only
pal vm cluster # node load + warm-pool capacity (owner only)
pal vm validate --kubeconfig ~/.kube/sandbox.yaml # operator harness: gVisor/Kata isolation on a real cluster

exec talks to the runtime manager directly; point it elsewhere with --manager-addr (default $PAL_MANAGER_ADDR). vm validate shells scripts/validate-cluster.sh (run it from inside the repo; needs kubectl), marks legs as skipped, not failed, when a RuntimeClass is absent, and takes --context and --report too.

Reads agent.yaml from the current directory, creates the agent on the control plane (or updates its system prompt and model if it already exists), saves the optional workflow graph, then calls POST /v1/agents/{name}/deploy. Manifest keys used: name (required), description, model, system_prompt, labels (set on create only; the update endpoint cannot change description or labels) and workflow. Other keys are ignored. Uses REST, so pass the HTTP address or let it derive one from the gRPC address.

Flags: --json (print the API response; same as -o json), --dry-run (one read-only lookup, reports create or update, changes nothing).

Exit codes: 0 ok, 3 unauthorized (401/403), 4 quota or budget (402), 5 not found (404), 6 server error (5xx), 1 anything else.

$ cat agent.yaml
name: demo-bot
description: demo
model: auto
system_prompt: You are terse.
$ pal --api-url http://localhost:8080 deploy --json
{
"agent": "created",
"agentName": "demo-bot",
"deployedAt": "...",
"environment": "cloud",
"id": "<agent-id>",
"status": "live",
"tenantId": "<tenant-id>",
"url": "http://localhost:8080/v1/agents/demo-bot/a2a/invoke"
}

Manage inbound webhook triggers: a public POST /v1/hooks/{id} URL bound to an agent, so GitHub, Sentry, PagerDuty or any HMAC-signing sender starts a run. Aliased as hook and triggers. See Webhook triggers.

create prints the signing secret once; --secret-stdin reads one from stdin instead (Sentry and PagerDuty generate their own). --template maps the payload into run input. update needs at least one of its flags; --enable / --disable and --template / --clear-template are mutually exclusive, and a disabled trigger answers deliveries with 404.

Terminal window
pal hooks create <agent> [--provider generic|github|sentry|pagerduty] [--template '{"pr":"{{payload.number}}"}'] [--secret-stdin]
pal hooks list <agent>
pal hooks get <trigger-id>
pal hooks update <trigger-id> [--enable | --disable] [--template JSON | --clear-template] [--secret-stdin]
pal hooks delete <trigger-id>

Manage MCP servers: browse the catalog, register your own remote server, attach servers to agents, and store their credentials. Tokens are read from stdin and never echoed; pal mcp creds shows metadata only. stdio catalog servers need --vm (a running sandbox VM id).

Terminal window
pal mcp servers
pal mcp add <slug> --url https://host/mcp --auth none|bearer|oauth [--name NAME]
pal mcp remove <slug>
pal mcp attach <agent> <slug> [--vm <vmId>]
pal mcp detach <agent> <slug>
pal mcp tools <agent> <slug>
pal mcp token <slug> < token.txt
pal mcp login <slug> [--scope S] [--client-id ID] [--no-wait]
pal mcp creds

Generate Terraform for the data plane on AWS, GCP or Azure (EKS, GKE, AKS). Only install does real work: it writes the Terraform files and prints the next steps (terraform init && plan && apply); it does not provision anything. It needs --tenant-id (or PAL_TENANT_ID).

Terminal window
pal infra install --cloud aws --tenant-id "$TENANT" [--region us-east-1] [--cluster-name NAME] \
[--existing-cluster NAME] [--instance-type TYPE] [--node-count 3] [--output-dir DIR] [--firecracker]
Terminal window
pal login [--email --password] # authenticate to the control plane
pal whoami # show current authenticated user
pal logout # clear stored credentials

A pal login session is a dashboard session JWT: it lasts PAL_JWT_TTL (12 hours by default) with no refresh, and it ends early when that user signs out anywhere (POST /auth/logout revokes all of the user’s sessions) or their password or role changes. pal logout only clears the locally stored credentials. For automation (CI, cron, scripts), use an API key (--api-key / PAL_API_KEY, created under Settings > API keys with only the scopes the job needs): keys do not expire every 12 hours and are revoked individually.

--api-url string Control plane address (default: $PAL_API_URL or localhost:50051, gRPC)
--api-key string API key (default: $PAL_API_KEY)
--tenant-id string Tenant ID (default: $PAL_TENANT_ID)
-o, --output string Output format: text or json (default text)
--rest Use REST instead of gRPC (or set PAL_USE_REST)