Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Shared packages in `packages/`: **`antgrid_relay_client`** (pure Dart relay/cryp
- **A failed Windows CMake configure poisons `app/build/` permanently.** The install-prefix block in `app/windows/CMakeLists.txt` sits *after* `include(flutter/generated_plugins.cmake)`, and `CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT` is true only on the FIRST configure — so one plugin error (a missing JDK will do it) leaves the prefix at `C:/Program Files/antgrid` and no later successful configure ever corrects it. Symptom is a clean compile that dies in INSTALL with "cannot create directory … Maybe need administrative privileges", which reads like a permissions problem and is not. Fix the plugin error, then delete `app/build/windows` — re-running the build alone cannot recover.
- **Windows ships only as a Store MSIX, and a packaged binary is launchable from OUTSIDE the package only if `AppxManifest.xml` declares it as an `<Application>`.** An undeclared one fails `CreateProcess` with `ERROR_ACCESS_DENIED` — surfaced by libuv as `EPERM: uv_spawn` — even though its DACL grants execute; only the app itself, which holds package identity, can spawn a sibling. Every agent's hook config names `antgrid-bridge.exe` by absolute path (`resolveHookCommand` bakes `process.execPath`), so losing that declaration kills every hook for every agent, silently and in the field. `scripts/patch-msix-manifest.ps1` adds it between `msix:build` and `msix:pack` — the `msix` package emits exactly one `<Application>` and its `execution_alias` only ever aliases the main exe — and `scripts/verify-msix-executables.ps1` gates the packed artifact. Two non-obvious parts of that declaration: an `<Application>` is **single-instance by default**, so the bridge needs `desktop4:SupportsMultipleInstances="true"` or concurrent hooks collapse into one process; and `Subsystem="console"` exists only on `uap5:AppExecutionAlias`, so the older `uap3` + `desktop:ExecutionAlias` spelling cannot carry it. `bridge/scripts/smoke-hook-binary.ts` cannot cover any of this: it runs the loose binary, which is the case that always works. **The helper must stay visible in the app list** — one `AppListEntry="none"` anywhere makes the whole package a headless app, which Store ingestion refuses without Microsoft's per-product `HeadlessAppBypass` waiver (request: storeops@microsoft.com). It fails at submission commit, minutes after a full upload, so pack and `verify-msix-executables.ps1` both pass first.
- **A Store update while the bridge host is alive permanently bricks the package.** Every child of the app inherits its Desktop AppX **silo** (measured on 26200 — children do NOT break away), so `antgrid-bridge.exe` and its whole PTY tree are silo members. The update force-kills the app, which fires neither `didRequestAppExit` nor `HostTeardownObserver`; `owner-watchdog.ts` notices ~2s later and then drains *gracefully* for up to 5s more, so members are still live while the Store destages. A silo destroyed with members leaks the package's Helium hives (`%LOCALAPPDATA%\Packages\<family>\SystemAppData\Helium\{User,UserClasses}.dat`) mounted with no owner; the next launch cannot convert a fresh job into that family's silo, fails `ERROR_SHARING_VIOLATION` (0x80070020 → AppXDeploymentServer events 215 + 208), and the shell reports "Another program is currently using this file". **Only sign-out or reboot clears it** — `Reset-AppxPackage`, `Add-AppxPackage -Register`, and service restarts all fail, and Windows' own `RepairAppRegistrationOption` retry is what fails on every launch. The hard backstop is `app/lib/launcher/windows_job_object.dart`: the app assigns each spawned host to a job with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` and never closes the handle, so the kernel sweeps the tree as it reaps the app — on force-kill too, which is the only path that matters. A nested job inside a silo IS permitted and DOES sweep grandchildren (measured). Keep the assignment BEFORE the stdin bootstrap write in `spawnHostProcess`: the host blocks on `readBootstrapPayload()` as its first act, which is the only thing making the window race-free — and it only holds while the spawned pid IS the bridge, not the `cmd.exe` of a `.cmd` `ANTGRID_AGENT_BIN`. POSIX has no equivalent that survives a SIGKILLed parent — the watchdog stays the sole backstop there — and no tree kill either: the host is spawned `ProcessStartMode.normal`, so it leads no process group and `kill(-pid)` is a guaranteed ESRCH. `terminateTree` (`app/lib/launcher/discovery.dart`) gets its reach on POSIX by SIGTERMing first, which the bridge traps and answers by sweeping its own PTYs with `killProcessTree`, escalating to SIGKILL only after a grace; going straight to SIGKILL there orphans every child that ignores the pty hangup. `UpdateInstallController` (`app/lib/update/update_install_controller.dart`) drains the host gracefully before handing an update to the Store, but that is an improvement on the sweep, never a replacement — the job object stays the only thing standing between a mistake here and a bricked package. The app remains fully interactive for the Store's whole window (two consent dialogs, download, deploy), so the drain alone would be undone by the first `ensureHost()`; `HostController.sealSpawns()` is what makes it hold, and every path that does NOT hand the process over owes an `unsealSpawns()` — a seal nothing lifts leaves the machine unable to start any agent.
- **The same job object one level down: a PTY's orphans are what strand an isolated session.** A coding agent's helpers (sandbox command runners, analysis servers) outlive the PTY that started them and become ORPHANS holding a checkout subdirectory as their cwd; Windows refuses to delete a directory that is any live process's current directory, so `git worktree remove` dies of a sharing violation and the session is undeletable forever. `killProcessTree` (`bridge/src/terminal-session.ts`) cannot reach them by construction — `taskkill /F /T` walks the LIVE parent-child table from a pid, and an orphan's parent is already gone — so every PTY is ALSO assigned to its own `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` job (`bridge/src/win32-process.ts`), the same primitive the app applies to the host above it. Membership is inherited at `CreateProcess` and survives the parent's death, which is exactly why a job reaches what a parent-link walk cannot. Three invariants a future edit can break silently: the pid is assigned the instant it exists, because anything the child spawns first is outside the job; the handle is closed on NATURAL PTY exit and not only on kill, since an agent finishing on its own is the common case and a handle nothing closes leaks its whole tree; and closing the handle IS the reap, so the kernel closing the bridge's handles as it dies sweeps every PTY tree with no shutdown handler involved — which is what covers a force-killed bridge, the majority of bridge lifetimes. Coverage is not total, measured: a child created through `ShellExecute` (`Start-Process`) is spawned by another process and joins THAT process's job. POSIX has no inherited-membership equivalent and needs none — the process-group kill stays the mechanism there, and an orphan blocks no delete because POSIX unlinks regardless.
- **Relay Bun floor**: the relay requires Bun ≥ 1.3.14 for iOS direct-APNs push — Bun 1.3.10 fails the APNs sandbox TLS/ALPN handshake. Pin CI/runtime to 1.3.14+, not floating `1.3`.
- **Encryption & command-exec boundaries are non-negotiable** — see Conventions.

Expand Down Expand Up @@ -97,8 +98,8 @@ in the CLI run. Never take an agent's `analyze_files` result as the gate.
## Conventions (MUST / NEVER)

- **Zod everywhere** — all message types and config schemas use Zod v4 for runtime validation.
- **Adding a message type** requires ALL of: schema in `protocol.ts` → add to `AbMessageSchema` union → add to `KNOWN_TYPES` set → export the type → handle the `case` in `handleAbMessage` (`bridge/src/agent-core.ts` — the inbound switch; `index.ts` is only the commander CLI and routes no message types). Miss one and it silently fails. If the type reads or writes the working tree, it also belongs in `CHECKOUT_VARIABLE_MESSAGE_TYPES` — see below.
- **Checkout-scoped routing** — an isolated session runs in a managed git worktree, so anything filesystem-variable (files, tree, search, Git, commands, preview, terminals, the handler's judge cwd and destructive-path floor) must resolve from the session's checkout, never from the project path. `CHECKOUT_VARIABLE_MESSAGE_TYPES` (`bridge/src/protocol.ts`) is the authoritative set and is mirrored BY HAND as `kCheckoutVariableMessageTypes` (`app/lib/project/project_message_classification.dart`); the two drifting apart is silent. An app that doesn't advertise the `checkoutRouting` capability is refused a project holding a managed session rather than shown main's workspace beside an isolated agent. `WORKTREE_SESSIONS_SUPPORTED` (`bridge/src/worktree-capability.ts`) is the kill switch. Inbound `session:*` verbs are deliberately NOT in that set — they name a `sessionId` and the bridge resolves the checkout from the entry, so a `checkoutId` on such a frame is a second, conflicting answer to a question already settled host-side (`session:result` is in the set because it carries the checkout back OUT). `bridge/tests/checkout-protocol-contract.test.ts` pins it, `session:setup` included.
- **Adding a message type** requires ALL of: schema in `protocol.ts` → add to `AbMessageSchema` union → add to `KNOWN_TYPES` set → export the type → handle in the `index.ts` switch. Miss one and it silently fails. If the type reads or writes the working tree, it also belongs in `CHECKOUT_VARIABLE_MESSAGE_TYPES` — see below.
- **Checkout-scoped routing** — an isolated session runs in a managed git worktree, so anything filesystem-variable (files, tree, search, Git, commands, preview, terminals, the handler's judge cwd and destructive-path floor) must resolve from the session's checkout, never from the project path. `CHECKOUT_VARIABLE_MESSAGE_TYPES` (`bridge/src/protocol.ts`) is the authoritative set and is mirrored BY HAND as `kCheckoutVariableMessageTypes` (`app/lib/project/project_message_classification.dart`); the two drifting apart is silent. An app that doesn't advertise the `checkoutRouting` capability is refused a project holding a managed session rather than shown main's workspace beside an isolated agent. `WORKTREE_SESSIONS_SUPPORTED` (`bridge/src/worktree-capability.ts`) is the kill switch.
- **Command execution is gated by account membership AND one machine-level remote-access switch, not by origin.** A phone is trusted the moment the bridge resolves its identity from the signed-in user's account inventory (no pairing ceremony); trust alone is NOT enough. A remote phone may drive project X iff it is account-trusted **AND** the machine's remote-access boolean is on (`remote-access-policy.ts`, the sole authorization store — `paired-phones.ts` is identity/push/last-seen only) **AND** X is in the host's project catalog (`seenProjects` in `host-server.ts`). Default is off on a fresh install; off is machine-wide and immediate. That catalog lookup plus `isSafeProjectId` are the *only* thing bounding which projectId a phone may name — nothing backs them up, so never refactor them away as redundant. Loopback/local callers are exempt by design (`currentPhoneAllowed()` in `agent-core.ts`): the desktop drives its own machine with the switch off. The `antgrid phones remove` CLI is **not** a revocation — see `docs/commands.md`.
- **NEVER make encryption optional.** All agent↔app messages are encrypted after handshake; the relay never holds decryption keys.
- **The repo is dual-licensed and the boundary is one-way.** `packages/antgrid-wire` and `packages/antgrid_relay_client` are Apache-2.0 and carry their own `LICENSE`; everything else Antgrid owns is ELv2 (`LICENSING.md` is the map). Apache code may be used inside an ELv2 component — **never move a file the other way**. Hoisting a shared helper out of `bridge/`, `relay/`, `web/` or `app/` into either package relicenses it permissively, and once published that cannot be undone. It compiles, CI stays green, and nothing warns you.
Expand Down
2 changes: 2 additions & 0 deletions THIRD-PARTY.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,8 @@ redistributed inside it.
| `@anthropic-ai/claude-agent-sdk` | 0.3.201 | proprietary — see above | Anthropic PBC |
| `@inquirer/prompts` | 8.4.2 | MIT | Copyright (c) 2025 Simon Boudrias |
| `@opencode-ai/sdk` | 1.15.10 | MIT | declared in `package.json`; the published package ships no licence file |
| `@xterm/addon-serialize` | 0.14.0 | MIT | The xterm.js authors; declared in `package.json`, the published package ships no licence file |
| `@xterm/headless` | 6.0.0 | MIT | The xterm.js authors; declared in `package.json`, the published package ships no licence file |
| `bun-pty` | 0.4.8 | MIT | Copyright (c) 2025 Dilip Thapa |
| `chokidar` | 5.0.0 | MIT | Copyright (c) 2012 Paul Miller, Elan Shanker |
| `commander` | 13.1.0 | MIT | Copyright (c) 2011 TJ Holowaychuk |
Expand Down
Loading
Loading