Skip to content
Merged
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
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: CI

# Runs the verification commands from AGENTS.md. Keep the two jobs in sync with
# that file when the expected commands change.

on:
push:
branches: [main]
pull_request:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
flutter:
name: Flutter client
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: subosito/flutter-action@v2
with:
# Pinned so analyzer output stays reproducible; bump deliberately.
flutter-version: 3.41.9
channel: stable
cache: true

- run: flutter pub get

- run: flutter analyze --no-pub

- run: flutter test --no-pub

server:
name: Node backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: server/package-lock.json

# node-pty builds a native addon; ubuntu-latest already ships Python 3,
# make, and a C++ compiler.
- run: npm ci
working-directory: server

- name: Syntax-check every backend source file
run: git ls-files '*.js' | xargs -r node --check
working-directory: server

- run: npm test
working-directory: server
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ app.*.map.json
/android/app/release
/android/app/google-services.json

# Release artifacts built by scripts/build_flow.sh or prepared for a tag.
# They are published as release assets, never committed.
*.apk
*.aab
*.tar.gz
*.zip
/SHA256SUMS

# Relay backend local state and secrets
/server/.env
/server/tokens.json
Expand Down
109 changes: 84 additions & 25 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Relay contributor guide

Relay is a Flutter client plus a self-hosted Node.js backend for controlling
Claude Code, Codex, Antigravity (`agy`), OpenCode, and Hermes on the backend
machine. Keep public usage guidance in the root READMEs, operational detail in
Claude Code, Codex, OpenCode, and Hermes on the backend machine. Keep public usage guidance in the root READMEs, operational detail in
`docs/handbook.md`, and security guarantees in `SECURITY.md`.

## Working safely
Expand All @@ -26,7 +25,7 @@ flutter analyze --no-pub
flutter test --no-pub
flutter test --no-pub test/agent_controls_test.dart

node --check server/server.js
(cd server && git ls-files '*.js' | xargs -r node --check)
npm --prefix server test
npm --prefix server start

Expand All @@ -43,11 +42,14 @@ clients and bundles CanvasKit locally instead of depending on gstatic.
platform adapters.
- `server/server.js`: server configuration, middleware, shared runtime state,
scheduling, route context, and optional Web static hosting.
- `server/routes/`: API routers for metadata, push, files, chat, BTW, Swarms,
agent login, sessions, quota, and the SSH terminal ticket.
- `server/routes/`: API routers for metadata, push, files, chat, Swarms,
sessions, quota, and the SSH terminal ticket.
- `server/lib/`: agent runners, settings/model discovery, persistence, auth,
filesystem policy, history, quota, push, and orchestration helpers.
- `backends/`: Linux, macOS, and Windows install/service adapters.
- `backends/`: Linux, macOS, and Windows install/service adapters. Each OS has
`setup`, `start`, `stop`, `status`, and `uninstall` entry points.
- `.github/workflows/ci.yml`: runs the verification commands below on pull
requests. Update it when those commands change.
- `scripts/`: development, deployment, and screenshot helpers.
- `test/` and `server/test/`: Flutter and Node test suites.

Expand All @@ -70,30 +72,80 @@ clients and bundles CanvasKit locally instead of depending on gstatic.

- `server/lib/agents.js` is the process-runner boundary. Pass per-request state
through `runAgent(..., { workdir, settings, sessionKey })`; do not add globals.
- `server/lib/agent-options.js` owns option validation and exact CLI argv.
`server/lib/agent-settings.js` persists normalized solo-chat settings.
- `server/lib/agent-options.js` owns option validation and the CLI, SDK, or
protocol representation of each setting. `server/lib/agent-settings.js`
persists normalized solo-chat settings.
- No agent runs one process per turn. All four keep a live session that turns
are fed into. Every pool is a *cache*: the stored session id stays
authoritative, so any scope without a live session cold-starts by resuming it
and degrades to exactly the old per-turn behaviour. Do not reintroduce a
per-turn `spawn` for an agent that has a pool.
- `server/lib/claude-session-pool.js` — one Agent SDK process per scope.
Settings resolve to SDK options (`claudeSdkOptions`) rather than argv, and
are fixed for the life of a process, so a change restarts it with `resume`.
- `server/lib/stdio-agent-pool.js` — the shared pool for the three CLIs that
speak line-delimited JSON-RPC on stdio. It owns the process, the wire, the
session cap, idle eviction and cancellation; a `driver` supplies the
protocol. One process per agent hosts *all* of that agent's scopes, since
each session carries its own `cwd`, so a large startup cost is paid once
instead of once per chat.
- `acp-session-pool.js` — the ACP driver (opencode, hermes). Settings apply
over the protocol (`acpSessionOptions`) with no restart. Capabilities from
`initialize` gate optional calls: hermes has no `session/close`, so an
evicted session is simply dropped.
- `codex-session-pool.js` — the codex app-server driver. `turn/start`
returns as soon as the turn is *accepted*; the turn is settled by the
later `turn/completed` notification. Everything except the sandbox applies
per turn (`codexSessionOptions`), and the sandbox is what the runner
passes as `fixedKey` so a change reopens the thread — still resuming the
same conversation, without respawning the process.
- Relay answers the agents' approval requests from the configured tier,
because there is no approval UI to route them to. The runner's policy
answers yes or no; translating that into each protocol's vocabulary is the
driver's job (`allow_once` vs `accept` vs `approved`).
- `runAcpAgent` in `agents.js` is the shared runner for opencode and hermes,
which differ only by their pool and their entries in the option tables.
- Deleting or clearing a conversation goes through `purgeSession`, not
`clearSession`: for a pooled agent it also requests CLI-side transcript
deletion on a best-effort basis. Use `clearSession` only for the internal
stale-session retry.
- Test files are `test/*.test.js`. Helper processes live in `test/fixtures/`,
which the runner would otherwise try to execute as tests.
- Fast mode is supported only by Claude Code and Codex and defaults off. Claude
receives a `fastMode` settings override; Codex receives an explicit
`service_tier="fast"` or `service_tier="default"` override.
`serviceTier` of `fast` or `default` on every turn.
- Codex models and model-specific reasoning levels come from structured CLI
metadata, with bundled/cache/static fallbacks. Do not reintroduce binary
string scanning for Codex model ids.
- `GET /api/agents` returns all five known agents with install/auth/usability
state. Claude, Codex, and Agy require OAuth; OpenCode and Hermes credentials
are managed on the host and become selectable when installed.
- The in-app OAuth bridge uses the backend host's `script -qfec` PTY utility.
Keep the process output redacted and never return credential values.
- `describeAgent` and `getSettings` are hot paths for option refreshes and every
turn, so keep them free of subprocesses and per-call file reads.
`model-discovery.js` re-locates a CLI at most once a minute and caches the
result (including "not installed"); `agent-options.js` caches
`models-extra.json` by mtime. A CLI update calls `clearModelDiscoveryCache`,
which is what makes new models appear at once.
- `GET /api/agents` returns all four known agents with install/auth/usability
state. Claude and Codex require OAuth; OpenCode and Hermes credentials are
managed on the host and become selectable when installed.
- Every credential is created on the backend host by the CLI itself. Relay does
not log an agent in. `server/lib/agent-status.js` reads auth state and, for
Claude and Codex, a `credentialExpiresAt` timestamp. `server/lib/usage.js`
separately reads and may refresh their OAuth credentials for quota reporting
and keepalive. A token value must never reach Relay's API or app.

