@rackbops/ac-agent 2.0.0-alpha.8 → 2.0.0-alpha.9

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.
package/README.md CHANGED
@@ -4,9 +4,13 @@ The per-machine host agent for [artifact-console](https://github.com/Rackbops/ar
4
4
  small HTTPS server, paired to a console over a token, that lets it reach one dev machine's
5
5
  `claude`/`git`/`gh` and file system through an allowlisted verb API. Console-initiated pairing (a
6
6
  one-time code exchanged for a per-machine token, #48), `repos` list/status/pull and `open` (#49,
7
- 49a), and `exec/claude` (with the job registry and its SSE log) plus `exec/gh` (#49, 49b) are all
8
- live; `scan/:kind` (49c) lands in a later issue. Every verb also requires a per-plugin capability
9
- grant, pushed by the console and enforced here (#50) -- see **Verbs** and **Security model** below.
7
+ 49a), `exec/claude` (with the job registry and its SSE log) plus `exec/gh` (#49, 49b), and the
8
+ `context-pressure` / `subagent-cost` session-log scans (#52, 52a), and `memory-footprint` /
9
+ `toolchain-inventory` (#52, 52b) are all live -- every `scan/:kind` kind #52 planned has landed.
10
+ Every verb also
11
+ requires a per-plugin capability grant, pushed by the console and enforced here (#50) -- see
12
+ **Verbs** and **Security model** below. `GET /v1/preflight` (#55) -- git/gh/node/claude executed,
13
+ chromium only resolved -- is control plane, not a verb; see **Preflight**.
10
14
 
11
15
  ## Install
12
16
 
@@ -14,6 +18,17 @@ grant, pushed by the console and enforced here (#50) -- see **Verbs** and **Secu
14
18
  npx @rackbops/ac-agent install
15
19
  ```
16
20
 
21
+ During the prerelease period, install the newest prerelease explicitly with the `next` dist-tag
22
+ (`npx @rackbops/ac-agent@next install`) -- `release.yml`'s publish step tags every `-alpha.N`/
23
+ `-beta.N`/`-rc.N` version `next` from here on (#269), since npm refuses a plain `npm publish` of a
24
+ prerelease version with no explicit `--tag`; a stable version still publishes under `latest`. The
25
+ very first publish (`2.0.0-alpha.8`, #268) was done by hand under `--tag alpha` before this existed
26
+ -- and because it was the package's first version ever, npm *also* pointed `latest` at
27
+ `2.0.0-alpha.8` (npm's own first-publish behaviour, not a deliberate choice here). So right now a
28
+ bare `npx @rackbops/ac-agent install` (no tag) resolves to `alpha.8`, with `alpha` and `latest` both
29
+ still pointing at it; `next` starts moving with the next prerelease tag, and `latest` only moves
30
+ again once a real stable version is published.
31
+
17
32
  Registers a Windows Task Scheduler task (`\ClaudeLocal\ac-agent`, a logon trigger for the current
18
33
  user) or, on Linux, a systemd `--user` unit (`ac-agent.service`, `enable --now`), copies the running
19
34
  bundle into the state dir (never the `npx` cache — that can be garbage-collected out from under a
@@ -33,6 +48,32 @@ elevated, `install` prints the exact one-liner to run in an elevated PowerShell
33
48
  `firewall: missing` until that's done. This is expected on a fresh install run from an ordinary
34
49
  shell — it is not a failure to work around.
35
50
 
51
+ ### Run install from a normal shell
52
+
53
+ The Claude desktop app is an MSIX package (`Claude_<hash>`, e.g. `Claude_pzs8sxrjxfjjc`). A Claude
54
+ Code session it starts — and every process that session spawns, including a PowerShell/Bash tool
55
+ shell running `npx @rackbops/ac-agent install` — inherits the package identity, and MSIX
56
+ **virtualizes AppData**: a write to a subfolder of `%LOCALAPPDATA%`/`%APPDATA%` is silently
57
+ redirected into the package's private `...\Packages\<pkg>\LocalCache\Local\...`/
58
+ `...\LocalCache\Roaming\...`, while the writing process itself believes it wrote the real path.
59
+ `install` writes the bundle copy, `launch.js`, the cert and `agent.toml` under the state/config
60
+ dirs, so run from such a session those files land somewhere the Windows logon task — launched later
61
+ with the user's real, unpackaged token — can never see, and the task exits `1` at once (#263).
62
+ `install` detects this (a nonce probe file, not just the presence of an old package copy, so a stale
63
+ VFS leftover from a prior virtualized install never blocks a later normal one) and refuses with:
64
+
65
+ ```
66
+ install ran inside a packaged app (<packageFamily>), which virtualizes AppData: the files landed
67
+ under <vfsPath> where the logon task cannot see them. Run ac-agent install from a normal PowerShell
68
+ window, not from a session started by the Claude desktop app.
69
+ ```
70
+
71
+ Run `install` from a normal PowerShell window opened from the Start menu, not from a Claude Code
72
+ session the desktop app started. `status`/`serve` don't refuse the same way (see **Status** and
73
+ **serve / stop / start** below) — a `serve` launched from a virtualized session still works, since
74
+ everything it reads and writes goes through the same virtualized view within that one session; it's
75
+ only a *task* launched later, outside that session, that can't see the files.
76
+
36
77
  ### Windows: no console window
37
78
 
38
79
  The task's action is `wscript.exe //B "<launch.js>"` against a small generated JScript launcher
@@ -75,9 +116,14 @@ npx @rackbops/ac-agent status
75
116
  Prints, as JSON: the config path and whether it came from a file or defaults, the state dir, the
76
117
  Task Scheduler/systemd service state, whether a pairing token is stored (`paired: true/false`), the
77
118
  cert's fingerprint, the result of a live `GET /v1/health` call against itself (using its own pinned
78
- cert, not a system CA — see **Security model** below), and the currently-held grants
119
+ cert, not a system CA — see **Security model** below), the currently-held grants
79
120
  (`{revision, plugins: <count>} | null`, from `grants.json` — `null` when it is absent or malformed,
80
- the same as a freshly-purged or never-pushed-to agent).
121
+ the same as a freshly-purged or never-pushed-to agent), and `virtualized: true/false` (#263) — `true`
122
+ when this session's own AppData writes to the resolved config/state dirs are virtualized by a
123
+ packaged app (see **Run install from a normal shell** above), which also prints a `warning: this
124
+ session's AppData is virtualized (<packageFamily>); the logon task cannot see <stateDir> -- see
125
+ README, Install` line to stderr. `serve` logs the same warning line once at startup, in the same
126
+ circumstance, without refusing to start.
81
127
 
82
128
  ## Uninstall
83
129
 
@@ -120,18 +166,26 @@ port = 8788
120
166
  bind = "0.0.0.0"
121
167
  machine = "melody"
122
168
  repos_root = "S:/Repos"
169
+ claude_projects_dir = "C:/Users/melody/.claude/projects"
123
170
  ```
124
171
 
125
172
  Unknown keys are ignored, so a future field never breaks an older agent reading a newer file.
126
173
  `repos_root` is overridable live with `AC_AGENT_REPOS_ROOT` the same way `port` is with
127
174
  `AC_AGENT_PORT` — absolute paths only; a relative override throws, naming the value.
175
+ `claude_projects_dir` (#52, 52a; default `<homedir>/.claude/projects`) is handled identically, with
176
+ its own live override `AC_AGENT_CLAUDE_PROJECTS_DIR` — it names the root the `context-pressure` and
177
+ `subagent-cost` scans read, overridable so a fixture tree or a non-default `CLAUDE_CONFIG_DIR`
178
+ still resolves. Unlike `repos_root`, `install` takes no flag for it and `status` does not report
179
+ it.
128
180
 
129
181
  ## Verbs
130
182
 
131
183
  Everything under `/v1/*` except `/v1/health` and `/v1/pair` requires pairing (see **Security
132
184
  model**). The verb registry fills `GET /v1/health`'s `capabilities` field with the sorted, unique
133
185
  list of every registered verb's capability name — with every verb group registered, that's
134
- `["agent.desktop.open", "agent.exec.claude", "agent.exec.gh", "agent.git.pull", "agent.git.read"]`.
186
+ `["agent.desktop.open", "agent.exec.claude", "agent.exec.gh", "agent.git.pull", "agent.git.read",
187
+ "agent.scan.context-pressure", "agent.scan.memory-footprint", "agent.scan.subagent-cost",
188
+ "agent.scan.toolchain-inventory"]`.
135
189
 
136
190
  **Every verb also requires two headers, checked in this order** (#50), after the bearer check
137
191
  passes: `X-AC-Plugin` (the calling plugin's id) missing or shaped wrong is `400 {ok: false, error:
@@ -167,20 +221,63 @@ capability, plugin}`. See **Security model** for how a grant reaches the agent i
167
221
  `xdg-open` on Linux) started — never a shell string, always argv — not that a browser or Claude
168
222
  Desktop actually opened; that's visible to the person at the desktop, not to the caller.
169
223
  - **`POST /v1/scan/<kind>`** (`agent.scan.<kind>`, one capability per kind), body `{}` — a pure
170
- dispatch shell (#49, 49c): this package ships no kinds itself, the four #52 scans
171
- (`context-pressure`, `subagent-cost`, `memory-footprint`, `toolchain-inventory`) register into it,
172
- so until then every `<kind>` is `404 {"error": "no such verb"}`, the same as any other unregistered
173
- route there is no separate `GET /v1/scan` list, since the kinds a machine implements are already
174
- visible as `agent.scan.<kind>` entries in `GET /v1/health`'s `capabilities`. One run per kind at a
175
- time: a second `POST` while one is still in flight is `409 {ok: false, error: "scan already
176
- running"}` (the #52 scans read whole `~/.claude` trees and must not stack); a kind's own `run()`
177
- gets an `AbortSignal` that fires at its `timeoutMs` (120s default) a run that doesn't honour it
178
- promptly answers the client `504 {ok: false, error: "scan timed out after <n> ms"}` while the run
179
- keeps going in the background (a later `POST` for that kind still gets `409`, now naming "timed
180
- out, still finishing", until the run actually settles); one that never settles at all is abandoned
181
- five timeouts later, freeing the kind up rather than locking it for the agent's whole lifetime. A
224
+ dispatch shell (#49, 49c) that mounts one verb per kind registered into it; an unregistered
225
+ `<kind>` is `404 {"error": "no such verb"}`, the same as any other unregistered route — there is
226
+ no separate `GET /v1/scan` list, since the kinds a machine implements are already visible as
227
+ `agent.scan.<kind>` entries in `GET /v1/health`'s `capabilities`. One run per kind at a time: a
228
+ second `POST` while one is still in flight is `409 {ok: false, error: "scan already running"}`
229
+ (these scans read whole `~/.claude` trees and must not stack); a kind's own `run()` gets an
230
+ `AbortSignal` that fires at its `timeoutMs` a run that doesn't honour it promptly answers the
231
+ client `504 {ok: false, error: "scan timed out after <n> ms"}` while the run keeps going in the
232
+ background (a later `POST` for that kind still gets `409`, now naming "timed out, still
233
+ finishing", until the run actually settles); one that never settles at all is abandoned five
234
+ timeouts later, freeing the kind up rather than locking it for the agent's whole lifetime. A
182
235
  successful run answers `200 {ok: true, kind, generatedAt, durationMs, payload}`; a rejected one,
183
236
  `502 {ok: false, error: "<message>"}`.
237
+ - **`context-pressure` and `subagent-cost`** (#52, 52a; `timeoutMs: 900_000` — a cold scan can
238
+ read on the order of a gigabyte of transcripts) both scan `claude_projects_dir` (default
239
+ `<homedir>/.claude/projects`) and are byte-compatible with the 1.x `artifacts-console`
240
+ scheduled tasks of the same name: same JSON shape, same key order, same incremental
241
+ `(mtime, size)` cache, same `os.linesep` cache-file line ending. `context-pressure` walks
242
+ `<root>/<encoded-project>/<session>.jsonl` (main session transcripts only — a subagent's own
243
+ transcript has its own separate window and is never folded in) and reports each session's peak
244
+ context-window occupancy (`input + cache_read + cache_creation` tokens, as a percentage of the
245
+ model's window) plus its compaction and unmarked-drop counts. `subagent-cost` walks
246
+ `<root>/<encoded-project>/<session>/subagents/**/agent-*.jsonl` (both the plain-Task layout and
247
+ the nested Workflow one) and reports each subagent's token totals and an API-equivalent dollar
248
+ figure at list rates (never money actually spent — this box runs Claude Code on a
249
+ subscription). Both read exactly six fields per transcript record (`type`, `subtype` /
250
+ `attributionAgent`, `timestamp`, `cwd`, `message.{id, model, usage}`); `message.content` --
251
+ prompts, tool results, file contents -- is never deserialised, by either scan. The cache lands
252
+ at `<stateDir>/scans/context-pressure.json` / `<stateDir>/scans/subagent-cost.json`, created on
253
+ first write and removed by `uninstall` regardless of `--purge`.
254
+ - **`memory-footprint`** (#52, 52b; no custom `timeoutMs` -- a cold run is kilobytes of I/O, not
255
+ the transcript-scale reads above) is a producer only, computed live on every request and never
256
+ cached: the fixed 8-layer set Claude Code assembles into a session's preamble (`enterprise`,
257
+ `user`, `user-rules` -- global, loaded into every session everywhere -- plus `project`,
258
+ `project-alt`, `project-rules`, `project-local`, `auto-memory` per repo, `auto-memory` truncated
259
+ to the first 200 lines of `MEMORY.md` under `claude_projects_dir`, same as `context-pressure`/
260
+ `subagent-cost`). The repo set is `GET /v1/repos`' own listing (every immediate child of
261
+ `repos_root` with a `.git` entry) -- unlike 1.x, which read the console's configured repo list,
262
+ so the payload's `repo` and `name` fields are both the directory name, never an `owner/name`
263
+ string. Tokens are a byte estimate (~4 chars/token; the payload says `estimated: true`); only
264
+ sizes and line counts ever leave this scan -- file content is never read into the payload.
265
+ - **`toolchain-inventory`** (#52, 52b; `timeoutMs: 600_000` -- about seventy version probes plus a
266
+ WSL probe) ships and runs 1.x's own `Get-ToolchainInventory.ps1` (vendored byte-for-byte,
267
+ `data/README.md`) through `pwsh -NoProfile -File <script> -OutFile <state>/scans/
268
+ toolchain-inventory-<hostname>.md -NoRender`, with the child's `cwd` set to `<state>/scans` --
269
+ **never a project directory**, since the script's own `pnpm --version` probe rewrites
270
+ `pnpm-lock.yaml` in whatever directory it runs in. **Windows-only**: on any other platform this
271
+ answers `{skipped: true, reason: "toolchain-inventory is Windows-only (1.x had no Linux
272
+ equivalent)"}` without spawning anything. Writes (and reads back) `<state>/scans/
273
+ toolchain-inventory-<hostname>.md` and its `.json` sidecar, and maintains its own copy of the
274
+ script at `<state>/scans/Get-ToolchainInventory.ps1` (rewritten only when the bundled text
275
+ changes, via a temp file + rename so a killed agent never leaves a half-written copy). A missing
276
+ `pwsh` on PATH is `502 {"error": "pwsh not found on PATH"}` before any spawn; the request timeout
277
+ kills only the `pwsh` child, never a hung probe or `wsl.exe` process it may have spawned (1.x had
278
+ the same scope). The hostname used for the file name and inside the script's own report is
279
+ `os.hostname()` -- never the operator's configured `machine`/`--name` override -- so the agent's
280
+ file name always matches 1.x's own for a byte-compare.
184
281
 
185
282
  ### `POST /v1/exec/claude` and the job registry
186
283
 
@@ -263,6 +360,49 @@ at all). Windows PATH resolution stops at the **first** directory with any match
263
360
  shim earlier on `PATH` is reported even if a native binary exists further down — that is what a real
264
361
  invocation would actually run, not "helpful" behavior a real shell wouldn't show either.
265
362
 
363
+ ## Preflight
364
+
365
+ **`GET /v1/preflight`** (#55): control plane, like `PUT /v1/grants` -- bearer-gated, no capability,
366
+ no `X-AC-Plugin`/`X-AC-Grants-Revision` headers, and refuses a caller carrying `X-AC-Plugin` with
367
+ `403 {ok: false, error: "preflight is the console's, not a plugin's"}`. The console's own
368
+ `POST /api/agents/:id/preflight` (`packages/host`) relays it into the Agents panel's Environment
369
+ section. Live and stateless: nothing is cached on either side, and a call answers `200 {ok: true,
370
+ machine, checkedAt, searchPath: "registry" | "process", rows}`, five rows always, in this order:
371
+
372
+ | `cmd` | required | executed | verdicts |
373
+ |---|---|---|---|
374
+ | `git` | yes | `git --version` | `ok` \| `missing` \| `broken` \| `off-path` |
375
+ | `gh` | yes | `gh --version`, then `gh auth status` | `ok` \| `missing` \| `broken` \| `off-path` \| `unauthenticated` |
376
+ | `node` | yes | `node --version` | `ok` \| `missing` \| `broken` \| `off-path` |
377
+ | `claude` | yes | `claude --version` | `ok` \| `missing` \| `broken` \| `off-path` |
378
+ | `chromium` | no | never | `ok` \| `missing` |
379
+
380
+ Each row is `{cmd, label, status, required, path, why, version, detail}` -- `version` the first
381
+ non-empty line of `--version`'s stdout (CRLF handled), `""` otherwise; `detail` status-dependent
382
+ free text (a `broken` row's last non-empty output line, `unauthenticated`'s
383
+ `run gh auth login [-- <stderr line>]`, `off-path`'s `found under a version manager, not on PATH`),
384
+ `""` when there is nothing to add.
385
+
386
+ - **Executed, not just resolved.** `git`/`gh`/`node`/`claude` are resolved on the refreshed PATH
387
+ (below) AND run (`<exe> --version`, a **15 s** timeout per spawn); `gh` additionally runs
388
+ `gh auth status` (needs the network -- offline it exits non-zero and the row reads
389
+ `unauthenticated` with the stderr line saying why, since a real, validated token is what bullet 2
390
+ wants, not merely a cached one `gh auth token` would report on regardless). `chromium` is
391
+ resolved only, never executed -- required: false, so amber (`missing`), never red, when absent.
392
+ - **A `.cmd`/`.bat` shim is `broken` without ever being run.** Node cannot spawn one without
393
+ `shell: true`, so a shim is reported the same way `exec/claude`/`exec/gh` already do -- distinct
394
+ from a bare "not found", naming the shim's own path and the exact fix (install the native
395
+ binary).
396
+ - **The PATH is the machine's, not this process's.** On Windows it is re-read from the registry
397
+ (`HKLM\...\Environment` then `HKCU\Environment` via `reg.exe`, spawned by its absolute
398
+ `System32` path) so a tool installed after the agent's logon task started is still seen; on
399
+ Linux the process PATH plus every version-manager bin directory (`fnm`/`nvm`/`volta`/`asdf`) is
400
+ searched as a fallback, so a tool invisible to a bare `which` reads `off-path`, never `missing`.
401
+ `searchPath` on the report names which: `"registry"` or `"process"`.
402
+ - **The whole check answers in a few seconds.** Five rows run concurrently; the worst case is
403
+ `gh`'s `--version` answering just under 15 s and then `auth status` timing out at 15 s too --
404
+ under the console's own 100 s edge-request budget behind Cloudflare Access.
405
+
266
406
  ## Security model
267
407
 
268
408
  - **HTTPS only**, with a self-signed certificate generated once at install (RSA 2048, SHA-256,