agl 21.0.2 → 22.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/changelog.md +9 -0
  2. package/dist_serve/bundle.js +1 -1
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.aglhome.d.ts +25 -0
  5. package/dist_ts/classes.aglhome.js +65 -0
  6. package/dist_ts/classes.authmodels.js +7 -12
  7. package/dist_ts/classes.authstore.js +2 -29
  8. package/dist_ts/classes.cli.js +93 -35
  9. package/dist_ts/classes.config.d.ts +2 -11
  10. package/dist_ts/classes.config.js +14 -80
  11. package/dist_ts/classes.controller.d.ts +4 -1
  12. package/dist_ts/classes.controller.js +64 -26
  13. package/dist_ts/classes.embeddeddb.js +6 -5
  14. package/dist_ts/classes.gitreversion.js +3 -2
  15. package/dist_ts/classes.upgradecoordinator.d.ts +6 -1
  16. package/dist_ts/classes.upgradecoordinator.js +588 -177
  17. package/dist_ts/classes.upgradetransaction.js +46 -12
  18. package/dist_ts/classes.uploadmanager.d.ts +12 -0
  19. package/dist_ts/classes.uploadmanager.js +204 -2
  20. package/dist_ts/constants.upgradeenvironment.d.ts +2 -0
  21. package/dist_ts/constants.upgradeenvironment.js +3 -0
  22. package/dist_ts/functions.controllerdataroot.d.ts +4 -5
  23. package/dist_ts/functions.controllerdataroot.js +4 -29
  24. package/dist_ts/functions.embeddeddb.d.ts +1 -1
  25. package/dist_ts/functions.embeddeddb.js +8 -3
  26. package/dist_ts/functions.runtimeenvironment.js +2 -1
  27. package/dist_ts/index.d.ts +1 -0
  28. package/dist_ts/index.js +2 -1
  29. package/dist_ts/interfaces.config.d.ts +5 -7
  30. package/dist_ts_migration/classes.documentmigrationrunner.js +3 -1
  31. package/dist_ts_migration/index.d.ts +2 -0
  32. package/dist_ts_migration/index.js +3 -1
  33. package/dist_ts_migration/v23_aglhome.d.ts +91 -0
  34. package/dist_ts_migration/v23_aglhome.js +1775 -0
  35. package/dist_ts_migration/v23_runtimeconfig.d.ts +11 -0
  36. package/dist_ts_migration/v23_runtimeconfig.js +87 -0
  37. package/dist_ts_migration/v2_controllerdataroot.d.ts +5 -0
  38. package/dist_ts_migration/v2_controllerdataroot.js +176 -15
  39. package/package.json +1 -1
  40. package/readme.md +116 -35
  41. package/readme.plan.md +24 -9
  42. package/ts/00_commitinfo_data.ts +1 -1
  43. package/ts/classes.aglhome.ts +116 -0
  44. package/ts/classes.authmodels.ts +5 -13
  45. package/ts/classes.authstore.ts +1 -39
  46. package/ts/classes.cli.ts +92 -34
  47. package/ts/classes.config.ts +20 -117
  48. package/ts/classes.controller.ts +92 -26
  49. package/ts/classes.embeddeddb.ts +5 -4
  50. package/ts/classes.gitreversion.ts +3 -2
  51. package/ts/classes.upgradecoordinator.ts +668 -191
  52. package/ts/classes.upgradetransaction.ts +46 -11
  53. package/ts/classes.uploadmanager.ts +231 -1
  54. package/ts/constants.upgradeenvironment.ts +2 -0
  55. package/ts/functions.controllerdataroot.ts +11 -47
  56. package/ts/functions.embeddeddb.ts +13 -2
  57. package/ts/functions.runtimeenvironment.ts +1 -0
  58. package/ts/index.ts +1 -0
  59. package/ts/interfaces.config.ts +5 -7
  60. package/ts_migration/classes.documentmigrationrunner.ts +2 -0
  61. package/ts_migration/index.ts +2 -0
  62. package/ts_migration/v23_aglhome.ts +2101 -0
  63. package/ts_migration/v23_runtimeconfig.ts +112 -0
  64. package/ts_migration/v2_controllerdataroot.ts +174 -16
  65. package/ts_web/00_commitinfo_data.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agl",
3
- "version": "21.0.2",
3
+ "version": "22.0.0",
4
4
  "controllerUpgradeManagementVersion": 2,
5
5
  "private": false,
6
6
  "description": "Agent Gateway Layer for OpenCode and FlexHarness sessions",
package/readme.md CHANGED
@@ -40,7 +40,7 @@ OpenCode and Flex are independently supervised. An unexpected Flex child exit cl
40
40
 
41
41
  FlexHarness private state is persisted in separate SmartData domains for scope metadata, public message projections, remembered permissions, canonical Agent events and archives, and background tool jobs. A fenced per-project writer lease excludes overlapping Flex children, while the parent reads a separately committed bounded public projection. Lease heartbeats and writes share one serialized transition boundary; an expired lease still held by the same controller recovers with a fresh owner token and incremented epoch before work continues. Hidden terminal staging and private canonical events never enter that browser-readable projection; a visible Flex save is acknowledged only after the corresponding bounded view is durable.
42
42
 
43
- Flex runtime implementation classes and child internals are private. Supported package surfaces include the `agl` CLI, protocol-v22 controller DTO and request contracts, and the root-exported controller-owned Git reversion types and engine.
43
+ Flex runtime implementation classes and child internals are private. Supported package surfaces include the `agl` CLI, protocol-v22 controller DTO and request contracts, the root-exported AGL home path helpers, and the root-exported controller-owned Git reversion types and engine.
44
44
 
45
45
  ## Requirements
46
46
 
