Components
Terminal attachment uses a separate Python interpreter (broker/pty_exec.py) in a new OS session to acquire the PTY as its controlling terminal before executing tmux. It needs no Linux setsid utility or threaded preexec_fn. Allocation, sizing, spawn and handshake share one cleanup lifetime. The output pump reads at most one 64 KiB chunk before awaiting socket drain, with no PTY reader registered while blocked. WebSocket writer claims are released even when connecting fails or is cancelled.
The terminal API reauthorizes both the current login role and the session's current ownership every 30 seconds. Owner demotion closes affected read-only and writable sockets immediately, preserving the person's own terminals and login.
The session terminal exposes xterm's accessibility tree by default, with a browser-local toggle. F6 exits from both the input and accessibility rows; a skip link bypasses the grid. Its text dialog reads an on-demand snapshot through GET /api/sessions/{id}/terminal-output, authorized by session ownership and routed to that session's broker account and machine. The read-only broker operation captures up to 10,000 tmux history rows plus the current screen, joins wrapped lines and returns at most 200,000 characters with a truncation flag. The response is not cached (Cache-Control: no-store), and no transcript is stored. A failed capture offers the parsed browser buffer with an explicit limitation. Details: docs/decisions/0045-accessible-terminal-output.md in the repository.
New session sends project_id to /api/providers/registry and /api/models. The API authorizes that project, then queries its host and execution account, rather than trusting arbitrary account selectors from the browser. Without a project it uses the current person's local account. The UI caches these results by project (or local account), separately from other screens' capability registry, and clears model/effort choices when switching scope.
Provider login dialogs are keyed by provider, account and machine. Each start operation owns its immutable scope and active lifetime. Cleanup cancels an existing flow or cancels a pending start when its response arrives. Polling is serial (the next timeout starts after the previous response), and late start/poll/submit responses cannot update a replacement dialog. Cancellation failures are reported to the browser console.
accd (src/acc/server)
app.pybuilds the shared context and wires the routers.api/: one router per area:auth,projects,sessions,inbox,operations(host, providers, models),catalog,publishing,accounts,terminal(websocket),wiki, andfrontend(built UI and security headers).context.pyholds shared state and the auth checks;schemas.pythe request and response shapes.supervisor.pywith mixinspolling.py,ingest.py,reconcile.py: the background loop.api/multiruns.pycomposes ordinary session launches and redacted Changes into person-owned groups. Each accepted attempt keeps an ordinary session and worktree; refused attempts remain visible. Keep/archive never removes these worktrees or branches. In-process initial launches are excluded from recovery until their first broker launch completes; after a dashboard restart the usual recovery rules apply.- Reconcile keeps a run open when tmux reports a dead pane without an exit status or signal: terminal closure can precede child exit. It records the result before scheduling a retry or replacing the pane, preserving recovery history across polling timing changes.
- One bounded pending-hook drain precedes both startup verification and exit closure. Team helpers can start after the iteration's ingest pass; slow intervening stages must not cause a false missing-start-hook alert when
SessionStartis already available. Both decisions wait for remaining known backlog, preserving final hook reasons. The normal startup timeout still applies when the hook is absent. - For a dead pane with no result, the broker runs a fixed
truejob on its own tmux socket and reads the panes again once. This wakes child collection on older tmux builds that can lose SIGCHLD during utempter cleanup. It changes no machine settings and never signals an arbitrary PID. compiler.py: turns launch inputs and the catalog into the exact CLI arguments and generated files. The New session preview uses the same function, so it matches the launch byte for byte.catalog.py,catalog_store.py,catalog_import.py: the repository catalog plus dashboard additions in a local overlay; GitHub import with review checks.local_workspace.py: workspace folders and the built-in group (builtin = 'local', one per account on this machine). It is found by account and host, and refused unless its owner, account, host and folder all match; the slugslocalandlocal-*are reserved at registration, import and promotion.push.py: Web Push (VAPID signing, aes128gcm encryption); the supervisor sends new inbox items to subscribed devices.publish.py,cloudflare.py: the publishing pipeline.github.py: read views and repository creation.pulls.py(routes inapi/pulls.py): a session's pull request (create, update, ready, reviewers, merge checks) and the redacted, untrusted-framed messages for its agent; owners' calls use the dashboard token, members' go through the broker'sgithub_apiop as their ownghlogin (docs/PR-FINISHING.md).tools.py,api/tools.py,acc.tooling: registry-only tool approvals, scoped authorization, manager validation/execution and resolved-version inventory. System changes useacc-packages; account changes use a fresh unprivileged broker connection. The guard classifies parsed installs, not unfamiliar executable names.vault.py: the key vault: encryption withdata/secrets/vault.key,.envparsing, which keys a launch gets (and the refusal of duplicate variables), the instructions paragraph, redaction, and the one-time move of the catalog's MCP secrets. The supervisor's_start(launch and resume) delivers keys through the broker'ssession.env.db.pyandmigrations/: SQLite by default or opt-in PostgreSQL, with Alembic migrations applied at start.reconcile.pyreads session identities and broker routing in a short database scope, fetches pane lists after closing it, then reloads each session in its own transaction. A request or external writer committing during the broker list call cannot invalidate the later pane-state update. Deleted sessions and sessions moved out of ACC management are skipped. Broker parameters that can load placement/routing relationships are resolved before committing, so evaluatingcwdorhostcannot reopen a read snapshot immediately before a broker await.history.pyfetches transcripts through the broker without holding a database transaction, then persists each session in a joined write-intent worker.db.write_in_threadkeeps lock waits off the event loop and drains the worker on cancellation. Automation cancellation and model-cache persistence use the same helper; host-pressure polling uses the shared write-intent decorator in its existing worker.
API (selection)
| Route | Purpose |
|---|---|
GET /api/sessions, POST /api/sessions, POST /api/sessions/preview |
List, launch, preview |
GET · POST /api/multiruns, GET /api/multiruns/{id} |
List, start 2–5 independent runs with partial budget refusals, inspect a group |
GET /api/multiruns/{id}/compare?left=...&right=..., POST /api/multiruns/{id}/keep |
Compare two authorized member sessions; mark a winner and optionally confirm stopping/archiving the others |
POST /api/sessions/{id}/stop · resume · interrupt · archive · rename · promote |
Session actions |
WS /api/sessions/{id}/tty |
Terminal |
GET /api/workspaces |
Local workspaces |
GET /api/wiki/{project|workspace}/{key} (+ /page, /attachment) |
Wikis; guide/acc is the Guide tab: this wiki, read by accd from its own checkout, for everyone signed in |
GET /api/projects, POST /api/projects, /import, /{id}/trust |
Projects |
GET /api/attention, POST /api/attention/{id}/ack |
Inbox |
GET /api/catalog, POST /api/catalog/preview · import · skills · mcps |
Catalog |
GET /api/publish, POST /api/publish, /{id}/approve · reject · unpublish |
Publishing |
POST /api/workspaces/{folder}/delete |
Delete a local workspace (typed folder name; archives its sessions) |
GET /api/push, POST /api/push/subscribe · unsubscribe · test |
Notification devices |
GET /api/host · /api/providers · /api/accounts · /api/audit |
Read views |
GET /api/host?machine= |
Another machine's details over its link (owners; node_host.py, docs/MULTIHOST.md "Host details") |
GET /api/board |
The status board's machines and projects (owners; api/board.py) |
GET /api/sessions/{id}/changes |
Current-branch diff, sharing the polled badge's baseline (broker workspace_changes) |
GET /api/search?q= |
History search, scoped like /api/sessions; optional project_id, provider, and inclusive UTC session-start date since=YYYY-MM-DD filters apply before ranking/limits. indexed_sessions counts only filtered, accessible conversations with indexed messages |
GET · PUT /api/guardrails |
Guardrail rules (everyone reads; owners write) |
GET · POST /api/schedules, PUT · DELETE /api/schedules/{id}, POST /api/schedules/{id}/enable · disable · run · cancel |
Person-owned saved launches and timing; authenticated people only, creator or owner access; stable run intent and conditional dispatch claims in the supervisor loop |
GET /api/tools, POST /api/tools/requests, /requests/{id}/approve · reject, /{id}/remove |
Account/machine tool requests, decisions, installed versions and approved removal |
GET · PUT /api/tools/policy |
Owner-only person/project tool-approval settings |
GET · PUT /api/budgets, POST /api/budgets/extra |
Your own usage budgets |
GET · POST /api/vault/keys, POST /api/vault/keys/{id} (+ /replace · remove · give · take) |
Key vault: your keys and shared ones; values are write-only |
POST /api/vault/import/preview · import |
Paste a .env: variables and problems (never values), then add the chosen ones |
GET /api/projects/{id}/keys |
Keys given to a project, and the ones you could give it |
GET /api/vault/people, POST /api/vault/people/{username}/remove |
Owners: key counts per person, delete a leaver's keys |
Broker (src/acc/broker)
server.py (JSON lines over a peer-checked Unix socket), ops.py (operations, including git: worktrees, workspace_changes, screen prompts), attach.py (framed PTY stream for the terminal), logins.py (CLI re-login flows; a pseudo-terminal for CLIs that read the code only from one), models.py (model catalog). Each CLI's conversation file is read for history search by providers/transcripts.py.
workspace_changes recomputes the current HEAD's merge-base with origin/main or local main first, then origin's recorded default, the configured upstream, origin/master or master. It never fetches. comparison_ref identifies that base in summaries and detail; without a shared reference it explicitly reports working changes against HEAD (the empty tree before the first commit). Unexpected Git errors remain errors. Plain folders and untracked nested scratch folders return git: false; tracked subfolders use relative, subfolder-scoped diffs. The stored launch base/argument remains for older node brokers. See docs/decisions/0066-current-branch-change-baselines.md in the source.
Codex question detection checks only the last eight non-empty screen rows and requires live footer controls: a positive question count with shift+← to answer at the bottom (optionally above the main prompt), or Type your answer with the submit/skip/main-prompt key hints at the bottom. Only the prompt detail reaches the supervisor. Reconciliation refreshes a screen-sourced non-command prompt's activity and Inbox detail when it changes (for example, opening a queued question), and clears them when the controls disappear. Hook-sourced waiting states remain owned by hooks.
The provider's screen_idle signal reports a Codex turn-ending error only when a line beginning with ■ is directly above the main input prompt in that live footer, with no esc to interrupt indicator. The broker passes the error text, not the screen, to reconciliation, which redacts it and records Idle with the error as activity. Other providers return no idle signal by default.
Node (src/acc/node)
acc-node runs on a machine the primary drives (multi-host, design in docs/MULTIHOST.md). It opens no port: it keeps one outbound WebSocket to the primary's /api/nodes/connect and relays channels to the node's own broker sockets.
cli.py:acc-node enroll | run | status.enrollmakes an Ed25519 key, posts the public half with the one-time code to/api/nodes/enrolland writes the config file; the key is replaced only after the primary accepts. Hidden:enroll --installed-state-dirandadopt node.key|ca.pem(stdin), which the Linux wrapper uses so root enrolls into a staging folder and theacc-nodeuser stores its own files. Files are written withwrite_atomic(random temporary, renamed over the target, never following a link).identity.py: the key, the challenge signature (nonce || primary id) and TLS trust. Every connection is fully verified (chain, hostname,CERT_REQUIRED), either against the system CAs (--trust system, a public certificate) or against the primary's private CA only (--ca-fingerprint, e.g. Caddy's internal CA). The leaf is never pinned, since it renews (Let's Encrypt about every 60 days, Caddy's internal CA every few hours).acc.wslsupplies shared WSL detection, effective interop observation and metadata-only drive access checks.client.pyasks each agent broker'sping(wsl_isolation=true)for access under that account, with bounded parallel calls; absent/old brokers report unknown. The authenticated hello and periodic update-state info carry the sanitized observations. Host warns on either reported exposure, not pending configuration. Owner-onlyPUT /api/nodes/{name}/wsl-isolationstores dismissal undernode.wsl_isolation.<node-id>in Setting and auditsnode.wsl_isolation; it does not change node configuration and cannot be set by node reports.deploy/node/install.shoffers isolation only as an explicit WSL opt-in (interactive default No, or--wsl-isolate;--wsl-disable-automountis a separate choice). It runs the stdlib-onlyacc.wsleditor in Python isolated mode, preserving comments/unrelated keys and making a protected backup before atomic replacement of a safe/etc/wsl.conf. Unparsable or linked files are refused;--updatenever changes this configuration.enroll.py: with--ca-fingerprint, the CA root comes fromGET /api/nodes/ca(fetched without verification, accepted only if the SHA-256 of its DER matches) and is saved asca.pem; the enrollment POST is already verified against it.--fingerprintis a deprecated alias.client.py: the connection (challenge → hello → ready; 4401 means rejected or revoked),ping/pongevery 20 s (three unanswered pings drop it), reconnects with backoff 1 s doubling to 60 s (back to 1 s after a successful handshake), and the relay. Frames aretype | channel id (4 bytes, big-endian) | payload:oopens a Unix connection to<socket dir>/<account>.sock(names must match^[a-z_][a-z0-9_-]{0,31}$, and a symlink out of the socket dir is refused), answered bykore;dcarries at most 1 MiB;ccloses either way. Each channel is its own connection; data for one broker that stops reading is capped at 16 MiB, then that channel is dropped. At most 64 channels, live or draining, exist at once (more opens gete); a channel the primary closed has 30 s to drain; a disconnect aborts them all. The primary's side (src/acc/server/nodes.py) has its own budgets, listed indocs/MULTIHOST.mdunder Budgets.
Web UI (web/)
React, TypeScript, Vite and Tailwind. components/ui.tsx holds the design system's primitives; pages in pages/. Built into src/acc/server/static and served by accd.
Scripts and helpers
bin/acc-hook,bin/acc-statusline: what sessions call; append to the spool.bin/acc-guard: every CLI's pre-tool hook. Records the step (the "doing now" line) and enforces the session'sguardrails.json, answering in each CLI's deny format (table indocs/PROVIDERS.md).bin/acc-request: agents ask to publish or change tools; tool coordinates are validated before spooling.bin/acc-wiki-check: checks public wikis.deploy/: systemd units, the root helpers, Caddy and AppArmor files,install.sh.deploy/containers/acc-containers: the trusted container launcher (root, standard library only). Its protocol, checks and lifecycle are indocs/CONTAINERS.md("Trusted launcher"). accd's side isserver/containers.py(limits, grants, audit ingestion) andapi/containers.py; the broker's isbroker/containers.py.
ACC updater
acc.server.updating checks stable releases daily and schedules opt-in installations. /api/updates is owner-only.
acc-update.path starts the root helper for action/version request files. It backs up with acc-backup, verifies
release SHA256SUMS, safe archive members and BUILD, swaps source, runs the new deploy/setup.sh --yes (which keeps the existing main settings and calls deploy/install.sh), and checks version/build
health. Failure restores the previous source and pre-install database before reinstalling the previous version.
Steps and results are in updates/state.json; full backups and previous source trees remain for recovery.
Linux nodes use the same helper in node mode, driven through update / update-state signed-link messages.
The shared source bundle validator lives in acc.bundle. See the repository's docs/UPDATES.md.
For ordinary older Linux/WSL nodes that cannot accept the update message, Host → Machines supplies copyable, enrollment-preserving root recovery commands from this main's verified source bundle. Mac rows offer an installer only if one exists for the build; pre-v1 builds have none. See older-machine recovery, including the separate independent-update rule for backup receivers.
Session automation
server/automation.py validates per-session settings and makes deterministic continuation decisions. Migration 0033 adds the session's JSON automation state: goal bounds, original start and counters, waiting reset identity, consumed reset, input cancellation and dispatch claim/result. Reconcile captures provider limit/permission signals through the broker's automation_screen operation, checks indexed assistant markers, then commits a stable claim before sending literal text and Enter. Claimed or uncertain sends are not replayed after restart. A missing delivery acknowledgement needs human inspection; this is at-most-once dispatch, not guaranteed delivery.
The API uses session ownership (or ACC owner), with session.autoresume / session.goal.* audit entries. Terminal input cancels before forwarding; submitted prompt hooks also cancel unless they match ACC's expected prompt. Goals run independently of Orchestrator teams and scheduled sessions. Budget pauses veto automated input. Plan readings belong to the default agent account; other accounts use CLI reset signals and wait when an optional usage cap cannot be measured.
Group authority API (phase 1)
All group routes live below /api/groups. Inaccessible resources return 404;
instance-owner-only actions return 403 to ordinary members. Authority writes carry the
current positive revision, compare it atomically and advance it on commit.
A 409 means refresh and review again. Shared group views never proxy a live checkout.
Route relative to /api/groups |
Contract |
|---|---|
GET /, GET /{group} |
Current memberships only, or all groups for a trusted instance owner |
POST / |
Instance owner: name, existing maintainer_id; explicit opt-in |
GET /{group}/people |
Maintainer/owner: existing ACC IDs, usernames, group roles/provenance only |
POST /{group}/members/{user} |
Maintainer/owner, or local self-removal: role (maintainer, member, viewer, remove); only instance owners appoint maintainers; departure revokes the person's group key consents atomically |
POST /{group}/archive |
Instance owner; preserves history, revokes all group key consents atomically and refuses future changes |
POST /{group}/unpause |
Maintainer/owner; audited override authority, execution still gated |
GET/POST /{group}/projects |
Members read safe metadata; maintainers/owners create with name, slug, nonempty maintainer owners, optional workspace |
POST /{group}/projects/{project}/owners |
Maintainer/owner: replace nonempty named maintainer owners atomically |
POST /{group}/projects/{project}/archive |
Maintainer/owner; metadata archival |
POST /{group}/projects/{project}/approvals |
Current named maintainer: branch, exact 40-character old_sha, new_sha; records receipt, never executes a merge |
GET/POST /{group}/projects/{project}/placements |
Own reservations only (all for instance owner); instance-owner-only approval takes user_id, derives same-host account/path, leaves trust pending |
POST /{group}/key-grants |
Key owner only: key_id, optional project_id and host; consent only, no delivery |
POST /{group}/key-grants/{grant}/revoke |
Key owner only; invalidates current consent; departure/archive already deletes affected grants |
GET /{group}/audit |
Maintainer/owner: allowlisted action/actor/time/receipt ID, no detail JSON |
Archive and membership changes invalidate earlier merge receipts through revision changes. Phase 2 Git exchange rechecks receipt, membership and named ownership under the ref-update fence; an HTTP receipt alone cannot authorize a merge.
Group exchange (phase 2)
The exchange adds authenticated upload-pack/receive-pack through private per-request staging and bounded pack helpers. Account brokers provision private placements and standalone session worktrees, with expanded-content checks before checkout. Published Files/Wiki reads Git blobs rather than live files. Group and private recovery use separate authorization/stores and existing host capture/restore. See Group Exchange for routes, tokens, limits, identity fences and crash recovery. Group sessions launch in private same-host checkouts (see Groups).
This page is generated from docs/wiki/Internal/Components.md; the same page is under Guide inside Rynhult.