Skip to content
Open
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ At the start of non-trivial work, list `docs/` and skim anything relevant to the
| [docs/floating-panels.md](docs/floating-panels.md) | Anchored popovers — Portal/Modal escape for Android, lifecycle gates, keyboard-shared-value, status-bar offset, the flash |
| [docs/file-icons.md](docs/file-icons.md) | Material icon theme integration for the file explorer |
| [docs/providers.md](docs/providers.md) | Adding a new agent provider end-to-end |
| [docs/custom-providers.md](docs/custom-providers.md) | Custom provider config: Z.AI, Alibaba/Qwen, ACP agents, profiles, custom binaries |
| [docs/service-proxy.md](docs/service-proxy.md) | Service proxy: exposing workspace scripts at public URLs, DNS setup, reverse proxy config |
| [docs/paseo-agent.md](docs/paseo-agent.md) | Paseo Agent provider (id `paseo`): in-process Pi harness, `agents.paseo` typed inference providers, auth, OAuth limitation |
| [docs/development.md](docs/development.md) | Dev server, build sync gotchas, CLI reference, agent state, Playwright MCP |
| [docs/rpc-namespacing.md](docs/rpc-namespacing.md) | WebSocket RPC naming convention — dotted namespaces and `.request`/`.response` pairs |
| [docs/terminal-performance.md](docs/terminal-performance.md) | Terminal latency pipeline, coalescing/backpressure invariants, benchmark + perf spec usage |
Expand Down
290 changes: 290 additions & 0 deletions docs/paseo-agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
# Paseo Agent provider

Paseo Agent is a built-in provider that runs Pi's coding-agent harness **in process** (no `pi` CLI, no `~/.pi` discovery). It is configured entirely by Paseo-owned config under `agents.paseo` in `$PASEO_HOME/config.json`. The model backends are "inference providers": one or more typed entries, each pointing at an API with its own key and models.

The provider id is **`paseo`** (the display name is "Paseo Agent"). Use it like any other provider, e.g. `paseo run --provider paseo --model <inferenceProviderName>/<modelId> ...`.

This is a prototype. There is no app UI yet. OpenRouter and ChatGPT setup have CLI
paths; other provider setup is still config-file based.

> Smoke note: the daemon supervisor runs from `packages/server/dist`. After changing
> provider/config code, run `npm run build:server` (or run a source/dev daemon) before
> smoking, otherwise a stale `dist` may reject the `agents.paseo` config. Always pass
> `--host <addr>` to CLI smoke commands so they hit your isolated daemon, not the real
> daemon on `:6767`.

## MCP tools

Paseo Agent bridges `AgentSessionConfig.mcpServers` into Pi custom tools, so the
daemon-injected `paseo` MCP server (and any other configured MCP server) is available to
the model. On session start the provider connects to each server, lists its tools, and
registers them as Pi tools named `<serverName>__<toolName>`; tool input schemas (JSON
Schema) are converted to TypeBox, calls are proxied to the MCP server, and results map
back to the model. Connections are torn down on session close. Servers that fail to
connect or list are logged and skipped rather than failing the session.

Transports: HTTP (streamable) is the primary path (the injected `paseo` server is HTTP);
SSE and stdio transports are also wired via the MCP SDK. No extra config is needed — MCP
servers come from Paseo's normal injection/config, not from `agents.paseo`.

## Agent definitions

Paseo Agent can load a Paseo-owned agent definition from `$PASEO_HOME/agents/*.md`.
Configure the default agent in `agents.paseo.defaultAgent`; `orchestrator` resolves
to `$PASEO_HOME/agents/orchestrator.md`. Only top-level markdown files are selectable
agents. Reusable partials can live anywhere under `$PASEO_HOME/agents`.

```jsonc
{
"agents": {
"paseo": {
"defaultAgent": "orchestrator",
"defaultModel": "openrouter-main/anthropic/claude-3.7-sonnet",
"providers": {},
},
},
}
```

Example `$PASEO_HOME/agents/orchestrator.md`:

```markdown
---
name: Orchestrator
description: Coordinates work through Paseo-managed agents
prompt: extend
mcp: [paseo]
model: openrouter-main/anthropic/claude-3.7-sonnet
tools: [read, grep, paseo__list_agents, paseo__create_agent]
permissions:
- tool: paseo__archive_*
action: deny
---

!{{./partials/collaboration.md}}

Use the Paseo MCP tools to inspect active agents, create focused helper agents, and
summarize handoffs clearly.

!{{./partials/review-rules.md}}
```

`prompt: extend` keeps Pi's default base prompt and prepends the composed agent body to
the append list. `prompt: override` uses the agent body as the custom base prompt, so
Pi's default base prompt is skipped. In both prompt modes, per-session `systemPrompt` is
appended after the agent, and the daemon-level append prompt is appended last.