@@ -48,6 +48,7 @@ Flex runtime implementation classes and child internals are private. Supported p
48
48
  - Git 2.43 or newer for workspace reversion and controller-owned worktrees
49
49
  - A WebAuthn-capable browser
50
50
  - Linux or macOS for detached `start` and CLI-managed `stop`
51
+ - A POSIX user identity for data-using startup, migration, and embedded-database commands
51
52
  - Native `node-pty` support. When no prebuilt binary is available, installation requires Python 3, `make`, and a C/C++ build toolchain.
52
53
 
53
54
  No database server is required: the controller ships an embedded engine
@@ -56,7 +57,7 @@ be configured instead.
56
57
 
57
58
  The package pins `@opencode-ai/sdk` and the official OS/architecture-specific OpenCode binary packages to the same tested version. It resolves the lockfile-installed platform executable directly, without a wrapper postinstall or unrelated global binary. This release is tested against OpenCode `1.18.15`.
58
59
 
59
- The OpenCode child, Flex child, and controller-owned terminal shells receive only ordinary runtime path, user, locale, temporary-directory, XDG, proxy, and CA environment variables by default. Controller/database variables and unrelated parent secrets are not inherited. Explicitly delegate additional environment variables when needed:
60
+ The OpenCode child, Flex child, and controller-owned terminal shells receive only ordinary runtime path, user, locale, temporary-directory, XDG, proxy, and CA environment variables by default. `AGL_HOME` is the deliberate controller-owned path exception; controller/database variables and unrelated parent secrets are not inherited. Explicitly delegate additional environment variables when needed:
60
61
 
61
62
  ```sh
62
63
  export HARNESS_CONTROLLER_FORWARD_ENV='ANTHROPIC_API_KEY,OPENAI_API_KEY'
@@ -120,9 +121,11 @@ agl help
120
121
 
121
122
  Use the controller's configured port instead of `4097` for both invocations.
122
123
  Do not skip the bridge or manually replace the package. This package transition
123
- does not move controller data again: the current installed data root remains
124
- `$XDG_CONFIG_HOME/hcon` (default `~/.config/hcon`), and existing internal and
125
- persisted identities remain unchanged.
124
+ itself leaves controller data under `$XDG_CONFIG_HOME/hcon`. The first data-using
125
+ command (`start`, `foreground`, `upgrade`, or `temp-password`) from an AGL
126
+ version with `AGL_HOME` support subsequently moves that retained state into the
127
+ canonical AGL home described below without changing persisted
128
+ controller, authentication, project, session, Flex, or Git identities.
126
129
 
127
130
  ### Upgrade behavior
128
131
 
@@ -165,6 +168,12 @@ stopped-controller upgrade has no inherited cohort and leaves the target
165
168
  stopped. The worker also refuses an upgrade when another pnpm-global controller
166
169
  instance exists outside the exact authorized process.
167
170
 
171
+ New coordination transactions live under `$AGL_HOME/upgrade`. A target invoked
172
+ by an already-released 21.0.2 worker recognizes that token-bound worker's legacy
173
+ `/tmp/harness-controller-upgrade-*` directory only until the inherited
174
+ transaction reaches its terminal cleanup; unrelated or unbound legacy paths are
175
+ never used as fallback coordination.
176
+
168
177
  `hcon upgrade` must be run from the active pnpm-global installation and is currently supported on Linux and macOS. Before starting the AGL transition, if a prior manual same-package upgrade replaced the management CLI while an older controller remained running, restore that exact older package first so its process identity can be stopped safely:
169
178
 
170
179
  ```sh
@@ -176,33 +185,86 @@ hcon start --port 4097
176
185
 
177
186
  This repair sequence does not replace the two bridge-migration invocations.
178
187
 
188
+ ## AGL home
189
+
190
+ AGL-owned persistent state has one private root selected by `AGL_HOME`. The
191
+ in-progress home migration also uses a transient sibling lock beside that root;
192
+ it is not normal persisted state. Installed packages default to
193
+ `$XDG_CONFIG_HOME/agl` (normally `~/.config/agl`); repository checkouts default to
194
+ `<checkout>/.nogit/agl`. `AGL_HOME`, when supplied, must be an absolute normalized
195
+ non-root path whose direct parent already exists; this parent requirement also
196
+ applies to the selected default root. AGL owns
197
+ these namespaces:
198
+
199
+ - `database` for the embedded SmartDB storage root;
200
+ - `credentials` for sealed Flex provider credentials;
201
+ - `git-reversion` for captures and controller-owned worktrees;
202
+ - `logs` for controller and upgrade logs;
203
+ - `upgrade` for durable upgrade coordination;
204
+ - `cache` for AGL-owned caches;
205
+ - `runtime/browser`, `runtime/uploads`, `runtime/sockets`, and
206
+ `runtime/opencode` for private runtime state;
207
+ - `migration` for the current journal and verified retained migration records.
208
+
209
+ Library consumers can resolve the same root-exported path contract and bind it
210
+ into a child environment without deriving paths from the child's CWD:
211
+
212
+ ```ts
213
+ import { bindAGLHomeEnvironment, resolveAGLHomePaths } from 'agl';
214
+
215
+ const paths = resolveAGLHomePaths();
216
+ const childEnvironment = bindAGLHomeEnvironment(process.env, paths);
217
+ ```
218
+
219
+ `resolveAGLHomePaths()` returns `development`, `root`, `database`,
220
+ `credentials`, `gitReversion`, `logs`, `legacyLogs`, `upgrade`, `cache`,
221
+ `runtime`, `browserRuntime`, `uploads`, `sockets`, `openCodeRuntime`, and
222
+ `migration`. `bindAGLHomeEnvironment()` returns a copy of the supplied
223
+ environment with `AGL_HOME` bound to `paths.root`.
224
+
225
+ The package installation itself, user-selected project directories, an explicit
226
+ `HARNESS_CONTROLLER_DB_DIR` (including one that names a recognized legacy
227
+ SmartDB path), an external MongoDB server, and OpenCode's own user-wide
228
+ configuration are not AGL-owned state and remain outside this root.
229
+ `AGL_HOME` is bound into controller, upgrade, Flex, and OpenCode process
230
+ boundaries so a detached process never derives ownership from its launch CWD.
231
+
232
+ The one-time v23 home migration runs before database model access from `start`,
233
+ `foreground`, `upgrade`, or `temp-password`. It first
234
+ normalizes the prior `harness-controller` to `hcon` migration when necessary,
235
+ then uses SmartDB and SmartSecret relocation APIs for the active database and
236
+ sealed credentials. Git state, browser state, logs, and verified legacy records
237
+ move under the same journal. The migration is same-filesystem only, checks
238
+ private ownership, exact source inventories, live writers, sockets, symlinks,
239
+ target collisions, and source identities before mutation, and safely reconciles
240
+ an interruption. Active controller-owned Git worktree state blocks migration
241
+ because Git's linked-worktree metadata contains absolute paths. Unknown or
242
+ ambiguous state fails closed. Verified historical database material is retained
243
+ under `$AGL_HOME/migration`; committed legacy roots are removed only when their
244
+ known entries are empty. Never move these directories or edit their journals by
245
+ hand.
246
+
247
+ An explicit non-default `AGL_HOME` in a development checkout starts as an
248
+ isolated fresh home and does not consume that checkout's shared legacy
249
+ `.nogit` state.
250
+
179
251
  ## Database configuration
