@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 +158 -18
- package/data/Get-ToolchainInventory.ps1 +1397 -0
- package/data/README.md +40 -0
- package/dist/ac-agent.mjs +1870 -17
- package/package.json +3 -2
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),
|
|
8
|
-
|
|
9
|
-
|
|
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),
|
|
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)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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,
|