Frontmatter supports `name`, `description`, `prompt`, `mcp`, `model`, `tools`,
`permissions`, and `projectContext`. `projectContext` is parsed for a future explicit
project-context model, but it does not activate implicit `AGENTS.md`/`CLAUDE.md`
discovery; Paseo Agent still keeps Pi context discovery off. `model` is an agent default:
an explicit session model wins, then the selected agent model, then
`agents.paseo.defaultModel`, then Pi's first available model.

Partials use bang braces and expand exactly where they appear: `!{{./partials/base.md}}`.
Paths are relative to the file containing the directive and are confined to
`$PASEO_HOME/agents`: absolute paths, directory escapes, cycles, overly deep partial
chains, oversized definitions, and frontmatter inside partials are rejected.

`tools` is the Pi tool allowlist for the agent: it controls what the model sees and can
call. Omit it to use Pi's default built-in tools plus bridged MCP tools. `permissions` is
an ordered first-match policy for active tool calls. The first matching `tool` pattern
wins; unmatched tools are allowed. Denied calls are blocked before execution through Pi's
tool preflight hook, so the policy applies to built-in, custom, and bridged MCP tools.

`mcp: [paseo]` is an expectation check, not a new injection mechanism. The normal daemon
MCP injection still supplies the actual server; if an agent declares an MCP server that
is not present in the session's `mcpServers`, Paseo Agent logs a warning and continues.

## Config shape

```jsonc
{
"agents": {
"paseo": {
// Optional. "<inferenceProviderName>/<modelId>". Used when an agent is
// started without an explicit model.
"defaultModel": "openrouter-main/anthropic/claude-3.7-sonnet",

// Optional. Loads $PASEO_HOME/agents/orchestrator.md by default for new
// Paseo Agent sessions.
"defaultAgent": "orchestrator",

// Legacy alias for defaultAgent. Still accepted for old configs.
"defaultProfile": "orchestrator",

// Inference providers, keyed by instance name. Names are free-form; you may
// run several entries of the same type against different APIs/keys/models.
"providers": {
"openrouter-main": {
"type": "openrouter",
"options": {
// apiKey may be omitted to fall back to the type's env var
// (here OPENROUTER_API_KEY). It may also be a literal, an env
// reference like "$OPENROUTER_API_KEY" / "${OPENROUTER_API_KEY}",
// or a "!command" that prints the key.
"models": [
{ "id": "anthropic/claude-3.7-sonnet", "label": "Claude 3.7", "reasoning": true },
{ "id": "openai/gpt-4o", "label": "GPT-4o" },
],
},
},
},
},
},
}
```

## Supported types

Each type supplies sensible defaults so you usually only provide an API key (or its
env var) and model ids.

| type | wire `api` | default base URL | default key env var |
| ------------------- | --------------------------- | --------------------------------- | --------------------- |
| `openrouter` | `openai-completions` | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` |
| `openai` | `openai-responses` | `https://api.openai.com/v1` | `OPENAI_API_KEY` |
| `anthropic` | `anthropic-messages` | `https://api.anthropic.com` | `ANTHROPIC_API_KEY` |
| `opencode` | `openai-completions` | `https://opencode.ai/zen/v1` | `OPENCODE_API_KEY` |
| `openai-compatible` | `openai-completions` | _(required: `options.baseUrl`)_ | _(none — set apiKey)_ |
| `openai-codex` | `openai-codex-responses` | `https://chatgpt.com/backend-api` | _(OAuth — see below)_ |
| `custom` | _(required: `options.api`)_ | _(required: `options.baseUrl`)_ | _(none — set apiKey)_ |

Per-entry overrides live in `options`: `baseUrl`, `api`, `apiKey`, `headers`,
`authHeader` (send `Authorization: Bearer <apiKey>`), and `models[]`. Each model may
override `api` (e.g. an `anthropic-messages` model behind an otherwise
`openai-completions` provider), plus `label`, `reasoning`, `contextWindow`, `maxTokens`.

### OpenCode Zen / Go (OpenAI-compatible)

OpenCode Zen models speak either `openai-completions` or `anthropic-messages`. Use the
`opencode` type for Zen, or `openai-compatible` with an explicit base URL for Go, and
override the per-model `api` where a model is Anthropic-family:

```jsonc
{
"type": "opencode",
"options": {
"models": [
{ "id": "big-pickle" },
{ "id": "claude-sonnet", "api": "anthropic-messages" }
]
}
}
// OpenCode Go:
{
"type": "openai-compatible",
"options": {
"baseUrl": "https://opencode.ai/zen/go/v1",
"apiKey": "$OPENCODE_API_KEY",
"models": [{ "id": "glm-5" }]
}
}
```

Pi attaches its own `x-opencode-*` attribution headers automatically when the base URL
is on `opencode.ai`, so you do not set those yourself.

### `custom` escape hatch

When a backend needs a wire protocol the named types don't cover, use `custom` and set
`options.api` to a Pi wire protocol (e.g. `google-generative-ai`, `mistral-conversations`,
`openai-codex-responses`) plus `options.baseUrl`. This is a thin pass-through, not a place
to embed raw Pi internals.

## Authentication