180
252
 
181
253
  Structured application state is stored exclusively through SmartData. There is no PID, configuration, authentication, or JSON persistence fallback on the filesystem.
182
254
 
183
255
  By default the controller runs an embedded `@push.rocks/smartdb` engine — no
184
256
  external database server. Storage lives under
185
- `$XDG_CONFIG_HOME/hcon/smartdb` (default `~/.config/hcon/smartdb`). A repository checkout instead uses
186
- `./.nogit/smartdb`, keeping development data inside the checkout. Override the
187
- directory with `HARNESS_CONTROLLER_DB_DIR` (absolute path). On the first
188
- installed command that needs controller data after upgrading, the controller
189
- completes any prior XDG-state database import under the retained
190
- `$XDG_CONFIG_HOME/harness-controller` root. When that config-root database
191
- exists, SmartDB's stopped-root relocation moves its exact inode to the `hcon`
192
- root and replaces the old `smartdb` pathname with a verified receipt, not a
193
- database backup. The controller verifies that receipt, storage identity, and
194
- logical content digest. This relocation is supported on SmartDB-qualified Linux
195
- filesystems and writable local APFS on macOS. When only the historical XDG-state
196
- database exists, it is instead imported directly into the new root with the same
197
- logical-digest verification while retaining that historical source as the
198
- non-authoritative backup. The outer migration is journaled and safely retries
199
- its exact operation after an interruption. Retained legacy sources are never
200
- imported again after completion; an older controller binary can still open and
201
- diverge a retained historical backup, so do not resume using it. Keep the old
202
- controller stopped throughout the one-time migration. Explicit database paths
203
- and external MongoDB configurations are never moved. The engine listens on a unix socket, and a second controller
257
+ `$AGL_HOME/database`. Override the directory with
258
+ `HARNESS_CONTROLLER_DB_DIR` (absolute path). Explicit database paths and
259
+ external MongoDB configurations are never moved. The engine listens on a
260
+ deterministic Unix socket under `$AGL_HOME/runtime/sockets`, and a second controller
204
261
  process on the same machine (for example `agl temp-password`) attaches to the
205
- running daemon's engine instead of opening the storage files again.
262
+ running daemon's engine instead of opening the storage files again. The complete
263
+ embedded Unix socket path must be no longer than 100 UTF-8 bytes; a second
264
+ controller must use the same `AGL_HOME` and database directory to attach.
265
+ During the one-time home migration only, AGL also probes the published legacy
266
+ `/tmp/harness-controller-<16-hex>.sock` name to prove that the old embedded
267
+ engine is stopped. New runtime sockets are never created at that legacy path.
206
268
 
207
269
  When upgrading from a controller release backed by FlexHarness 2, the new Flex child migrates each active project's legacy snapshot during initialization, before admitting runtime work. Migration preflights every destination, writes per-session domains before publishing scope discovery, repairs crash-interrupted streaming messages as cancelled, and safely reruns after partial completion. A durable completion marker prevents the retained source from being reapplied after live FlexHarness 3 state evolves. The legacy private snapshot remains retained as a non-authoritative recovery source until that project is removed; startup also repairs any bounded public projection whose prior save was not acknowledged.
208
270
 
@@ -226,12 +288,18 @@ Use the `MONGODB_URL` from `.nogit/env.json` as `HARNESS_CONTROLLER_MONGO_URL`.
226
288
 
227
289
  ## First start
228
290
 
229
- From the workspace OpenCode should control:
291
+ Start AGL without creating an implicit project:
230
292
 
231
293
  ```sh
232
294
  agl start
233
295
  ```
234
296
 
297
+ To register one existing directory during startup, pass it explicitly:
298
+
299
+ ```sh
300
+ agl start --directory /absolute/path/to/project
301
+ ```
302
+
235
303
  The detached process first reserves the controller listener (`127.0.0.1:4097` in local mode; `0.0.0.0:4097` only with `--behind-tls-proxy`), spawns the isolated Flex child, starts the official OpenCode server in headless server mode on `127.0.0.1:4098`, and validates local PTY support. On an unsupported runtime the spawned Flex child reports `unsupported`; Flex startup is isolated and does not block OpenCode readiness. Before the first enrollment only, the terminal prints a one-time setup code — a random 32-byte value by default, or the operator's own value when `--setup-code` is passed.
