AGL
AGL is a self-hosted workspace for working with OpenCode, FlexHarness, and Codex from one secure browser interface. It runs the agent runtimes behind a local controller, keeps conversations organized by project, and adds durable resources, model management, permission handling, workspace history, and safe lifecycle management.
The default setup is local-first: AGL listens only on 127.0.0.1, opens no agent runtime directly to the network, and protects the browser with a passkey. A trusted TLS reverse proxy can provide remote access when needed.
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.
What AGL Provides
- One browser workspace for OpenCode, FlexHarness, and native Codex conversations.
- Explicit projects backed by existing directories on disk.
- Live text, reasoning, tool activity, todos, permissions, and questions.
- OpenCode, Flex, and Codex model selection, including per-conversation choices.
- Conversation groups, ordering, search, rename, archive, and deletion.
- Synchronized drafts, file attachments, scratchpads, tasks, and goals.
- Native slash commands with transcript and Git-aware undo/redo.
- Optional detached Git worktrees owned and validated by the controller.
- Durable terminal resources that survive browser reloads.
- Claude chats: controller-owned Claude Code terminals that resume the same conversation after a restart or upgrade.
- Durable browser resources for human use and permission-gated agent automation.
- Workspace resources that serve a project's working tree to the browser, confined to the project and its secret-file policy.
- Read-only Session Intelligence for questions about a conversation and other managed sessions.
- Passkey authentication, temporary CLI passwords, and bounded audit records.
- Detached start, verified stop, health status, and coordinated in-place upgrades.
OpenCode owns OpenCode sessions, FlexHarness owns Flex sessions, Codex app-server owns Codex threads, and every mutation is sent to the runtime that owns the conversation.
How It Works
Browser
-> AGL controller and authenticated TypedSocket API
-> official OpenCode SDK
-> private OpenCode server on 127.0.0.1
-> private framed IPC
-> isolated FlexHarness child
-> native Codex app-server protocol over owned stdio or an explicit WebSocket endpoint
-> controller-owned terminals, browsers, uploads, and Git state
-> SmartData persistence and audit records
OpenCode and FlexHarness are supervised independently. Flex can restart without taking down OpenCode, and an unsupported Flex runtime does not prevent OpenCode sessions or controller-owned terminals from working. Saved sidebar groups remain readable while a runtime or Codex profile restarts: archive repair keeps rows whose runtime cannot currently prove an archive. A failed layout refresh retains confirmed groups and permits the next event-driven refresh to recover.
An OpenCode replacement reports starting until its controller-owned event stream connects and session identities have been reconciled. Ordinary requests remain fenced until that recovery completes; only then does AGL report the runtime ready and announce its availability to the browser.
OpenCode is automatically recovered only after AGL observes the exact owned child exit. AGL immediately fences the exited runtime generation, aborts its in-flight requests, completes generation cleanup, and retries a fresh health-checked runtime before reopening admission. Generic or otherwise uncertain OpenCode failure remains fail-closed and is not automatically restarted. Fatal Flex child failures close admission and use bounded status and resource cleanup; if graceful disposal stalls, a watchdog forces the isolated child to exit so exact-generation recovery can proceed.
Each Flex project has its own request queue in the Flex child, so a slow request of one project never holds another project's requests. When a request is cancelled — a browser tab that reloads or closes cancels its in-flight reads — the child answers at once: a request still waiting in its project's queue leaves the queue without starting, and a running read is answered as cancelled and stops holding the queue while it finishes in the background. A running write is always carried to its end, and a child that does not settle a cancelled request within two seconds is still stopped and replaced, with the request that missed the grace named in the controller log. Each child generation holds its Flex writer leases under a writer identity of its own. Once AGL has confirmed that a generation's whole process group has exited, the next generation takes over that generation's leases at once, fenced by a higher epoch, instead of waiting 30 seconds for them to expire; a lease whose writer is not proven gone still protects it until it expires. A child that fails to start reports its cause, which the controller log names.
The browser receives normalized AGL data. It never receives the OpenCode server password, provider credentials, private database descriptors, raw provider responses, or Flex private snapshots.
Live Transcript Delivery
Controller protocol v28 sends bounded live Assistant text and reasoning deltas when the browser already owns the exact preceding transcript revision. Every delta carries its stream epoch, revision, and UTF-8 coordinates; Flex deltas also carry the harness's authoritative transcript order. The browser applies only contiguous, correctly sized updates to the canonical transcript; a gap, stale owner, invalid coordinate, or queue overflow blocks further deltas from that epoch and requests authoritative session-detail hydration instead. Only adjacent compatible pending deltas may be coalesced, so interleaved updates retain their original order.
Streaming text is rendered without Markdown parsing. AGL refreshes the affected messages and enables normal Markdown rendering after the harness reports terminal state or authoritative hydration settles the transcript. This keeps long responses responsive without creating a second browser-side message graph.
Requirements
- Node.js 24, 25, or 26.
- pnpm 11.21 or newer.
- Git 2.43 or newer for workspace history and managed worktrees.
- A WebAuthn-capable browser.
- Linux or macOS for detached
start,stop, and managed upgrades. - A prebuilt
node-ptyplatform package for Linux, macOS, or Windows on x64 or arm64. - For FlexHarness, a usable local TPM2, access to its resource-manager device (normally through the
tssgroup), and/usr/bin/systemd-credswith its TPM2 runtime libraries.
FlexHarness currently runs on Linux x64 with Node.js 24 or 25 and fails closed when its TPM requirements are unavailable. Other supported controller platforms still provide OpenCode and terminal functionality.
AGL includes an embedded NoSQLDB engine (@lossless.org/nosqldb), so a separate database server is optional. It also pins the official OpenCode SDK and platform binary packages to one tested version.
Install
Install the public package:
pnpm add --global agl
To install from a specific registry, use pnpm's normal registry option:
pnpm add --global agl \
--registry=https://registry.npmjs.org
Confirm the installation:
agl --version
agl help
AGL brings the one authswitch of the host: the package also provides the authswitch command, which runs the authswitch release AGL depends on exactly as that release's own command does. pnpm installs no second global package that provides authswitch, so a separately installed authswitch is removed first:
pnpm remove --global @modelprofile.com/authswitch
The accounts service AGL reads is authswitch's authority service. Install and start it through AGL's authswitch (authswitch authority service install, then enable and start); agl upgrade keeps it on the authswitch each new AGL brings.
Quick Start
Start AGL without creating an implicit project:
agl start
The first start prints a local URL and a one-time setup code. Open the URL, enter the code, and create the controller's passkey.
To register one existing directory during startup:
agl start --directory /absolute/path/to/project
AGL never derives a project from the shell's current directory and never creates project directories. Additional projects can be registered from the browser.
Check controller and harness health:
agl status
agl status --json
The default ports are:
- Browser URL and default WebAuthn origin:
http://localhost:4097. - Controller bind endpoint:
127.0.0.1:4097. - Private OpenCode server:
127.0.0.1:4098.
Accounts
Settings uses a section menu for General, Configuration, Projects, Accounts, and Codex connections. The modal keeps its size while each section scrolls independently of the heading and actions; navigation moves above the content on phones. General drafts and the Accounts screen's state survive section changes. The footer's Accounts entry opens Settings at Accounts.
The accounts service (authswitch's authority daemon) is the one place accounts and their logins live; Flex, Codex, OpenCode and Claude Code only use its accounts. Settings → Accounts is one list of its accounts, grouped under ChatGPT and Claude, one row per account. A row names the account, its email and plan, and one status in plain words: Ready; Needs sign-in or Needs a fresh sign-in, with Sign in again on the row; Renewal retrying, with the next attempt; Refreshed by the Codex app, Refreshed by Claude Code or Refreshed by OpenCode for a login that tool renews itself, with where to sign it in again when it needs that; and, while the accounts service is not current, the service's state (Accounts service not running, Accounts service needs a restart, …) on each of its rows. A chip names each harness that uses the account, and where: Flex · agl, website for the projects whose Flex conversations run on it, Codex · AGL for an account whose own Codex app-server serves AGL projects set to it, Claude Code · AGL terminals for the account new Claude terminals run on, Claude Code · agl for a project whose Claude terminals still run on it, and Codex · this host or Claude Code · this host for the tool whose own sign-in on this host runs on it. A harness that does not use the account has no chip. The account's usage follows as one meter per window, with the reset in local date and time; it is read from the accounts service once when the screen opens and again with Refresh.
Flex uses every OpenAI account the service holds with a login of its own: each is the Flex connection flex:authority:<accountId>, listed wherever Flex accounts are chosen. Flex keeps no login of its own.
A ChatGPT account whose login the accounts service renews offers Use for Codex on its row: it switches Codex's own sign-in on this host to the account (authswitch.authority.codex.switch), so the conversations of this host's Codex -- AGL's and every other client's -- continue on it. It asks first, saying what changes: Codex restarts for the switch, so Codex turns running now end, in AGL and everywhere else on this host, and existing conversations stay and continue on the chosen account. While the accounts service stops Codex's app-server, writes the account's sign-in, starts it again and has Codex confirm which account it runs on, which can take a few minutes, the row says so and the screen's other actions wait; then the screen says how the switch settled: done, undone with the reason -- Codex reported an API key or another login its environment or configuration selects, its app-server did not come back -- or held until it is resolved, with Read diagnostics to see what holds it. The account Codex runs on carries Codex · this host and offers nothing. The service switches only a Codex sign-in it manages and only to a login it renews; a row says which is missing, and while a switch is under way or could not be settled, the others wait. A Claude account and a removed account are not offered it. See Codex on an account of the accounts service.
Codex renews the login of the account it runs on, and a login has one renewer, so the accounts service refuses the switch while any runtime is bound to the account. AGL's own use of it -- Flex, AGL's Claude terminals through the gateway, the account's own Codex app-server -- is therefore stopped and its bindings released first, the same step Remove takes, and nothing of AGL binds the account while the switch runs. While this host's Codex runs on an account, the account is Codex's alone: its row carries Flex · not while this host's Codex runs on it, the Flex account picker shows it disabled with the reason, its models are not offered, Flex work and Claude terminals still on it fail with ACCOUNT_IN_HOST_CODEX ("This account now runs this host's Codex, …") instead of moving to another account, and Use for Claude Code on it is disabled with the same reason. A switch the service refuses or undoes leaves the account to AGL again at its next use, and once this host's Codex is switched to another account, the previous one is Flex's and Claude Code's again by itself.
The same row offers Use for Claude Code. It asks first, saying what changes: new Claude terminals in AGL run on the account and reach its ChatGPT models through AGL, and terminals already created keep running where they are. The account then carries Claude Code · AGL terminals and offers Stop using for Claude Code, which returns new Claude terminals to this host's own Claude Code sign-in. A removed account is not offered it, and an account without a login the service renews shows it disabled with the reason. Once the accounts service no longer holds the account, new Claude terminals return to this host's own sign-in by themselves. See Claude chats on a ChatGPT account.
On a Claude account, Use for Claude Code switches Claude Code's own sign-in on this host to the account (authswitch.authority.claude.switch), which every Claude Code session started on this host uses, AGL's Claude terminals included; it also returns AGL's new Claude terminals from a ChatGPT account to the host's sign-in. It asks first, then waits while the accounts service switches Claude Code's files and proves which account Claude Code uses, and says how the switch settled: done, undone with the reason -- a Claude Code session the service cannot check, an API key or token that overrides the sign-in, a Claude Code release it cannot switch safely, and so on -- or held until it is resolved, with where to read more. The account Claude Code runs on carries Claude Code · this host and offers nothing while AGL's terminals use the host's sign-in. The service switches only a host sign-in it manages and only to a Claude sign-in it holds; a row says which of the two is missing.
Add Claude account starts a Claude sign-in in the accounts service: it makes a private Claude Code home for it and names the command that signs Claude Code in there. The sign-in waits above the list with that command, Open in a terminal, which closes Settings and runs the command in a new terminal of the project in view, and Cancel sign-in. Once Claude Code signed in, the service takes the login over -- it is the login's only renewer from then on -- and the new account appears in the list; the screen says how the sign-in ended. Add Claude Code's current sign-in adds the account Claude Code on this host is already signed in to, once, so the service manages that sign-in from then on (the claude_native source of the service's import inventory); Claude Code keeps renewing its own login. It is offered until the service manages Claude Code's sign-in on this host, and it is refused while Claude Code runs. Add Codex's current sign-in does the same for Codex (the codex_native source): the account Codex on this host is signed in to is added once, the service manages Codex's sign-in from then on and can switch it, and Codex keeps renewing its own login. The service restarts Codex's app-server to prove the sign-in. It is offered until the service manages Codex's sign-in on this host.
Add ChatGPT account starts a device sign-in in the accounts service, and Sign in again starts one for a login the service renews. The sign-in waits in the list -- above it for a new account, in the account's row otherwise -- with where to confirm it and the code, and can be cancelled there; once it ends the screen says how, and a new account appears in the list. One new-account sign-in runs at a time.
Identifiers stay in each row's Details: the provider, the usage state, the account and login references, who renews each login, when the service renews it next and last checked it. Details also hold Rename, offered only for a name the service's own label rule accepts and refused if the account changed since the screen showed it; Remove, confirmed first, which stops AGL's own use of the account before it asks the service — Flex's bindings of it are released and none is made while the removal runs, and the account's own Codex app-server is stopped; once the account is gone, projects set to it return to this host's Codex — and is refused while a runtime AGL cannot stop still uses the account, naming it, or while a tool on this host still keeps one of its logins; Read usage again, which asks the provider instead of the service's recent reading; Test this login, which spends real quota and therefore runs only behind a confirmation, and whose receipt names a usage or rate limit that kept the test from running, with its window and reset, and the stated cause of a sent test that failed; and, for an account Flex can use, Refresh Flex models.
The accounts service's own panel sits collapsed at the top, headed by its status (Accounts service · Current, Last known 14:02:11 or Unavailable). Opened, it shows the connection, how many accounts the service holds and how many sign-ins wait, the service's stored diagnostics once Read diagnostics is pressed, and what AGL cannot do from the screen with the command that does it: OpenCode renews its own sign-in on this host, so running it on another account is opencode auth login in a terminal.
Nothing on the screen polls: it reads again when the controller announces a change, after its own actions, and when asked. When the service is not installed or not running, the screen names that state and what it means for running work instead of showing an empty list; when it runs an older authswitch release than AGL reads — it was not restarted after an upgrade — the screen names the restart that repairs it (authswitch authority service stop, then authswitch authority service start) and connects again by itself once it is done; when it stopped answering, the last values it delivered stay on screen, dimmed and timestamped.
Authswitch owns provider sign-in and native credential changes. Terminal users switch the Codex, OpenCode and Claude Code logins saved in authswitch with the authswitch command AGL provides (authswitch codex login and authswitch opencode login sign one in); Settings no longer lists those saved logins per harness. AGL's own passkey authentication remains separate.
AGL runs no account manager of its own; it is only a client of the accounts service. An authswitch opencode change meets AGL's OpenCode as any running OpenCode: authswitch offers to stop it first, and AGL starts it again as after any other exit. Saving the current login replaces nothing and needs no stop.
Once the accounts service has taken a saved login over, saving or switching that login is refused: its saved copy is stale, and activating it would hand the tool a dead login. The same happens for every saved login while the service is installed but not answering. The refusal is authswitch's, in its own words. Removing a saved copy is never refused.
Authswitch preserves and verifies an outgoing native login before replacing it. Saving an account needs no exited process, including for Claude Code while AGL-managed Claude terminals run. Remote Codex connections keep their own account and are not changed by this host's authswitch.
Native Codex Harness
Choose New → Chat session and pick the Codex harness to use Codex inside AGL. Native threads support text and reasoning streaming, tool activity, the Codex model and reasoning-effort catalog, command and file approvals, ordinary user-input questions, interruption, history pagination, rename, archive, and deletion. AGL Ask/Yolo controls apply to the exact pending native approval request; approvals never outlive its server request.
Install Codex CLI 0.156.0 or newer and sign in using Codex before starting AGL. The local connection uses the existing Codex app-server control socket when present. This is the WebSocket-over-Unix transport described by the Codex app-server protocol, at CODEX_HOME/app-server-control/app-server-control.sock (the default home is ~/.codex). Codex publishes that path as a link to a socket in its private per-user directory, and AGL attaches only to that published link. AGL owns only its connection to that shared server, and accepts it only when the Codex home the server reports in initialize is AGL's own. If nothing exists at the path, AGL starts its own codex app-server --listen stdio:// child, unless Codex's managed daemon serves this home: its state under CODEX_HOME/app-server-daemon/ (daemon.lock, settings.json, daemon.pid or the older app-server.pid) proves it, and AGL then waits for the daemon instead of starting a second writer for the same threads. Codex never removes that state, so start the daemon with codex app-server daemon start; AGL rejoins it on its own or on Reconnect. Anything else there, including a socket bound directly at the path or a link whose socket is gone after the daemon died, is reported as a connection failure, as is a published socket that does not answer; AGL never starts a private server beside it. Set AGL_CODEX_EXECUTABLE to an absolute executable path when Codex is not on the controller's PATH; an explicit absolute CODEX_HOME is forwarded to the owned child.
Open Settings → Codex to save named remote connections, test them, or reconnect one profile. A project can also be set to a ChatGPT account of the accounts service, whose own Codex app-server then runs the project's new conversations apart from this host's Codex (see Codex on an account of the accounts service). The usage of the account the local Codex runs on is on its row in Settings → Accounts. Use ws:// for a loopback endpoint or wss:// for remote TLS. Enter a bearer token separately; credentials are sealed through the controller host's TPM and persisted encrypted through SmartData. URLs remain immutable so an existing conversation cannot be redirected to another server. Credential replacement disconnects the old connection before activating the new revision. Retiring a profile prevents new conversations from using it while preserving existing origins.
For each project, select its connection and server-native directory. AGL checks that directory through Codex before saving it. Existing conversations retain their original server and directory when the project mapping changes. Models, reasoning efforts, availability and local-attachment support follow that same conversation or draft context. Remote connections disable local file attachments because the remote host cannot access AGL's upload directory. Existing AGL_CODEX_SERVER_URL and AGL_CODEX_SERVER_TOKEN configuration is imported once during the versioned migration; subsequent changes use Settings.
The sidebar automatically discovers non-archived CLI and Desktop conversations from the mapped project directory. Manual enrollment remains available in Settings for an explicit lookup. Opening the same thread on the same app-server shares its native history and live state with other clients, including a Desktop remote connection; sharing an account alone does not select the same thread. AGL never starts or stops an externally owned server. The open conversation's context menu offers Stop following in AGL while AGL is following the conversation, which unsubscribes AGL after its own active and queued prompts have settled, and Resume in AGL on a released conversation to join again; while AGL owns a running turn, or prompts of its own are still queued, there is nothing to hand over and neither action is offered. Between the transcript and composer, Codex reports another client's turn, a released conversation, a model reroute, a paused prompt queue, and — while AGL runs the turn — which model Steer and Queue will use. A completed diff is summarized as added and removed line counts; View edits opens that turn's captured patch. Binary, incomplete, and unparseable patches are labelled instead of receiving invented counts.
When another Codex client owns the active turn, its conversation is marked orange in the sidebar and the whole composer stays read-only and copyable. Attachments, model, mode, effort, send, steer, and queue controls remain unavailable until ownership returns. Stop is independent: it is enabled only when the controller can interrupt the exact observed turn. Local unsent text and attachments survive ownership changes, refreshes, and rejected steering. While another client runs a turn, AGL follows its live updates and accepts the next prompt after that turn finishes. Neither client needs to release the conversation first.
While AGL owns a running Codex turn, Steer sends the message into that exact turn and Queue retains a separate follow-up with its selected model (see Steering and pending messages). A steer that no turn can take any more — the turn ended, or another one runs — becomes the next turn's input. Codex records a steer still pending when its turn completes into the conversation; one it dropped because the turn was stopped or failed is held. Queued prompts survive an isolated Codex restart or reconnect within the running controller. Stop holds queued work instead of cancelling it. An undispatched failure preserves the queued prompt and offers Resume queue; an uncertain dispatched request is never replayed. Interrupted tools show Stopped, and native plans, diffs and observed model reroutes remain attached to their turn.
AGL records a durable creation intent and selected connection before asking Codex for a server-assigned thread ID. It admits that exact ID into managed conversation state and finalizes the requested title and model before sending any turn. New public IDs include their profile identity; shipped legacy IDs remain unchanged. The durable origin outlives creation-intent retention. If initial settings cannot be finalized, the composer keeps that conversation and its unsent draft; the next send finishes setup on the same thread. Local shutdown verifies the owned process group before releasing its uploads; losing a shared or remote connection is not proof that its server stopped. An unavailable generation stays fenced until a reconnect succeeds. When the local shared app-server goes away, for example because its daemon restarted or updated, AGL rejoins it on its own: after the lost generation is cleaned up it retries after 1 second, doubling to 30 seconds, for up to 10 minutes, and each attempt needs the published socket, a supported Codex version and a server that reports AGL's Codex home. Once the profile has connected to the shared server since the controller started, the attempt also needs the server identity (socket, private socket and Codex home) AGL recorded at that last connection; a profile whose shared server was absent at startup is checked by the socket and Codex home alone until it connects. While it waits, a Codex operation fails with harness_unavailable naming the profile, its diagnostic and the reconnection state, and the conversation says it is reconnecting. A returned server with another identity stays fenced with its own diagnostic, and after the deadline the conversation offers Reconnect, the same explicit reconnect as Settings → Codex. Both run the same recovery, and an explicit reconnect cancels the automatic one or, while an attempt is connecting, waits for it and then runs as explicit; queued prompts that were never dispatched resume, while a queue paused by a failed dispatch waits for Resume queue or an explicit reconnect. Remote profiles and AGL's own local child are reconnected only explicitly. The codex entry of agl status and controller.status is healthy only when every connected and required profile is, and lists each under profiles with its reconnection state. An unresolved remote creation requires the original server's thread identity to be resolved before recovery can safely proceed. Codex migration or connection failure leaves OpenCode and Flex available.
Native Codex root conversations in the mapped folder appear in the workspace regardless of which client created them. Discovery preserves each conversation's server origin when the project mapping changes. Native Codex uses its selected server's account; AGL's Flex account selector does not change it. The supported Codex commands are /model, /plan [prompt], /fork, /review [instructions], /rename <title>, /new, /clear, /resume, /copy, /status, /pwd, /usage, /agent (also /subagents), /archive, and /delete. They are discovered and authorized by the controller before AGL opens the corresponding picker, conversation/sidebar view, confirmation dialog, or native operation. /permissions, /mention, and /compact remain visible with their unavailable reason; Codex does not return the compaction turn ID needed to correlate /compact safely. A mode mutation blocks subsequent mode-dependent dispatch until an exact native settings notification confirms the applied mode. A transport-unknown outcome stays explicitly unconfirmed and is never treated as success from a cached session read. Unknown command-shaped input is rejected without becoming a paid chat prompt, while absolute paths remain ordinary prompt text. Secret-input and unsupported custom host requests are rejected explicitly. Codex has no Session Intelligence adapter. Browser resources can attach to Codex conversations. A directly launched Codex task can control its attached browser through agl mcp; trusted Flex run channels remain a separate integration.
Codex on an account of the accounts service
Codex runs on the accounts the accounts service holds in two ways: this host's own Codex switches between them, and a project can run on an account's own Codex app-server apart from it.
Use for Codex (controller.codex.account.use { accountId }) switches this host's own Codex home -- CODEX_HOME or ~/.codex -- to a ChatGPT account whose login the service renews. The service (authswitch.authority.codex.switch) stops Codex's app-server (codex app-server daemon stop), so running Codex turns end; puts the outgoing sign-in into its own custody and the incoming one into Codex's auth.json; starts the app-server again with the environment it ran with; and commits only once Codex itself reports the incoming account. The conversations in that home continue on the account, and AGL rejoins the app-server as after any restart. The controller follows the switch with the service's long poll (watchCodexNativeHandoff) until it is settled and answers it: committed, aborted with the problem or proof failure that undid it, or quarantined, which holds Codex's sign-in until the owner repairs it and is listed by authswitch authority doctor. An account Codex already runs on changes nothing. The service switches only a home it adopted: Add Codex's current sign-in (controller.accounts.codex.adopt) submits the codex_native source of its import inventory, once. Codex's sign-in is one grant per account: Codex renews the login it runs on, and the service every other one.
A project set to an account runs its new conversations on the account's own Codex app-server, apart from this host's Codex. The project's connection picker in Settings → Codex lists every ChatGPT account the service holds; choosing one without a profile makes it (controller.codex.account.profile { accountId }, "Codex · ") before the project is mapped to it. The account this host's Codex runs on is listed as unavailable with the reason and refused by the controller (codex_account_unavailable), since its one grant is in this host's Codex; so is an account without a login the service renews. The service is the login's only refresher on such a server. Codex's own credential store for it is ephemeral and no auth.json is written. Codex sends its token-refresh request (account/chatgptAuthTokens/refresh) to every attached client and takes the first answer, so AGL leaves it unanswered on every Codex connection and never keeps it pending: AGL never supplies a login to Codex, and whoever did answers.
- The service runs one app-server per account for this AGL installation, in the scope
agl/<installationId>/codex/<accountId>, on the account's own Codex home,$AGL_HOME/codex-homes/<accountId>, which holds that server's configuration and threads. AGL accepts the server only when it reports that home. - Every conversation keeps the profile it was created on; a project's other conversations stay where they are when its connection changes.
- The app-server is the service's and outlives the controller. A controller that restarts, or whose connection ended, asks the service again and rejoins the same server, or one the service starts anew, with the automatic reconnection the shared app-server has. While the service cannot give one, the profile's diagnostic is
account_unavailable. - Removing the account in Settings → Accounts stops its app-server first; the service refuses to remove an account whose managed Codex may still run. The Codex home stays.
- Once Codex can no longer run on an account apart from this host -- the service no longer holds it, removed in AGL or anywhere else, while the controller runs or before it starts, or this host's Codex was switched to it, which stops its app-server -- no project stays mapped to it: every project mapped to its profile moves to the default, or to this host's own Codex while the default is a remote server, and a default on it returns to this host's own Codex. The controller log names the accounts and how many projects moved. Only a current report of the service decides this; the last-known one kept while it is away does not.
MCP Server
The installed agl command includes a unified stdio MCP server that combines 59 tools on one connection:
- Nine CrossHarness tools for explicit OpenCode and Codex harness-server connections.
- Six system inspection and reclaim-planning tools.
- Thirteen durable
tstasktools. - Thirty-one AGL controller tools for projects, task discovery and control, models, scratchpads, resource lifecycle and attachment, and managed jobs.
Configure an MCP host to execute:
agl mcp --port 4097
--port takes precedence over AGL_CONTROLLER_PORT; otherwise the controller port defaults to 4097. agl mcp starts and exposes all 59 tools without a running AGL controller. Only the AGL controller tools require a ready controller on the configured port; the CrossHarness, system, and durable-task tools remain available independently. agl mcp requires Linux x64 with Node.js >=24.12.0 <27.
On supported systems, a ready controller publishes a process- and generation-fenced private loopback endpoint under $AGL_HOME/runtime/mcp. The descriptor and bearer token remain owner-only runtime state. They are discovered and verified automatically; do not copy them into MCP configuration. The endpoint accepts only the explicitly registered, bounded AGL methods and is removed before controller shutdown proceeds.
Controller upgrades
A running agl mcp keeps serving its MCP session when the controller moves to a newer build, for example after agl upgrade. The first AGL tool call that finds the controller running a newer release than its own moves the process onto that build in place: it re-executes itself with process.execve, keeping its PID, stdin, stdout and stderr, and the new build continues the session. The call that noticed the upgrade is answered by the new build, since it never reached the controller; the client receives notifications/tools/list_changed, because the new build may offer other tools; and nothing restarts from the MCP host's point of view.
The target is the build the controller itself runs: the cli.js named on the command line of the controller process that the verified runtime descriptor identifies, inside a package whose package.json is this package at the controller's version. The process never re-executes into an older or equal release, and a process that was re-executed for a release but did not land on it never tries that release again, so a mismatched installation cannot start a re-exec loop. The client's initialize request, its initialized notification, the calls to re-run and any stdin bytes read but not yet parsed pass to the new build in a one-shot record, mode 0600 in $AGL_HOME/runtime/mcp-handover, which the new build deletes as it reads it. The new build refuses a record that names another process ID, and the next handover removes records older than ten minutes that no build consumed.
Some state lives only in the MCP process, and a re-exec would lose it. While the process holds durable-task claim authority (a tstask lease it claimed and still holds), an open CrossHarness connection or a CrossHarness dispatch record (a send_message_async still running, or a settled reply check_reply can still return), the move is deferred: the tool call returns BRIDGE_UPGRADE_DEFERRED naming both releases and what is still held, and the next AGL tool call after it is released tries again. New tstask claims are fenced from the moment the process checks until it re-executes, so no claim starts in between; the fence is lifted when the move is deferred or fails. Other MCP requests still in flight are awaited for up to five seconds before the move is deferred as well. BRIDGE_UPGRADE_FAILED means the controller's build could not be verified or re-executed, and its message says whether to call again (the controller was not ready) or to restart the MCP server; BRIDGE_UPGRADE_UNSUPPORTED means this Node.js has no process.execve, and the MCP server must be restarted. process.execve is experimental in Node.js; every supported Node.js release provides it. Before Node.js 26.1 a failed execve ends the process instead of returning an error, so the process checks the Node.js executable before it re-executes.
Caller identity
Every MCP call carries the identity of the chat it serves, and the controller authorizes on that identity instead of on a task id supplied in the request. When AGL starts a chat it mints a per-start caller credential and writes it into that process's environment as AGL_MCP_CALLER_CREDENTIAL; the agl mcp server the chat launches reads it from the environment and presents it on the private endpoint. The credential is never an argument, never inheritable, never persisted, never logged, and never echoed in a response. It dies with the chat: a terminal exit, a task losing its exact managed identity, an OpenCode restart, or controller shutdown all revoke it, and the next start mints a new one.
A caller is one of three subjects:
- Terminal — a chat running as a terminal resource, such as a Claude chat. The terminal conversation itself is the subject: resources are attached to the terminal, and the caller sees and acts on exactly what is attached to that conversation. The fence is the coding agent's own conversation id, so an attachment survives a terminal restart that resumes the same conversation and is refused once the conversation changes. A terminal subject manages an attached browser — create, attach, rename, start, stop, retire — and drives it as its own conversation: BrowserRuntime issues driving authority to a conversation, and the conversation that owns the terminal is one. A plain shell terminal owns no conversation and therefore drives nothing.
- Runtime — a shared harness server. OpenCode runs one server per controller and addresses MCP servers per directory rather than per task, so an OpenCode-launched caller is identified as that runtime, not as one chat: it may act for its own tasks in the project named in each request. This is deliberately coarser than a terminal subject, and narrowing it requires per-task MCP servers upstream in OpenCode.
- Task — a chat AGL manages as a task, acting only for that task in that task's project. The subject exists in the model and is enforced everywhere, but no task credential is issued yet: AGL cannot stamp a per-task credential into a harness that shares one server across tasks. Until OpenCode can carry a task id into the MCP server it launches, OpenCode chats act through the runtime subject.
Reading is open to every caller that holds the descriptor token, identified or not: controller_status, projects_list, sessions_list, context_resolve, models_list, session_read and session_scratchpad_read behave exactly as before. That keeps the cross-harness workflow intact, where a chat running outside any AGL terminal — and therefore without an AGL identity — inspects AGL-managed tasks. resources_list shows the unattached project resources to such a caller.
Writing is not open. Task mutations (session_send, session_scratchpad_update, session_model_set, session_rename, session_archive, session_stop) require the calling chat to be the task itself, or its shared harness runtime; a terminal caller owns no task and an unidentified host owns nothing. Resource mutations, project_add, session_create and browser_action require an AGL identity as well, and so does every job tool: jobs belong to a project, and any identified caller of that project may inspect, wait for or stop them. Every caller must still run as the controller's local OS user; a remote MCP process cannot access this local endpoint without a separate authenticated transport.
| Tools | Behavior |
|---|---|
controller_status, projects_list, project_add |
Inspect the controller and register existing project directories. |
context_resolve, sessions_list |
Resolve an exact canonical project folder and discover its nonarchived native root tasks from the configured OpenCode, Flex, and Codex connections. |
session_read, session_send |
Read a task, or submit text through the existing harness connection. Reading is open to any caller; sending requires the calling chat to own the task. |
models_list, session_model_set |
Inspect available models and reasoning variants; save the model used by subsequent AGL submissions. Pass project and task IDs to use a particular Codex task's connection. Saving requires the calling chat to own the task. |
session_create, session_rename, session_archive, session_stop |
Create or rename a task, archive idle work, or interrupt a running task. Creation does not send a prompt and requires an AGL identity; the rest require the calling chat to own the task. |
session_scratchpad_read, session_scratchpad_update |
Read or revise shared task notes. Reading is open to any caller; revising requires the calling chat to own the task. |
resources_list, resource_create, resource_rename |
Inspect and create project browser, terminal or workspace resources. The listing shows the resources attached to the calling chat plus the unattached ones. A chat creates a workspace at the project directory; where its tree starts is the human's choice. |
resource_attach, resource_detach |
Attach a resource to the calling chat, or detach a resource already attached to it, using its current attachment revision. A shared runtime caller names which of its own tasks to attach to; attaching a resource to another chat is a workspace-UI operation. |
resource_start, resource_stop, resource_retire |
Control the lifecycle of a resource attached to the calling chat or of an unattached one. Retirement removes it from the active resource list. |
browser_action |
Inspect, navigate, click, fill, press keys, or capture an image in a browser attached to the calling chat, alongside human viewers. The caller acts as its own conversation — for a terminal subject, the conversation that owns the terminal. |
workspace_reveal |
Show one path in a workspace attached to the calling chat: a file opens in the editor, a directory in the file tree. The path is project-relative and must lie inside the workspace's base directory. The reply is an acknowledgement and the entry type; the tool never reads a file. |
job_start, jobs_list, job_status |
Start a host command as a managed job that outlives chats and controller restarts, list a project's newest jobs, or read one job's state, exit code or signal, times and log size. |
job_tail, job_wait, job_stop |
Read a bounded range of a job's log, wait up to four minutes for it to end, or stop its whole process group. |
To work with resources for an externally launched task, call context_resolve with its absolute directory and optional { harnessId, nativeId } task ID. A raw Codex thread ID is accepted only if exactly one configured connection matches; an ambiguous ID requires selecting the qualified ID returned by sessions_list. Discovery records the verified native task in AGL without resuming it or acquiring its Codex writer. Subsequent operations reuse the normal UI validation, audit, admission, and ownership checks. Targets must belong to the exact project and configured native connection. Adding a project does not connect an arbitrary external harness server.
Use the returned project and task IDs with resources_list or resource_create, then resource_attach with the resource's attachment.revision as expectedAttachmentRevision. Then call browser_action with the project and resource IDs, the current expectedAttachmentRevision, and an action such as { "action": "snapshot" } or { "action": "navigate", "url": "https://example.com/" }. The same workflow works for OpenCode, Flex, and Codex attachments. Several chats may be attached to one browser and all of them can drive it; each acts as itself, another chat attaching or detaching does not end your authority, and your own actions are still ordered against theirs by the attachment revision you observed. A terminal subject drives the browsers attached to its terminal as the conversation that owns that terminal, so an agent running in an AGL terminal drives its own browser; borrowing another chat's conversation is still refused rather than allowed, and a plain shell terminal, which owns no conversation, drives nothing. Browser actions have a 20-second deadline. Screenshots return MCP image content from that action's own lease and remove the temporary artifact after reading it; images are limited to 512 KiB, so use JPEG with lower quality if necessary. Live terminal input continues to use its resource transport.
Private controller requests are limited to 256 KiB and responses to 1 MiB. AGL text tool results are limited to 512 KiB; screenshot image bytes are separately limited to 512 KiB before base64 encoding, and session_send accepts at most 64 KiB of UTF-8 text.
session_send submits its supplied text directly through normal AGL session admission, model validation, queueing, audit, and harness acknowledgement. It never reads, clears, or emits browser composer draft state. If a mutation reports OUTCOME_UNKNOWN, inspect current state before acting again: the operation may already have succeeded. The MCP client does not replay mutations automatically.
session_scratchpad_read returns the shared durable scratchpad and its current revision. session_scratchpad_update accepts at most 32,768 characters and 128 KiB of UTF-8 text, requires the revision returned by the read, and reports CONCURRENT_CHANGE instead of overwriting a newer browser, Session Intelligence, or agent edit. Successful MCP updates are attributed to the agent.
Task mutations are limited to the calling chat's own task. Every task operation revalidates the recorded project, origin, generation, and deletion/archive boundaries, and can discover an externally created native root before use. Browser-only OpenCode direct-child scopes, child attention, child transcript reads, and child replies are never exposed through the private MCP endpoint.
Managed jobs
A job runs one long host command — a release, a multi-gigabyte proof download, a CI poll — so that it finishes no matter what happens to the chat that started it. The controller launches a small supervisor in its own session; the supervisor starts the command in a process group of its own, copies the command's output into the job log, and atomically writes an exit record once the command and every process it left in its group have ended. Stopping or upgrading the controller never signals a job. The next controller adopts every running job: a supervisor whose PID and kernel start time still match is watched again, a job whose exit record appeared meanwhile gets that exit recorded, and a job with neither is marked lost together with the log size it reached. lost means the supervisor vanished without recording an end: when something outside AGL killed the supervisor itself, the command may still be running, because it runs in its own process group and AGL can no longer prove which group that is.
job_start { projectId, argv, cwd, title, env? }runsargvdirectly, never through a shell; pass["bash", "-lc", "…"]explicitly when a shell is really wanted.argv[0]is resolved to an absolute program path against the controller's own sanitizedPATH(or againstcwdwhen it contains a/) before anything starts, and every job reports it asexecutablePath; the program still seesargv[0]as its name.cwdmust be an existing directory inside the project, reached without symbolic links.envadds up to 64 variables to the sanitized controller environment; only the variable names are recorded, never their values. A project runs at most 16 jobs at once.envmay set neither a controller-reserved variable nor a known execution-altering variable; such a request fails withjob_env_forbidden. The denylist is defense in depth, not a guarantee: no list of such variables is complete, so a policy hook must evaluate theenvnames andcwdtogether withargv. The denylist (jobExecutionAlteringEnvironment, matched case-insensitively) covers the program search path and configuration roots (PATH,HOME,XDG_CONFIG_HOME,SHELL); shell startup (ENV,BASH_ENV,BASHOPTS,SHELLOPTS,PROMPT_COMMAND,PS4,IFS,ZDOTDIR,BASH_FUNC_*); the dynamic loader (LD_*,DYLD_*,GCONV_PATH); interpreter preloads and module paths (NODE_OPTIONS,NODE_PATH,NODE_REPL_EXTERNAL_MODULE,PERL5OPT,PERL5LIB,PERLLIB,PERL5DB,PYTHONPATH,PYTHONHOME,PYTHONSTARTUP,PYTHONWARNINGS,PYTHONBREAKPOINT,PYTHONUSERBASE,PYTHONEXECUTABLE,RUBYOPT,RUBYLIB,LUA_INIT,LUA_PATH,LUA_CPATH,PHPRC,PHP_INI_SCAN_DIR,JAVA_TOOL_OPTIONS,_JAVA_OPTIONS,JDK_JAVA_OPTIONS); the git repository selection, whose hooks and configuration then run (GIT_DIR,GIT_COMMON_DIR,GIT_WORK_TREE); the helper commands of git, ssh, sudo, editors and pagers (GIT_*_COMMAND,GIT_CONFIG*,GIT_SSH,GIT_ASKPASS,GIT_EXEC_PATH,GIT_EDITOR,GIT_SEQUENCE_EDITOR,GIT_PAGER,GIT_EXTERNAL_DIFF,GIT_TEMPLATE_DIR,SSH_ASKPASS,SSH_ASKPASS_REQUIRE,SUDO_ASKPASS,GNUPGHOME,EDITOR,VISUAL,PAGER,MANPAGER,BROWSER); build tools and toolchains (CC,CXX,CPP,LD,AR,MAKE,MAKEFLAGS,MFLAGS,MAKEFILES,GOFLAGS,GOTOOLCHAIN,GOENV,GOROOT,RUSTC,RUSTC_WRAPPER,RUSTC_WORKSPACE_WRAPPER,RUSTDOC,RUSTUP_TOOLCHAIN,RUSTUP_HOME,CARGO_HOME,CARGO_BUILD_RUSTC,CARGO_BUILD_RUSTC_WRAPPER,CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER,CARGO_BUILD_RUSTDOC,CARGO_TARGET_*_RUNNER,CARGO_TARGET_*_LINKER,JAVA_HOME,CLASSPATH,GRADLE_USER_HOME,GRADLE_OPTS,MAVEN_OPTS,ANT_OPTS,GEM_HOME,GEM_PATH,BUNDLE_GEMFILE,NPM_CONFIG_*); cloud CLI configuration that can name credential commands (KUBECONFIG,AWS_CONFIG_FILE,AWS_SHARED_CREDENTIALS_FILE,AWS_PROFILE,AWS_DEFAULT_PROFILE,TF_CLI_CONFIG_FILE); and the Docker endpoint and configuration (DOCKER_HOST,DOCKER_CONTEXT,DOCKER_CONFIG).job_statusandjobs_listreport the state —running,exited,stoppedorlost— with the exit code or signal, start and end times,executablePath, and the caller that started the job, recorded as its subject kind and audit digest, never as its credential.job_statusadds the log size and, once the log reached its cap,logTruncatedAt.job_tail { jobId, fromOffset?, maxBytes?, stripAnsi? }returns at most 64 KiB (default 32 KiB) and thenextOffsetto continue from; a chunk never splits a UTF-8 character. WithoutfromOffsetit returns the newest bytes.stripAnsiremoves terminal escape sequences; offsets always count raw log bytes.job_wait { jobId, timeoutMs }returns as soon as the job ends, or withtimedOut: trueafter at most 240,000 ms, which fits a four-minute heartbeat. The tool waits in controller slices of at most 20 seconds, so a controller restart during a long wait surfaces as an error to retry, never as a lost job.job_stop { jobId, signal? }asks the supervisor to end the command's process group:SIGTERM(orSIGINT) reaches the whole group andSIGKILLfollows after 10 seconds, orSIGKILLends it at once; the job becomesstopped, neverexited. The controller signals only the supervisor, and only while its PID proof holds, so a reused PID is never hit. When the command ends by itself, processes it left behind in its group get the sameSIGTERM, thenSIGKILL, before the exit is recorded.- The log keeps at most 256 MiB of output. At the cap the supervisor stops appending, writes one
agl job: output truncated after <n> bytesline and a log record, and keeps the job running;job_statusandjob_tailreportlogTruncatedAt. When a write to the log fails, for example on a full disk, the supervisor stops logging the same way, recordslogFailurewith the error code when it still can, and keeps supervising: the job still ends, stops and records its exit as usual. Should the supervisor itself hit a fault it cannot handle, it ends the command's process group before it exits, so the job reads aslostrather than running unsupervised. Output is copied through the supervisor, so stdout and stderr each keep their order but interleave by arrival.
Job records live in the controller database as state transitions and are never deleted. The log (output.log), the exit record (exit.json) and the log record (log.json) are written by the detached supervisor, which has to survive the controller, under $AGL_HOME/runtime/jobs/<jobId>/ with mode 0600. They are disposable process artifacts, not application data: the controller reads exit.json to tell exited from lost, and the durable job state is the database record. If a later release keeps job logs as lasting history, they move to @lossless.org/client/objectstorage instead of staying on the filesystem. Jobs run on the host as the controller's user. A service manager that kills the controller's whole control group on stop, such as a systemd unit with KillMode=control-group, kills jobs as well; agl upgrade does not. The web UI view of jobs and log retention follow in a later release.
The optional @modelprofile.com/mcp-tstask dependency is required when starting agl mcp. If the package manager omitted it, the server exits with a clear startup error instead of exposing a partial tool manifest.
Everyday Workflows
Configuration: one master for every harness
AGL keeps one master configuration and projects it onto Claude Code, Codex, OpenCode and Flex. The master is harness-agnostic and versioned in AGL's database; you never edit a harness's native files by hand again.
- Master — shared instructions (the
AGENTS.mdbody) plus optional notes for one harness, the drivers (the model and reasoning effort each harness runs on, in that harness's own names — e.g. Claude Code onclaude-opus-5-5[1m]athigh, Codex ongpt-6-astraatxhigh, OpenCode onopenai/gpt-6-astra, Flex on an account connection and model), skills, subagents, slash commands, MCP servers and the command policy. Secrets never enter it: MCP servers name the host environment variables that hold them (envFrom,headersFrom), and a master carrying credential-shaped text (private keys, provider or forge tokens, JWTs, URLs with a password) is refused. - Project — a separate Flex run for each harness, on the master's Flex driver, adapts its instructions. The runs proceed concurrently, each with a ten-minute deadline; a failure cancels and joins the others, and no partial projection is saved. Each run adapts: its tool names, how it starts subagents, loads skills and runs commands. Everything a harness enforces is mapped by code, exactly or not at all: models and efforts (
settings.jsonmodel/effortLevel,config.tomlmodel/model_reasoning_effort,opencode.jsoncmodel, agent front-matter), agents, commands (Codex runs them as skills), skills, MCP servers and the command policy. An element a harness has no exact form for is left out of that harness and reported — for example an MCP server whose environment comes from host variables on Claude Code or OpenCode. The adapted text lands only in the instruction file, and is refused when it drops a heading or content, adds a URL, or carries a credential. Projections are immutable. Master documents cross the browser and owner-CLI protocol as JSON text so native permission-rule ordering survives transport. Controller protocol 39 requires matching clients; the CLI's JSON input and output stay unchanged. - Review — each harness shows its state (In sync, Changes pending, Edited outside AGL, Blocked, Home not connected) and every file the projection changes, with the current and projected text. A change that replaces a file or setting AGL did not manage yet, or one edited since AGL wrote it, says so; the reviewed diff is the consent. Native settings AGL does not set stay as they are.
- Apply — writes the reviewed changes of the selected harnesses. Each harness's review is pinned: when its files changed after the review, the apply is refused and the harness has to be reviewed again. Native harnesses are written only after their home is connected (Connect home), and only by the installed AGL on its canonical home and database.
- History — every apply, with Roll back to this on earlier ones: a rollback reviews and applies that earlier projection again. Restore the files as they were before AGL reviews and returns Claude Code, Codex and OpenCode to their files from before AGL (below). A conflicted apply or restore offers Retry and Abandon; one whose writer died offers Recover.
What a restore guarantees. The first time an apply writes a native path, AGL keeps what the path held before: the exact bytes and mode of a file, a link's target, or that the path did not exist, including the folders the apply creates. This happens before the write, for every file an apply changes, creates or removes — CLAUDE.md with its @~/.config/opencode/AGENTS.md import line, settings.json with every setting and hook AGL does not set, ~/.claude.json, config.toml with its comments, rules/agl.rules, opencode.jsonc, AGENTS.md, and every agent, command and skill file; a skills folder that was a link keeps its link. Later applies and rollbacks keep nothing new for that path. A restore then returns every path that still holds AGL's last write to that state byte for byte, mode included, removes the files, links and empty folders AGL created, puts back a link AGL replaced with a folder, and releases everything AGL managed on the harness; the next apply starts from the restored files. A file changed outside AGL since AGL last wrote it is shown as a conflict and blocks its harness's restore: it is never overwritten. The exception is a settings file AGL merges its settings into (settings.json, ~/.claude.json, config.toml, opencode.jsonc), which other tools write too: there the settings AGL changed return to their values before AGL and the outside edits made since AGL's last write stay, unless one of them changed a setting AGL changed, which is a conflict. A restore keeps only the outside edits made since AGL last wrote a file: an outside edit that a later apply merged over is part of that write, so the restore reverts it along with AGL's settings; the review's per-setting diff shows it. A folder AGL created that now holds files AGL did not write stays. Rolling back to an earlier projection still applies that projection: it does not restore files. The state before AGL is kept only from this release on: files AGL wrote under an earlier release have no such state, the restore review lists them, and a restore leaves them as they are. What is kept is bounded: at most one state per native path, in AGL's database, its bytes stored once and deleted when the path's state changes; a file larger than 8 MiB is not written at all (the review blocks its harness with baseline_too_large). These copies hold whatever the native files held, credentials included, so they have the protection of AGL's database; the review withholds a text that looks like a credential.
The command policy is a list of allow, ask or deny rules on command globs; the last matching rule decides, and a command no rule matches is asked. Globs read as OpenCode reads them: \ counts as / in the command and the glob, * matches any run of characters, ? one character, and a trailing * makes the arguments optional, so git push * matches git push as well as git push origin main but not git pushx. AGL's Claude Code hook uses the same matcher. OpenCode receives it as its Bash permission table, as a whole and in order behind a leading "*": "ask", so OpenCode asks for an unmatched command too instead of falling back to its own default. Claude Code enforces every deny through AGL's PreToolUse hook (agl-policy-hook.mjs with agl-policy.json), which replaces another Bash policy hook for the same matcher and keeps unrelated hooks. Codex receives prefix rules in rules/agl.rules: ask and deny only narrow, and an allow is written only when every command its prefix admits is one the master allows — a rule without an exact Codex form is reported instead. A subagent's own command policy is enforced by OpenCode; Claude Code and Codex run subagents under the master policy. Flex has no command rules and keeps asking before each command it has no permission for.
Flex reports a projection active only after a managed run of an exact captured session generation reaches configuration-backed model preparation after the apply; older sessions keep the projection they captured. For a bound Flex session, the effective system text follows the model base, the Flex instructions of its projection, the current project-root AGENTS.md (32 KiB, read anew on each run), any caller addition or the selected subagent's body, and the skill catalogue. The skill_read tool pages a skill's files of the captured projection in UTF-8-safe ranges of at most 16 KiB.
Settings → Configuration → Open configuration shows the configuration in the main pane and adds one AGL Configuration row, with a settings icon, at the top of the ungrouped part of the session list. The row stays while conversations and resources are shown, across project switches — the configuration belongs to the controller — and across a reload, as the conversations do; selecting it shows the configuration, and opening the configuration again shows that same row. Its × button, Delete on the row, or Close in its Actions menu (also right-click, ContextMenu or Shift+F10) closes the configuration and removes the row; while the pane holds unsaved master edits, closing asks first, as leaving the pane does. The row cannot be dragged, grouped or attached, and whether it is open is remembered per browser profile.
The master also owns Harness settings: non-secret Claude permissions and hooks, Codex sandbox and approval settings, OpenCode built-in agent overrides and tool permissions, and supported plugin enablement. Models and MCP servers remain in their dedicated sections. Native settings are validated before saving; credentials, sign-ins, plugin caches and session history remain with their harness. OpenCode permission objects retain insertion order through storage, projection and apply, including drift detection.
Agents and commands can set targets to limit where they appear and harnessInstructions to preserve a native body that differs from the shared instructions. Omitting targets projects to every harness. Agent native.codex preserves sandbox_mode and approval_policy; a read-only agent stays read-only. MCP servers also support targets and validated native extras, including Codex tool approval settings. A native-only definition therefore need not become an unrestricted agent on another harness.
Authoring the master from the command line
The configuration pane's Apply and history section (open it from Settings → Configuration) creates a short-lived owner capability for agl config. It is shown once, is never passed in arguments and is not forwarded to agent runtimes. agl config reads it from AGL_CONFIG_OWNER_TOKEN: pass it to each command, never export it in a shell where agents run, because an agent started from that shell inherits it and could apply a configuration itself. Keep it in an unexported shell variable instead:
read -rs token # paste the capability; the variable stays unexported
AGL_CONFIG_OWNER_TOKEN="$token" agl config master get | jq .master > master.json # the current master, or an empty one
AGL_CONFIG_OWNER_TOKEN="$token" agl config master save --expected-revision none < master.json # the first master; later: the revision from `master get`
AGL_CONFIG_OWNER_TOKEN="$token" agl config project # Flex-adapts and projects the current master
AGL_CONFIG_OWNER_TOKEN="$token" agl config state # projection progress, harness states, history
AGL_CONFIG_OWNER_TOKEN="$token" agl config review # every harness against the latest projection, with preview ids
AGL_CONFIG_OWNER_TOKEN="$token" agl config apply --revision <projection> --previews claude=<id>,codex=<id>,opencode=<id>,flex=<id>
AGL_CONFIG_OWNER_TOKEN="$token" agl config restore review # what returning to the files before AGL puts back, with preview ids
AGL_CONFIG_OWNER_TOKEN="$token" agl config restore --previews claude=<id>,codex=<id>,opencode=<id>
A master document looks like this (the fields below are required; lists and maps may be empty). Optional nativeSettings maps claude, codex and opencode to their supported settings:
{
"instructions": "# Global Development Guidelines\n…",
"harnessInstructions": { "claude": "## Claude Code\n- Spawn subagents with the Agent tool …" },
"drivers": {
"claude": { "model": "claude-opus-5-5[1m]", "effort": "high" },
"codex": { "model": "gpt-6-astra", "effort": "xhigh" },
"opencode": { "model": "openai/gpt-6-astra" },
"flex": { "connection": "flex:authority:<account id>", "model": "gpt-6-astra", "effort": "xhigh" }
},
"skills": [{ "name": "commit-release", "files": [{ "path": "SKILL.md", "text": "---\nname: commit-release\n…" }] }],
"agents": [{
"name": "sanity-checker", "description": "…", "instructions": "…",
"drivers": { "claude": { "model": "claude-opus-5-5", "effort": "high" }, "opencode": { "model": "openai/gpt-6-sol", "effort": "high" } },
"policy": { "commands": [{ "pattern": "git commit*", "decision": "deny" }] },
"native": { "opencode": { "mode": "subagent", "steps": 15 }, "claude": { "tools": "Read, Grep, Glob, Bash", "maxTurns": 15 } }
}],
"commands": [{ "name": "c-fix", "description": "Investigate and fix issue", "instructions": "# Fix: $ARGUMENTS" }],
"mcpServers": [
{ "name": "playwright", "transport": "stdio", "command": "npx", "args": ["@playwright/mcp@latest"], "env": {}, "envFrom": {} },
{ "name": "docs", "transport": "http", "url": "https://developers.openai.com/mcp", "headersFrom": {} }
],
"policy": { "commands": [{ "pattern": "*", "decision": "ask" }, { "pattern": "git status*", "decision": "allow" }, { "pattern": "git push*", "decision": "deny" }] }
}
Names of skills, agents and commands are lowercase path segments; a command cannot share a skill's name. Claude Code global efforts are low, medium, high or xhigh; subagent drivers also accept max; Codex efforts are checked against Codex's own model catalog (models_cache.json) and Flex choices against the live Flex catalog. native carries front-matter a harness needs where the master has no concept (OpenCode mode, Claude Code tools, …); it may not set names, descriptions, models or efforts, nor Claude Code permissionMode or allowed-tools. Codex sandbox_mode and approval_policy are supported native agent settings. OpenCode shell rules belong in the agent policy; native Bash rules and allowing wildcards that reach Bash are rejected, including the permission: "allow" shorthand. A native deny wildcard is allowed, with explicit agent shell rules projected after it. Any other front-matter, such as an OpenCode permission for edit, lands in the agent's file and shows in that file's diff at review. The master is at most 4 MiB, each text at most 256 KiB.
The footer's Stats button shows current CPU, memory, aggregate network
rates, and the main disk and each monitored volume with capacity and IO busy
figures. Metric cells reserve their
width and use tabular digits, so changing readings do not move adjacent cells;
the strip scrolls horizontally on a narrow screen. Stats stays pinned with a
reserved ! or ? cue for stale or unavailable live samples; its tooltip and
accessible name give the sample age. One fixed rail control shows the scroll
direction and the number of strained cells with clipped or offscreen values. Values
at a clipped edge disappear as a whole until scrolling reveals them, so a
partial digit is never mistaken for a complete reading. Keyboard focus and
horizontal scrolling reach every disk, the IO figures, both network rates,
and the legal link at the end of the rail. Mount labels retain the path tail,
while each cell's tooltip gives the full path. Open Stats for a live
host-statistics panel
with the sample age, memory used and total, the main disk, and every monitored
volume. A sample older than six seconds is marked stale; its last observed
values and filesystem rows are dated without reducing text contrast, while missing current values
are identified as unavailable. Escape, Close and an
outside press dismiss the panel; the panel updates without rerendering the
workspace. The controller samples CPU, memory and network rates through a bounded
smartsystem worker. Initial network discovery and later topology rescans run
outside the controller event loop, keeping terminal echo and browser input
responsive while metrics are collected. The controller owns and stops that worker.
Clicking one footer reading opens only that reading's one-hour graph in a compact anchored panel; network receive and transmit remain paired. Stats continues to open the complete dashboard. Accounts beside Stats opens Settings at Accounts, where every account's usage is shown.
The controller records a sample every ten seconds in SmartData even while the panel is closed. The panel displays the last hour of CPU, memory, network and per-mount disk capacity and busy readings using dees-catalog charts. The time axis remains one hour wide after startup or a collection outage: unsampled intervals and unavailable readings are gaps, not zero usage. The panel labels partial and stale history, names the end of recorded history separately from the live sample, and marks mounts shown only in history, and reports history retrieval failures. Distinct series colors identify paired readings, and the charts leave room for the graph and its axis labels. The history survives a controller restart and expires after its retention window.
The panel shows disk usage for the filesystem holding / and all monitored
volumes, each with its mount path, used and total size and filesystem type.
Volumes are enumerated through
@modelprofile.com/mcp-system, which bounds every mount's statfs on its own,
so a hung network mount shows -- with the reason in the panel instead of
delaying the sample or the other volumes. Pseudo filesystems and volumes below
1 GiB are left out.
Every disk and volume row carries a second figure, IO, next to its capacity:
how busy the disks behind that mount were over the last sampling interval. It
is the share of the interval in which the busiest of those disks had I/O in
flight — iostat's %util — so it answers "how much of the disk's time is
already spoken for" the way CPU answers it for the processor. The figure takes
the warning style from 80% and the error style from 95%. A mount's first sample
is only a baseline and shows IO -- with the reason, as does its first sample
after more than 30 seconds without one, so a stretch nobody watched is never
passed off as current activity, and a mount whose disks cannot be attributed,
for example a network share.
The panel adds the disks the figure comes from, read and write throughput and
IOPS, and on the main disk the host's I/O pressure (/proc/pressure/io, the
10-second and 60-second some and full averages) where the kernel reports it.
Volumes on one disk show that disk's figures; the panel names the disk. When a
figure is shown, the panel also carries the caveat that busy is the share of time the
disk had I/O in flight, and that SSDs and NVMe can read 100% before they are
saturated.
The footer keeps the existing sustained-high-CPU alert. Memory flashes red when active use reaches 90% of total, and each disk flashes red when capacity use or its busy share reaches 90%; its tooltip names the cause. On Linux active memory excludes reclaimable page cache. Missing or stale readings never trigger a new alarm. Reduced-motion settings keep the alert red without flashing. The panel marks disk busy share from 80% as a warning and from 95% as critical. The panel always shows every rate and volume, and the legal link remains in the bar.
Errors
A failed workspace operation is not painted across the workspace. It goes into a journal in the browser tab, and the header shows an Errors button next to Settings carrying the number of failures not read yet. Its dialog lists them newest first with what the UI was doing, the time, the message, and the controller's reference for the failure. Errors that belong to one conversation — a scratchpad save, a session intelligence question — keep their banner next to that view as well, and are journaled too.
What the composer needs before it can send stays on a line below it and never enters the journal: the account and model a Flex message needs, an incomplete attachment, or composer text over the 64 KiB limit. The line goes as soon as the next keystroke or the next send answers it, or when the workspace opens something else. A change holds only its own conversation, the new conversation it creates, or the workspace (projects, groups, resources and settings), so a message on its way in one conversation leaves every other conversation, the sidebar and the new-conversation box usable. A send that cannot start because another change to the same conversation is still in progress, the conversation is still being created, or the controller is not connected says so on that line and keeps the message in the composer. Deregistering a project waits until no change to one of its conversations is in progress, and says so in the sidebar. When Archive finds another change to its conversation in progress, the guidance appears in the sidebar and clears when that change finishes; it is not journaled. A Codex conversation whose setup did not finish is both — the instruction to send again stands at the composer, and the failed controller call is journaled.
Copy on an entry, and Copy all, put a plain-text report on the clipboard: the AGL version, the time, the operation, the message, the reference, and the controller-side cause with its stack where the controller kept one. Request payloads are not recorded; exception messages and stacks are copied as recorded. Clear empties the journal. The last fifty entries are kept, in memory, for the life of the tab.
When the controller cannot classify a failure it answers The controller operation failed. — internal error text never travels to a client — with an opaque reference. It logs the cause under that reference and keeps the cause in a bounded in-memory journal of the last hundred failures, which only the authenticated owner can read, through controller.failure.detail.get. The Errors dialog resolves the references it collected through that request, so the dialog names the actual error behind the generic sentence. A private MCP request that fails the same way journals it under the method that failed and hands its caller the same reference.
Projects
A project is an explicitly registered existing directory. Every managed conversation, durable resource, and workspace operation is scoped to one project. Conversation model overrides are project/session scoped, while harness default models are controller-wide settings.
Projects are administered in Settings -> Projects, which lists every registered project with its directory and deregisters one. There is no project selector in the sidebar: the conversation list spans projects, and selecting a conversation moves the workspace to its project.
The same section edits the standard project directories: absolute existing directories AGL offers as project locations. Their immediate subdirectories are searched when you open a conversation and offered when you start one, but nothing is registered until a conversation is actually created or opened there. agl settings --add-standard-dir <path> and agl settings --remove-standard-dir <path> edit the same list from the terminal, and plain agl settings prints it.
The browser accepts absolute paths and paths relative to the configured projects root. It suggests matching directories while typing, rejects dot navigation, and reports unavailable paths instead of creating them. A directory typed into the new-conversation box is registered as a project when the conversation starts.
AGL records the complete filesystem ancestry for each project and revalidates it before path-sensitive work. Project removal is a durable deregistration, not a list-only action. Durable terminal, browser and workspace resources must be retired first. For a bound project, AGL then retires controller-managed state, deletes managed OpenCode and Codex conversations, cleans managed Flex roots, Git captures and managed worktrees, and removes project metadata through a resumable process. Removing metadata for an unbound project retains artifacts associated with the untrusted path. AGL never deletes files in the registered working tree.
The Git teardown works from what the reversion engine has records for — the remembered repository catalog and the owned worktree records — instead of walking the project directory, so a project with more subdirectories than the Git discovery limit still finishes its removal. Each record's repository binding is still re-verified against the live filesystem before anything is touched.
A removal that fails is resumed with backoff, and it resumes across restarts and upgrades because the intent is durable. A failure that repeating cannot fix — the Git discovery limit, fenced Git state, a dirty owned worktree, a permission error — stops the retry loop instead: nothing is deleted or abandoned, and the workspace shows a Removal blocked row naming the project and the reason, with Retry to resume it once the cause is resolved. Retry applies to a blocked removal only; a removal that is still being retried keeps its own schedule and is not interrupted. Starting AGL again also resumes a blocked removal once, so an upgrade that fixes the cause clears it without operator action. A pending removal, blocked or not, accepts no new work and is absent from the normal project list.
Conversations
AGL tracks conversations explicitly. A conversation appears in the sidebar because it was created through AGL or opened through the conversation search, never because it happened to exist in a project folder. The list spans every project, each row carries its project name, and the list header offers three actions.
New opens a menu: Chat session, then Terminal, Claude terminal, and Browser. The menu hangs under the button, is keyboard-operable, and returns focus to the button when dismissed.
New → Chat session opens the new-conversation box: the project (or a directory to register), the harness, and the model. The empty workspace shows exactly the same box, so the options never differ between the two. The harness is preselected from the one used last and is otherwise an explicit choice — AGL never starts a harness nobody picked. A new conversation remains a browser draft until its first message is submitted, so abandoning it does not leave an empty harness session. The first message appears in the conversation the moment it is sent, and the chat reads Starting the … conversation… while AGL creates the conversation and sends the message. Leaving the new conversation meanwhile does not withdraw the message: it is delivered, and the sidebar lists the conversation. A conversation that cannot be created returns the message to the composer, one that does not take it keeps it in its own composer, and one whose connection to the controller is lost before the message goes out is discarded again with the message back in the composer; every such failure is journaled.
The terminal and browser entries create in the current project. When no project is in context, one registered project is used without asking, several are picked from in a dialog, and with none registered the menu opens the flow that registers one.
A conversation's own context menu offers the same resources as New attached Terminal, New attached Claude terminal and New attached Browser, in that conversation's project. The controller records the attachment in the same write that creates the resource, so it belongs to the conversation from its first moment and a refused conversation leaves no resource behind. The entries appear only while that project is registered here.
Open searches harness conversations that AGL does not track yet, across every registered project and the immediate subdirectories of the standard project directories. Results show harness, project, and title, and mark what is already open. Picking one tracks it and registers its directory as a project when it is not one yet.
Last active uses the harness’s session-update time, shown in your local timezone.
Archive lists the conversations archived in AGL, across projects, and returns one to the active list. This archive is AGL's own: the harness conversation is untouched and keeps its own archive state.
Conversation features include:
- Steering into a running turn, and queued, steering and held messages that outlive a restart (see below).
- Tip-first transcript loading with older-history pagination.
- Live Assistant text, reasoning, tool state, todos, and subagent cards.
- Inline permission requests and questions in transcript order.
- Synchronized drafts and private temporary attachments.
- Durable model choices and scratchpads.
- Rename, archive, delete, abort, and session-specific Ask/Yolo controls.
- Token and context metrics when the harness can prove them from complete history.
Steering and pending messages
A message sent while a conversation's turn runs is delivered one of three ways (delivery on controller.session.send, TControllerPromptDelivery); every one starts a turn when nothing runs:
steer, the composer's main action for Flex and Codex (Steer, "Read at the next step"): the message goes into the running turn, which takes it in at its next step — after the tool results of its current model step and before its next model call; a running tool call or a pending permission is not interrupted. A message the turn did not take in before it ended becomes the next turn's input.next-turn, OpenCode's main action (Send, "Read after this turn"), since OpenCode reads messages only between turns: the message is the next turn's input, ahead of messages queued for after the turn. OpenCode refusessteer.queue, Queue ("Queue for after this turn", Alt+Enter) beside the main action: the message runs as its own turn after every earlier message. MCPsession_sendand a send withoutdeliveryqueue.
Next-turn messages, and steers that became the next turn's input, wait ahead of queued ones, each group in the order it was sent. A message the conversation has not taken in stays under the transcript's Pending divider and says where it stands (IControllerPendingPrompt.state): Steering · read at the next step, Queued for the next turn, or Held · not sent.
Stop halts the running turn only; it sends nothing and keeps the draft. What the turn had not taken in — its unread steers and the queued messages — is held (heldReason stopped), and so is the prompt of a turn stopped before its model produced anything, since such a turn leaves no trace. A steer of a turn that failed is held as failed; an OpenCode turn that failed holds its queued messages the same way. When the Flex or OpenCode runtime exits or restarts, the messages it had not taken in are held as interrupted; a Codex reconnect holds the unread steers of the turn it lost, while its queued prompts wait for the new connection. A held message is never sent on its own: Resend sends it again like a new message (controller.session.heldprompt.release with action: 'resend' and the busy delivery), Discard drops it (action: 'discard').
Queued, steering and held messages are kept in the controller's database (agl_controller_pending_prompts), so a controller restart does not lose them: every message that was still queued or steering when the controller stopped is held as interrupted at the next start, since the turn it waited for is gone. Their text and model are kept; their attachments are kept only while the controller runs, so a message held across a restart has none. The prompts of a Flex limit hold with Start at reset are restored with that hold instead (see below).
Resending atomically transfers the held message to a new operation before dispatch, so a failed cleanup cannot restore the old held copy. If a restart leaves that resend's delivery uncertain, it is held as uncertain; the browser asks the user to check the transcript before sending another copy. Failed hold and discard writes retain ownership until persistence succeeds.
A prompt sent to a Flex conversation that is still working is steered into its running prompt or waits in AGL's prompt queue, the same controller queue OpenCode and Codex conversations use; this holds for the browser composer and for MCP session_send, which queues. Flex hands a steer to the running run of the prompt (FlexHarness steerPrompt), which takes it in at its next step boundary as a user message of the same turn and reports the steers it did not take in when it ends. A steer with attachments lets the run read their upload directory like its own. The Flex child is only ever handed a new prompt for an idle conversation. If it still reports work in progress, the prompt returns to the head of the queue and waits for that work to finish instead of failing. A Flex slash command runs only on a conversation without other queued or running work and is refused otherwise.
When a Flex provider account reaches a usage or rate limit, the conversation says so: the failed run carries the provider's message and the limit (kind, reset time, plan and window), and a model call that is being retried shows the attempt and the time of the next try. The prompt that met the limit goes back to the head of the conversation's queue, together with everything sent after it, and the queue is held: nothing is dispatched into the same limit. The controller also remembers the limit for the account and reads the account's limits when asked (a read serves for a minute), so a prompt for an account that is known to be at its limit is held without calling the provider. A held queue waits for the user, who can send the held prompts now or cancel them; Stop holds them as messages to resend or discard, like every queued message. Two per-conversation options, both off by default, change that: Start at reset starts the held prompts on their own once the limit resets, after reading the account's limits again and re-arming the hold if the account is still exhausted, and Compact first runs the conversation's /compact before they start, because compaction uses the same account. With Start at reset on, the hold and the text of its held prompts are kept in the controller's session store, so they survive a controller restart; attachments of a held prompt do not. A hold resumes at most once, however often its timer or a restart triggers it. The hold stays armed until its prompts are handed back to the queue, so a restart or shutdown during the re-check or the compaction keeps them; once a hold has resumed, been sent, cancelled or stopped, or start-at-reset is turned off, the stored prompt text is dropped. The account whose limits a Flex conversation shows is its chosen account or, without one, the account its last prompt ran on since the controller started, so after a controller restart such a conversation shows its account again after its next prompt.
In the browser a busy Flex conversation offers Steer and Queue like Codex. A line above the composer states the limit: "Usage limit reached" or "Rate limit reached", the account, and the reset time with a live countdown, or that the provider reported no reset time. While the queue is held it counts the held prompts, says when they start or when the account is checked again, and carries the Start at reset and Compact first switches with Send now and Cancel. A switch shows its new state at once and settles on what the controller confirms; while a change runs, the conversation's controls are disabled and other changes to it are refused. A model call that is being retried reads, for example, "Rate-limited · retrying in 16 s (4/8)". A Flex run that failed for another reason shows the harness's message as the conversation's status.
Archived conversations are hidden from the normal list and available in the Archive view, which serves them from a durable title cache and therefore starts no harness read. Archive is admitted only after active prompts, permissions, commands, and Session Intelligence work have settled.
AGL never automatically redispatches a Flex archive whose outcome is unknown. It performs bounded exact-session readback and finalizes a confirmed archive. Otherwise it stops the exact Flex generation before releasing archive admission; if that stop cannot yet be confirmed, AGL retains the fences and retries cleanup until the generation exits. An authoritative refresh that confirms the conversation is archived clears a stale browser error.
Restarting a conversation's runtime
The composer's More options button (⋮, directly left of Send) offers Restart for the runtime the open conversation runs on: Restart Codex (this host), Restart Codex · , Restart OpenCode or Restart Flex. It asks first and says what the restart ends, states "Restarting …" where the composer is while it runs, and afterwards how the runtime came back; a runtime AGL cannot restart, a remote Codex server, is listed disabled with the reason. A restart that fails is journaled. The menu follows the composer: while the composer is locked (another Codex client runs the turn, or the conversation is read-only), it is too.
A conversation runs on one runtime, and the browser's read of a conversation (controller.session.get) names it in runtime: Codex (this host) for Codex on this host's own login, Codex · <account> for Codex on an account of the accounts service, Codex · <server> for a remote Codex server, OpenCode or Flex. controller.session.harness.restart { projectId, sessionId } restarts that runtime through the component that owns it, so a changed login, for example after an account switch, takes effect:
- Codex on this host. When Codex's managed daemon serves this host's Codex home, AGL restarts its app-server through authswitch's
CodexDaemon.restart():codex app-server daemon stopwhen it runs, then alwaysstart, in its own systemd user scope from$HOMEwith the environment the running app-server had. The answer'snoticesays how it came back, in authswitch's words: whether it ran before, why it has no systemd scope of its own, and which environment it got. An app-server the daemon does not manage is not restarted, and the answer says why. Without the daemon, AGL's own app-server is stopped and started again. - Codex on an account. The accounts service stops the account's app-server, and starts it again, on the same Codex home, when AGL reconnects.
- A remote Codex server is refused with
harness_restart_unsupported: AGL only connects to it. - OpenCode and Flex. AGL stops its OpenCode or Flex child and starts the next one through the same recovery that follows a crash.
Every conversation on the restarted runtime loses its running turn; Codex conversations of the profile reconnect through the profile's explicit reconnect, and the answer comes once the runtime serves again (for Codex, within 60 seconds, after which AGL's bounded rejoin keeps trying). A restart that did not end with the runtime serving again fails with harness_restart_failed, the reason, and a failure-journal reference where the owning component gave one. Every request is audited as session.harness.restart.
Managed Conversation Authority
Controller-created root conversations are automatically managed. On the first compatible startup, AGL seals one bounded migration cohort per project and harness and admits provider-present, non-deleted conversations that already have durable legacy AGL session state. Opening a project also discovers non-archived native root conversations in that exact folder, independently of which client created them. OpenCode uses exhaustive native cursor pagination, Codex uses the configured server and directory mapping, and Flex uses the project's registered native storage scope. Discovery creates durable managed membership before granting access; deletion fences and tombstones still take precedence. Archived conversations stay out of the active sidebar, and managed archives remain available in the archive dialog. Subagent conversations remain accessible through their parent. Sessions in other folders or unconfigured servers are not admitted. Unmanaged conversations remain excluded from ordinary reads, mutations, events, layouts, resources, Session Intelligence, and lifecycle operations.
Provider-declared parent/child relationships never grant ordinary managed-session authority. AGL does not infer or create child membership from a relationship, and independently managed children continue to use normal managed-session access.
For OpenCode, the browser can also receive a temporary scoped view of a provider-observed direct child of one active managed root. The child remains absent from generic session lists, reads, mutations, layouts, resources, Session Intelligence, lifecycle operations, and MCP. The scope permits bounded transcript reads and replies to exact pending child permissions or questions only. Permission replies are limited to one-time acceptance or rejection and cannot create a persistent provider grant.
Each direct-child scope is bound to the exact browser peer and credential, project, managed parent identity, parent and child provider generations, and OpenCode runtime generation. Active scopes expire after five idle minutes or 30 minutes total. A completed child receives a fixed ten-minute read-only grace period; if it resumes, the old scope is revoked and the browser must obtain a new generation. Capacity is limited to 16 scopes per peer and parent, 32 per peer, and 512 controller-wide.
Pending direct-child attention is attached only to the browser's managed-parent detail and is limited to 16 children, 32 requests, and 256 KiB. The browser reports when that transfer was truncated. Child invalidations are sequence-ordered, bounded, and delivered only to the exact authenticated peer; overflow or authority loss revokes the scope.
Conversation search accepts a non-empty printable title query of at most 2,048 UTF-8 bytes. A truncated response means more matches or more candidate directories exist: refine the title query, because the response and UI provide no continuation cursor.
Deleting a managed OpenCode conversation deletes the selected OpenCode conversation. Deleting a managed Flex root also deletes the exact-generation descendant subtree captured with that root. Flex session deletion and project removal require a complete snapshot and exact-generation cleanup cohort, each bounded to 2,048 entries; an oversized scope fails closed.
Groups And Ordering
Conversations and resources are peers in one ordered list per controller. The list holds the tracked conversations of every project and the resources of the project in context; drag either into a group, reorder it within or between groups, or use a group's context menu to rename or remove the group. Selecting a standalone terminal or browser deselects the conversation view; a resource selected beneath an attached conversation retains that parent context. Dragging into a group, between groups, or into the gap between rows is purely positional and never changes an attachment; only a drop on the centre half of a conversation row attaches a resource to that conversation, as described under Resources.
Removing a group does not remove its members. They return to the ungrouped section in their existing order. Confirmed layouts are revisioned, synchronized across browsers, and preserved if a later layout request fails.
Models And Provider Accounts
AGL stores an independent default model for OpenCode and FlexHarness. A conversation can override its harness default without changing other conversations.
Flex model identity consists of the provider, model, and optional variant, independently of any provider account. AGL presents each model once and reports variants and default status per connected account. An explicitly selected or persisted account is validated exactly and is never silently replaced. Without an account selection, AGL proceeds only when exactly one active capable account is available; otherwise the browser asks for an account selection or connection. The conversation's account picker lists every account the controller lists: one that needs a new sign-in stays listed, disabled, with where to sign in again -- Settings → Accounts, where a login Flex holds itself is logged out and signed in again -- and a conversation whose own account needs it shows no account and refuses a message at the composer until the account is signed in again or another is chosen. The accounts service's accounts are listed only while the service is current; in every other state a note under the picker names it in the Accounts screen's words, with the repair where there is one.
Flex provider accounts are connected from Settings through the provider's supported login flow. Credentials remain in a Flex-owned sealed store and never enter SmartData or the browser. The store's master key is sealed to the local TPM2 and survives normal host reboots, kernel updates, bootloader updates, and firmware updates. AGL never resets an unreadable credential store during ordinary startup. Clearing, replacing, or detaching the TPM requires restoring usable TPM access and explicitly invoking SmartSecretSealedFileStore.resetTpm2() with the exact service, store, and path identity while AGL is stopped. AGL currently provides no operator-facing reset command. That destructive reset discards the old ciphertext, and saved connections then require reauthentication through Settings. Account model catalogs, quota windows, and runtime availability are exposed as bounded normalized metadata.
An active Flex OpenAI account can also be selected for the controller-owned OpenCode runtime. AGL pauses the exact active OpenCode cohort, replaces the private OpenCode runtime, verifies health, and reopens admission only after the transition is settled.
Commands And Workspace History
The composer exposes one harness-aware slash-command catalog. OpenCode templates and built-ins remain OpenCode-owned; Flex commands are listed and executed through FlexHarness; Codex commands use the native Codex app-server command surface plus AGL client actions that the controller explicitly authorizes. Commands that are available during an AGL-owned Codex turn still run as commands; they are never forwarded as steered or queued input. Idle-only and otherwise unavailable commands preserve the exact synchronized draft.
Flex /undo and /redo move transcript and workspace history together. For Git-backed projects, AGL captures tracked state and non-ignored untracked paths in controller-private storage without adding controller refs or generated objects to the source repository. Unsupported or ambiguous repository states fail closed or become explicit non-revertible barriers.
FlexHarness agent generation-lease cleanup retains its independent 30 second deadline, followed by a 35 second ordinary Flex IPC control deadline. Each controller-owned Git host operation is bounded to five minutes, Flex reversion maintenance has a separate 5 minute 35 second budget, and the enclosing Flex startup and destructive lifecycle operations remain bounded to 30 minutes.
Controller readiness never waits on Flex initialization for more than 30 seconds before stopping the Flex child and continuing. A Flex child that is still initializing when that boot budget expires is brought back through Flex recovery once the controller is ready, so agl start and agl upgrade observe readiness promptly while Flex keeps its full lifecycle deadline for maintenance-heavy startups.
A terminated Git child gets one second to exit after SIGTERM and then five seconds after SIGKILL to confirm close. A child that has exited but whose close event is late, for example because a descendant inherited its pipes, only fails its own command. Only a child that is still alive after SIGKILL seals the controller's Git reversion engine; AGL then closes that engine and constructs a replacement during the next Flex recovery attempt, or on its own backoff schedule while Flex stays operational, so reversion never stays disabled until a controller restart.
Flex also supports explicit managed worktrees:
/worktree create
/worktree list
/worktree remove <worktreeId>
Worktrees are never created automatically. Removal refuses a worktree with tracked, untracked, or ignored changes.
Tasks, Goals, Scratchpads, And Delegation
Flex conversations include session-local tools for structured tasks, the current goal, and durable scratchpad context. These records are generation-fenced so deleted and recreated sessions cannot inherit stale state.
Flex can delegate one level to a general subagent. Delegation runs asynchronously by default: the parent receives the admitted task ID and can continue while the child works. delegate_result reads its progress or waits for a bounded interval; foreground: true makes delegation wait for completion. Omitting taskId creates a child; an exact ID returned by an earlier completed delegation can resume that child in a later run. The child inherits the active parent's immutable model, variant, and provider account through generation- and run-scoped delegated authority that closes on terminal completion, cancellation, cleanup, or runtime loss. It never receives managed-session membership, and nested delegation is unavailable.
Pending child permissions remain visible in the managed parent conversation. Global Yolo and the exact active parent's session Yolo can reply once to that exact child, and a manual parent reply can accept once or reject. An approved delegation admission also records a read-only transcript grant bound to the controller, project and exact parent and child generations. This enables live previews, transcript drill-in and older-history paging from the managed parent, including after the child finishes. Relationship metadata alone grants no access; older children without an admission grant remain unavailable unless they independently have managed membership. Deleting or replacing either generation invalidates the grant. The transcript endpoint cannot answer permissions, send prompts or stop the child.
With FlexHarness 10, settled model history is compacted automatically before a turn when it exceeds the default 256 KiB context budget. The compactor uses that turn's resolved model; successful automatic archival advances the undo horizon. Startup reports ready after storage initialization and migrations, while tracked recovery loads existing session histories in the background. Requests join the same namespace recovery, and a recovery failure closes the runtime to new work.
Session Intelligence
Session Intelligence can answer a question about one managed conversation and, after reading it successfully, inspect only other managed sessions in the same project. Relationship metadata does not expand that access. It runs in an ephemeral, read-only FlexHarness session with no filesystem, shell, browser, mutation, or delegation tools.
The Session Intelligence panel remains visible while model availability is being checked or is unavailable. Its question composer is enabled only after the selected active OpenAI connection, or one unambiguous active fallback connection, validates openai/gpt-5.6-luna; otherwise the panel provides safe availability guidance, including connect or reconnect instructions when applicable, without exposing credential details.
Answers and scratchpad suggestions are bounded and revision-aware. At most four analyses run controller-wide and one per source conversation.
Resources
Resources are durable project-scoped capabilities separate from conversations.
A resource may be attached to several subjects at once. Every attached conversation may use it: a second chat attaching does not take the resource away from the first, both drive it, and each action is fenced by the attachment revision that caller observed. Another chat's attachment grants a caller nothing — every gate asks whether the set holds that caller's own subject.
Every conversation attachment stores the exact immutable managed-session identity, not only the reusable runtime session ID. A stale or legacy identity-less attachment is detached or rejected during reconciliation, and recreating a conversation with the same runtime ID never inherits its resources. Deleting a conversation removes only that conversation's attachment; the resource stays attached to the others.
Only a browser may be attached to a terminal, and a terminal carries conversation attachments only, so an attachment cycle is impossible by construction rather than by cycle detection. A terminal attachment also puts the conversation that owns the terminal into the browser's driving set, so that agent can act on the browser it attached; the terminal itself is never a member, because it is a process rather than a conversation.
In the sidebar a resource row shows one marker per attachment — two by name, the rest as a +N count. Dropping a resource on the centre of a conversation row attaches it there; the edges of that row and the gaps between rows stay positional, so reordering never attaches by accident. A drag attaches an unattached resource and moves a singly attached one, while a resource attached to several conversations is only repositioned by drag, because only an explicit choice can say which attachment was meant.
A resource created from a conversation's context menu is attached to it by the creating request itself, so it is never persisted or shown unattached. The resource context menu offers Attach to … when nothing is attached, and Additionally attach to …, Move to … and Detach when something is. Each … opens a searchable conversation picker instead of listing every conversation, and never offers a conversation the resource is already attached to. Move and Detach ask which attachment they mean only when there are several. A Move is a single request, so the resource is never momentarily unattached, and Detach removes exactly the membership it named rather than clearing the set.
Terminals
New → Terminal creates a terminal resource, which runs a real shell in the project directory through the controller's shell-free node-pty integration.
- Closing or reloading the browser does not stop the terminal.
- Reopening a terminal shows its current screen, not its history: the controller sends the reconstructed state with up to a thousand rows of scrollback instead of replaying every byte the root produced.
- Manually assigned terminal names, attachments, stopped state, and last exit code are durable.
- New unnamed shells follow the shell or application’s reported window title (OSC 0/2), so a shell with command titles can show
claude agents. Renaming a terminal fixes its name, including after a restart. Automatic titles are live display metadata and are never saved; after the process ends the durable name returns. Existing saved names and shells without title reporting keep their names. - A stopped terminal offers Acknowledge & Restore in its workspace. This starts a fresh shell in the same project directory; processes lost during a controller restart cannot be recovered.
- Terminal input, output, and scrollback are never persisted.
- A terminal can be renamed, stopped, restarted, or retired from its context menu.
- Selecting, opening or creating a terminal puts the caret on it, so the next keystroke reaches the pty. A reattach after a dropped connection never does, so a reconnecting terminal cannot take focus back from wherever you moved on to.
The controller parses each terminal's output as it arrives, so it always knows what that terminal shows, including output produced before anyone attached. An attachment opens with that state at an exact position in the output stream and continues with live output from exactly there, and a viewer that falls behind the in-flight window is handed the current state again rather than a gap. A repeated hand-over carries the current screen without its reconstructed history, so a viewer on a slow connection can catch up with a terminal that keeps producing; one that still cannot is dropped and re-attaches rather than having the controller serialize for it forever. Alternate-screen applications, mouse reporting with SGR coordinates, cursor visibility and style and the scroll region are restored with it; the window title, hyperlinks and underline style are not. A root that produces output faster than the controller can parse it is paused at the pty, as it would be behind a slow physical terminal.
AGL allows at most eight running terminals per project and 32 controller-wide.
Claude chats
New → Claude terminal creates a terminal resource whose root process is Claude Code rather
than a shell. AGL mints the conversation's session id, persists it, and reuses it forever: a
controller restart or agl upgrade brings the chat back with claude --resume <id> in the same
project directory, and the conversation continues.
- The restart intent is explicit. A chat you stop stays stopped; a chat the controller took down comes back. Controller shutdown never records a stopped intent.
- Agent roots receive SIGTERM before SIGKILL so Claude Code can flush its transcript. The last in-flight assistant turn may still be lost; every completed turn is kept.
- Before resuming, AGL proves the conversation is not already open elsewhere and refuses to start
it if it is. Two processes resuming one conversation branch its transcript and silently discard a
branch, so an unverifiable liveness listing is treated as unproven rather than free. Do not
resume an AGL-owned chat by hand with
claude -c,--resume, or the/resumepicker. - A chat that fails to start three times in a row flips to stopped and shows why, instead of retrying on every controller start.
- Claude Code must be installed and on the controller's
PATH, or pointed at byAGL_CLAUDE_EXECUTABLE. It authenticates with its own credentials underHOME, unless the chat runs on a ChatGPT account (below).
A Claude chat is a terminal, not an AGL session. It does not appear in the task sidebar or MCP session tools, and has no model picker, token metrics, permission projection, or upgrade pause prompt. The terminal is the interface.
Resources are scoped to the controller port. Starting AGL on a different port hides a project's Claude chats and does not restart them; their conversations remain on disk under Claude Code's own storage and are reachable again by starting AGL on the original port.
Claude chats on a ChatGPT account
While Use for Claude Code in Settings → Accounts names a ChatGPT account of the accounts
service (controller.claude.account.use { accountId }, read back with controller.claude.account.get),
a new Claude terminal runs on that account instead of this host's own Claude Code sign-in. It keeps
the account for its whole life, across restarts; { accountId: null } returns new terminals to the
host's sign-in, and terminals created before keep what they were created on.
- Claude Code talks to AGL's gateway, which the controller runs on
127.0.0.1on a port the system chooses, from the first such terminal on. The gateway serves the Anthropic Messages API over the account's ChatGPT models and answers only requests addressed to exactly its host and port that carry the credential of a terminal still running; each start of a terminal gets a new one. - The account's login stays in the accounts service. The controller binds the account for the project
(
agl/<installationId>/claude/<projectId>/<accountId>, runtimeclaude) when its first such terminal starts, asks the service for access for every model request, and gives the binding back when the project's last such terminal stops, and when the controller stops. Remove in Settings → Accounts releases it first, like Flex's. - The terminal starts with the account's default model for every model name, the context window the
account's catalog states for it, and the account's models as the whole
/modelpicker. Claude Code sends no experimental betas, no nonessential traffic and no attribution block. A request field the gateway cannot pass on faithfully is refused by name; hints to Anthropic's own service are ignored. A reasoning effort the model lacks is sent as the nearest lower one it has, and the response says so. - Thinking is signed with a key the controller keeps in its database, bound to the account, so a conversation keeps its reasoning across restarts and reasoning never crosses to another account.
- Every such terminal shares one Claude Code configuration directory,
$AGL_HOME/claude-gateway, apart from the host's. Claude Code writes it, and asks its first-run questions there once; AGL only creates it. Transcripts, the resume check and the liveness listing all read that directory. - An account that needs a new sign-in, or that the service no longer holds, fails the terminal's next model request with that reason instead of retrying; a service that is not answering yet is retried.
Browsers
New → Browser creates a browser resource, which provides tabs, navigation, shared viewport control, mouse, keyboard, and text input through a native video viewer. The controller opens one authenticated persistent event stream and one paired operation stream for each human view. These carry state, input, and video signaling. WebRTC carries video directly between the browser host and viewer, using LAN or WireGuard routes with no external STUN or TURN service by default. The endpoints must be able to reach each other over UDP.
The DevTools button toggles the official Chrome DevTools frontend inside the browser pane, with Elements, Console, Network, and Sources, and stays pressed while the inspector is open. The panel icon beside it — Show browser diagnostics, which becomes Hide browser diagnostics while the panel is open — toggles a panel on the right of the browser pane whose Screencast section carries the full diagnostics — codec, encoder, frame rate, bitrate, dropped frames, decode, frame age, capture-to-display, jitter buffer, round trip, input reply and queue, commands in flight, GPU compositing and video encoding — and stays open until you close it, on this browser. Show stats on video at the end of that section enables the one-line video overlay for this page lifetime; it is off by default and does not stop metrics collection or the sidebar readings. A reading the renderer has not reported is left out rather than shown as a zero. Website JavaScript errors appear as a compact Website error notice with a Console action; browser and transport failures retain their own diagnostics. The inspector is scoped to the selected tab and the authenticated human view. Closing or replacing that view, changing tabs, revoking authority, or recovering its transport closes the inspector. Document navigation and obsolete input preserve the inspector. Reopen DevTools after a signaling transport recovery.
Website alerts, confirmations, prompts, and before-unload dialogs appear in an origin-labelled modal. Prompt text preserves whitespace and empty responses. Only the exact pending dialog can be answered; switching views invalidates its UI. Held keys and buttons are retained while a dialog blocks input and released when it closes.
Inspection uses a separate pair of bounded authenticated streams, allowing debugger Resume commands while another CDP command is waiting. Small CDP events are batched into ordered writes of at most one MiB, with up to eight writes awaiting acknowledgement and one pending batch. Partial batches flush on the next event-loop turn. The native source charges retained payload and entry overhead against a byte budget, so large replays of small events do not hit a fixed event-count cutoff. Inspector failures retain their original reason and distinguish oversized messages from an exhausted receiver backlog. CDP access does not expose a public debugger socket, browser-wide targets, the private capture page, or another viewer's session. Static frontend assets and translations ship locally through SmartBrowser; no remote DevTools CDN is needed. This release bundles the Chromium 151 frontend and reports a version mismatch explicitly if the host browser uses another major version.
Every signaling pair is bound to the exact peer, credential, resource authority, view, and stream generation. BrowserRuntime owns each viewer's media peer; the client cannot supply another viewer's peer identifier. Offers and answers accept one video media section, with bounded payloads and negotiation identifiers. Ordinary websites retain the runtime's network confinement; the private capture extension owns media transport.
Chrome captures the active tab directly and uses native video encoding, decoding, congestion control, and pacing. GPU compositing and encoding are requested automatically where supported; the actual capabilities and encoder are reported rather than assumed. The detail quality policy preserves text resolution and lets Chromium adapt frame rate under pressure. Capture is capped at 2560 by 1600 pixels, 60 frames per second, and 16 Mbps; a second application downscaling loop is no longer used. The higher frame cadence reduces the wait between input and its visible result; congestion control and CPU adaptation still apply. The receiver requests a minimal jitter buffer where supported, and the browser applies its own minimum. Hidden viewers close their media peer and reconnect when visible; the final viewer leaving stops native capture. Human video viewers do not enable JPEG screencasting. Agent screenshots remain available independently.
While a view stays open, the controller renews its BrowserRuntime lease before expiry after rechecking the stored resource, authenticated viewer, and runtime authority. Renewal preserves the media connection and browser incarnation. Closing the view cancels renewal, closes the viewer's media peer while signaling is available, then revokes the lease and closes the transport. A stalled renderer has a bounded cleanup deadline; lease revocation still owns final media cleanup.
Select Settings → General → Browser video → NVIDIA GPU (native) to use the Rust capture, NVENC encoder, and direct WebRTC sender. This backend requires Linux, Xorg, NVIDIA graphics libraries, and an NVENC-capable GPU. It does not use GStreamer or a VM. The native backend uses physical pixels, preserves source resolution, and caps capture at 30 fps. The viewer must negotiate a codec level that supports the requested dimensions; unsupported receiver limits produce an explicit negotiation error. GPU compositing and encoder statistics report the capabilities actually in use.
The setting is persisted by the controller and takes effect after restart. Local operators can use the owner-authenticated control endpoint through the CLI:
agl settings --port 4097 --browser-video-backend native --json
agl stop --port 4097
agl start --port 4097
agl settings --port 4097 --json
The result distinguishes the selected backend from the active backend and reports
restartRequired. Use --browser-video-backend chromium to select Chromium capture.
Native presentation fences and aligned-frame cropping travel through browser protocol
v28. DOM pointer timestamps survive the transport so short drag gestures retain their
occurrence timing.
State and operation results retain reliable FIFO delivery with bounded JSON packets and queue sizes. Four ordered event writes can await acknowledgement, refilling as replies complete. A full event queue reserves one close announcement, which waits for earlier writes before transport teardown. Operation IDs, pending bytes, cancellation, event delivery, and recovery attempts are bounded. Adjacent compatible pointer-move or viewport operations may be coalesced. Video does not occupy this reliable control queue.
The browser installs its renderer before activating a view. A recoverable signaling failure suspends input, retires the old stream pair, starts the next generation, and resumes renderer listeners before activating the replacement. Document navigation keeps the native video peer, while input waits for current document state and a subsequent presentation. A failed video connection offers Reconnect video without closing DevTools or replacing the authenticated control view. Stale activation, negotiation answers, cancellation, and close messages cannot act on another generation.
Agents and multiple human viewers share one browser incarnation. BrowserRuntime owns execution scheduling; the controller submits commands in order and retains bounded cancellation and byte ownership until they settle. Four wheel operations can be in flight while mouse/key and semantic operations preserve ordering barriers. One statistics observation runs independently, so delayed telemetry cannot block input, navigation, or media shutdown. Dialog replies have an authorized interrupt slot so a paused page operation cannot block its own reply. The effective viewport takes the smallest width, height, and device scale requested by connected human viewers, and grows when a smaller viewer leaves. Viewport acknowledgements include the accepted dimensions and revision. A conversation leaving the attachment set revokes that conversation's agent authority while preserving every conversation that stayed attached and all human viewers; while such a rebind settles the runtime reports the resource as busy and the controller retries the action rather than failing it. Independent MCP actions receive temporary capability bindings for the exact project, resource, attachment revision, and acting conversation — the calling chat's own task, proven by its caller credential; cancellation and shutdown retain capability cleanup ownership. Trusted Flex channels additionally require the active run and generation.
When attached to a Flex conversation, the resource provides permission-gated navigate, snapshot, screenshot, click, fill, and press actions to that exact run. Frames for closed Flex channels or an exhausted frame backlog are dropped without stopping the Flex child or affecting other tasks.
Resource stop and retirement announce view_closed before ending human streams. A resource authority replacement announces resource_changed; an unrecoverable view failure announces view_failed. Close announcements precede stream teardown. Authority replacement can reopen a selected active resource, bounded at three attempts within 30 seconds. Transport recovery allows six attempts within 45 seconds. Human input, statistics, and transport chunks check the exact live peer, credential, lease, resource, and generation without database reads. View activation, renewal, viewport changes, and semantic commands retain durable checks; mutation authorization runs after Runtime reserves execution order. Delivery failures retain the original reason, stream generation, and queue pressure in logs.
Wheel input is accumulated once per animation frame, with up to four input commands in flight. Each completed wheel command frees its slot without waiting for the whole batch. Coalescable input is merged or dropped under pressure so sustained scrolling does not exhaust the input queue. The viewer shows presented frames per second, bitrate, dropped frames, estimated capture-to-display latency when available, jitter-buffer delay, network RTT, and input latency. Receiver sampling runs independently of remote statistics requests; stale frame-rate samples show as unavailable rather than zero. The Sidebar panel's Screencast section reports decode time, the age of the latest captured frame, codec, encoder, and host GPU capabilities. Resizing preserves the video connection and fits both capture dimensions with one scale, preventing encoded letterboxing after changes to pane size or display scale. A pane without a view distinguishes loading, a closed view, and an unavailable runtime.
Workspaces
A workspace resource serves one project's working tree to the browser: its files, an editor and the changes on disk. Workspaces run on Linux hosts only, because their confinement rests on Linux's /proc/self/fd; on any other host a workspace is neither created nor served, and every request is refused as platform_unsupported. It owns no process, so on Linux it is available whenever the controller is, and it attaches to a conversation — or to a Claude chat's terminal — like a browser does. Its base directory is where its tree starts: the project directory or any directory inside it, chosen when the workspace is created and switched by the human with controller.workspace.base.set, which names the base revision it was based on so two viewers switching at once cannot overwrite each other unnoticed. A chat never moves the base directory; workspace_reveal shows a path inside it and is refused outside it.
The project directory is the only root, whatever the base directory is: every path is project-relative on the wire and resolved on the controller, links included, and a path or link that leads outside the project is refused. Nothing is then done by path. The directory an operation works in is opened without following a link and trusted only once the kernel's own name for the descriptor is the path that was checked, and every entry inside it — a file read or written, a file or directory created, removed, moved or copied — is addressed through that descriptor; a recursive removal or copy walks the tree descriptor by descriptor and never follows a link. A directory swapped for a link at any moment therefore cannot redirect an operation out of the project; watches are held the same way, so they report names of the project's tree only. A directory is never moved or copied into itself (path_within_source), and a removal, move or copy walks at most 128 levels deep (too_deep) and 50,000 entries (too_large).
Two limits remain, both requiring a process that can already write where it acts. A rename whose target a concurrent process creates between the existence check and the rename replaces that target, since there is no rename that refuses to replace. And a process with write access both inside and outside the project can move a directory the controller has already verified out of the project; the operation then still lands in that directory, through its descriptor.
- A secret-shaped file —
.envand its variants,.npmrc,.envrc, private keys and the credential stores AGL's secret-file policy names — is listed, never read, written, created, moved or copied, so no rename turns it into a file that may be read. It can be removed. The policy judges names, matched without regard to case: a hard link to a secret under an ordinary name is readable under that name. Reads cannot refuse every file with a second name, since package managers such as pnpm hard-link every file innode_modules. - Nothing inside
.gitis changed —.GITincluded — and a tree holding a nested repository is neither removed nor copied as a whole. - The base directory and every directory holding it are never moved or removed.
- A write replaces a file only while it still holds the content the write was based on (
changed_on_diskotherwise) and creates one only where nothing is. Every change of one project — writes, new directories, removals, moves, copies and base-directory switches — runs one at a time, so a removal always sees the base directory it has to protect. A file with a second hard link is never written, since the write would change it under its other names too. - Only UTF-8 text up to 4 MiB is read or written; a byte-order mark survives the round trip.
Every refusal carries its own code (path_outside_project, secret_file, git_protected, base_protected, changed_on_disk, …) instead of a generic failure. Changes on disk are audited; reads and watches are not. A browser watches a file or a directory — recursively without .git, node_modules and .nogit — and receives coalesced controller.workspace.changed pushes on its own connection only; a watch ends with its unwatch, its connection, its workspace's retirement or the controller's stop, also while it is still starting (watch_ended). A connection holds at most 64 watches, one watch covers at most 2,048 directories and all watches together 16,384; beyond a bound a watch is refused as watch_limit, and a burst beyond 256 changes arrives as one push that says overflow.
In the browser a workspace opens in place of the chat, as a terminal or a browser does: the file tree from its base directory and the catalog's editor, which reads and saves through the controller and offers no terminal, since a workspace runs no process. New → Workspace creates one at the project directory, and a conversation's context menu offers New attached Workspace. Open files and expanded directories follow the changes on disk through the pane's watches, and a save names the content it replaces, so a file changed on disk since it was opened is never overwritten unseen.
- Leaving a workspace whose editor holds unsaved edits — for a conversation, another resource, a new conversation or another project — asks first: Keep editing stays in the workspace, Discard edits leaves them behind.
- Base directory… browses the project's directories through the workspace's own confined listing and switches to the chosen one. With unsaved edits in the editor the switch asks first, because it leaves them behind. When another view moves the base while this editor holds unsaved edits, the editor stays where it is and says where the base went until the human follows it; a clean editor follows at once.
- A chat's
workspace_revealshows its path in the workspace on screen — a file opens in the editor, a directory is expanded and selected in the tree. A workspace that is not on screen stays where it is. - Send to conversation puts the file or directory selected in the tree into an attached conversation's composer — as long as that conversation is still attached when the block is appended — as project-relative paths under one line naming the project directory. Nothing is submitted: the human sends the prompt. The block is appended by the browser's own draft writer, so it survives text typed but not yet saved, and a concurrent write to the draft is kept beside it. With several conversations attached the open one is offered first; the conversation inside a Claude chat's terminal has no composer and is never offered.
- The editor, Monaco, is served from AGL's own origin at
/monaco-editor/min/vs, from exactly themonaco-editorbuild@design.estate/dees-catalogpins, and never from a CDN. Monaco starts its language workers fromblob:bootstrap scripts that load the worker code from that same origin, so the page's content security policy allowsworker-src 'self' blob:. Scripts still load from AGL's origin only, and nothing needsunsafe-eval. The build is sent withCache-Control: no-cacheand an ETag, so a browser keeps it and revalidates it on each load instead of downloading it again. Only files insidemin/vsare served; a path that names.., in any encoding, is refused. - Losing the connection to the controller closes the workspace pane with the rest of the workspace. It is the one way off the pane that cannot ask first: edits not yet saved are lost, and the pane opens again from disk once the connection is back.
Authentication
AGL creates exactly one passkey credential per controller database. Enrollment is first-wins and there is no weaker online passkey-reset path.
The one-time setup code expires after 30 minutes. Omitting --setup-code uses a random 32-byte code. A custom setup code is visible in shell history and weak codes are recoverable by anyone who can read the database, so prefer the generated value.
For temporary or scripted browser access, mint a CLI password:
agl temp-password --ttl-hours 2
Temporary passwords expire after at most 24 hours. Only their SHA-256 hashes are stored, and at most eight can be active.
Authentication belongs to one physical TypedSocket connection. Resume tokens are single-use, rotate on resume, remain only in browser session storage, and are invalidated by controller restart.
CLI
agl start [options]
agl status [--port 4097] [--json]
agl settings [--port 4097] [--browser-video-backend chromium|native]
[--add-standard-dir <path>] [--remove-standard-dir <path>] [--json]
agl stop [--port 4097]
agl upgrade [--port 4097] [--registry <url>] [--grace-period-seconds 300] [--continue-sessions] [--json]
agl mcp [--port 4097]
agl foreground [options]
agl temp-password [--port 4097] [--ttl-hours 24]
agl help
agl --version
Important startup options:
--port <port> Controller port; default 4097
--opencode-port <port> Private OpenCode port; default 4098
--directory <path> Register one existing project on this start
--projects-root <path> Base for relative project paths and suggestions
--public-origin <origin> Exact browser WebAuthn origin
--rp-id <hostname> WebAuthn relying-party ID
--behind-tls-proxy Declare trusted TLS termination upstream
--setup-code <code> Supply an operator-chosen initial setup code
settings prints the browser capture backend and the standard project directories; --add-standard-dir and --remove-standard-dir edit that list, which is the same list Settings -> Projects edits in the browser. Directories must be absolute and must exist on the controller's disk.
Runtime configuration is durable and immutable per controller port. Later management commands must use the same --port. The --directory option is transient and can register another project during a later stopped start.
stop verifies the exact package CLI, process, process group, command, port, and process-start fingerprint before signaling anything. AGL never kills processes by name.
Upgrades
Use the active global installation:
agl upgrade --port 4097
By default, upgrade package commands use pnpm's effective registry configuration. Override it for one durable upgrade transaction when needed:
agl upgrade --port 4097 --registry=https://registry.npmjs.org
Registry URLs must use HTTPS, except that HTTP is accepted for localhost, 127.0.0.1, and [::1]. They cannot contain credentials, query parameters, or fragments. Keep authentication in pnpm configuration rather than command arguments.
The upgrade worker:
- Resolves the registry's
latestversion and never downgrades. - Pauses the exact active-session cohort before stopping a running controller (see below).
- Waits for admitted operations and data writers to drain.
- Verifies that the stopped process has released its kernel resources before replacing packages; disappearing command-line arguments do not count as process exit.
- Installs while respecting pnpm's configured dependency build policy; AGL uses prebuilt terminal binaries.
- Restores and restarts the exact source version if installation fails before target startup and, for package transitions, before the commit begins.
- If the controller was running, restarts the exact target and reopens prompt admission.
- Moves the accounts service onto the authswitch the new AGL brings, when it brings its own
authswitchcommand, with authswitch's ownauthority service stop,installandstart: the service is stopped before the package changes and started again, on the new release, when it ran before; a separately installed@modelprofile.com/authswitchis removed, since pnpm installs no second package that provides the command. A host without the service's unit keeps it that way, and the service's store is never touched. Stopping the service also stops the managed Codex runtimes it runs; they start again when next asked for. AnauthswitchonPATHoutside pnpm's global command directory refuses the upgrade before anything is stopped; remove it with the tool that installed it. The log names what was removed, stopped and started; a failed upgrade restores the previous AGL first, then a removed authswitch and the running service, and each step is recorded in the upgrade transaction, so recovering a lost worker finishes or undoes it. - Preserves its transaction across worker or controller failure.
The cohort is every conversation with an AGL prompt that is queued or still running when the upgrade starts. AGL seals prompt admission and asks each of them to pause with the message "please pause at the earliest convenience for a harness upgrade":
- A Codex conversation whose AGL turn is running receives the message steered into that exact turn. It counts as paused once that turn ends; it gets no second pause turn.
- OpenCode and Flex conversations are not steered for the pause; they wait for the turn to end. A Codex conversation whose turn does not accept the steered message waits the same way.
- A conversation that is idle by then, including every one that waited, receives the message as a turn of its own, and AGL waits for that turn to end.
The upgrade waits up to the grace period (--grace-period-seconds, 300 by default) for the cohort's turns to end and never interrupts the turn of a prompt sent through AGL. If one is still running at the deadline, the upgrade fails before anything is stopped or installed, prompt admission reopens, and no continuation message is sent. An upgrade interrupts only a Codex turn it did not start through a prompt sent to AGL: when a conversation's pause turn is due, or when an upgrade that failed after pausing recovers a conversation that received its pause as a turn, it interrupts whatever interruptible turn that conversation is then running -- its own pause turn, or a turn another Codex client, such as the Codex CLI, started there.
Use --continue-sessions to submit the continuation message "harness has been updated, please continue" to sessions paused for the upgrade, once per session; the original prompt is never sent again. Without it, AGL reopens admission without sending another message.
If a forward-only upgrade worker is lost after target startup or package commit begins, a later fixed AGL installation can adopt exactly one stalled transaction during an explicit agl upgrade. Adoption requires a free port, no live worker, controller, Flex child, temporary-password writer, or token-bound metadata, and a compatible installed version. It preserves the original preparation timeline and paused cohort. Do not delete upgrade files, copy transactions between roots, downgrade the package, or bypass recovery with agl start.
Upgrade coordination lives under $AGL_HOME/upgrade. Private logs live under $AGL_HOME/logs.
A failure reference from the Errors dialog appears in those logs on the line the controller wrote when the failure happened, together with the operation and the cause. The journal the dialog reads it from is in memory only, so it is gone after a controller restart while the log line remains.
Migrating From hcon
Starting from @modelprofile.com/harness-controller@20.0.2, run the active command twice:
hcon upgrade --port 4097
hcon upgrade --port 4097
The first invocation installs the same-package bridge. The second performs the exact package transition to AGL. Wait for each command to finish and use the controller's configured port. This transition lands on AGL 21.0.0. Afterward, use agl and follow the exact migration-bridge sequence below before upgrading to the current release.
The historical hcon transition uses pnpm's configured registry and does not support --registry. Configure pnpm before starting that migration when a specific registry is required.
Upgrading A Legacy Database
AGL 34 removes the SmartDB engine and the historical home converters. Existing NoSQLDB homes upgrade normally. Homes from 32.x or earlier must first complete migration with AGL 33.0.1, which is the final migration bridge, before installing 34. Do not skip that bridge: 34 refuses an unconverted home or an incomplete migration journal instead of creating an empty replacement database.
agl upgrade resolves latest; it has no version-selection flag. For an installation with no unfinished upgrade transaction, use this exact bridge sequence. Keep the controller's existing port and home, let running work settle first, and stop if any command fails:
- Run
agl stop --port 4097from the active installation. - If the authswitch authority service exists, record whether it is running, then run
authswitch authority service stop. Before package replacement, remove a separately installed global@modelprofile.com/authswitchwithpnpm remove --global @modelprofile.com/authswitch; AGL 32.20 and newer already bundle that command. Remove any otherauthswitchon PATH outsidepnpm bin --globalwith the installer that owns it. - Run
pnpm add --global --save-exact agl@33.0.1using the same pnpm installation and registry configuration. Do not substitute a temporarypnpm dlxrunner: lifecycle commands require the active global installation. - If the authority service existed, run
authswitch authority service installfrom the same shell; runauthswitch authority service startonly if it was previously running. The authority store stays in place. - Rename configured
HARNESS_CONTROLLER_*controller variables to their documentedAGL_*replacements, then runagl start --port 4097andagl status --port 4097. Wait for the controller to report ready; this start completes and verifies the database migration. - Run
agl upgrade --port 4097to install the current release through the normal coordinated upgrade.
If an upgrade transaction is unfinished, follow its recovery flow instead of manually replacing the package or bypassing it with start. Never delete its journals.
The bridge verifies document contents, counts and indexes before completing the conversion. Preserve the resulting NoSQLDB directory and migration records together. AGL 34 validates the recorded destination identity at startup; it does not re-read the retired database. Treat the conversion as forward-only.
AGL Home And Persistence
AGL owns one private root:
- Global installations:
$XDG_CONFIG_HOME/agl, normally~/.config/agl. - Repository checkouts:
<checkout>/.nogit/agl. - Explicit override: absolute normalized
AGL_HOMEwhose parent already exists.
Important directories include:
nosqldbfor the embedded NoSQLDB database.databasemay remain as a retained legacy backup after migration through AGL 33.0.1. AGL 34 uses onlynosqldband does not open or convert that backup. Never run an older controller against a migrated home.credentialsfor sealed Flex provider credentials.git-reversionfor private captures and managed worktrees.logsfor controller and upgrade diagnostics.upgradefor durable upgrade coordination.runtimefor private browser, upload, socket, OpenCode, job-output andagl mcphandover state. A repository checkout keeps its embedded database socket in the per-user runtime directory instead (/run/user/<uid>/agl-<hash>when that directory exists and is private), because a Unix socket path is limited to about 100 bytes and a checkout can live at any depth.claude-gatewayfor the Claude Code configuration of Claude terminals that run on a ChatGPT account through AGL's gateway.migrationfor verified migration journals and retained source records.
Do not move these directories or edit migration and upgrade journals by hand.
The NoSQLDB document models in @lossless.org/client/nosqldb store runtime configuration, passkey metadata, projects, managed-session memberships, crash-recoverable session creation and deletion obligations, layouts, model choices, scratchpads, tasks, goals, resources, Flex public projections, provider metadata, and bounded audit events. Prompt text is not written to audit records.
Managed job logs and exit records are disposable process artifacts under runtime/jobs; the job records themselves are in NoSQLDB. Terminal I/O, active browser views and their paired transport streams, composer drafts and their pending attachment state, active capability leases, and running processes remain memory-only. Once a prompt is submitted, attachment files exist only in private operation directories for the lifetime of that exact harness operation.
Library consumers can resolve the same home contract:
import { bindAGLHomeEnvironment, resolveAGLHomePaths } from 'agl';
const paths = resolveAGLHomePaths();
const childEnvironment = bindAGLHomeEnvironment(process.env, paths);
Database Configuration
The embedded database requires no separate service. Override its directory with an absolute path. The directory must hold a NoSQLDB database or nothing yet: legacy SmartDB directories are refused.
export AGL_DB_DIR='/private/absolute/path'
To use an external MongoDB-compatible server:
export AGL_MONGO_URL='mongodb://user:password@127.0.0.1:27017'
export AGL_MONGO_DB='opencode_controller'
AGL_MONGO_DB defaults to opencode_controller.
Releases before 33.0.0 read these settings as HARNESS_CONTROLLER_*. Those names are no longer read, and start, foreground, temp-password and upgrade refuse to run while one is set, naming the AGL_* variable that replaced it.
Runtime Environment
OpenCode, FlexHarness, and terminal shells receive a minimal allowlisted environment. Controller and database variables, unrelated parent secrets, and generated OpenCode credentials are not inherited. AGL_HOME is the intentional exception: AGL forwards its canonical value so controller-owned child processes resolve the same private root.
Every AGL_* name is reserved for the controller: it cannot be forwarded and a job cannot set one. Delegate additional non-controller variables explicitly when required:
export AGL_FORWARD_ENV='MY_TOOL_CONFIG,MY_CA_FILE'
Flex provider credentials do not use this mechanism. They stay in the sealed provider store. OPENCODE_AUTH_CONTENT is always rejected; AGL never reads or edits OpenCode's user-wide auth.json.
Credential-free HTTP_PROXY and HTTPS_PROXY values are forwarded. Proxy URLs containing credentials are not forwarded automatically.
Remote Browser Access
Remote WebAuthn requires HTTPS. Start AGL behind a trusted TLS reverse proxy:
export AGL_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
The proxy must:
- Remove client-supplied copies of AGL transport headers.
- Inject the configured secret as
X-Harness-Controller-Proxy-Secret. - Set
X-Forwarded-Proto: httpson WebSocket upgrades. - Preserve the exact public Host and Origin.
- Forward WebSocket upgrades.
- Restrict direct access to the backend listener.
Backend isolation is mandatory. Proxy mode binds the controller to all interfaces, while origin-less loopback CLI management remains intentionally available. Never expose the plain backend port directly.
Security Boundaries
- Local mode binds the controller to loopback only.
- The private OpenCode server always remains on loopback and uses a generated per-process password.
- Application RPC starts only after the exact TypedSocket package-major handshake.
- Client-managed connection tags are disabled; authentication and routing use server-owned peer state.
- Setup attempts are globally rate limited before enrollment.
- Permission auto-accept is an explicit server policy, never an assumption based on an open browser.
- Questions are never auto-answered.
- Unknown, malformed, concurrent, or unverifiable lifecycle state fails closed.
The global auto-accept permissions (yolo) setting replies once to pending permissions across registered projects and writes those replies to the audit log. Each conversation also has a run-local Ask/Yolo switch. For an active managed OpenCode root, that switch covers its currently active direct children under the same exact family authority. For an active managed Flex parent, it covers only the exact active delegated child bound to that parent run; a relationship alone grants nothing. Terminal children are never auto-accepted. Disabling either policy seals new automatic replies and drains already admitted replies before returning. Neither mode creates persistent broad provider grants.
Development
Install dependencies and run the project checks:
pnpm install
pnpm run build
pnpm run check:test
pnpm test
Run the CLI from source:
node cli.js help
node cli.js foreground --port 4097
The default test suite uses disposable embedded databases. Set AGL_TEST_MONGO_URL to include the external MongoDB integration test.
The rootless Docker environment backend is under integration and is not yet available in the controller. ControllerContainerEnvironment requires a durable SmartData-backed setup-readiness provider keyed by the immutable container ID: a failed or interrupted setup blocks the next start until an explicit rebuild. The backend accepts caller-owned environment identity and mounts, so account isolation can be decided by the controller without changing its lifeline or terminal execution. It uses only the rootless user daemon, disables the optional Docker image store, and opens a ready event monitor before each physical start. Monitor loss stops the current run; event timestamps and run leases fence late events and terminal cleanup from earlier runs of the same container. Rootless Docker can omit an OOM event and report OOMKilled: false for exit 137. Without positive OOM evidence, the cause is uncertain even if an earlier Docker signal was handled: the environment remains failed and requires explicit recovery. An explicit start that discovers a dead run reports that loss before restarting. Its credential-free live test uses an isolated pinned Ubuntu container:
AGL_CONTAINER_LIVE_TEST=1 pnpm exec tstest test/test.containerenvironmentlive.node.ts --verbose --logfile --timeout 120
Measure browser input feedback with a local Chrome installation:
AGL_BROWSER_LATENCY=1 pnpm exec tstest test/test.browserlatency.node.ts --verbose --logfile --timeout 180
The benchmark sends DOM key and wheel events through the shipped renderer, AGL browser protocol client, real TypedSocket/WebSocket transport, controller admission and dispatch, BrowserRuntime, and Chrome. The inspected page paints a binary counter into the returned native WebRTC video. The viewer recognizes the changed counter and reports input-to-expectedDisplayTime, local dispatch wait, operation reply duration, and callback lateness. A 40-event wheel burst checks the final cumulative scroll delta and reports feedback for counters actually presented. Pixel reads are confined to this benchmark; normal viewing retains native video presentation. Authority records use a test fixture, and the server binds only to loopback. These measurements estimate compositor presentation on the local host, not physical input-to-photon latency or remote-network performance.
End-to-end tests
pnpm test:e2e builds the package and runs test/e2e/e2e.*.node.ts against it the way an operator runs AGL: the output of pnpm pack is unpacked into a private temporary root and started with cli.js start as a detached controller that supervises a real OpenCode server, a real Flex child and a real codex app-server. Every home the controller and its children touch (AGL_HOME, HOME, the XDG directories, CODEX_HOME and the account service's sockets) lives under that root, $TMPDIR/agle2e-*, on free loopback ports. Three things reach outside it: the engine socket directory is /run/user/<uid>/agl-<hash of the fixture AGL_HOME>, because a development checkout keeps it in the platform user runtime directory whatever XDG_RUNTIME_DIR says (it is removed with the run); pnpm pack and git init of the staged package run with the real HOME and read the user's pnpm and Git configuration; and Chrome runs with the runner's environment. A local fake model server answers Codex and OpenCode through their own provider configuration and Flex through an in-test HTTPS proxy with a certificate made for the run; a real authswitch account service holds one fixture account with a made-up token. The proxy is the only way out for the controller and its children: it refuses every other outbound connection and records which process asked, and the suite fails on any but one. That one is OpenCode's own: on start its config loader installs @opencode-ai/plugin into each of its config directories from registry.npmjs.org in the background, which no OpenCode 1.18 setting turns off; refused, OpenCode logs it and carries on. Codex's curated-plugin sync from GitHub is turned off in the fixture's config.toml ([features] plugins = false). Chrome does not use the proxy, so the refusal does not cover it; it signs in to the controller on loopback with agl temp-password.
Every process of a run carries the run's token (TEST_AGL_RUN_TOKEN), Chrome included, and the root holds a marker naming the token and the test process. On SIGTERM, SIGINT or SIGHUP the suite sends those processes SIGKILL, removes the run's root and socket directory, and exits, within the 5 seconds tstest waits after a file timeout's SIGTERM. A run killed outright, or one whose cleanup did not finish in time, is cleaned up by the next run's start: it ends the processes carrying the token of each agle2e-* root whose marker names a test process that is gone, then removes that root and its socket directory, and touches nothing else.
A run that fails keeps its evidence before its root goes: when a test of the file has failed by its cleanup, when the stack does not start, or when a signal ends the run, the suite copies the controller's log (agl/logs), OpenCode's (data/opencode/log), Codex's log database (codex/logs_*.sqlite) and conversation rollouts (codex/sessions), and the Flex state a restart reads -- the flex_* collections of the controller's database and the Git reversion state (agl/git-reversion) -- 4 MiB at most of each (a log keeps its tail, a larger database is left out) and 24 MiB in all, into .nogit/e2e-failures/<stack name>-<run token>/ of the checkout that runs it (the stack is named after its file, for example conversations), with fixture.json: the run's ports and package version, every request the fake model server answered and every connection the proxy refused. It prints that path. Nothing else of the root is copied, so the account service's data, the run's certificate authority, AGL's credentials and the controller's other collections stay out; the ten newest failed runs are kept.
The suite guards, end to end: opening every harness's conversation across page reloads without a harness child being replaced, recovering the Flex child after SIGKILL within 15 seconds, rejoining the shared Codex daemon on its own after that daemon restarts (the conversation opens and answers again within one rejoin backoff step, without Reconnect), starting the controller again (as the same and as the next build) with a browser resource attached to a Codex, Flex and OpenCode chat, listing a project's resources right after a browser is attached to each new chat and after a restart, the Flex model list carrying the account's current catalog, a Flex conversation answering, and opening and answering again after a restart, in a project whose id begins with - or _ (the file creates projects until one does), and an agl mcp bridge answering tool calls from the same process after the controller moves to the next build. It is the test step of gitzone release (release.preflight in .smartconfig.json) and takes about eight minutes.
Host prerequisites: Linux, Chrome (google-chrome, chromium or chromium-browser), openssl, ss (iproute2, to name the process behind a refused connection) and codex 0.156.0 or newer on PATH. Set AGL_E2E_PACKAGE_ROOT to another installed and built checkout, for example an earlier release, to run a scenario against it and prove it fails there:
AGL_E2E_PACKAGE_ROOT=/path/to/agl-32.15.1 pnpm exec tstest test/e2e/e2e.conversations.node.ts --verbose --timeout 900
License and Legal Information
This repository's own source code is licensed under the MIT License. A copy of the license can be found in license.md.
The distributed browser bundle includes third-party packages, among them PDF.js 4.10.38 under the Apache License, Version 2.0. Every build writes the license and notice texts of each bundled package to dist_serve/bundle.js.third-party-notices.txt and .json, which ship with the package and are served beside the bundle. third-party-notices.md covers the assets served unbundled, the PDF.js modification notice and the upstream notices the npm packages do not ship.
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.