AGL - Agent Gateway Layer
AGL, the Agent Gateway Layer, runs OpenCode and FlexHarness behind a passkey-authenticated controller with a detached CLI lifecycle and a foss.global browser UI.
In local mode the controller binds its UI and @api.global/typedsocket RPC listener to 127.0.0.1 only — plain-HTTP admission rests on browser-supplied headers, so the loopback bind is the network boundary. Only proxy mode (--behind-tls-proxy) binds all interfaces, for a trusted TLS reverse proxy. The supervised OpenCode server always remains on 127.0.0.1, uses a generated per-process Basic Auth password, and is accessed only through the official @opencode-ai/sdk v2 client. OpenCode's own browser application is not embedded, proxied, scraped, or duplicated.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Security posture
The controller runs on @api.global/typedserver 11 and @api.global/typedsocket 8, whose exact package-major handshake completes before application RPC and whose client connection tags are default-deny and owner-controlled. The controller registers no clientTagPolicy, so every client-initiated tag mutation is rejected at the transport layer; connection identity and authentication state live exclusively in server-owned peer state, and event, terminal-output, and browser-frame pushes target connections by verifying that state server-side — never by client-managed tags. (Earlier revisions of this repository were deliberately withheld from publication until these upstream packages shipped, rather than concealing the boundary with a local shim.)
Setup-code attempts are globally rate limited (10 per minute, wrong codes included). During the pre-enrollment window a client that can reach the listener could therefore hold enrollment closed by burning the budget — a deliberate lockout-over-guessing tradeoff: the window exists only before the first enrollment, and in local mode only loopback processes can reach it at all.
Architecture
Browser
└─ exact configured HTTP(S) origin
└─ controller UI + TypedSocket RPC
(127.0.0.1:<controller-port> local mode; 0.0.0.0 behind the TLS proxy)
├─ WebAuthn passkey verification
├─ SmartData configuration, ceremonies, audit events, and public projections
├─ controller-owned Smartshell/node-pty terminal processes
├─ official OpenCode SDK v2
│ └─ authenticated OpenCode server (127.0.0.1:<opencode-port>)
└─ private framed IPC
└─ isolated FlexHarness child
├─ filesystem and shell tools
├─ ephemeral read-only Session Intelligence workers
├─ provider adapters
└─ kernel-keyed sealed provider credentials
The browser receives only controller DTOs and never receives the OpenCode Basic Auth password, provider credentials, raw provider responses, or the private database descriptor used to initialize the Flex child. An authenticated browser can register existing filesystem directories as projects and select models from either runtime's normalized catalog; the controller validates and scopes every resulting operation.
OpenCode and Flex are independently supervised. An unexpected Flex child exit closes prior-generation operations and browser channels, then restarts with capped backoff and reconciles the exact registered projects before becoming ready. An unavailable or unsupported Flex runtime does not take down OpenCode, and durable bounded Flex projections remain readable while the Flex child is offline. Flex is currently supported on Linux x64 with Node.js 24 or 25; other supported controller platforms continue to provide OpenCode sessions and controller-owned terminals.
FlexHarness private state is persisted in separate SmartData domains for scope metadata, public message projections, remembered permissions, canonical Agent events and archives, and background tool jobs. A fenced per-project writer lease excludes overlapping Flex children, while the parent reads a separately committed bounded public projection. Lease heartbeats and writes share one serialized transition boundary; an expired lease still held by the same controller recovers with a fresh owner token and incremented epoch before work continues. Hidden terminal staging and private canonical events never enter that browser-readable projection; a visible Flex save is acknowledged only after the corresponding bounded view is durable.
Flex runtime implementation classes and child internals are private. Supported package surfaces include the agl CLI, protocol-v22 controller DTO and request contracts, and the root-exported controller-owned Git reversion types and engine.
Requirements
- Node.js 24 or newer
- Git 2.43 or newer for workspace reversion and controller-owned worktrees
- A WebAuthn-capable browser
- Linux or macOS for detached
startand CLI-managedstop - Native
node-ptysupport. When no prebuilt binary is available, installation requires Python 3,make, and a C/C++ build toolchain.
No database server is required: the controller ships an embedded engine
(@push.rocks/smartdb) by default. An external MongoDB-compatible server can
be configured instead.
The package pins @opencode-ai/sdk and the official OS/architecture-specific OpenCode binary packages to the same tested version. It resolves the lockfile-installed platform executable directly, without a wrapper postinstall or unrelated global binary. This release is tested against OpenCode 1.18.15.
The OpenCode child, Flex child, and controller-owned terminal shells receive only ordinary runtime path, user, locale, temporary-directory, XDG, proxy, and CA environment variables by default. Controller/database variables and unrelated parent secrets are not inherited. Explicitly delegate additional environment variables when needed:
export HARNESS_CONTROLLER_FORWARD_ENV='ANTHROPIC_API_KEY,OPENAI_API_KEY'
The comma-separated names must already exist in the controller environment. Controller variables, the delegation directive itself, and generated OpenCode server credential names can never be forwarded. Forwarded variables are present in the Flex child's sanitized runtime as well as OpenCode's.
Flex provider authentication does not consume delegated environment credentials: its initial provider integration uses the OpenAI device OAuth flow in the authenticated settings UI. The controller's source-of-truth credential persistence is a Flex-child-owned SmartSecretSealedFileStore, with authenticated ciphertext under the controller data directory and its master key in the Linux kernel keyring; provider credentials never enter SmartData or a controller plaintext filesystem fallback. Existing kernel-only credentials migrate idempotently before Flex becomes ready, preserving their exact validated bytes and deleting the source only after the sealed destination is confirmed. A fresh empty store creates its master key normally; retained ciphertext with unavailable key material, conflicting or malformed credentials, and unreconciled mutation outcomes fail closed instead of replacing credential material. An individually absent credential marks that connection as requiring reauthentication. Settings shows normalized account quota windows without exposing the credential or raw provider response and directs the user to log out and connect OpenAI again when possible.
OPENCODE_AUTH_CONTENT is rejected case-insensitively before any controller-owned OpenCode, Flex, or terminal runtime is spawned. The controller never reads or edits OpenCode's auth.json; a selected Flex OpenAI credential reaches the controller only through private framed IPC, then reaches the authenticated loopback OpenCode process through the official SDK's auth endpoint.
Credential-free HTTP_PROXY and HTTPS_PROXY URLs are part of the minimal runtime environment. Proxy URLs containing user information, and malformed proxy values, require explicit delegation through HARNESS_CONTROLLER_FORWARD_ENV.
Install and build
pnpm install
pnpm build
node cli.js help
The package is published to npm as agl with the binary name agl:
pnpm add --global agl \
--allow-build=node-pty \
--allow-build=fs-ext-extra-prebuilt
agl help
Alternatively, install from the repository:
git clone https://code.foss.global/modelprofile.com/agl.git
cd agl && pnpm install && pnpm build
node cli.js help
Bridge migration to AGL
The package transition is deliberately split across two invocations.
Starting from @modelprofile.com/harness-controller@20.0.2, run
the active hcon command twice and wait for each command to finish:
hcon upgrade --port 4097
hcon upgrade --port 4097
The first invocation installs the exact same-package bridge
@modelprofile.com/harness-controller@20.0.3 through coordination transaction
format 2; its command remains hcon. The second invocation, once the exact
target is available, resolves and validates agl@21.0.0 before controller
preparation or package mutation, then performs the format-3 cross-package
transition. After that invocation succeeds, hcon is replaced and all further
management commands use agl:
agl help
Use the controller's configured port instead of 4097 for both invocations.
Do not skip the bridge or manually replace the package. This package transition
does not move controller data again: the current installed data root remains
$XDG_CONFIG_HOME/hcon (default ~/.config/hcon), and existing internal and
persisted identities remain unchanged.
Upgrade behavior
AGL uses upgrade management version 2.
Each invocation launches a detached worker with a private log and follows it to
a terminal result. The same-package invocation compares the installed semantic
version with the old package's npm latest tag and never downgrades; the exact
20.0.3 bridge invocation instead resolves the fixed AGL target described above.
If the controller is running, it authorizes the worker through a one-time local
grant, asks the controller to accept preparation through a bounded management
request, and polls the private durable transaction while the controller
gracefully pauses the exact active-session cohort. Only preparation completed
within the absolute grace deadline and durably committed to the transaction lets
the worker stop the exact verified process, change the package installation, and
restart the controller with its existing environment and stored configuration.
If the controller is stopped, it remains stopped. The public management
transport exposes versioned launch, prepare-begin, legacy prepare-completion,
and finalize methods, but every fresh state-changing handler accepts only a
locally created, single-use 256-bit grant bound to the exact controller process
and package installation.
In normal progress mode, the command prints the worker PID and log path before
allowing the worker to stop anything; --json records those values durably
before admission and emits them only in its terminal document. Concurrent
upgrades and pnpm-global controller starts are serialized. A same-package
failure before target startup restores the previous version when safe. During
the format-3 package transition, failures before the durable target-commit
boundary restore exact bridge-only ownership; from that boundary onward,
recovery converges only to exact agl@21.0.0 and never restores the bridge.
Session compensation is attempted where rollback remains safe, but any session
whose pause cleanup cannot be proven settled remains fail-closed instead of
receiving an overlapping continuation. Once the new controller has been
invoked, automatic rollback is intentionally disabled because startup may have
applied forward migrations. A retained target process keeps its transaction
token and session barrier and remains fail-closed until exact recovery can
complete; inspect its retained log and correct the reported problem rather than
spawning another controller or reinstalling the previous package. A
stopped-controller upgrade has no inherited cohort and leaves the target
stopped. The worker also refuses an upgrade when another pnpm-global controller
instance exists outside the exact authorized process.
hcon upgrade must be run from the active pnpm-global installation and is currently supported on Linux and macOS. Before starting the AGL transition, if a prior manual same-package upgrade replaced the management CLI while an older controller remained running, restore that exact older package first so its process identity can be stopped safely:
pnpm add --global @modelprofile.com/harness-controller@<previous-version>
hcon stop --port 4097
pnpm add --global @modelprofile.com/harness-controller@latest
hcon start --port 4097
This repair sequence does not replace the two bridge-migration invocations.
Database configuration
Structured application state is stored exclusively through SmartData. There is no PID, configuration, authentication, or JSON persistence fallback on the filesystem.
By default the controller runs an embedded @push.rocks/smartdb engine — no
external database server. Storage lives under
$XDG_CONFIG_HOME/hcon/smartdb (default ~/.config/hcon/smartdb). A repository checkout instead uses
./.nogit/smartdb, keeping development data inside the checkout. Override the
directory with HARNESS_CONTROLLER_DB_DIR (absolute path). On the first
installed command that needs controller data after upgrading, the controller
completes any prior XDG-state database import under the retained
$XDG_CONFIG_HOME/harness-controller root. When that config-root database
exists, SmartDB's stopped-root relocation moves its exact inode to the hcon
root and replaces the old smartdb pathname with a verified receipt, not a
database backup. The controller verifies that receipt, storage identity, and
logical content digest. This relocation is supported on SmartDB-qualified Linux
filesystems and writable local APFS on macOS. When only the historical XDG-state
database exists, it is instead imported directly into the new root with the same
logical-digest verification while retaining that historical source as the
non-authoritative backup. The outer migration is journaled and safely retries
its exact operation after an interruption. Retained legacy sources are never
imported again after completion; an older controller binary can still open and
diverge a retained historical backup, so do not resume using it. Keep the old
controller stopped throughout the one-time migration. Explicit database paths
and external MongoDB configurations are never moved. The engine listens on a unix socket, and a second controller
process on the same machine (for example agl temp-password) attaches to the
running daemon's engine instead of opening the storage files again.
When upgrading from a controller release backed by FlexHarness 2, the new Flex child migrates each active project's legacy snapshot during initialization, before admitting runtime work. Migration preflights every destination, writes per-session domains before publishing scope discovery, repairs crash-interrupted streaming messages as cancelled, and safely reruns after partial completion. A durable completion marker prevents the retained source from being reapplied after live FlexHarness 3 state evolves. The legacy private snapshot remains retained as a non-authoritative recovery source until that project is removed; startup also repairs any bounded public projection whose prior save was not acknowledged.
To use an external MongoDB-compatible server instead, set:
export HARNESS_CONTROLLER_MONGO_URL='mongodb://user:password@127.0.0.1:27017'
export HARNESS_CONTROLLER_MONGO_DB='opencode_controller'
HARNESS_CONTROLLER_MONGO_DB defaults to opencode_controller in both modes.
For local package development, MongoDB is declared as the GitZone service:
gitzone services start mongo
gitzone services status
Use the MONGODB_URL from .nogit/env.json as HARNESS_CONTROLLER_MONGO_URL. GitZone service data remains under .nogit/ and is not application persistence or package content.
First start
From the workspace OpenCode should control:
agl start
The detached process first reserves the controller listener (127.0.0.1:4097 in local mode; 0.0.0.0:4097 only with --behind-tls-proxy), spawns the isolated Flex child, starts the official OpenCode server in headless server mode on 127.0.0.1:4098, and validates local PTY support. On an unsupported runtime the spawned Flex child reports unsupported; Flex startup is isolated and does not block OpenCode readiness. Before the first enrollment only, the terminal prints a one-time setup code — a random 32-byte value by default, or the operator's own value when --setup-code is passed.
Open the printed URL, enter the setup code, and create a passkey. The code expires after 30 minutes and is atomically consumed by the first successful credential enrollment. If an unenrolled code expires, stop and start the controller to rotate it.
--setup-code <code> accepts 4–256 printable non-whitespace ASCII characters and trades entropy for convenience — understand what it weakens before using it. The enrollment rate limiter (10 attempts per minute) is in-memory and resets on every restart, so a fixed code accumulates guessing budget across restarts instead of being invalidated by rotation the way random codes are. The stored verifier is an unsalted SHA-256 hash, so a low-entropy code is trivially recoverable by anything that can read the controller database. And while the code never appears in the detached child's process list (it travels over parent-child IPC), it is visible in the parent CLI's own argv and your shell history. On a shared machine, prefer a long random value or omit the option entirely.
Exactly one passkey credential exists per controller database, permanently: enrollment is first-wins and no re-enrollment path is exposed. A lost passkey is not recoverable through the controller — recovery means deleting the controller's database (or its auth collections) and enrolling again from a fresh setup code. This is deliberate; an online credential-reset path would be a second, weaker way in. Up to eight separately expiring CLI temporary-password credentials may be active at once.
Authentication is bound to the exact TypedSocket peer and disappears when that socket disconnects. A fresh browser connection can authenticate with a passkey or CLI temporary password, or re-authenticate by consuming the prior peer's bearer resume token. The raw resume token is kept only in browser session storage while its SHA-256 hash stays in controller memory; it is single-use, rotates on every resume, is invalidated by controller restart, and has a fixed maximum chain lifetime of 12 hours that is additionally capped by temporary-password expiry. The controller does not issue cookies, trusted client tags, or localhost authentication bypasses.
CLI
agl start [options]
agl status [--port 4097] [--json]
agl stop [--port 4097]
agl upgrade [--port 4097] [--grace-period-seconds 300] [--continue-sessions] [--json]
agl foreground [options]
agl temp-password [--port 4097] [--ttl-hours 24]
agl help
agl --version
First-start options:
--port <port> controller port, default 4097
--opencode-port <port> loopback OpenCode port, default 4098
--directory <path> fixed OpenCode workspace, default current directory
--projects-root <path> default base directory: relative project paths and
initial suggestions resolve here; explicitly typed
absolute projects may be outside it; adopted once on
first v2 start, immutable afterward
--public-origin <origin> exact browser WebAuthn origin
--rp-id <hostname> WebAuthn RP ID
--behind-tls-proxy declare trusted TLS termination upstream
--setup-code <code> operator-chosen setup code (see the warning above)
Runtime configuration is durable and immutable per controller port. Later management commands must use the same --port; omitted first-start options are loaded from SmartData.
upgrade keeps the CLI attached to its detached, crash-survivable worker and streams progress until the durable transaction succeeds or fails. Before stopping a running controller it closes new harness-prompt admission, durably records the exact cohort and absolute preparation deadline, then acknowledges the bounded request while controller-owned work waits for active operations and asks those sessions to pause at the earliest convenience. The worker polls that private transaction with bounded source-process checks; it cannot stop the controller or enter package installation until preparation completion is durably committed on time. The global upgrade lock also drains controller-start and temp-password database-writer leases, then package replacement requires the source port to be closed and no exact source controller, temp-password, or Flex child writer to remain. A failed or expired preparation compensates settled pauses and leaves uncertain cleanup fail-closed. The retained cohort is limited to 64 exact sessions. --grace-period-seconds accepts 1–3600 seconds and defaults to 300. Without --continue-sessions, the upgraded controller reopens prompt admission without sending another message. With it, the matching continuation message is submitted at most once to the durably retained paused cohort after the upgraded controller returns. Rollback recovery attempts to compensate paused sessions within a separate maximum 60-second finalization window and does not overlap an unresolved pause with a continuation. --json replaces progress lines with one terminal JSON document after worker admission; preflight and launch failures still use normal CLI errors. That document includes worker identity and log path, whether the controller was running, source version, optional target version, terminal phase/success/error, and pause/continuation state counts; failures exit nonzero after writing it.
The detached CLI does not write a PID file. It appends the controller's stderr diagnostics to $XDG_CONFIG_HOME/harness-controller/controller-<port>.log (default ~/.config/harness-controller/, or ./.nogit/ in a repository checkout; rotated to .old past 1 MiB on start; stdout stays discarded because the transport logs every connection there). The log file must be a regular file owned by the current user without group or world access — the start fails loudly rather than logging somewhere surprising. The initial setup code travels once over parent-child IPC. status connects only to 127.0.0.1 and validates package and protocol identity. stop additionally validates the exact CLI path (relative launches are resolved against the process's working directory), command, effective port (an argv without --port verifies as the default port), process group, and process-start fingerprint. It sends cooperative SIGTERM only to that exact PID so the controller can stop harnesses and database resources in order. If the process remains after 30 seconds, a foreground controller receives exact-PID SIGKILL; a detached controller must still be the reverified process-group leader before its complete group receives SIGKILL. It never kills processes by name.
foreground is available for attached operation and diagnostics. Detached management is intentionally limited to Linux and macOS in this release.
CLI temporary password
temp-password mints a random 24-byte password that signs the browser UI in without a passkey — the "Use a CLI temporary password" link on both the initial setup screen and the normal login screen opens the matching input. It exists for temporary and scripted access (for example, using shell access to authorize a browser that does not hold the enrolled passkey, or letting an agent drive the real UI for debugging) without enrolling another passkey.
The command talks to the controller database directly, so it requires the same database environment as the running controller (in embedded mode it attaches to the daemon's engine socket) — minting authority is database access, and nothing network-reachable can issue one. Only the SHA-256 hash is stored. The lifetime is between one minute and 24 hours (--ttl-hours, default 24), expiry is enforced on every password lookup and subsequent resume, at most 8 can be active at once, and the password is reusable until expiry. An already-authenticated TypedSocket peer remains authenticated until disconnect even if its password expires. Logins use the same exact origin checks, generic unauthenticated admission limits, per-connection authentication, and audit trail (temppassword.create / temppassword.login); WebAuthn-specific ceremony limits do not apply. A temporary password can also authenticate one browser while initial passkey setup remains pending; it neither consumes the setup code nor enrolls a passkey, and unauthenticated browsers continue to see the setup screen.
Projects and default models
Chats are organized into projects. A project is any registered existing directory on disk: the browser's add-project field accepts full absolute paths (/any/path/on/disk) as well as paths relative to the configured --projects-root default base (org/repo), and suggests matching folders live while typing either form — absolute queries browse the actual filesystem. Hidden directories are never suggested (though an explicitly typed absolute path may contain dot segments), dot navigation is rejected, and registration fails visibly when the path does not exist — the controller never creates project directories. Projects inside the default base get short relative display names; projects elsewhere are named by their absolute path. Every session method is scoped to one project. One supervised OpenCode server serves all projects through per-call directory scoping, while the Flex child resolves each session to its registered project root before providing filesystem or shell tools. On the first v2 start the fixed workspace directory is registered as the initial project automatically. Removing a project first blocks new resource operations and requires every durable terminal and browser resource to be retired. The controller then persists removal intent before destructive work, hides the project from normal admission, drains Session Intelligence and harness operations, clears controller-owned state, asks Flex to remove that exact project namespace, and removes controller-owned Git worktrees, worktree records, and reversion capture roots. Interrupted removals resume on startup and remain pending when required runtime cleanup cannot be confirmed. Flex storage is never deleted merely because a project is absent from an active-project snapshot. Files in the registered project working tree are never deleted; path cleanup is limited to controller-owned worktrees, while controller-private capture stores and worktree records are removed only from controller-owned storage. Note the trust consequence: an authenticated browser session can register and operate on any directory the controller's user can read.
Registration persists the complete ordered device/inode ancestry from the filesystem root through every project path component, with the target recorded as a directory. Controller requests, Flex tools, Git reversion, background cleanup, and shutdown revalidate the entire chain before using the path, so preserving the project leaf inode beneath a substituted ancestor does not authorize the replacement. The first upgrade to this format binds each currently available truly legacy project exactly once under its owning controller. A legacy project that is unavailable during that migration remains terminally unbound, and an incomplete pre-release bound record becomes terminally unbound instead of acquiring missing ancestors. Such projects stay visible for operator diagnosis but are excluded from runtime admission and path-sensitive cleanup. Their removal retires controller-owned metadata while intentionally retaining retired resource tombstones and path-scoped OpenCode, Flex, and Git artifacts. A pending bound removal remains durable until its original complete binding is available, rather than completing against an untrusted replacement.
The settings dialog stores one independent default model per session harness. OpenCode models come from its configured provider catalog. Flex models are associated with an opaque provider connection. After OpenAI device login succeeds, the browser automatically starts that connection's catalog refresh and renders its progress live; active connections can refresh models and account usage manually. While Flex recovers, controller.provider.connection.list returns allowlisted persisted account metadata with runtimeAvailable: false; Settings preserves those accounts, labels them read-only, and blocks login, logout, activation, catalog refresh, and usage refresh until both the RPC and controller health report readiness. A stored default applies to that harness across projects and browsers. The chat composer exposes an account picker for Flex and stores its exact model plus reasoning-effort choice durably for that controller/project/session, without changing either global default. Every Flex choice is validated by the child against the active connection and its refreshed catalog before persistence or execution. Automatic selection occurs only when exactly one active account has exactly one provider-advertised default model; multiple accounts or ambiguous defaults require an explicit choice. Logging out first persists a non-active connection fence, then clears every matching session choice and the matching global default before removing the credential. Provider-scoped serialization prevents a concurrent update from restoring those choices, while live and startup recovery finish any fenced logout interrupted by a timeout or process exit. Project, provider, model, and settings changes are pushed live over the authenticated TypedSocket connection.
Each active OpenAI connection also offers Use for OpenCode. Switching seals only OpenCode admission, captures the exact active OpenCode cohort, waits its accepted work to settle, and asks those conversations to pause before replacing the controller-owned OpenCode supervisor, SDK client, and event stream. Once accepted, the switch is owned by the controller: it survives disconnection of the initiating browser peer, but controller shutdown still aborts it. The selected credential is installed through the official SDK; only the opaque selected connection ID is persisted, after the staged runtime is healthy. Every captured conversation must accept its continuation prompt before admission reopens; an incomplete continuation stops the selected runtime and leaves OpenCode sealed. Flex conversations remain available throughout. A pre-commit failure keeps the old runtime only after successful compensation and a fresh health check, or restores only a confirmed prior controller-managed account after replacement; if neither is safe, OpenCode stops and admission remains sealed. The persisted selection is restaged before OpenCode-dependent startup work after every controller restart, and restoration failure likewise leaves OpenCode fail-closed. The selected account cannot be logged out until another active account has replaced it.
Controller protocol v22 exposes one harness-aware slash surface through controller.slash.list and controller.slash.execute. The list returns native built-ins, templates, or handlers with hints, availability, workspace-reversion capability, and exact undo/redo state for the selected session. The browser shows local harness built-ins immediately, enriches them from the session-scoped server catalog, and delegates keyboard, touch, and composition behavior to the controlled dees-harness-chat composer. Flex session details also show the deduplicated affected Git repository union and any truncated or non-revertible history barrier reported by the catalog. Execution consumes one exact attachment-free draft revision. OpenCode templates expand server-side and honor command-pinned agents and models; OpenCode compact, init, undo, and redo remain native operations. Flex delegates parsing, listing, and execution to FlexHarness. Only an authoritative unknown command falls back to an ordinary prompt, preserving the draft bytes exactly under the same submission reservation; malformed commands, known-command failures, cancellation, timeout, disconnect, and unknown transport outcomes never fall back.
Flex /undo and /redo move transcript and workspace history together. Starting new Flex work from an undone position commits a branch and permanently removes that redo history. Retention pruning and successful Agent-event archival bound older traversal, and legacy transcript-only history is a non-revertible barrier. When a project lies inside a Git working tree, that containing repository is one atomic reversion unit: capture and apply cover tracked state plus non-ignored untracked paths across that working tree rather than only the registered project subdirectory; ignored paths remain outside reversion and unchanged. Descendant repositories are separate atomic units. Current HEAD and its attached branch may move between captured sides and remain reversible. Shallow repositories, partial clones, promisor remotes, object-directory alternates, unsupported object formats, and invalid repository layouts are rejected before capture. In-progress Git operations are recorded as non-revertible barriers; later out-of-scope ref, symbolic-ref graph, worktree or nested-repository topology, repository identity, and ignore-policy changes fence traversal. An index replaced by an unsupported file fails apply rather than becoming a barrier.
Each capture owns one controller-private captures/<captureKey> root. Every participating repository gets a private bare Git metadata store and disposable object generations; the bound source object database is available only as a read-only alternate. Capture, finalize, inspection, and release therefore do not add controller refs, metadata blobs, trees, or generated objects to the source repository. Apply is the sole capture-traversal operation that mutates captured worktree, index, or ref state: before mutation it creates an operation-unique, self-contained closure pack in private storage, imports that exact bounded pack without a reverse index or controller ref, and retains it with an operation-owned .keep file through roll-forward or compensation. The explicit worktree create/remove methods separately update the source repository's linked-worktree metadata. Private publication and capture-root lifecycle are journaled so startup can recover interrupted publication, apply, creation, and deletion without trusting files inside the tree being removed.
Library consumers can construct the root-exported ControllerGitReversion with IGitReversionOptions, register each project with its canonical IControllerProjectFilesystemIdentity, and call init() or recover() before source-sensitive work. Capture flow uses IGitReversionCaptureRequest with prepare(), inspectCapture(), and finalize(); undo and redo use IGitReversionApplyRequest with inspectApply() and apply(). IGitReversionMetadataRequest drives listAffected() and getReversionMetadata(), while IGitReversionWorktreeRequest and IGitReversionWorktreeRemoveRequest drive the detached-session worktree methods. Only when no further undo, redo, inspection, or recovery is needed should terminal cleanup call release() with IGitReversionReleaseRequest, which permanently deletes the capture root. Call close() to seal admission and drain or fail closed on owned work.
Flex also exposes /worktree create, /worktree list, and /worktree remove <worktreeId>. These commands explicitly create and manage detached, locked, controller-owned worktrees for the current conversation; worktrees are never created automatically. Creation durably binds the private parent before git worktree add, then binds the child before activation. Recovery, cleanup, and removal revalidate the required parent and child bindings; a replacement is fenced and retained. Listing validates the registered project before reading bounded private records, but does not dereference or revalidate every listed worktree path. The run-scoped git_worktree tool provides the same create/list/remove operations to the agent, with permission required for create and remove. Removal refuses any worktree containing tracked, untracked, or ignored changes.
Git recovery is an engine-wide admission barrier and all queues, commands, capture stores, metadata, generated objects, worktrees, scans, and path sets are bounded. At most four Git commands run concurrently and 256 commands or ownership waits may queue. The default persistent bounds include 1,024 captures, 128 controller-owned worktrees, 64 repositories per project, 1 GiB and 1,000,000 objects per generation, 4 GiB and 4,000,000 objects per capture, and 16 GiB and 16,000,000 objects controller-wide. Limit breaches fail closed or create a durable non-revertible barrier where capture can safely report one. Repository-free captures use the same private lifecycle and remain until explicit release or project removal; they are never TTL/LRU evicted.
Filesystem checks intentionally document, rather than overstate, their authority boundary: validation and use are separate pathname operations, so a same-UID actor able to replace components in the narrow interval between the final identity check and the operation can still race them. The implementation does not claim descriptor-relative traversal or Linux openat2 elimination of that race.
The UI shows the running controller version in the header and compares it with the version compiled into the browser bundle. Upgrade status carries the source and target versions before shutdown, survives reconnect through a sanitized projection of the durable private transaction plus browser session storage, and triggers one cache-busted reload when the upgraded server returns. A repeated mismatch stops in an explicit stale-bundle state instead of looping. After authentication, the bottom status bar samples whole-system CPU usage, active memory usage, and aggregate non-virtual-interface network receive/transmit rates approximately every two seconds through the protected controller.system.metrics method. CPU flashes a warning after whole-system usage remains above 50% for 10 seconds. Host metrics are neither requested nor rendered before authentication.
Remote browser access
WebAuthn requires a secure context away from localhost. Plain HTTP is therefore accepted only for the exact development origin http://localhost:<controller-port>.
TLS reverse proxy
export HARNESS_CONTROLLER_TRUSTED_PROXY_SECRET="$(node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))")"
agl start \
--public-origin https://controller.example.com \
--rp-id controller.example.com \
--behind-tls-proxy
Remote mode deliberately requires a trusted TLS reverse proxy so loopback CLI management remains available over the controller's HTTP listener. The proxy must remove client-supplied copies of the transport headers, inject the configured secret as X-Harness-Controller-Proxy-Secret, and set X-Forwarded-Proto: https on WebSocket upgrades; browsers never receive the secret. The controller rejects browser WebSockets without both values. Preserve the exact public Host and browser Origin, forward WebSocket upgrades, apply source-address limits to upgrades and unauthenticated ceremonies, and restrict direct access to the controller backend to the trusted proxy network. Do not expose an unprotected plain-HTTP backend route.
Backend isolation is a hard requirement in proxy mode, not defence in depth. The backend listener binds all interfaces there, and origin-less WebSocket connections (the loopback CLI management path) are admitted without the proxy secret by design — the controller cannot see the peer's network address to restrict this itself. Every pre-authentication surface on a directly reachable backend port is exactly as exposed as that port.
The proxy secret authenticates only the TLS termination boundary. It is not a user credential and never replaces required per-connection passkey, temporary-password, or resume-token authentication.
RP IDs must equal the public-origin hostname or a valid parent domain. Public suffixes, private multi-tenant suffixes, IP addresses, and unrelated domains are rejected.
Session and permission control
After per-connection user authentication, the UI can:
- list, create, select, and delete sessions in registered projects;
- choose OpenCode or Flex when creating a session;
- read normalized messages, reasoning, tools, usage, and todos through qualified, collision-safe runtime IDs;
- follow bounded live OpenCode agent-shell output while a tool is running;
- inspect session-level token metrics when the complete harness history makes them provable;
- maintain a revisioned session scratchpad and ask Session Intelligence questions when
openai/gpt-5.6-lunais connected; - send text prompts and abort a busy session;
- display OpenCode and Flex permission requests as first-class UI state;
- explicitly allow a permission once or reject it;
- answer OpenCode questions with the advertised options (single- or multi-select) or a free-text answer where the question allows one.
Permission requests and questions render inline in the conversation, interleaved with messages like any other turn; answered cards stay in the transcript showing what was decided. Oversized transcript pages preserve every selected source bundle while progressively omitting tool input, tool output, reasoning, and text payloads; individually oversized Flex messages already enter the public projection as metadata-preserving omission placeholders.
Opening a conversation is tip-first. Controller protocol v21 returns up to 20 newest source-message bundles immediately. When an intentional upward scroll reaches the transcript top, the browser requests one older page of up to 50 bundles; the same demand-driven behavior applies to the read-only subagent drill-in. Session metrics and Session Intelligence capability load through a separate auxiliary request and never delay the transcript. Paging keeps the newest 200 source bundles within a 512 KiB transcript budget, preserves the visible scroll anchor while prepending, and exposes partial history when retention, byte limits, cursor rejection, or a repeated cursor prevents safe continuation. A stream-epoch change invalidates that load and starts a fresh authoritative read. Provider cursors are opaque best-effort continuation tokens rather than immutable snapshots, so duplicate source IDs retain the newer core page and older pages only fill missing bundles. The exported controllerInitialMessageBundleLimit, controllerMaximumMessageBundles, and controllerMaximumTranscriptBytes constants define these bounds; IControllerMessagePage.historyLimited reports controller-side source-bundle or payload omission.
Live OpenCode tool output comes from ordinary message.part.updated snapshots on the official SDK's GET /global/event stream. Agent shell tools are named bash; their stdout and stderr are already combined by OpenCode. Running and errored tools expose the current output through state.metadata.output, while completed tools expose final output through state.output; the controller always treats these values as complete replacements, never append-only deltas. OpenCode retains only the final 30,000 UTF-16 code units in a running preview and normally limits final inline output to 2,000 lines or 50 KiB, with larger complete output available only in OpenCode's server-local temporary file. Controller tool events are additionally bounded to 48 KiB including their protocol envelope; the exported controllerLiveToolPayloadNotice marks an input or output deliberately omitted to keep that envelope bounded.
Live Assistant text and reasoning use separate protocol-v21 message stream epochs and cursors, independent of tool delivery. The OpenCode adapter builds bounded replace-only snapshots from each authoritative message.part.updated base plus its exact message.part.delta sequence; missing bases, unsupported deltas, removals, reconnects, and size overflow fail closed to immediate authoritative hydration. Flex keeps its ordered child part snapshots on the same public event DTOs over Flex IPC v13. controller.session.get returns both toolStreamCursor and messageStreamCursor; browsers render current snapshots directly and retire a cursor-covered overlay only when the hydrated payload semantically represents it. Incomplete, elided, or failed hydration retains the overlay.
The global event stream has no replay cursor. The controller therefore assigns a monotonic revision inside a stream epoch, advances the epoch whenever a connection or delivery gap can have lost snapshots, and emits an ordered harness.changed barrier before later events from that epoch. controller.session.get merges controller-observed snapshots that raced the authoritative OpenCode read and returns the latest covered toolStreamCursor; browsers discard overlays from older epochs and reject delayed events at or below that cursor unless a matching terminal event supplies fields omitted by hydration. That omission-only supplement preserves every defined durable field and cannot override a newer terminal result. Interactive PTYs remain a separate controller-owned terminal feature and never share this agent-tool stream.
A new chat starts as a client-side draft: the chat pane shows the project directory the session will start in, and nothing is persisted until the first message is sent. The generic New conversation action always starts OpenCode; the New menu offers an explicit Flex chat. On first send, the selected OpenCode or Flex harness creates its session and receives the message in one step; an abandoned draft leaves no empty session behind. Right-clicking a conversation opens a context menu with Rename (updates the owning OpenCode or Flex harness's session title), Archive (archives it through that owning harness), and Delete (with confirmation). Archive requires the exact conversation to have no active prompt, pending permission, background command, or Session Intelligence job; active work must settle first. A successful delete removes the local conversation immediately and refreshes the authoritative list in the background, so New is available without waiting for a supplemental reload. Archived chats are hidden from the conversation list by default and live in a dedicated archived dialog reachable from the sidebar. Only top-level conversations appear in the list. Subagent child sessions stay internal to their parent chat: active children show bounded live transcripts, while recent completed or errored children remain as result cards. A dedicated read-only drill-in opens the larger retained child-session view.
Flex titles can be changed while prompts are queued or running, while a permission is pending, and after archival. Each Flex conversation also receives FlexHarness's unpermissioned rename_session metadata tool. The Harness validates a non-empty title of at most 2,048 UTF-8 bytes, persists it as supplied, and updates only the exact project and session whose run invoked it.
Flex conversations expose FlexHarness's foreground delegate tool through one depth-1 general subagent. The child inherits the project's selected model and the same run-scoped project tools. Delegation, filesystem and shell mutations, and browser mutations remain permission-gated; Harness-owned rename_session, task, goal, and scratchpad operations do not request permission. Direct-child permissions render in the parent conversation and replies are routed to the exact child session. Flex IPC v13 carries cumulative child-session and model metadata so the existing task cards, live previews, retained results, and drill-in work before and after durable transcript hydration. Bounded public projection placeholders preserve as many task links as fit, and root conversations are retained before child sessions when the projection reaches its session limit.
Every Flex conversation also receives FlexHarness's session-local project-management tools: task lists and mutates structured work items, goal gets or changes the current goal, and scratchpad gets, replaces, appends, or clears durable working context. The controller stores this exact generation-fenced state with the conversation's controller-owned session state. Browser and Session Intelligence scratchpad writes synchronize into the same Flex snapshot, while task-only and goal-only writes advance only the independent project-management revision. Legacy Flex conversations are backfilled idempotently at startup after their session generations are repaired.
Conversation cards are collapsed independently and use status accents for idle, working/retrying, finished, pending permission/question, and error states. A known chat auto-expands when it starts working; selecting a chat does not change expansion. Finished stays green in the current browser unless error or attention takes precedence, and clears on reconnect, its next local turn, or later work. Pending permissions and questions use purple, with unselected cards pulsing for attention, while OpenCode session errors stay red until later work starts. Durable terminal and browser resources remain in the separate Resources section.
The application header can hide and restore the controller's conversation sidebar; focusing conversation search restores it automatically. Session metrics report only facts the controller can establish from exhaustive history. OpenCode can expose lifetime token use, compaction count, tokens before the latest compaction, current context use, and the active model's context limit. Current-context facts stay absent while a turn is busy, incomplete, errored, or not newer than the latest compaction. Flex currently exposes only provable lifetime token use from its bounded projection.
Each OpenCode or Flex conversation has controller-owned state scoped to the exact controller, project, and qualified session. Its selected model is durable, and scratchpad saves use an expected revision so one browser cannot silently overwrite a newer edit. The model choice, scratchpad text, newest eight bounded Session Intelligence exchanges, and optional exact Flex project-management record persist in SmartData; deleting a session or project immediately replaces retained controller content with a tombstone before best-effort TTL cleanup. Flex project-management tombstones additionally carry the exact session generation so stale work cannot cross a deleted and recreated Flex session. Removing a project purges the retired Flex project-management namespace while preserving the controller tombstone.
Session Intelligence runs one ephemeral, in-memory FlexHarness session per analysis. That worker receives no filesystem, shell, browser, mutation, or subagent tools: it can only read a qualified session, list related sessions in the exact project after successfully reading the requested source, and submit structured { answer, scratchpad } completion. OpenCode reads travel through an explicitly authenticated, exact-origin and exact-directory CrossHarness connection; Flex reads use the controller's integrity-checked public projection. A private generation-bound capability token authorizes those reads without entering prompts, tool schemas, logs, errors, or model-visible output. The selected OpenCode account is preferred when it can run the exact openai/gpt-5.6-luna model; otherwise exactly one capable active Flex connection is required, with child-side revalidation before execution and no model or variant fallback. At most four analyses run controller-wide and one per qualified source session, enforced in both controller and child. A newer user scratchpad revision wins over a concurrent intelligence update while preserving the answer and exposing the conflict. New jobs create no durable harness session or temporary-session cleanup obligation; startup and periodic cleanup remain only for temporary OpenCode sessions persisted by earlier releases.
Conversations can be organized into groups: the "New group" button below the list creates one, dragging any conversation card onto a group files it there, and right-clicking a group header offers rename and delete. Desktop dragging opens a full-card insertion slot and reorders conversations within or between groups; deleting a group never touches its conversations, which return to the ungrouped section in their existing order. Group membership and explicit ungrouped order are stored per project in the controller database, while new or otherwise unlisted conversations follow the list's deterministic fallback order. The layout survives reloads and stays in sync across connected browsers. Archiving or deleting a chat removes it from the persisted layout. Durable resources have their own project section and are intentionally not part of the session layout. The divider beside the sidebar can be dragged between 260 and 520 pixels, supports arrow, Home, End, and Escape keys, and stores its width in the local browser; the divider is hidden in the stacked mobile layout.
Controller protocol v4 adds IControllerSession.attention, authoritative sessionStatus and sessionError event markers, and the revisioned IControllerSessionLayout contract. controller.sessiongroups.get now returns the complete layout, while controller.sessiongroups.update accepts the ungrouped order and an expectedRevision for concurrent-write detection and returns the confirmed layout.
Controller protocol v5 adds controller.session.discard-empty. It discards only a controller-tracked session that has not been claimed by any session mutation; discarded reports deletion and retryable distinguishes a transient deletion failure from a session that is no longer eligible. This lets the browser clean up a materialized draft without risking deletion of a conversation another client has started using.
Controller protocol v6 qualifies harness and controller runtime identifiers such as sessions, messages, permission and question requests, jobs, logins, and terminals as { harnessId, nativeId }; opaque provider connection IDs remain strings and are not qualified. It replaces the single OpenCode status with IControllerStatus.harnesses[], adds the harness.changed event, and adds explicit harnessId ownership fields to session creation, model choices, and controller events. It also adds Flex sessions and provider management, changes settings to defaultModels, and adds the API-only controller.session.messages.page and controller.session.message.get methods. Persisted v5 OpenCode settings and layouts migrate automatically and idempotently to qualified IDs.
Controller protocol v7 adds optional IControllerSessionDetail.sessionMetrics, the revisioned scratchpad, sessionIntelligenceEnabled, and bounded intelligenceExchanges. It adds the controller.session.scratchpad.save and controller.session.intelligence.ask methods. The browser consumes the matching harness-scratchpad-save and harness-session-intelligence-ask catalog events, while the application shell owns the Conversations toggle; stale responses remain scoped to the project and qualified session that initiated them.
Controller protocol v8 adds IControllerToolExecution, IControllerToolStreamCursor, IControllerToolCall.exitCode, the exported controllerMaxToolEventBytes limit, the session.tool.updated event, and OpenCode stream-epoch barriers on harness.changed. Tool events are correlated by qualified session, message, part, and call identifiers, contain bounded replace-only snapshots, and are reconciled against the cursor returned by controller.session.get.
Controller protocol v9 adds IControllerProviderConnection.status, which distinguishes active connections from reauthRequired connections without exposing provider credentials or responses. The browser uses that state to prevent unusable model refreshes and direct reauthentication through logout followed by a new device login.
Controller protocol v10 adds durable IControllerSessionDetail.modelChoice, optional initial choices on controller.session.create, controller.session.model.update, and metadata-only controller.provider.connection.ratelimits.get. Flex IPC v2 adds authoritative model.choice.validate and provider account rate-limit retrieval. Flex model options now carry the provider-advertised isDefault marker used for exact sole-account initialization.
Controller protocol v12 adds TControllerSessionId, TControllerTerminalId, and the mixed TControllerLayoutItemId union to the revisioned project layout. The historical sessionIds and ungroupedSessionIds wire and persistence keys are retained, so existing conversation layouts require no data migration. Controller-owned entries are accepted only as exact qualified IDs and are reconciled against the project's live terminal manager inside the serialized layout update boundary.
The same v12 protocol and Flex IPC v3 extend the existing session.tool.updated stream to Flex. FlexHarness's allowlisted tool-part snapshots are transferred through bounded active and terminal phases, receive an independent Flex stream epoch, and are merged into controller.session.get under the exact returned cursor until the durable terminal projection semantically covers them.
Controller protocol v13 and Flex IPC v4 add optional { messageIndex, partIndex } transcript order to normalized messages. Flex projection chains are repaired from authoritative private snapshots before public reads, public message pages paginate by persisted source index, and sequential Flex parts retain their original reasoning, tool, and text chronology even when run messages share timestamps.
Controller protocol v14 adds controller-memory composer drafts with compare-and-swap revisions, controller.session.draft.get, controller.session.draft.update, and session.draft.changed. Authenticated browsers synchronize text and attachments for the same qualified session, while pre-session drafts remain local to one browser until the first session is materialized. Send, slash-command, and built-in-command requests consume one exact draft revision; successful admission clears only that revision, concurrent newer edits survive, and failed or outcome-unknown admissions retain the draft. Draft text is limited to 64 KiB of UTF-8; each draft accepts at most eight attachments, 10 MiB per attachment, and 10 MiB combined. Attachments are written into private controller-owned temporary directories and represented to OpenCode and Flex only by absolute paths. Flex IPC v5 correlates each optional upload directory with one exact run and exposes it only as an additional filesystem read root. Operation completion, session or project removal, child exit, and controller shutdown remove the corresponding temporary files.
Controller protocol v15 adds sanitized upgrade status and upgrade.changed events. Upgrade management version 1 has one-time controller.upgrade.prepare and controller.upgrade.finalize grants so the source and target controller protocols may differ while the detached worker retains exact process authority. Durable private transaction state contains bounded versions, options, timestamps, progress text, token hashes, worker process identity, project/session identities, and transition outcomes. Browser status exposes only source version, target version, and phase, never tokens, grants, worker identity, or the paused cohort.
Controller protocol v16 removes controller-owned prompt scheduling. Every OpenCode send is submitted immediately through promptAsync with a caller-selected native message ID; OpenCode alone owns coalescing and turn order, while the controller tracks only bounded draft/upload cleanup until the exact terminal parent is observed. Exact terminal reconciliation and explicit aborts settle that ownership; accepted operations have no wall-clock lifetime, while repeated recovery failure stops OpenCode and releases every remaining OpenCode operation. Flex IPC v7 continues to admit every Flex send immediately into FlexHarness's bounded per-session FIFO and correlates it by exact project, session, queue, run, and promoted message identities. Before either harness exposes the user turn through transcript hydration, the controller projects only accepted or outcome-unknown prompts as normal user bubbles and supplies the exact pending count to the composer. Flex text and reasoning parts stream from FlexHarness's canonical ordered part events through bounded replace-only controller events and reconcile under the existing Flex stream cursor; terminal text snapshots stop streaming, and durable projection hydration supersedes them. At that protocol revision, /compact was available for both harnesses while other built-ins remained OpenCode-only; protocol v22 supersedes that command boundary with the unified harness-native slash catalog.
Controller protocol v17 adds metadata-only IControllerProviderConnection.selectedForOpenCode?: true, present only on the selected connection, and controller.provider.connection.activate-opencode. Its public request is { providerConnectionId }; its response is { activated: true, pausedSessions, continuedSessions }. Flex IPC v8 adds the private credential-directory initialization field and provider.connection.opencode-auth.get; the latter returns OpenCode OAuth material only to the controller parent. Flex credentials move from individual kernel entries to the kernel-keyed sealed store through the fail-closed startup migration described above. OpenCode SDK traffic is generation-fenced until each bounded response body settles, while separate controller-only SDK traffic performs pause, auth installation, and continuation during the sealed restart transaction.
Controller protocol v18 adds durable project resources with explicit terminal and browser kinds. Resource attachment uses a stable authority plus a monotonic revision: the pending attachment is persisted before runtime side effects and committed only after reconciliation, retirement is persisted before cleanup, session deletion detaches first, and project removal is rejected while any non-retired resource remains. Startup resumes pending attachment and retirement work. Session layouts become session-only; migration removes legacy controller-terminal IDs without changing conversation order. The public API adds list, create, rename, attach, detach, start, stop, and retire methods plus exact-revision human browser-view methods and resources.changed events. The controller supports at most 64 active resources per project and 4,096 across the controller.
Controller protocol v19 replaces IControllerSessionDetail.messages with the bundle-preserving messagePage, removes sessionMetrics and sessionIntelligenceEnabled from that core response, and adds controller.session.auxiliary.get returning IControllerSessionAuxiliary. controller.session.get reads only the newest bounded tail; top-level conversations and child-session drill-ins use controller.session.messages.page for best-effort older history. Message pages may report harness retention loss through truncated and controller transfer omission through historyLimited.
Controller protocol v20 separates Assistant text/reasoning stream ordering from tool ordering on the v19 paging architecture. It generalizes the replace-only text and reasoning update DTOs to OpenCode and Flex, adds IControllerMessageStreamCursor, IControllerSessionDetail.messageStreamCursor, and IControllerEvent.messageStreamEpoch, and requires Flex source order while allowing OpenCode updates without one. The newest source-message bundles include snapshots covered by the returned cursor; older page requests stay source-authoritative and are fenced by both stream epochs in the browser. A stream-specific loss advances only its affected hydration barrier; saturation of the shared 512-event queue advances both. Retained Flex tool snapshots are bounded to 8 MiB and message snapshots to 16 MiB. Terminal durable transcript state remains authoritative over stale running snapshots. The provider-connection list includes runtimeAvailable so clients can distinguish live Flex data from the persisted metadata-only fallback. Flex IPC v10 additionally projects foreground Task lineage, family-scoped permissions, child-session identity, and child model metadata without changing the public controller protocol. Flex IPC v11 adds bounded host calls for exact project-management load, compare-and-swap save, tombstone, and namespace purge operations.
Flex IPC v12 adds isolated Session Intelligence admission, status, cancellation, and capability-scoped CrossHarness read requests. Cancellation is serialized behind admission and does not acknowledge success until the ephemeral worker has disposed. On job failure, the controller cancels and drains the worker before revoking its read capability; capability closure aborts in-flight OpenCode, Flex projection, and legacy-session persistence reads and waits for them to drain. Flex child recovery fences new analysis admission, confirms prior-generation process exit, and drains retained read capabilities before starting the replacement generation.
Controller protocol v21 replaces base64 browser-frame payloads with one finite integrity-checked server-created virtual stream per frame and adds controller.browser.view.activate. A human view opens inactive; the browser installs and starts its renderer before activation can begin frame delivery. Stream reads revalidate the exact authenticated physical peer, view, resource, attachment revision, and process-local BrowserRuntime lease authority. Request cancellation propagates through queued and active browser work, while disconnect, resource transition, queue overflow, and shutdown close the exact view and retain retry ownership until capability revocation succeeds. Flex IPC v13 adds the exact Flex run ID to every browser-channel binding and revalidates it before admitting the channel.
Controller protocol v22 replaces the separate command-list, session-command, and built-in-command requests with controller.slash.list and controller.slash.execute, and adds session.history.changed to invalidate stale transcript and reversion state after undo, redo, or branching. Flex IPC v14 carries FlexHarness's native slash catalog and execution result union, exact queue/run acknowledgement, request cancellation, session reversion metadata, complete project ancestry bindings, and parent-owned Git reversion and linked-worktree host requests. Git captures preserve index and worktree sides separately, keep generated metadata and objects private outside apply, and retain later non-conflicting edits across undo and redo. Project registration and removal are explicit serialized child requests; durable parent-side removal intent is required before Flex state, controller-owned linked worktrees, worktree records, or private capture roots are deleted.
The Resources section can create a terminal: a durable resource whose real shell is started directly by the controller through Smartshell's shell-free node-pty API in the project directory. On POSIX the controller validates the absolute $SHELL executable and falls back to /bin/sh; Windows foreground operation uses a trusted absolute COMSPEC or SystemRoot cmd.exe. The browser renders the terminal with xterm.js; keystrokes and output travel over the same authenticated TypedSocket connection as everything else — no extra port and no CDN scripts. Terminal resource identifiers are controller-generated opaque values and never expose process IDs. The resource, title, command, attachment, stopped state, and last exit code are durable; the running PTY and its scrollback remain memory-only. Every restart revalidates the project's complete filesystem identity and requires its canonical directory to equal the persisted terminal cwd before spawning. Output is delivered only to peers attached to that terminal; each peer has an independent replay cursor over at most 2 MiB of bounded controller scrollback, and trailing output is delivered before the terminal-ended event. Closing or reloading the browser never ends the PTY, and reattaching replays retained scrollback while it remains available. A terminal can be renamed, stopped, restarted, or retired from its resource context menu. Shutdown waits for independent PTY-root exit confirmation and fails closed instead of dropping ownership of a process whose exit cannot be confirmed. When several browsers view one terminal, the smallest client's grid determines the PTY size. The controller admits at most eight running terminals per project, 32 running terminals in total, and 32 attached peers per terminal. Terminal keystrokes are not written to the audit log; lifecycle actions are.
A browser resource is also durable. An authenticated human can open its exact committed attachment revision as an interactive canvas with tabs, navigation, viewport updates, mouse, wheel, keyboard, and text input. Browser frames use finite binary virtual streams rather than JSON/base64 payloads; each stream belongs to one exact server push and becomes unusable when its peer, view, attachment, or runtime authority is no longer current. A browser resource attached to a Flex session contributes a run-scoped BrowserRuntime tool provider through FlexHarness 4.1.1. The private parent-child channel is bound to the exact project, resource authority, attachment revision, Flex session, run, channel, actor, and live child generation; available agent actions are navigate, snapshot, screenshot, click, fill, and press. BrowserRuntime capability revocation and channel cleanup retain retry ownership after failures.
By default the controller never auto-accepts a permission. The settings dialog offers a global auto-accept permissions (yolo) toggle, persisted server-side: while enabled, the controller itself replies once to every pending permission request in all registered projects (including requests that were already waiting when the toggle turned on), and each auto-reply is written to the audit log like a manual one. Each chat additionally has an Ask / Yolo switch in its composer: Yolo auto-accepts that conversation's permission requests for the lifetime of the controller run without touching the global default (the durable setting stays the global toggle). Questions are never auto-answered. Persistent broad always grants remain unexposed by the protocol — the inline card's "always" answer grants once and OpenCode simply asks again — and no background or sleeping browser is treated as an approval agent: auto-accept is a deliberate server-side policy switch, not a client behavior.
Project-scoped OpenCode events are accepted only from registered project directories. Flex events already carry their registered project scope. Both are normalized with their harness owner and pushed only to authenticated TypedSocket peers. The UI applies correlated OpenCode and Flex tool, Assistant text, and Assistant reasoning snapshots directly when their message already exists, retains bounded per-harness overlays when an event beats or is not semantically covered by hydration, and debounces ordinary session events into refreshes rather than polling harness state.
Persistence and audit boundaries
SmartData owns:
- the immutable runtime configuration;
- setup authority hashes and generations;
- passkey credentials and compare-and-swap counters;
- single-use WebAuthn ceremony records with expiry;
- project-scoped session-only layouts and controller/project/qualified-session model choices, scratchpads, bounded intelligence exchanges, exact Flex project-management records, revisions, and content-clearing tombstones;
- durable project terminal and browser resources, attachment authorities and revisions, pending transitions, lifecycle state, and terminal stopped metadata;
- Flex session snapshots, bounded public session/message projections, provider connection metadata, and selected-model metadata;
- the opaque provider connection selected for the controller-owned OpenCode runtime;
- bounded, correlated audit events for controller, setup completion, authentication, session, terminal, permission, question, and semantic browser actions. Periodic maintenance prunes toward seven-day and 2,000-record targets, with a 90-day database TTL fallback. Audited operations first persist an
attemptedrecord before any state transition or managed side effect, then append the terminal outcome. BrowserRuntime supplies the correlated operation identity for navigation, tab, and agent actions; frame acknowledgement, viewport updates, raw input, checkpoints, and transport acceptance do not create audit rows.
Audit records may contain bounded peer, credential, session, or permission-request identifiers. They cannot contain setup codes, WebAuthn challenges or payloads, prompt text, OpenCode credentials, provider credentials, device-flow responses, or arbitrary fields.
Flex private snapshots and public projections are published separately. The controller reads only integrity-checked public records. For each project, every public candidate is bounded to 2,048 sessions, 2,048 messages, 64 MiB of message data, and 480 KiB per message; up to four public candidates are retained per project. Pages contain at most 50 messages or 512 KiB. Unreferenced staged candidates are age-gated and reconciled through paged, batched maintenance. Provider credentials never enter these records.
Running terminal processes, output scrollback, peer cursors, grid state, active human browser views, browser-frame streams, BrowserRuntime capabilities and leases, Flex browser channels, and composer drafts are memory-only. Durable terminal resource identifiers may appear as bounded request identifiers in lifecycle audit records, but session layouts contain only OpenCode and Flex session IDs. Terminal input and output are never persisted. Draft text and attachment bytes are never written to SmartData or audit records. Attachment files exist only in private controller-owned temporary directories for the lifetime of their exact harness operation and are removed on operation cleanup or process shutdown.
Development
pnpm build
pnpm run check:test
pnpm test
The default suite runs without a database service. To include the exact-persistence integration
test against a disposable MongoDB test service, set HARNESS_CONTROLLER_TEST_MONGO_URL before
running pnpm test; the test creates a unique database name for that run.
The production build uses tsbuild tsfolders so server code, shared TypedRequest contracts, and browser sources are checked and emitted, then tsbundle produces the static browser bundle.
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license.md file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.