236
304
 
237
305
  Open the printed URL, enter the setup code, and create a passkey. The code expires after 30 minutes and is atomically consumed by the first successful credential enrollment. If an unenrolled code expires, stop and start the controller to rotate it.
@@ -255,27 +323,28 @@ agl help
255
323
  agl --version
256
324
  ```
257
325
 
258
- First-start options:
326
+ Initial runtime options:
259
327
 
260
328
  ```text
261
329
  --port <port> controller port, default 4097
262
330
  --opencode-port <port> loopback OpenCode port, default 4098
263
- --directory <path> fixed OpenCode workspace, default current directory
331
+ --directory <path> register this existing directory as an initial project;
332
+ omitted means no implicit project
264
333
  --projects-root <path> default base directory: relative project paths and
265
334
  initial suggestions resolve here; explicitly typed
266
- absolute projects may be outside it; adopted once on
267
- first v2 start, immutable afterward
335
+ absolute projects may be outside it; fresh installs
336
+ default to the user home and it is immutable afterward
268
337
  --public-origin <origin> exact browser WebAuthn origin
269
338
  --rp-id <hostname> WebAuthn RP ID
270
339
  --behind-tls-proxy declare trusted TLS termination upstream
271
340
  --setup-code <code> operator-chosen setup code (see the warning above)