### Backend modules and persistence

- Each route factory receives dependencies through `routeContext`. When a route
destructures a new helper, add it to the context in `server/server.js`.
- Use `server/lib/json-store.js` for JSON state: cached reads, atomic replace,
and owner-only file permissions. Do not create ad hoc read/modify/write stores.
- A generated state file may accept a `RELAY_*_FILE` absolute-path override so
its module is testable without touching deployment state. When adding one to a
file that the file API denies, take the path from the owning module rather than
rebuilding it in `server/lib/filesystem.js`.
- New notifications should go through `server/lib/notify.js`, which fans out to
configured Web Push and FCM channels.
- Prompts are passed as one argv token and are capped by `PROMPT_MAX_BYTES`.
Preserve that validation in every chat path.
- Chat prompt payloads and generated Swarm prompts are capped by
`PROMPT_MAX_BYTES`. Preserve that validation in every chat path.

### Client boundaries

Expand All @@ -113,13 +165,14 @@ clients and bundles CanvasKit locally instead of depending on gstatic.
always applies the precise sensitive-path denylist and optional
`RELAY_FS_ROOTS` allowlist in `server/lib/filesystem.js`.
- A Swarm owns one canonical transcript and private resumable sessions per
member. One human message snapshots the transcript once, then mentioned
members run in parallel from their own delta prompts.
member. A round runs in waves: each wave snapshots the transcript once and
runs everyone summoned in it in parallel from their own delta prompts. The
human's `@mentions` open wave one; `@mentions` inside a member's reply summon
the next wave, bounded by `RELAY_SWARM_MAX_HOPS` (default 3, 0 disables) since
two members naming each other would otherwise never stop. A member never
summons itself, and a failed or cancelled turn summons no one.
- Swarm configuration is stored under the workspace that lists it, while its
chosen work tree is the directory members actually use.
- BTW is read-only and isolated from the main session. Claude forks natively;
Codex and Agy clone their native persisted conversations before resuming the
side scope.
- The SSH terminal exchanges the bearer credential for a short-lived,
single-use WebSocket ticket. Never put the bearer token in a socket URL. A
token record owns one resumable PTY, which runs with the full permissions of
Expand All @@ -131,9 +184,10 @@ clients and bundles CanvasKit locally instead of depending on gstatic.

Generated files under `server/` include `.env`, `tokens.json`, credentials,
agent/chat sessions, history, settings, groups, quota state/schedules, usage
cache, and push/FCM stores. They are deployment state, not fixtures. Keep them
out of patches and release archives. `server/models-extra.json` is also a local
override, not a shared catalog.
cache, and push/FCM stores. They are deployment state, not fixtures. Keep them,
along with any referenced FCM service-account JSON, out of patches and release
archives. `server/models-extra.json` is also a local override, not a shared
catalog.

## Verification expectations

Expand All @@ -144,4 +198,9 @@ override, not a shared catalog.
- Cross-stack API changes: verify both suites and keep old payload parsing safe
when adding response fields.
- Documentation changes: verify local Markdown links, commands, environment
names, and English/Chinese README parity against code rather than old docs.
names, English/Chinese README parity, and the embedded guides in
`getting_started_screen.dart` and `deploy_backend_screen.dart` against code
rather than old docs.
- Release bumps touch four places, which drift apart if any is missed:
`pubspec.yaml`, `server/package.json`, `_applicationVersion` in
`lib/features/settings/app_settings_screen.dart`, and a `CHANGELOG.md` entry.
Loading
Loading