Files
b12daabe35 perf(app): row-grained replica cache persistence (#3755)
* feat(hub): guide starter setup (#3651)

* feat(hub): guide setup with provider IDs

* fix(hub): preserve login setup state

* feat(hub): select starter agent runtime

Use the connected daemon's provider snapshot so generated Hub bundles only
contain complete provider, model, and mode combinations.

* fix(hub): stage starter runtime choices

Keep providers with valid modes selectable when the daemon has no default mode, so users explicitly choose the complete runtime.

* fix(hub): scope runtime choices to workspace

Provider capabilities can vary by repository, so request the snapshot for the bundle directory that guided setup will deploy.

* fix(server): stop short start gates from undercutting provider startup budgets (#3621)

* fix(server): stop short start gates from undercutting provider startup budgets

Agent creation still failed on contended cold starts after #3578, because two
shorter gates on the same path fire before the event-stream readiness budget can
apply. A traced creation reached session.create twelve seconds in, past the 10s
session.create gate, and the agent was then killed by a bare "timeout" at fifteen
seconds from AGENT_RUN_START_TIMEOUT_MS while the SSE connect was still running.

A run-start bound of 15s wraps a path that is deliberately allowed 30s, so the
outer gate always wins and the inner budget is unreachable.

Give session.create the shared OPENCODE_SERVER_STARTUP_TIMEOUT_MS budget, since
creating the first session for a directory is part of OpenCode coming up, and
derive its error message from the constant instead of hardcoding "10s". Raise
AGENT_RUN_START_TIMEOUT_MS to 60s with a comment recording that it must stay
above the slowest provider startup budget; it stays a literal because that module
is provider-agnostic.

Related to #3571.

* test(server): drive the run start budget through a real AgentManager

The run start boundary tests replaced AgentManager.waitForAgentRunStart with a
bare timer, so they never exercised the production run-state transition or the
agent_state subscription the real wait depends on. That path could regress while
the tests stayed green.

Build the scenario on a real AgentManager with a provider session that holds its
turn start open for a configurable span, so pendingRun.started, the lifecycle
change to "running", and the agent_state notification are what the wait actually
observes. Budgets stay independent literals and the settled-before/after
assertions are unchanged.

* fix(server): describe agent run start timeouts

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>

* fix(relay): validate handshake keys (#3669)

* fix(files): keep large source previews responsive (#3665)

Large read-only files bypassed the editor's virtualization and eagerly mounted every highlighted token, which could stall the renderer for seconds and consume gigabytes of memory. Route source previews through bounded platform renderers and reject reads above their display budgets before accumulation.

* fix(hub): make guided setup choices actionable (#3677)

Show a concise login success state, scope starter choices to available Hub connections, wait for daemon capabilities, and avoid partial starter bundles when setup cannot proceed.

* Keep released agent SDK versions reproducible (#3678)

Pin the validated provider and protocol SDK dependency pair so fresh installs cannot silently float after a Paseo release. Refresh the Nix npm dependency hash for the resulting lockfile.

* feat(app): refine agent profile creation (#3680)

Keep profiles as the richer primitive while restoring the familiar star cue and clearer guidance for agent selection. Account for borders in shared field geometry so controls preserve their declared heights across form consumers.

* fix(app): restore native composer auto-growth (#3681)

Native multiline inputs were always scrollable, which kept iOS content-size measurement pinned to one line. Keep native sizing intrinsic below the cap and enable internal scrolling only after the composer reaches it.

* docs(hub): document zero-friction onboarding (#3657)

Lead with the browser-to-CLI onboarding journey, document the generated starter bundle, and align guided setup with available Hub connections and daemon-reported runtime defaults.

* Surface workspace changes from agent panes (#3682)

* feat(app): refine workspace pane actions

Prioritize Changes when available, keep Files useful before a selection, and reserve desktop diff clicks for selection so comments require the explicit affordance. Move the Changes shortcut to Shift+G to avoid consuming a likely copy variant.

* feat(app): improve access to workspace changes

* Expand workspace panel actions and keyboard navigation (#3685)

* fix(app): keep file navigation in place

Retargeting a File tab allocated a new tab identity, causing the tab track to unmount and remount during navigation. Preserve the identity for File-to-File changes while retaining new instances for cross-kind replacements.

* feat(app): expand workspace panel actions

Keep the Command Center aligned with workspace tab and pane primitives. Add explicit panel placement, focused open behavior, keyboard navigation for empty panes, and a Changes pill that toggles the visible Side panel without closing its tab.

* Restore compact and stable chat timelines (#3716)

Restore compact message and footer spacing, reserve independent composer-pill clearance for transcript content and floating controls, and keep visible web tool-call expansion from shifting the reading position.

* perf(app): defer replica persistence until user inactivity

Whole-replica capture and serialization blocked the JS thread during navigation and streaming. Defer that work until the user has been inactive while allowing long-running streams to persist without becoming the idle signal.

* Give every pane a browser-style New tab (#3715)

* feat(app): replace empty panes with New tabs

Give every retained pane an independently identified New tab, replace it in place when a destination is chosen, and preserve split close and move invariants. Keep existing workspace startup and compact explorer routing explicit, with focused unit and browser regression coverage.

* test(app): count content tabs in subagent integration

* fix(app): preserve workspace shortcut ownership

* fix(app): keep compact model options scrollable

The compact model sheet had no sheet-aware scroll owner when profiles filled its viewport, leaving the controls below them unreachable. Keep the root provider view inside Gorhom's scroll container so native sheet gestures work on both platforms.

* fix(app): order new tab launcher actions by workflow

* fix(app): show the New agent shortcut

The command center resolved New agent through the New tab binding, so it advertised Cmd+T despite creating an agent. Resolve the target-agent binding instead and cover the mapping.

* fix(app): prioritize side panel launcher actions

Lead with supporting workspace tools when the launcher is in the Side panel, while ordinary panes continue to lead with agent creation.

* fix(app): align terminal profiles header

* docs: add 0.5.0-beta.5 changelog

* test(app): align new tab launcher expectations

* chore(release): cut 0.5.0-beta.5

* fix: update lockfile signatures and Nix hash [skip ci]

* fix(app): make compact git actions menu-only

Keep compact toolbars narrow and require users to choose potentially consequential Git and workspace actions from the menu.

* fix(app): keep pinned project actions reachable (#3722)

* fix(app): keep pinned project actions reachable

Preserve an empty project shell after its final workspace is hoisted into the pinned section so project-scoped workspace creation remains available.

Co-authored-by: Agent Op <agentop718@gmail.com>

* test(app): verify pinned project workspace context

---------

Co-authored-by: Agent Op <agentop718@gmail.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>

* fix(app): stop a string SVG transform from crashing the app (#3711)

* fix(app): stop a string SVG transform from crashing the app

A root <Svg> is a real React Native view, so react-native-svg rewrites a
*string* `transform` into RN style syntax in `extractTransformSvgView`,
using a PEG parser whose grammar only accepts SVG transform functions.
Upstream lets that parser's SyntaxError escape, so any string it does not
understand takes the whole app down during render:

  SyntaxError: Expected transform functions but "n" found.
  "found": "n", "location": { "start": { "offset": 0 } }

`none` reproduces that message character for character; so does any other
CSS value (`rotate(-90deg)`, `translate(10px, 0)`). Every sibling
extractor in that file already catches the same error and degrades, so
the asymmetry is the bug: a string that is merely ignored on a <Path>
crashes on an <Svg>.

We feed that path untrusted input. The daemon scans a project for
favicon.svg / icon.svg / logo.svg (packages/server/src/utils/project-icon.ts)
and ships it to the client, where ProjectIconImage hands it to SvgCss.
Running the library's own XML parser over `<svg style="transform:none">`
yields exactly `transform: "none"` on the root, which is why the crash
starts once a host connects and the project icons render, with no
terminal involved.

Note this is *not* reachable from an RN-style array transform: the
extractor returns arrays untouched and only parses strings, so
`style={{ transform: [{ rotate: "-90deg" }] }}` (the context window meter
and the checks ring) was never a factor.

Patch the three builds a consumer can resolve - Metro takes the package's
`react-native` field (src/*.ts), Node and Vitest take main/module - and
register react-native-svg with the postinstall patch runner so the patch
actually applies. The new test pins all three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDwfsMWoSFfPm2cFXe5noQ

* test(app): cover parsed root SVG transforms

Exercise the dependency patch with root props produced by the library's XML and CSS parsers, including embedded style rules that can arrive through project icons. Keep dependency-patch documentation focused on the reusable install and resolution constraints.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>

* fix(pairing): warn that pairing links grant daemon access (#3734)

A pairing offer remains valid across daemon restarts, so remove the incorrect rotation guidance and warn users where the durable credential is displayed.

* fix(app): preserve timeline retry backoff (#3736)

Equivalent visibility publications were acting as implicit retry commands, so unrelated tab and layout changes could restart failed timeline work. Keep visibility identity stable and reserve immediate retries for explicit user action.

* Open the tab chooser beside the pointer (#3735)

* feat(app): open the tab chooser beside the pointer

Keep keyboard-created and empty-pane New tabs as persistent launcher surfaces, while giving pointer users an anchored chooser at the pane-local plus button. Both presentations resolve the same launch catalogue so future ordering and customization cannot drift.

* test(app): follow the anchored tab chooser

The browser helpers still searched inside the pane for persistent-launcher controls after clicking the portalled menu trigger. Exercise the menu through its own roles and stable catalogue IDs.

* fix(app): restore composer height lifecycle (#3740)

Native Fabric can omit the initial content-size callback and retain stale size after deletion. Keep the common composer surface free of platform branches while native and web adapters own their measurement mechanisms.

* fix(server): stop quota fetcher from writing provider credentials (#3442)

* fix(server): recover already-settled interrupts (#3742)

* fix(server): recover already-settled interrupts

Provider state can drift from Paseo’s tracked run. Treat an authoritative already-idle interrupt as settled and retire the captured run token so retrying can start new work without weakening ambiguous-cancellation protection.

* fix(codex): fence interruption during turn startup

Reserve foreground-start ownership before asynchronous provider setup. An interrupt can now cancel preflight before turn/start or wait for an issued start to become interruptible, preventing acknowledged cancellation from leaving unmanaged work.

* docs(changelog): prepare 0.5.0 stable release

* chore(release): cut 0.5.0

* fix: update lockfile signatures and Nix hash [skip ci]

* fix(app): stabilize native composer resizing

JavaScript height state competed with native intrinsic layout across draft and agent ownership transitions. Let native inputs own sizing and scrolling while preserving DOM measurement on web.

* docs(changelog): prepare 0.5.1 stable release

* chore(release): cut 0.5.1

* fix: update lockfile signatures and Nix hash [skip ci]

* perf(app): move replica cache validation to restore

Add an observational live measurement harness for the row-store migration baseline. Trust already-shaped cache values on persist while retaining full schema validation at the restore boundary.

* feat(app): add replica cache row store

Ship the platform storage engines behind a dark interface so the cache migration can wire them separately. Both engines share a contract, including atomic writes and destructive schema-version resets.

* feat(app): switch replica cache to row storage

Persist only changed replica entities so steady-state writes scale with the update rather than the complete cached world. Restore remains an all-or-nothing validated boundary, with whole-host byte-budget eviction.

* fix(app): harden replica row persistence

Serialize pending batches at write execution time so retries cannot overwrite newer rows or resurrect removed hosts. Reject oversized hosts before storage writes and preserve unchanged directory identities so routine reconciliation stays row-grained.

---------

Co-authored-by: BrianAguilarWasco <146237330+BrianAguilarWasco@users.noreply.github.com>
Co-authored-by: paseo-ai[bot] <266920839+paseo-ai[bot]@users.noreply.github.com>
Co-authored-by: Yuri Gui <yugui923@users.noreply.github.com>
Co-authored-by: Agent Op <agentop718@gmail.com>
Co-authored-by: Omer Cohen <639682+omercnet@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Dan Berindei <dan.berindei@gmail.com>
2026-08-24 20:45:49 +02:00

11 KiB

title, description, nav, order, category
title description nav order category
Security Security model for Paseo: architecture overview, connection methods, relay encryption, and best practices. Security 5 Getting started

Security

Paseo follows a client-server architecture, similar to Docker. The daemon runs on your machine and manages your coding agents. Clients (the mobile app, CLI, or web interface) connect to the daemon to monitor and control those agents.

Your code never leaves your machine. Paseo is a local-first tool that connects directly to your development environment.

Architecture

The Paseo daemon can run anywhere you want to execute agents: your laptop, a Mac Mini, a VPS, or a Docker container. The daemon listens for connections and manages agent lifecycles.

Clients connect to the daemon over WebSocket. There are two ways to establish this connection:

  • Relay connection (recommended), The daemon connects outbound to our relay server, and clients meet it there. No open ports required.
  • Direct connection, The daemon listens on a network address and clients connect directly.

The relay is the simplest way to connect from your phone. It requires no VPN setup, no port forwarding, and no firewall configuration. The daemon can stay bound to localhost or a socket file, it connects outbound to the relay, and your phone meets it there. The official relay server is the open-source Elixir service at getpaseo/paseo-relay.

Relay is off on new installations. When you pair a device from paseo, paseo daemon pair, or Paseo Desktop, Paseo asks before enabling it. Choosing not to enable relay leaves the daemon available for direct TCP, Tailscale, or other VPN connections and does not create a pairing QR code. Use --relay with the CLI pairing or startup command to opt in without an interactive prompt.

The relay is designed to be untrusted. All traffic between your phone and daemon is end-to-end encrypted. The relay server cannot read your messages, see your code, or modify traffic without detection. Even if the relay is compromised, your data remains protected.

How it works

  1. The daemon generates a persistent ECDH keypair and stores it in $PASEO_HOME/daemon-keypair.json
  2. When you scan the QR code or click the pairing link, your phone receives the daemon's public key
  3. Your phone sends a handshake message with its own public key. The daemon will not accept any commands until this handshake completes.
  4. Both sides perform a Curve25519 ECDH key exchange to derive a shared key. All subsequent messages are encrypted with XSalsa20-Poly1305 (NaCl box).

The relay sees only: IP addresses, timing, message sizes, and session IDs. It cannot read message contents, forge messages, or derive encryption keys from observing the handshake.

Why the relay can't attack you

The daemon requires a valid cryptographic handshake before processing any commands. A compromised relay cannot:

  • Send commands, Without your phone's private key, it cannot complete the handshake
  • Read your traffic, All messages are encrypted with XSalsa20-Poly1305 (NaCl box) after the handshake
  • Forge messages, NaCl box provides authenticated encryption; tampered messages are rejected
  • Replay old messages, Each session derives fresh encryption keys

Trust model

The QR code or pairing link is the trust anchor. It contains the daemon's public key, which is required to establish the encrypted connection. Treat it like a password, don't share it publicly.

Direct connections

By default, the daemon listens on 127.0.0.1:6767 (localhost only). This is safe for local CLI usage but not reachable from your phone or other devices.

For relay and Tailscale setup instructions, see Connectivity.

Socket file (CLI only)

For maximum isolation, you can configure the daemon to listen on a Unix socket file instead of a TCP port. This prevents any network access entirely, only processes on the same machine can connect. The CLI supports this mode, but the mobile app and web interface require a network connection.

VPN access

Use a VPN such as Tailscale when you want a direct connection outside your local network. The VPN encrypts the traffic and keeps the daemon off the public internet. Bind the daemon to its VPN address, set a Paseo password, then add that address as a direct connection in the client.

Binding to 0.0.0.0

Warning: Binding to 0.0.0.0 makes the daemon reachable on all network interfaces, including public Wi-Fi and local networks. This can expose your daemon to unauthorized access. If you must bind to all interfaces, ensure you have proper firewall rules and review your hostnames configuration.

DNS rebinding protection

CORS is not a complete security boundary. It controls which browser origins can make requests, but does not prevent a malicious website from resolving its domain to your local machine (DNS rebinding).

Paseo uses a host allowlist to validate the Host header on incoming requests. Requests with unrecognized hosts are rejected.

Configure via daemon.hostnames in config.json:

  • Default ([]): allow localhost, *.localhost, and all IP addresses
  • ['.example.com']: allow example.com and any subdomain, plus defaults
  • true: allow any host (not recommended)

Password authentication

By default, anyone who can reach the daemon's listening address can connect. On localhost this is fine, only local processes have access. But if you bind to a network interface (e.g. your LAN IP or 0.0.0.0), or if you don't fully trust your local network, you can require a password.

When a password is configured, all HTTP requests must include an Authorization: Bearer <password> header and all WebSocket connections must authenticate via subprotocol. Unauthenticated requests receive a 401 Unauthorized response. Only the /api/health liveness endpoint is exempt, so that process supervisors and load balancers can probe without credentials.

If you enable the bundled web UI, its static files are also served without the password so the login screen can render. This is by design, the API and WebSocket still require authentication before any agent data is returned or any command runs. Set a password before binding the daemon to a network so the data behind the page stays protected.

The password is stored as a bcrypt hash in config.json, the daemon never stores it in plaintext. See Configuration for setup instructions.

What password auth does and does not do

  • Does: Prevents unauthorized clients from controlling your agents, even if they can reach the daemon over the network.
  • Does not: Encrypt traffic. Password auth protects access, not confidentiality. If you need encrypted connections over an untrusted network, use the relay (which provides end-to-end encryption) or a VPN like Tailscale.

When to use it

  • You want to bind the daemon to a LAN or Tailscale address and restrict who can connect.
  • You don't fully trust your local network (shared office, public Wi-Fi with a VPN, etc.).
  • You're exposing the daemon via a reverse proxy and want an additional authentication layer.

We still recommend the relay for mobile access, it combines authentication with end-to-end encryption out of the box. Password auth is primarily useful for direct LAN or VPN connections where you want access control without the relay.

Docker self-hosting

The official Docker image runs the daemon and bundled web UI in one container. It binds to 0.0.0.0:6767 inside the container so Docker port publishing and reverse proxies work normally.

For Docker deployments:

  • Set PASEO_PASSWORD before publishing the port to a LAN, VPN, or public address.
  • Use HTTPS at your reverse proxy for browser access outside localhost.
  • Set PASEO_HOSTNAMES for any DNS names you use to reach the container.
  • Keep /workspace mounts scoped to repositories the agents should be able to read and write.
  • Treat /home/paseo as sensitive, it can contain daemon state and provider credentials.

The image runs the daemon and launched agents as the non-root paseo user, but container user isolation is not a substitute for careful mounts. Agents can still access whatever code and credentials you mount into the container.

See Docker for Compose and reverse proxy examples.

Agent authentication

Paseo wraps agent CLIs (Claude Code, Codex, OpenCode) but does not manage their authentication. Each agent provider handles its own credentials:

  • Claude Code, authenticates via Anthropic's OAuth flow, stored in ~/.claude/
  • Codex, uses your OpenAI API key or OAuth session
  • OpenCode, configured via provider-specific API keys

Paseo never stores or transmits provider API keys. Agents run in your user context with your existing credentials.

Hub identities and credentials

Hub CLI login and daemon enrollment are separate identities. paseo hub login [origin] stores a durable organization-scoped human credential in a private file under PASEO_HOME, keyed by the normalized Hub origin. A stored credential is never sent to another origin. Protect PASEO_HOME as sensitive local state.

Hub CLI credentials are bearer secrets. Remote Hub origins must use HTTPS; cleartext HTTP is accepted only for loopback development origins (localhost, 127.0.0.1, and [::1]).

paseo hub connect [origin] uses that credential, or an explicit API key, only to request a short-lived one-time enrollment token. The daemon exchanges the token and retains its own independently generated relationship credential. Logging out of the CLI does not silently remove daemon authority. Interactive logout completes any accepted same-origin daemon disconnection before deleting the login. In JSON and noninteractive use, logout never prompts or disconnects; pass --disconnect-daemon only when automation intends to remove both identities.

--api-key and PASEO_HUB_API_KEY override stored login without being persisted. Prefer environment or secret-manager injection for automation, and avoid command-line flags when local process listings or shell history are visible to other users.

Recommendations

  • Use the relay for mobile access, it's the simplest option and all traffic is end-to-end encrypted
  • Treat the QR code like a password, anyone with the pairing offer can connect to your daemon
  • Set a password if you bind to a network address, it prevents unauthorized clients from controlling your agents
  • Never bind to 0.0.0.0 without a password, without one, any device on your network can connect
  • Scope Docker mounts tightly, agents can access mounted workspaces and provider credentials
  • Keep your daemon updated, security improvements are released regularly
  • Protect the Hub configuration branch, push access to the .paseo bundle controls what that project can reach, see How Hub works