272
341
  ```
273
342
 
274
- Runtime configuration is durable and immutable per controller port. Later management commands must use the same `--port`; omitted first-start options are loaded from SmartData.
343
+ Runtime configuration is durable and immutable per controller port. Later management commands must use the same `--port`; omitted initial runtime options are loaded from SmartData. `--directory` is different: it is a transient request to register one existing project and may be supplied on a later start without changing the durable runtime configuration.
275
344
 
276
345
  `upgrade` keeps the CLI attached to its detached, crash-survivable worker and streams progress until the durable transaction succeeds or fails. Before stopping a running controller it closes new harness-prompt admission, durably records the exact cohort and absolute preparation deadline, then acknowledges the bounded request while controller-owned work waits for active operations and asks those sessions to pause at the earliest convenience. The worker polls that private transaction with bounded source-process checks; it cannot stop the controller or enter package installation until preparation completion is durably committed on time. The global upgrade lock also drains controller-start and `temp-password` database-writer leases, then package replacement requires the source port to be closed and no exact source controller, `temp-password`, or Flex child writer to remain. A failed or expired preparation compensates settled pauses and leaves uncertain cleanup fail-closed. The retained cohort is limited to 64 exact sessions. `--grace-period-seconds` accepts 1–3600 seconds and defaults to 300. Without `--continue-sessions`, the upgraded controller reopens prompt admission without sending another message. With it, the matching continuation message is submitted at most once to the durably retained paused cohort after the upgraded controller returns. Rollback recovery attempts to compensate paused sessions within a separate maximum 60-second finalization window and does not overlap an unresolved pause with a continuation. `--json` replaces progress lines with one terminal JSON document after worker admission; preflight and launch failures still use normal CLI errors. That document includes worker identity and log path, whether the controller was running, source version, optional target version, terminal phase/success/error, and pause/continuation state counts; failures exit nonzero after writing it.
277
346
 
278
- The detached CLI does not write a PID file. It appends the controller's stderr diagnostics to `$XDG_CONFIG_HOME/harness-controller/controller-<port>.log` (default `~/.config/harness-controller/`, or `./.nogit/` in a repository checkout; rotated to `.old` past 1 MiB on start; stdout stays discarded because the transport logs every connection there). The log file must be a regular file owned by the current user without group or world access — the start fails loudly rather than logging somewhere surprising. The initial setup code travels once over parent-child IPC. `status` connects only to `127.0.0.1` and validates package and protocol identity. `stop` additionally validates the exact CLI path (relative launches are resolved against the process's working directory), command, effective port (an argv without `--port` verifies as the default port), process group, and process-start fingerprint. It sends cooperative `SIGTERM` only to that exact PID so the controller can stop harnesses and database resources in order. If the process remains after 30 seconds, a foreground controller receives exact-PID `SIGKILL`; a detached controller must still be the reverified process-group leader before its complete group receives `SIGKILL`. It never kills processes by name.
347
+ The detached CLI does not write a PID file. It appends the controller's stderr diagnostics to `$AGL_HOME/logs/controller-<port>.log` (rotated to `.old` past 1 MiB on start; stdout stays discarded because the transport logs every connection there). The log file must be a regular file owned by the current user without group or world access — the start fails loudly rather than logging somewhere surprising. The initial setup code travels once over parent-child IPC. `status` connects only to `127.0.0.1` and validates package and protocol identity. `stop` additionally validates the exact CLI path (relative launches are resolved against the process's working directory), command, effective port (an argv without `--port` verifies as the default port), process group, and process-start fingerprint. It sends cooperative `SIGTERM` only to that exact PID so the controller can stop harnesses and database resources in order. If the process remains after 30 seconds, a foreground controller receives exact-PID `SIGKILL`; a detached controller must still be the reverified process-group leader before its complete group receives `SIGKILL`. It never kills processes by name.
279
348
 
280
349
  `foreground` is available for attached operation and diagnostics. Detached management is intentionally limited to Linux and macOS in this release.
281
350
 
@@ -287,7 +356,19 @@ The command talks to the controller database directly, so it requires the same d
287
356
 
288
357
  ## Projects and default models
289
358
 
290
- Chats are organized into projects. A project is any registered **existing** directory on disk: the browser's add-project field accepts full absolute paths (`/any/path/on/disk`) as well as paths relative to the configured `--projects-root` default base (`org/repo`), and suggests matching folders live while typing either form — absolute queries browse the actual filesystem. Hidden directories are never suggested (though an explicitly typed absolute path may contain dot segments), dot navigation is rejected, and registration fails visibly when the path does not exist — the controller never creates project directories. Projects inside the default base get short relative display names; projects elsewhere are named by their absolute path. Every session method is scoped to one project. One supervised OpenCode server serves all projects through per-call directory scoping, while the Flex child resolves each session to its registered project root before providing filesystem or shell tools. On the first v2 start the fixed workspace directory is registered as the initial project automatically. Removing a project first blocks new resource operations and requires every durable terminal and browser resource to be retired. The controller then persists removal intent before destructive work, hides the project from normal admission, drains Session Intelligence and harness operations, clears controller-owned state, asks Flex to remove that exact project namespace, and removes controller-owned Git worktrees, worktree records, and reversion capture roots. Interrupted removals resume on startup and remain pending when required runtime cleanup cannot be confirmed. Flex storage is never deleted merely because a project is absent from an active-project snapshot. Files in the registered project working tree are never deleted; path cleanup is limited to controller-owned worktrees, while controller-private capture stores and worktree records are removed only from controller-owned storage. Note the trust consequence: an authenticated browser session can register and operate on any directory the controller's user can read.
359
+ Startup preserves the exact project registry: an empty controller remains
360
+ empty, and only an explicit `--directory` registers an initial project. The
361
+ supervised OpenCode server runs from
362
+ `$AGL_HOME/runtime/opencode/<controller-hash>`, never from a project or the
363
+ invoking shell's CWD for its implicit runtime state. Relative `--directory` and
364
+ `--projects-root` values are resolved against the invoking shell before the
365
+ detached process starts. A stored `projectsRoot` is validated lexically during
366
+ startup and resolved only when a relative project operation needs it, so a
367
+ temporarily unavailable root does not stop the controller. This v23 persisted
368
+ document/home migration revision supersedes the historical v2 automatic-workspace
369
+ registration; the wire protocol remains v22.
370
+
371
+ Chats are organized into projects. A project is any registered **existing** directory on disk: the browser's add-project field accepts full absolute paths (`/any/path/on/disk`) as well as paths relative to the configured `--projects-root` default base (`org/repo`), and suggests matching folders live while typing either form — absolute queries browse the actual filesystem. Hidden directories are never suggested (though an explicitly typed absolute path may contain dot segments), dot navigation is rejected, and registration fails visibly when the path does not exist — the controller never creates project directories. Projects inside the default base get short relative display names; projects elsewhere are named by their absolute path. Every session method is scoped to one project. One supervised OpenCode server serves all projects through per-call directory scoping, while the Flex child resolves each session to its registered project root before providing filesystem or shell tools. Startup never derives or registers a project from the process CWD. Removing a project first blocks new resource operations and requires every durable terminal and browser resource to be retired. The controller then persists removal intent before destructive work, hides the project from normal admission, drains Session Intelligence and harness operations, clears controller-owned state, asks Flex to remove that exact project namespace, and removes controller-owned Git worktrees, worktree records, and reversion capture roots. Interrupted removals resume on startup and remain pending when required runtime cleanup cannot be confirmed. Flex storage is never deleted merely because a project is absent from an active-project snapshot. Files in the registered project working tree are never deleted; path cleanup is limited to controller-owned worktrees, while controller-private capture stores and worktree records are removed only from controller-owned storage. Note the trust consequence: an authenticated browser session can register and operate on any directory the controller's user can read.
291
372
 
292
373
  Registration persists the complete ordered device/inode ancestry from the filesystem root through every project path component, with the target recorded as a directory. Controller requests, Flex tools, Git reversion, background cleanup, and shutdown revalidate the entire chain before using the path, so preserving the project leaf inode beneath a substituted ancestor does not authorize the replacement. The first upgrade to this format binds each currently available truly legacy project exactly once under its owning controller. A legacy project that is unavailable during that migration remains terminally unbound, and an incomplete pre-release `bound` record becomes terminally unbound instead of acquiring missing ancestors. Such projects stay visible for operator diagnosis but are excluded from runtime admission and path-sensitive cleanup. Their removal retires controller-owned metadata while intentionally retaining retired resource tombstones and path-scoped OpenCode, Flex, and Git artifacts. A pending bound removal remains durable until its original complete binding is available, rather than completing against an untrusted replacement.
293
374
 
@@ -301,7 +382,7 @@ Flex `/undo` and `/redo` move transcript and workspace history together. Startin
301
382
 
302
383
  Each capture owns one controller-private `captures/<captureKey>` root. Every participating repository gets a private bare Git metadata store and disposable object generations; the bound source object database is available only as a read-only alternate. Capture, finalize, inspection, and release therefore do not add controller refs, metadata blobs, trees, or generated objects to the source repository. Apply is the sole capture-traversal operation that mutates captured worktree, index, or ref state: before mutation it creates an operation-unique, self-contained closure pack in private storage, imports that exact bounded pack without a reverse index or controller ref, and retains it with an operation-owned `.keep` file through roll-forward or compensation. The explicit worktree create/remove methods separately update the source repository's linked-worktree metadata. Private publication and capture-root lifecycle are journaled so startup can recover interrupted publication, apply, creation, and deletion without trusting files inside the tree being removed.
303
384
 
304
- Library consumers can construct the root-exported `ControllerGitReversion` with `IGitReversionOptions`, register each project with its canonical `IControllerProjectFilesystemIdentity`, and call `init()` or `recover()` before source-sensitive work. Capture flow uses `IGitReversionCaptureRequest` with `prepare()`, `inspectCapture()`, and `finalize()`; undo and redo use `IGitReversionApplyRequest` with `inspectApply()` and `apply()`. `IGitReversionMetadataRequest` drives `listAffected()` and `getReversionMetadata()`, while `IGitReversionWorktreeRequest` and `IGitReversionWorktreeRemoveRequest` drive the detached-session worktree methods. Only when no further undo, redo, inspection, or recovery is needed should terminal cleanup call `release()` with `IGitReversionReleaseRequest`, which permanently deletes the capture root. Call `close()` to seal admission and drain or fail closed on owned work.
385
+ Library consumers can construct the root-exported `ControllerGitReversion` with `IGitReversionOptions`, register each project with its canonical `IControllerProjectFilesystemIdentity`, and call `init()` or `recover()` before source-sensitive work. Its default private root is under `$AGL_HOME/git-reversion`; `temporaryRoot` can still supply an explicit absolute owner root. Capture flow uses `IGitReversionCaptureRequest` with `prepare()`, `inspectCapture()`, and `finalize()`; undo and redo use `IGitReversionApplyRequest` with `inspectApply()` and `apply()`. `IGitReversionMetadataRequest` drives `listAffected()` and `getReversionMetadata()`, while `IGitReversionWorktreeRequest` and `IGitReversionWorktreeRemoveRequest` drive the detached-session worktree methods. Only when no further undo, redo, inspection, or recovery is needed should terminal cleanup call `release()` with `IGitReversionReleaseRequest`, which permanently deletes the capture root. Call `close()` to seal admission and drain or fail closed on owned work.
305
386
 
306
387
  Flex also exposes `/worktree create`, `/worktree list`, and `/worktree remove <worktreeId>`. These commands explicitly create and manage detached, locked, controller-owned worktrees for the current conversation; worktrees are never created automatically. Creation durably binds the private parent before `git worktree add`, then binds the child before activation. Recovery, cleanup, and removal revalidate the required parent and child bindings; a replacement is fenced and retained. Listing validates the registered project before reading bounded private records, but does not dereference or revalidate every listed worktree path. The run-scoped `git_worktree` tool provides the same create/list/remove operations to the agent, with permission required for create and remove. Removal refuses any worktree containing tracked, untracked, or ignored changes.
307
388
 
@@ -442,7 +523,7 @@ Audit records may contain bounded peer, credential, session, or permission-reque
442
523
 
443
524
  Flex private snapshots and public projections are published separately. The controller reads only integrity-checked public records. For each project, every public candidate is bounded to 2,048 sessions, 2,048 messages, 64 MiB of message data, and 480 KiB per message; up to four public candidates are retained per project. Pages contain at most 50 messages or 512 KiB. Unreferenced staged candidates are age-gated and reconciled through paged, batched maintenance. Provider credentials never enter these records.
444
525
 
445
- Running terminal processes, output scrollback, peer cursors, grid state, active human browser views, browser-frame streams, BrowserRuntime capabilities and leases, Flex browser channels, and composer drafts are memory-only. Durable terminal resource identifiers may appear as bounded request identifiers in lifecycle audit records, but session layouts contain only OpenCode and Flex session IDs. Terminal input and output are never persisted. Draft text and attachment bytes are never written to SmartData or audit records. Attachment files exist only in private controller-owned temporary directories for the lifetime of their exact harness operation and are removed on operation cleanup or process shutdown.
526
+ Running terminal processes, output scrollback, peer cursors, grid state, active human browser views, browser-frame streams, BrowserRuntime capabilities and leases, Flex browser channels, and composer drafts are memory-only. Durable terminal resource identifiers may appear as bounded request identifiers in lifecycle audit records, but session layouts contain only OpenCode and Flex session IDs. Terminal input and output are never persisted. Draft text and attachment bytes are never written to SmartData or audit records. Attachment files exist only in private operation directories under `$AGL_HOME/runtime/uploads` for the lifetime of their exact harness operation and are removed on operation cleanup or process shutdown.
446
527
 
447
528
  ## Development
448
529
 
package/readme.plan.md CHANGED
@@ -36,14 +36,27 @@ targets.
36
36
  credential records, SmartSecret service/store/account identities, harness
37
37
  IDs, audit vocabulary, browser storage keys, and persisted protocol fields.
38
38
  - Preserve existing `HARNESS_CONTROLLER_*` environment variables, private IPC
39
- identities, log locations, and management coordination paths.
40
- - Preserve user projects, workspaces, controller runtime configuration, session
41
- state, Flex credentials, Git captures, and controller-owned worktrees.
42
- - Keep the current installed data root at `$XDG_CONFIG_HOME/hcon` (default
43
- `~/.config/hcon`). Its migration is already committed; neither upgrade phase
44
- performs another data-root move or imports the old root again.
45
- - Development checkouts continue to use `.nogit`. Package and CLI branding may
46
- change only at the public target boundary, not inside persisted identities.
39
+ identities, and persisted controller, project, session, Flex, Git, and
40
+ WebAuthn identities. Physical AGL-owned paths may move only through a
41
+ versioned migration under `ts_migration/`.
42
+ - Post-transition AGL versions use one canonical `AGL_HOME`: installed packages
43
+ default to `$XDG_CONFIG_HOME/agl`, and development checkouts to `.nogit/agl`.
44
+ The v23 migration moves the retained `hcon`, `harness-controller`, and
45
+ historical state assets into that root without reimporting committed data.
46
+ - Persisted `workspaceDirectory` is removed by the v23 document migration.
47
+ Existing project records remain authoritative; an empty registry remains
48
+ empty, and `--directory` is a transient project-registration request that can
49
+ also be supplied on a later start.
50
+ A missing persisted `projectsRoot` adopts the legacy workspace when present;
51
+ otherwise fresh configuration defaults it to the user home.
52
+ - New logs and management coordination live under `AGL_HOME`; the in-progress
53
+ home-migration lock is a transient sibling beside that root. The legacy `/tmp`
54
+ coordination path remains recognized only for an exact token-bound worker
55
+ inherited from the already-published 21.0.2 implementation.
56
+ - An explicit `HARNESS_CONTROLLER_DB_DIR` remains outside the migration root and
57
+ is never relocated, even when it names the recognized legacy SmartDB path.
58
+ - The v23 document/home revision is persisted-state migration metadata; the wire
59
+ protocol remains v22.
47
60
 
48
61
  ## Coordination Compatibility
49
62
 
@@ -185,4 +198,6 @@ for recovery.
185
198
  no-traversal recovery, bounded discovery barriers, and crash-safe replay.
186
199
  - Final E2E from actual published 20.0.2 to bridge 20.0.3 to target 21.0.0,
187
200
  confirming data, credentials, sessions, Git state, command ownership, and the
188
- unchanged `~/.config/hcon` root.
201
+ preserved legacy `~/.config/hcon` root before the first v23 startup; then
202
+ confirm that v23 moves that state into canonical `AGL_HOME` without identity
203
+ changes.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: 'agl',
6
- version: '21.0.2',
6
+ version: '22.0.0',
7
7
  description: 'Agent Gateway Layer for OpenCode and FlexHarness sessions'
8
8
  }
@@ -0,0 +1,116 @@
1
+ import * as plugins from './plugins.js';
2
+
3
+ export const aglHomeEnvironmentVariable = 'AGL_HOME';
4
+
5
+ export interface IAGLHomeOptions {
6
+ packageRoot?: string;
7
+ homeDirectory?: string;
8
+ environment?: NodeJS.ProcessEnv;
9
+ }
10
+
11
+ export interface IAGLHomePaths {
12
+ development: boolean;
13
+ root: string;
14
+ database: string;
15
+ credentials: string;
16
+ gitReversion: string;
17
+ logs: string;
18
+ legacyLogs: string;
19
+ upgrade: string;
20
+ cache: string;
21
+ runtime: string;
22
+ browserRuntime: string;
23
+ uploads: string;
24
+ sockets: string;
25
+ openCodeRuntime: string;
26
+ migration: string;
27
+ }
28
+
29
+ const modulePackageRoot = plugins.path.resolve(
30
+ plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url)),
31
+ '..',
32
+ );
33
+
34
+ const isDevelopmentCheckout = (packageRootArg: string): boolean => {
35
+ try {
36
+ const stats = plugins.fs.lstatSync(plugins.path.join(packageRootArg, '.git'));
37
+ return stats.isDirectory() || stats.isFile();
38
+ } catch {
39
+ return false;
40
+ }
41
+ };
42
+
43
+ const assertAbsoluteNormalizedPath = (pathArg: string, labelArg: string): string => {
44
+ if (
45
+ typeof pathArg !== 'string'
46
+ || !plugins.path.isAbsolute(pathArg)
47
+ || plugins.path.normalize(pathArg) !== pathArg
48
+ || plugins.path.parse(pathArg).root === pathArg
49
+ || pathArg.includes('\0')
50
+ || Buffer.byteLength(pathArg, 'utf8') > 4096
51
+ ) {
52
+ throw new Error(`${labelArg} must be an absolute normalized non-root path.`);
53
+ }
54
+ return pathArg;
55
+ };
56
+
57
+ const defaultConfigHome = (
58
+ environmentArg: NodeJS.ProcessEnv,
59
+ homeDirectoryArg: string,
60
+ ): string => {
61
+ const configured = environmentArg.XDG_CONFIG_HOME?.trim();
62
+ return configured && plugins.path.isAbsolute(configured)
63
+ ? plugins.path.normalize(configured)
64
+ : plugins.path.join(homeDirectoryArg, '.config');
65
+ };
66
+
67
+ export const resolveAGLHomePaths = (
68
+ optionsArg: IAGLHomeOptions = {},
69
+ ): IAGLHomePaths => {
70
+ const environment = optionsArg.environment ?? process.env;
71
+ const packageRoot = assertAbsoluteNormalizedPath(
72
+ plugins.path.normalize(optionsArg.packageRoot ?? modulePackageRoot),
73
+ 'AGL package root',
74
+ );
75
+ const homeDirectory = assertAbsoluteNormalizedPath(
76
+ plugins.path.normalize(optionsArg.homeDirectory ?? plugins.os.homedir()),
77
+ 'User home directory',
78
+ );
79
+ const configuredHome = environment[aglHomeEnvironmentVariable]?.trim();
80
+ const development = isDevelopmentCheckout(packageRoot);
81
+ const root = assertAbsoluteNormalizedPath(
82
+ configuredHome
83
+ ? configuredHome
84
+ : development
85
+ ? plugins.path.join(packageRoot, '.nogit', 'agl')
86
+ : plugins.path.join(defaultConfigHome(environment, homeDirectory), 'agl'),
87
+ aglHomeEnvironmentVariable,
88
+ );
89
+ const runtime = plugins.path.join(root, 'runtime');
90
+ const logs = plugins.path.join(root, 'logs');
91
+ return {
92
+ development,
93
+ root,
94
+ database: plugins.path.join(root, 'database'),
95
+ credentials: plugins.path.join(root, 'credentials'),
96
+ gitReversion: plugins.path.join(root, 'git-reversion'),
97
+ logs,
98
+ legacyLogs: plugins.path.join(logs, 'legacy'),
99
+ upgrade: plugins.path.join(root, 'upgrade'),
100
+ cache: plugins.path.join(root, 'cache'),
101
+ runtime,
102
+ browserRuntime: plugins.path.join(runtime, 'browser'),
103
+ uploads: plugins.path.join(runtime, 'uploads'),
104
+ sockets: plugins.path.join(runtime, 'sockets'),
105
+ openCodeRuntime: plugins.path.join(runtime, 'opencode'),
106
+ migration: plugins.path.join(root, 'migration'),
107
+ };
108
+ };
109
+
110
+ export const bindAGLHomeEnvironment = (
111
+ environmentArg: NodeJS.ProcessEnv,
112
+ pathsArg: IAGLHomePaths = resolveAGLHomePaths({ environment: environmentArg }),
113
+ ): NodeJS.ProcessEnv => ({
114
+ ...environmentArg,
115
+ [aglHomeEnvironmentVariable]: pathsArg.root,
116
+ });
@@ -92,30 +92,22 @@ const isValidPort = (valueArg: unknown): valueArg is number =>
92
92
  const isRuntimeConfig = (valueArg: unknown): valueArg is IControllerRuntimeConfig => {
93
93
  if (!isPlainObject(valueArg)) return false;
94
94
  if (
95
- // projectsRoot is optional only because pre-v2 documents lack it; once
96
- // present it must be a canonical absolute path like the workspace.
97
95
  !hasExactKeys(valueArg, [
98
96
  'controllerPort',
99
97
  'publicOrigin',
100
98
  'rpId',
101
- 'workspaceDirectory',
99
+ 'projectsRoot',
102
100
  'opencodePort',
103
101
  'tlsMode',
104
- ], ['projectsRoot'])
105
- || (
106
- Object.hasOwn(valueArg, 'projectsRoot')
107
- && (
108
- !isNonEmptyString(valueArg.projectsRoot)
109
- || !plugins.path.isAbsolute(valueArg.projectsRoot)
110
- )
111
- )
102
+ ])
103
+ || !isNonEmptyString(valueArg.projectsRoot)
104
+ || !plugins.path.isAbsolute(valueArg.projectsRoot)
105
+ || plugins.path.normalize(valueArg.projectsRoot) !== valueArg.projectsRoot
112
106
  || !isValidPort(valueArg.controllerPort)
113
107
  || !isValidPort(valueArg.opencodePort)
114
108
  || valueArg.controllerPort === valueArg.opencodePort
115
109
  || !isNonEmptyString(valueArg.publicOrigin)
116
110
  || !isNonEmptyString(valueArg.rpId)
117
- || !isNonEmptyString(valueArg.workspaceDirectory)
118
- || !plugins.path.isAbsolute(valueArg.workspaceDirectory)
119
111
  || !['local', 'proxy'].includes(valueArg.tlsMode as string)
120
112
  ) {
121
113
  return false;
@@ -154,11 +154,7 @@ const cloneRuntimeConfig = (
154
154
  controllerPort: runtimeConfigArg.controllerPort,
155
155
  publicOrigin: runtimeConfigArg.publicOrigin,
156
156
  rpId: runtimeConfigArg.rpId,
157
- workspaceDirectory: runtimeConfigArg.workspaceDirectory,
158
- // The key must be absent, not undefined-valued, for exact persistence.
159
- ...(runtimeConfigArg.projectsRoot !== undefined
160
- ? { projectsRoot: runtimeConfigArg.projectsRoot }
161
- : {}),
157
+ projectsRoot: runtimeConfigArg.projectsRoot,
162
158
  opencodePort: runtimeConfigArg.opencodePort,
163
159
  tlsMode: runtimeConfigArg.tlsMode,
164
160
  });
@@ -169,7 +165,6 @@ const runtimeConfigsEqual = (
169
165
  ): boolean => leftArg.controllerPort === rightArg.controllerPort
170
166
  && leftArg.publicOrigin === rightArg.publicOrigin
171
167
  && leftArg.rpId === rightArg.rpId
172
- && leftArg.workspaceDirectory === rightArg.workspaceDirectory
173
168
  && leftArg.projectsRoot === rightArg.projectsRoot
174
169
  && leftArg.opencodePort === rightArg.opencodePort
175
170
  && leftArg.tlsMode === rightArg.tlsMode;
@@ -726,39 +721,6 @@ export class SmartDataAuthStore implements IAuthStore {
726
721
  const existing = await ControllerAuthModel.exact.findStoredOne({ id: controllerId });
727
722
  if (existing) {
728
723
  const document = ControllerAuthModel.exact.toPersisted(existing);
729
- if (
730
- document.config.projectsRoot === undefined
731
- && runtimeConfigArg.projectsRoot !== undefined
732
- ) {
733
- // One-time v2 upgrade: adopt the resolved projects root into the
734
- // stored document; every other field must still match exactly.
735
- this.assertRuntimeConfigMatches(
736
- { ...document.config, projectsRoot: runtimeConfigArg.projectsRoot },
737
- runtimeConfigArg,
738
- );
739
- const upgraded = cloneRuntimeConfig(runtimeConfigArg);
740
- const result = await ControllerAuthModel.exact.transition({
741
- current: existing,
742
- change: (model) => {
743
- model.config = upgraded;
744
- },
745
- });
746
- if (result.status === 'transitioned') {
747
- return this.finalizeRuntimeConfigDocument(
748
- ControllerAuthModel.exact.toPersisted(result.document),
749
- runtimeConfigArg,
750
- );
751
- }
752
- const reconciled = await ControllerAuthModel.exact.findStoredOne({ id: controllerId });
753
- if (!reconciled) {
754
- throw new AuthError(
755
- 'ambiguous_write',
756
- 'Controller runtime configuration upgrade has an ambiguous outcome.',
757
- );
758
- }
759
- const reconciledDocument = ControllerAuthModel.exact.toPersisted(reconciled);
760
- return this.finalizeRuntimeConfigDocument(reconciledDocument, runtimeConfigArg);
761
- }
762
724
  return this.finalizeRuntimeConfigDocument(document, runtimeConfigArg);
763
725
  }
764
726