- **API key / env var / command** — works for every type. Omit `apiKey` to use the
type's default env var; or set a literal, a `$ENV` reference, or a `!command`.
- A provider only counts as "available" (and its models listable for use) when its
resolved key is actually present — a literal value, a set env var, or a command.

### ChatGPT / OpenAI subscription (OAuth) — `openai-codex`

The `openai-codex` type uses a ChatGPT/OpenAI **subscription** via OAuth instead of an
API key, against `https://chatgpt.com/backend-api` with the `openai-codex-responses` wire
API. Paseo **owns** the auth: `paseo login chatgpt` runs Pi's browser PKCE/callback
OAuth flow by default, stores the credential in a Paseo-controlled file, and lets Pi
refresh/rotate it there. Paseo does **not** read ChatGPT/Codex/OpenCode/Pi or any other
tool's auth files.

Config — just declare the provider and its models (no credential field):

```jsonc
{
"agents": {
"paseo": {
"providers": {
"chatgpt": {
"type": "openai-codex",
"options": { "models": [{ "id": "gpt-5.3-codex", "reasoning": true }] },
},
},
},
},
}
```

> Use a model id that ChatGPT-account Codex supports — e.g. `gpt-5.3-codex` (live-verified),
> or another Pi codex id like `gpt-5.2`, `gpt-5.4`, `gpt-5.4-mini`. The non-subscription id
> `gpt-5-codex` is **not** accepted on a ChatGPT account (the backend returns a 400
> "model is not supported when using Codex with a ChatGPT account").

Then log in once (the credential is stored under the `chatgpt` provider instance):

```bash
paseo login chatgpt
# Opens your browser to approve (OAuth PKCE + local callback on 127.0.0.1:1455).
# If the browser can't open, the URL is printed to copy; you can also paste the code.
```

Headless machines (no browser) can use the device-code fallback:

```bash
paseo login chatgpt --device-code # prints a URL + code to enter on another device
```

Both store the credential at `$PASEO_HOME/paseo-agent/auth.json` (mode `0600`, created by
Pi's AuthStorage; pass `--home` to target a specific Paseo home). On each session the
provider loads that credential, and Pi refreshes expired access tokens — persisting any
rotated refresh token **back into Paseo's own file**. A codex provider counts as
"available" once a credential is stored. Token values are never logged or printed.

> **Rotation is handled within Paseo.** Because Paseo owns the store and Pi writes
> refreshed tokens back to it, rotation does not break Paseo. Run `paseo login chatgpt`
> again any time to re-authorize (e.g. after a long idle period or an explicit logout).

**Advanced / manual override (not the normal path).** If you already hold your own refresh
token you may set `options.refreshToken` to a literal, `"$ENV"`/`"${ENV}"`, or `"!command"`.
It is seeded into the Paseo store at session start. This is for power users/automation; the
normal path is `paseo login chatgpt`. Paseo still never reads another tool's auth files.

Other OAuth providers (Anthropic Pro/Max, Copilot) remain unwired; for those you can pass a
pre-obtained bearer token via `apiKey`/env where accepted (e.g. `ANTHROPIC_OAUTH_TOKEN`).

## CLI setup

Configure an OpenRouter provider through the selected daemon:

```bash
export OPENROUTER_API_KEY=...
paseo provider add openrouter openrouter-main \
--model anthropic/claude-3.7-sonnet \
--host localhost:7777
```

For shell-history-safe key entry, pipe the key instead:

```bash
printf '%s\n' "$OPENROUTER_API_KEY" |
paseo provider add openrouter openrouter-main \
--api-key-stdin \
--model anthropic/claude-3.7-sonnet \
--host localhost:7777
```

`paseo login chatgpt --host <host>` runs browser OAuth on the CLI machine, then sends
the returned credential to the selected daemon. The credential is stored in that daemon's
`$PASEO_HOME/paseo-agent/auth.json`; token values are not printed. `--device-code` is
currently local-only and is rejected when combined with `--host` until a daemon-run
device-code RPC exists.
1 change: 1 addition & 0 deletions nix/desktop-package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ buildNpmPackage rec {

nodejs = nodejs_22;
inherit (paseo) npmDeps;
npmDepsFetcherVersion = 2;

# Prevent onnxruntime-node's install script from running during automatic
# npm rebuild. We manually rebuild only node-pty in buildPhase.
Expand Down
2 changes: 1 addition & 1 deletion nix/npm-deps.hash
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sha256-lwIf9Z0uwDdNyAFu+L03pVwlQSuasYWIpn1Fsx3zxJw=
sha256-RBD8SQx4szE41qGDIYcaKWVhzfC1kVUBC7M5OOQBQQY=
1 change: 1 addition & 0 deletions nix/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ buildNpmPackage rec {
# Default hash lives in nix/npm-deps.hash (see arg default above).
# CI auto-updates that file when package-lock.json changes (see .github/workflows/).
inherit npmDepsHash;
npmDepsFetcherVersion = 2;

# Prevent onnxruntime-node's install script from running during automatic
# npm rebuild (it tries to download from api.nuget.org, which fails in the sandbox).
Expand Down
Loading
Loading