@rackbops/ac-agent 2.0.0-alpha.40 → 2.0.0-alpha.41
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 +143 -17
- package/data/Get-ToolchainInventory.ps1 +456 -27
- package/data/README.md +20 -16
- package/dist/ac-agent.mjs +7634 -47
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ small HTTPS server, paired to a console over a token, that lets it reach one dev
|
|
|
6
6
|
one-time code exchanged for a per-machine token, #48), `repos` list/status/pull and `open` (#49,
|
|
7
7
|
49a), `exec/claude` (with the job registry and its SSE log) plus `exec/gh` (#49, 49b), and the
|
|
8
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
|
|
9
|
+
`toolchain-inventory` (#52, 52b) are all live -- every `scan/:kind` kind #52 planned has landed --
|
|
10
|
+
as are the OneDrive mirror (`publish/mirror`, #53) and the tunnel-config lint (`tunnel/lint`, #416).
|
|
10
11
|
Every verb also
|
|
11
12
|
requires a per-plugin capability grant, pushed by the console and enforced here (#50) -- see
|
|
12
13
|
**Verbs** and **Security model** below. `GET /v1/preflight` (#55) -- git/gh/node/claude executed,
|
|
@@ -18,16 +19,13 @@ chromium only resolved -- is control plane, not a verb; see **Preflight**.
|
|
|
18
19
|
npx @rackbops/ac-agent install
|
|
19
20
|
```
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`
|
|
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.
|
|
22
|
+
Install with the plain command above, no dist-tag: since #564, every release -- prerelease included --
|
|
23
|
+
publishes with `--tag latest` while 2.0 is prerelease-only, so an unversioned `npx @rackbops/ac-agent`
|
|
24
|
+
resolves to the newest release. Two older tags are frozen: `alpha` (the very first publish,
|
|
25
|
+
`2.0.0-alpha.8`, #268, was done by hand under `--tag alpha`) still points at `2.0.0-alpha.8`, and
|
|
26
|
+
`next` (what `release.yml` gave every prerelease from #269 through `2.0.0-alpha.40`) stays at
|
|
27
|
+
`2.0.0-alpha.40`. Nothing moves either any more, and the maintainer removes or moves them by hand.
|
|
28
|
+
`latest` is expected to go back to stable-only once the first stable `2.0.0` ships.
|
|
31
29
|
|
|
32
30
|
### Upgrade with the Tooling script
|
|
33
31
|
|
|
@@ -68,7 +66,7 @@ agent.
|
|
|
68
66
|
|
|
69
67
|
### Linux: no special `npx` spelling needed (#278)
|
|
70
68
|
|
|
71
|
-
The plain `npx @rackbops/ac-agent
|
|
69
|
+
The plain `npx @rackbops/ac-agent <verb>` line above is the one spelling that works on Linux
|
|
72
70
|
regardless of npm version -- confirmed on Ubuntu 26.04 with both npm 11.19 and npm 12.0.2. `#278` was
|
|
73
71
|
originally filed as an npm-11-vs-12 `npx` regression (alpha.9 silently printing `npm notice run` and
|
|
74
72
|
exiting 0 with no verb output on a npm 12 box), but the real cause was narrower and applied to *every*
|
|
@@ -309,6 +307,18 @@ its own live override `AC_AGENT_CLAUDE_PROJECTS_DIR` — it names the root the `
|
|
|
309
307
|
still resolves. Unlike `repos_root`, `install` takes no flag for it and `status` does not report
|
|
310
308
|
it.
|
|
311
309
|
|
|
310
|
+
`tunnel_config` and `tunnel_preflight` (#416; both unset by default, because most machines do not run
|
|
311
|
+
the tunnel) are nullable like `publish_dir`, so `agent.toml` simply omits them: `tunnel_config` is
|
|
312
|
+
the absolute path of the `cloudflared` config that `GET /v1/tunnel/lint` reads, `tunnel_preflight`
|
|
313
|
+
the absolute path of Tooling's `console_tunnel_preflight.py`. Live overrides `AC_AGENT_TUNNEL_CONFIG`
|
|
314
|
+
and `AC_AGENT_TUNNEL_PREFLIGHT`. **Absolute paths only**, because `tunnel_preflight` is executed and
|
|
315
|
+
must not resolve against whatever directory the service starts in: a relative env override throws at
|
|
316
|
+
load, naming the value, like every other `AC_AGENT_*` path; a relative value in the file is loaded as
|
|
317
|
+
written (as `publish_dir`'s is, so a typo in an optional key can never stop `serve`, `stop` or
|
|
318
|
+
`uninstall`) and the verb then refuses to read or run it, answering `tunnel_config is not an absolute
|
|
319
|
+
path` (or a `preflight-unusable` warning naming `tunnel_preflight`). A blank or non-string value in the
|
|
320
|
+
file is treated as unset. `install` takes no flag for either and `status` does not report them.
|
|
321
|
+
|
|
312
322
|
## Verbs
|
|
313
323
|
|
|
314
324
|
Everything under `/v1/*` except `/v1/health` and `/v1/pair` requires pairing (see **Security
|
|
@@ -316,7 +326,7 @@ model**). The verb registry fills `GET /v1/health`'s `capabilities` field with t
|
|
|
316
326
|
list of every registered verb's capability name — with every verb group registered, that's
|
|
317
327
|
`["agent.desktop.open", "agent.exec.claude", "agent.exec.gh", "agent.git.pull", "agent.git.read",
|
|
318
328
|
"agent.publish.mirror", "agent.scan.context-pressure", "agent.scan.memory-footprint",
|
|
319
|
-
"agent.scan.subagent-cost", "agent.scan.toolchain-inventory"]`.
|
|
329
|
+
"agent.scan.subagent-cost", "agent.scan.toolchain-inventory", "agent.tunnel.lint"]`.
|
|
320
330
|
|
|
321
331
|
**Every verb also requires two headers, checked in this order** (#50), after the bearer check
|
|
322
332
|
passes: `X-AC-Plugin` (the calling plugin's id) missing or shaped wrong is `400 {ok: false, error:
|
|
@@ -520,12 +530,19 @@ running"}`.
|
|
|
520
530
|
- **HTML**: each file's bytes are compared against what's on disk (size, then sha256) — unchanged
|
|
521
531
|
bytes are never rewritten (OneDrive re-uploads every rewrite); a changed or new file is written to
|
|
522
532
|
`<publish_dir>/html/<href>.tmp-<pid>` then renamed into place. A write/rename failure (a file
|
|
523
|
-
OneDrive has locked mid-sync) counts as `failed
|
|
533
|
+
OneDrive has locked mid-sync) counts as `failed`, is named in `html.failures` with its error's
|
|
534
|
+
message, and the run continues with the next file.
|
|
524
535
|
- **PDF** (when `pdf: true`): one `pdf/<href>.pdf` per `wanted` report (never for an `index.html`
|
|
525
536
|
page), rendered with a local headless Chromium-family browser only when missing or older than its
|
|
526
537
|
html — resolved via the same search `GET /v1/preflight`'s `chromium` row uses (winPaths, then
|
|
527
538
|
PATH, then a version manager's bin dir), never executed otherwise. No resolvable browser is not an
|
|
528
|
-
error: html still mirrors, the response's `pdf.browser` is `null`.
|
|
539
|
+
error: html still mirrors, the response's `pdf.browser` is `null`. A render that produced no file
|
|
540
|
+
counts as `failed` and is named in `pdf.failures` by its `.pdf` href: `timed out after 90s` when the
|
|
541
|
+
browser launch timed out, `exit <code>: <the last 160 characters of its stderr>` when it exited
|
|
542
|
+
non-zero (just `exit <code>` when it printed nothing), else `no pdf produced within 30s`. The poll for
|
|
543
|
+
the file still decides success, so a browser that exits non-zero, or times out, but wrote the pdf is a
|
|
544
|
+
success. A report whose html is not on disk is skipped, never
|
|
545
|
+
named.
|
|
529
546
|
- **Ownership, not "not in my docroot"** (1.x issue #230): the publish target is shared across every
|
|
530
547
|
paired machine that mirrors to it, so pruning (`prune: true`) is scoped to what THIS machine's
|
|
531
548
|
ledger — `<stateDir>/published-ledger.json`, machine-local, never inside the publish target itself
|
|
@@ -535,8 +552,20 @@ running"}`.
|
|
|
535
552
|
the mirror in one run — the response's `html.veto`/`pdf.veto` names the reason; re-run with the
|
|
536
553
|
same request (or a `--force`-equivalent isn't exposed here — see `publish-adopt` below for the
|
|
537
554
|
one-time claim that closes an adoption gap) once satisfied it's intended.
|
|
538
|
-
- Response `200 {ok: true, dest, html: {written, unchanged, failed, pruned, veto}, pdf:
|
|
539
|
-
upToDate, failed, pruned, veto, browser}}`.
|
|
555
|
+
- Response `200 {ok: true, dest, html: {written, unchanged, failed, pruned, veto, failures}, pdf:
|
|
556
|
+
{rendered, upToDate, failed, pruned, veto, browser, failures}}`. **`failures`** (added in #376) is
|
|
557
|
+
`[{href, reason}]` naming each report that failed to write or render (a pdf by its `.pdf` href),
|
|
558
|
+
capped at 50 per kind (`failed` keeps counting past it). It is always present from this agent (`[]`
|
|
559
|
+
when nothing failed) and absent from an older one, so a console must treat it as optional. A `reason`
|
|
560
|
+
is one line of at most 200 characters (whitespace collapsed), with this run's `publish_dir` replaced
|
|
561
|
+
by `<dest>` and the browser's temporary profile dir by `<tmp>` wherever they appear as literal text:
|
|
562
|
+
as configured, `path.normalize`d, with either separator, or percent-encoded as in a `file://` URL. The
|
|
563
|
+
response's `dest` already names the former. Only an absolute path is replaced (a relative `publish_dir`
|
|
564
|
+
is left alone rather than rewriting every occurrence of its name). A path that differs in case, is a
|
|
565
|
+
Windows short (8.3) name or is the `\\?\UNC\...` form of a UNC dest is not recognised, and neither is
|
|
566
|
+
any other path a browser prints on stderr (its last 160 characters are kept, trimmed and collapsed,
|
|
567
|
+
otherwise as printed). The agent's own log line ends its counts with ` failures: <named html>/<named
|
|
568
|
+
pdf>` when either is non-zero; the names stay out of the agent's log.
|
|
540
569
|
|
|
541
570
|
**`ac-agent publish-adopt`** (CLI, not a verb; one-time): refuses when `publish_dir` is unset,
|
|
542
571
|
otherwise claims every file currently in the mirror for this machine's ledger. Run exactly once, only
|
|
@@ -550,6 +579,103 @@ files a run both mirrors and finds already present.
|
|
|
550
579
|
platform, and on a Windows box with neither OneDrive env var. `status` reports it under `publishDir`
|
|
551
580
|
(`null` when unconfigured).
|
|
552
581
|
|
|
582
|
+
### `GET /v1/tunnel/lint`
|
|
583
|
+
|
|
584
|
+
**`GET /v1/tunnel/lint`** (`agent.tunnel.lint`, #416) lints the `cloudflared` config on the machine
|
|
585
|
+
that runs the tunnel, so the Admin security card can show it (that card is PR 2 of #416, not yet
|
|
586
|
+
built). **Why an agent verb:** the console is a container that cannot read the tunnel host's config
|
|
587
|
+
and has no Tooling clone, so the lint runs where the config lives and the console is meant to ask each
|
|
588
|
+
paired machine (`host.agent.call`), the way `scans`, `repos` and `publish` already work.
|
|
589
|
+
|
|
590
|
+
The verb takes **no request input**: which file is read and which script is run come only from
|
|
591
|
+
`tunnel_config` / `tunnel_preflight` (or their `AC_AGENT_TUNNEL_*` overrides) on this machine. It
|
|
592
|
+
reads the file afresh on every call, so a result is never stale, and answers `200` with one of:
|
|
593
|
+
|
|
594
|
+
```json
|
|
595
|
+
{"available": true, "engine": "builtin", "ok": false, "errors": 1, "warns": 1,
|
|
596
|
+
"hostnames": ["console.example.com"],
|
|
597
|
+
"findings": [{"id": "ingress-catch-all", "severity": "error", "message": "...", "location": "ingress[1]"}]}
|
|
598
|
+
{"available": false, "reason": "no tunnel_config in agent.toml"}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
`errors`/`warns` count every finding; `findings` is capped at 200 and `hostnames` at 200, and each
|
|
602
|
+
string in them is cut for both engines (messages at 500 characters, locations 200, ids 100, a hostname
|
|
603
|
+
253; a cut string ends in `...`, so the longest is N + 3). **Fail-soft everywhere:** anything that
|
|
604
|
+
stops a lint from running is `200 {available: false, reason}`, never a non-2xx from this handler (the
|
|
605
|
+
grants guard in front of it still answers its own `400`/`409`/`403`), and a failing log write does not
|
|
606
|
+
change the answer. Reasons (each cut at 600 characters): `no tunnel_config in agent.toml` (this machine
|
|
607
|
+
does not run the tunnel); `tunnel_config is not an absolute path`; `cannot read tunnel_config <path>:
|
|
608
|
+
<why>`, where `<why>` is an OS error code such as `ENOENT`, `EACCES` or `EINVAL`, `not a regular
|
|
609
|
+
file`, `file is over 65536 bytes`, `timed out after 3000 ms`, or `an earlier read has not finished
|
|
610
|
+
(...)`; `tunnel_config <path> is not valid YAML (<code> at line <n>)` (the parser's error code and
|
|
611
|
+
line, never the file's text; an alias bomb is `excessive aliasing`, an alias with no anchor
|
|
612
|
+
`unresolved alias`) or `is not a YAML mapping`; and `tunnel lint failed: <error>` for anything
|
|
613
|
+
unexpected.
|
|
614
|
+
|
|
615
|
+
**The config read is bounded.** Only a regular file is read. A FIFO or a named pipe is refused
|
|
616
|
+
(`not a regular file`) before it is opened, because opening a pipe with no writer blocks one of libuv's
|
|
617
|
+
four thread-pool threads for good and a few such calls would stall every filesystem call in the agent;
|
|
618
|
+
a Windows device name the OS cannot stat (`CON`) fails at that stat with its own code, never reaching
|
|
619
|
+
an open. The read runs under a 3 s deadline. Calls that overlap a read still inside its deadline share
|
|
620
|
+
it; once a read has missed its deadline and is still outstanding, the next calls answer `an earlier
|
|
621
|
+
read has not finished` instead of starting another, so a share that is not answering can never fill
|
|
622
|
+
the pool. The file is read up to **64 KiB** and refused beyond that: a cloudflared config is tens of
|
|
623
|
+
lines, and the limit bounds parse time, which is quadratic in a flat mapping and runs on the agent's
|
|
624
|
+
one event loop (under a second at the limit, tens of seconds at 1 MiB).
|
|
625
|
+
|
|
626
|
+
**Two engines, one result shape** (`engine`):
|
|
627
|
+
|
|
628
|
+
- **`"builtin"`** (the default, needs no Python and no Tooling checkout): a structural lint in
|
|
629
|
+
TypeScript, using the preflight's own finding ids where a rule overlaps. It does **not** judge
|
|
630
|
+
safety (no loopback or Access checks; that is what the preflight adds), so an `ok: true` from it
|
|
631
|
+
means "none of these checks found a problem", not "safe". **The shared ids do not always mean the
|
|
632
|
+
same verdict**, so show `engine` beside a finding: the preflight reports any rule before the end
|
|
633
|
+
that has no hostname (a path-only rule included) as `ingress-catch-all`, where the built-in accepts
|
|
634
|
+
a path-only rule; and `duplicate-hostname` and `origin-url` exist only here (the preflight judges
|
|
635
|
+
origins by its own `origin-loopback`).
|
|
636
|
+
|
|
637
|
+
| id | severity | when |
|
|
638
|
+
|---|---|---|
|
|
639
|
+
| `named-tunnel` | error | no `tunnel:`, or no `credentials-file:` |
|
|
640
|
+
| `has-ingress` | error | no `ingress:` list, or an empty one |
|
|
641
|
+
| `ingress-catch-all` | error | the final rule is not a bare `service: http_status:404` (no `hostname`); or an entry before it is not a mapping; or a rule before it has neither a `hostname` nor a `path`, which matches everything and makes every later rule dead (a path-only rule is fine) |
|
|
642
|
+
| `origin-url` | error | a rule with a `hostname` whose `service` is missing or is neither a URL (`http`, `https`, `ws`, `wss`, `tcp`, `ssh`, `rdp`, `smb`, with a host) nor `http_status:<3 digits>`, `hello_world`, `bastion`, `unix:<path>` or `unix+tls:<path>`; the value is never echoed, since a service URL may carry credentials |
|
|
643
|
+
| `duplicate-hostname` | error | the same hostname (case-insensitive, a trailing dot ignored) **and** the same `path` twice: the later rule is dead. The same hostname with different paths is normal |
|
|
644
|
+
| `no-tls-verify` | warn | `noTLSVerify: true` in the top-level `originRequest` or a rule's |
|
|
645
|
+
|
|
646
|
+
The file is read as **YAML 1.1, which includes merge keys**, the dialect the preflight (PyYAML)
|
|
647
|
+
reads: `noTLSVerify: yes` and `on` are true, `<<: *defaults` merges, anchors and aliases resolve (an
|
|
648
|
+
aliased `originRequest` still warns), and the parser's alias limit refuses a bomb. Two differences
|
|
649
|
+
from PyYAML: the library also reads 1.1's `y` and `n` as booleans, and a duplicate key is refused
|
|
650
|
+
(`DUPLICATE_KEY`) where PyYAML keeps the last. `hostnames` is de-duplicated here (case-insensitive, a
|
|
651
|
+
trailing dot ignored); the preflight's list is passed on as it reported it.
|
|
652
|
+
- **`"preflight"`**, when `tunnel_preflight` is set and Python resolves: the agent runs Tooling's
|
|
653
|
+
`console_tunnel_preflight.py` and returns its findings (the security lint). The command is
|
|
654
|
+
`<python> <tunnel_preflight> --json --config <tunnel_config>`, spawned from an argument array with
|
|
655
|
+
no shell, in the script's own directory, with a 4 s timeout (a call that hits it and then meets a slow
|
|
656
|
+
config read can take the sum of the two deadlines). **Lint only: `--probe` is never passed**, so the
|
|
657
|
+
preflight makes no network call. Its output is **capped at 1,048,576 characters while it is
|
|
658
|
+
collected** (the child is killed past that; `output over 1048576 characters` in the reason), so a
|
|
659
|
+
runaway script cannot grow the agent's memory. Python is the first of `python3`,
|
|
660
|
+
`python` on PATH (`python`, `py` on Windows). Only exit codes `0` and `1` (no errors / errors found)
|
|
661
|
+
carry a report, and it must be JSON with `apiVersion: 1`; the agent counts `errors` and `warns` from
|
|
662
|
+
the findings (never the payload's own numbers), reports `ok` only when the payload says so and no
|
|
663
|
+
finding is an error, and passes only `id`/`severity`/`message`/`location` strings on, bounded as
|
|
664
|
+
above. **Anything else** (no Python, a launch failure, a timeout, output over the cap, exit `2`, junk
|
|
665
|
+
output, another API version, a relative `tunnel_preflight`) falls back to the built-in lint, with a
|
|
666
|
+
`preflight-unusable` warning as the first finding that names why, so a broken hand-off is visible and
|
|
667
|
+
not silently downgraded. The why is one line: an exit-`2` `error` payload is cut to its first line,
|
|
668
|
+
and a failed child's stderr to its exception line (or first line), because PyYAML's errors put a
|
|
669
|
+
snippet of the config after the first line. That first line can still quote a short token of the
|
|
670
|
+
config (a tag or alias name), at most 200 characters of it.
|
|
671
|
+
|
|
672
|
+
Both paths are absolute-only (see **Config and state directories**). What the verb returns is counts,
|
|
673
|
+
hostnames and finding text, never the file itself. The built-in lint never echoes a `service` value;
|
|
674
|
+
the preflight's own finding messages are passed through as the script wrote them (cut as above), and
|
|
675
|
+
**may quote config values, service URLs included**. The agent logs one line per call, `tunnel lint:
|
|
676
|
+
<engine> ok=<bool> errors=<n> warns=<n> hostnames=<n>` or `tunnel lint: unavailable (<reason>)`, with
|
|
677
|
+
any line break in a path flattened to a space.
|
|
678
|
+
|
|
553
679
|
## Health: `claudeDesktop`
|
|
554
680
|
|
|
555
681
|
`GET /v1/health` carries `claudeDesktop: boolean` (#457) — whether this machine can open a
|