@rackbops/ac-agent 2.0.0-alpha.54 → 2.0.0-alpha.56
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 +82 -15
- package/data/README.md +21 -20
- package/dist/ac-agent.mjs +230 -124
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@ one-time code exchanged for a per-machine token, #48), `repos` list/status/pull
|
|
|
10
10
|
as are the OneDrive mirror (`publish/mirror`, #53) and the tunnel-config lint (`tunnel/lint`, #416).
|
|
11
11
|
Every verb also
|
|
12
12
|
requires a per-plugin capability grant, pushed by the console and enforced here (#50) -- see
|
|
13
|
-
**Verbs** and **Security model** below. `GET /v1/preflight` (#55) -- git/gh/node/claude
|
|
14
|
-
chromium only resolved -- is control plane, not a verb; see **Preflight**.
|
|
13
|
+
**Verbs** and **Security model** below. `GET /v1/preflight` (#55) -- git/gh/node/claude and (#828)
|
|
14
|
+
docker/cloudflared executed, chromium only resolved -- is control plane, not a verb; see **Preflight**.
|
|
15
15
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
@@ -27,6 +27,26 @@ resolves to the newest release. Two older tags are frozen: `alpha` (the very fir
|
|
|
27
27
|
`2.0.0-alpha.40`. Nothing moves either any more, and the maintainer removes or moves them by hand.
|
|
28
28
|
`latest` is expected to go back to stable-only once the first stable `2.0.0` ships.
|
|
29
29
|
|
|
30
|
+
**Node 26 or newer** (`engines.node: ">=26"`, #827), the fleet's one Node line and the console image's
|
|
31
|
+
major. `npx` treats `engines` only as a warning, so the agent checks for itself before any verb: on an
|
|
32
|
+
older Node every verb (`install`, `status`, `stop` and `uninstall` included) does nothing and exits 2,
|
|
33
|
+
its stderr ending with the line `ac-agent requires Node >=26, found <version>`. On Ubuntu, the
|
|
34
|
+
distribution's own `nodejs` is older; Node 26 comes from NodeSource's `node_26.x` apt repository
|
|
35
|
+
(`https://deb.nodesource.com/node_26.x`).
|
|
36
|
+
|
|
37
|
+
Move a machine's Node to 26 **before** its agent updates to a release that carries this floor. The
|
|
38
|
+
console's update (`POST /v1/update`, below) checks the staged bundle by running its `version --json`
|
|
39
|
+
with the agent's own Node, so on an older Node the update is refused (422, naming that line) and the
|
|
40
|
+
running agent stays as it was. A plain `npx @rackbops/ac-agent install` on an older Node is refused the
|
|
41
|
+
same way, before anything is copied or registered.
|
|
42
|
+
|
|
43
|
+
- **The installed task or unit keeps the Node `install` ran under**, as an absolute path
|
|
44
|
+
(`process.execPath`). An apt or MSI upgrade in place moves that Node with it. A version manager
|
|
45
|
+
(fnm, nvm, volta) does not: after switching it to 26, run `install` again under 26, or the agent
|
|
46
|
+
keeps running the old Node and its updates are refused with that line.
|
|
47
|
+
- **To stop or uninstall from a machine still below 26**, run a release from before the floor, e.g.
|
|
48
|
+
`npx @rackbops/ac-agent@2.0.0-alpha.55 stop`, or move the machine's Node first.
|
|
49
|
+
|
|
30
50
|
### Upgrade with the Tooling script
|
|
31
51
|
|
|
32
52
|
For an existing install, an up-to-date Tooling checkout provides
|
|
@@ -61,8 +81,9 @@ tarball, and reads target health, but runs no install, `scp`, or mutating SSH co
|
|
|
61
81
|
Run Windows `-Local` from a [normal PowerShell window](#run-install-from-a-normal-shell). Every
|
|
62
82
|
Windows machine runs `-Local` for itself; remote Windows installation is unsupported. `gh` must have
|
|
63
83
|
access to the release repository. Linux targets need working noninteractive SSH and `node`/`npx` on
|
|
64
|
-
their `PATH
|
|
65
|
-
|
|
84
|
+
their `PATH`; the target's own Node runs the new bundle, so a release that carries the Node 26 floor
|
|
85
|
+
needs Node 26 there too (see above). `-Port` changes only the health-check port (default `8788`); it
|
|
86
|
+
does not reconfigure the agent.
|
|
66
87
|
|
|
67
88
|
### Linux: no special `npx` spelling needed (#278)
|
|
68
89
|
|
|
@@ -416,10 +437,24 @@ capability, plugin}`. See **Security model** for how a grant reaches the agent i
|
|
|
416
437
|
`owner/name` resolved from its `origin` remote (`git remote get-url origin`, parsed the way 1.x's
|
|
417
438
|
`init_config.py:parse_remote` does), or `null` when there's no remote or it isn't a GitHub-shaped
|
|
418
439
|
URL — never an error. This makes the route spawn one `git remote get-url` per repo (parallelized),
|
|
419
|
-
so it is no longer git-free the way it was before #304.
|
|
440
|
+
so it is no longer git-free the way it was before #304. **All three repo routes need git**: they
|
|
441
|
+
look for it first (see below), and a machine without it answers
|
|
442
|
+
`502 {ok: false, error: "git not found on PATH"}` (or, when PATH holds only a `.cmd`/`.bat` shim,
|
|
443
|
+
`git is a .cmd shim; install the native binary`) before any repo work, a bad `:name` included.
|
|
420
444
|
- **`GET /v1/repos/:name/status`** (`agent.git.read`) → `{ok, name, path, ownerName, branch,
|
|
421
445
|
defaultBranch, upstream, ahead, behind, dirty, inProgress}` (`ownerName` as above), read-only (no
|
|
422
446
|
fetch — `pull` is what refreshes the refs `status` reads).
|
|
447
|
+
- **git is run by absolute path** (#699). Each of the three requests resolves git once, on the
|
|
448
|
+
refreshed PATH (the registry's on Windows, the process PATH elsewhere: the search the preflight and
|
|
449
|
+
the toolchain upgrade use), and every git spawn of that request names the path it found, made
|
|
450
|
+
absolute if PATH gave a relative entry (`exec/claude` does the same for `claude`, the other spawn
|
|
451
|
+
whose working directory a request chooses). This matters on Windows, where a bare `git` is looked for in the
|
|
452
|
+
spawn's working directory first (`git.com`, then `git.exe`), and every call here has a repo checkout
|
|
453
|
+
as its working directory: a `git.exe` that a clone, a branch or a fast-forward put in the repo's root
|
|
454
|
+
would otherwise run as the agent's user, unattended (the daily `repo.pull` job reaches it). A bare
|
|
455
|
+
name is looked up on PATH only elsewhere, so Linux was not exposed to that; a relative PATH entry is
|
|
456
|
+
the one way a working directory could matter there, and the absolute path closes it too. See
|
|
457
|
+
**Security model** for the second, general layer.
|
|
423
458
|
- **`POST /v1/repos/:name/pull`** (`agent.git.pull`), body `{dryRun?: boolean}` → `{ok: true, row}`
|
|
424
459
|
where `row.status` is `updated | would | current | skipped | error` and `row.reason` names which
|
|
425
460
|
of the six 1.x-derived guards applied (`dirty`, `branch`, `ahead`, `in-progress`, `no-upstream`,
|
|
@@ -429,7 +464,8 @@ capability, plugin}`. See **Security model** for how a grant reaches the agent i
|
|
|
429
464
|
timeout.
|
|
430
465
|
- **`:name` is an allowlist, not a path** — it must match a machine-generated repo name pattern, must
|
|
431
466
|
not be `.`/`..`, and must actually be one of the entries `GET /v1/repos` would list; anything else
|
|
432
|
-
is `404 {"error": "no such repo"}` before any git call runs
|
|
467
|
+
is `404 {"error": "no such repo"}` before any git call runs (on a machine with git: without it the
|
|
468
|
+
`502` above comes first).
|
|
433
469
|
- **`POST /v1/open`** (`agent.desktop.open`), body `{url}` → `{ok: true}`. Only a `https:` or
|
|
434
470
|
`claude:` URL is accepted (the console's own deep-link shape,
|
|
435
471
|
`claude://code/new?q=<prompt>&folder=<path>`); anything else is `400` before any process spawns.
|
|
@@ -479,8 +515,8 @@ capability, plugin}`. See **Security model** for how a grant reaches the agent i
|
|
|
479
515
|
string. Tokens are a byte estimate (~4 chars/token; the payload says `estimated: true`); only
|
|
480
516
|
sizes and line counts ever leave this scan -- file content is never read into the payload.
|
|
481
517
|
- **`toolchain-inventory`** (#52, 52b; `timeoutMs: 600_000` -- about seventy version probes plus a
|
|
482
|
-
WSL probe) ships and runs 1.x's own `Get-ToolchainInventory.ps1` (vendored from 1.x
|
|
483
|
-
|
|
518
|
+
WSL probe) ships and runs 1.x's own `Get-ToolchainInventory.ps1` (vendored byte for byte from 1.x
|
|
519
|
+
`main` at `5de373e`, `data/README.md`) through `pwsh -NoProfile -File <script> -OutFile <state>/scans/
|
|
484
520
|
toolchain-inventory-<hostname>.md -NoRender`, with the child's `cwd` set to `<state>/scans` --
|
|
485
521
|
**never a project directory**, since the script's own `pnpm --version` probe rewrites
|
|
486
522
|
`pnpm-lock.yaml` in whatever directory it runs in. **Windows-only**: on any other platform this
|
|
@@ -773,7 +809,7 @@ anything starts; a request that fails is `400 {ok: false, error: "<field>: <why>
|
|
|
773
809
|
| Field | Meaning | Must be |
|
|
774
810
|
|---|---|---|
|
|
775
811
|
| `tool` | the standard's tool key; the job's label and the audit lines only, never part of a command | `^[A-Za-z0-9][A-Za-z0-9 ._+-]{0,63}$` (words, spaces allowed as in `GitHub CLI`: no `"`, `:`, `(`, `)`, `>` or `,`, and the audit lines print it in quotes, so one run's line cannot imitate another's outcome) |
|
|
776
|
-
| `cmd` | the tool whose `--version` is read before and after; found with `which`, run for nothing else | one of **`git`, `gh`, `node`, `claude
|
|
812
|
+
| `cmd` | the tool whose `--version` is read before and after; found with `which`, run for nothing else | one of **`git`, `gh`, `node`, `claude`** (`VERSION_READABLE_CMDS`, an explicit list): tools the agent's own preflight already runs `--version` on, which are the `cmd`s of the preflight rows the Environment panel sends. The preflight also executes `docker` and `cloudflared` (#828), but those rows are graded only: upgrading either can restart the host's containers or tunnels, so the verb refuses them with the error below (its wording predates #828), and adding a preflight row never widens this list. Any other name is `400 cmd: not a tool this agent reads the version of` |
|
|
777
813
|
| `manager` | which package manager | `winget` (Windows only), `apt` (Linux only) or `native` |
|
|
778
814
|
| `packageId` | the package | `winget`: `^[A-Za-z0-9][A-Za-z0-9.+_-]{0,127}$` with a `.` (a `Publisher.Name` id); `apt`: the Debian package-name grammar `^[a-z0-9][a-z0-9+.-]{1,127}$`, not ending in `-`; `native`: exactly `claude` (and `cmd` exactly `claude`) |
|
|
779
815
|
|
|
@@ -913,7 +949,7 @@ for as long as that file is, not for ever.
|
|
|
913
949
|
foreground `serve`, or a task/unit whose own environment sets it). `status` shows what a start would
|
|
914
950
|
load; the running agent's `403 not-opted-in` or its first `202` is the proof. Then grant
|
|
915
951
|
`agent.toolchain.upgrade` to the agents plugin from the Grants grid; the plugin declares that capability
|
|
916
|
-
|
|
952
|
+
(the console's PR 2 for #675), so the grid has a row for it once the console runs that release.
|
|
917
953
|
- **Linux**: `sudo -n` cannot ask for a password, so the agent's user needs a sudoers rule scoped to this
|
|
918
954
|
one form. In `/etc/sudoers.d/ac-agent-upgrade`, mode `0440`, checked with `visudo -c -f`:
|
|
919
955
|
|
|
@@ -992,7 +1028,7 @@ no `X-AC-Plugin`/`X-AC-Grants-Revision` headers, and refuses a caller carrying `
|
|
|
992
1028
|
`403 {ok: false, error: "preflight is the console's, not a plugin's"}`. The console's own
|
|
993
1029
|
`POST /api/agents/:id/preflight` (`packages/host`) relays it into the Agents panel's Environment
|
|
994
1030
|
section. Live and stateless: nothing is cached on either side, and a call answers `200 {ok: true,
|
|
995
|
-
machine, checkedAt, searchPath: "registry" | "process", rows}`,
|
|
1031
|
+
machine, checkedAt, searchPath: "registry" | "process", rows}`, seven rows always, in this order:
|
|
996
1032
|
|
|
997
1033
|
| `cmd` | required | executed | verdicts |
|
|
998
1034
|
|---|---|---|---|
|
|
@@ -1000,16 +1036,25 @@ machine, checkedAt, searchPath: "registry" | "process", rows}`, five rows always
|
|
|
1000
1036
|
| `gh` | yes | `gh --version`, then `gh auth status` | `ok` \| `missing` \| `broken` \| `off-path` \| `unauthenticated` |
|
|
1001
1037
|
| `node` | yes | `node --version` | `ok` \| `missing` \| `broken` \| `off-path` |
|
|
1002
1038
|
| `claude` | yes | `claude --version` | `ok` \| `missing` \| `broken` \| `off-path` |
|
|
1039
|
+
| `docker` | no | `docker --version` | `ok` \| `missing` \| `broken` \| `off-path` |
|
|
1040
|
+
| `cloudflared` | no | `cloudflared --version` | `ok` \| `missing` \| `broken` \| `off-path` |
|
|
1003
1041
|
| `chromium` | no | never | `ok` \| `missing` |
|
|
1004
1042
|
|
|
1043
|
+
The `docker` and `cloudflared` rows (#828) are two of the fleet standard's tools: the console host and
|
|
1044
|
+
the server stacks run in Docker, and cloudflared runs the tunnels that put them behind Cloudflare
|
|
1045
|
+
Access. Both are optional: a missing one is amber, never a failed preflight. The standard applies Docker
|
|
1046
|
+
to `linux-server` and `windows-dev` hosts and cloudflared to `linux-server` only, and the Environment
|
|
1047
|
+
panel grades them there, but never offers to upgrade them: `POST /v1/toolchain/upgrade` refuses both as
|
|
1048
|
+
a `cmd`.
|
|
1049
|
+
|
|
1005
1050
|
Each row is `{cmd, label, status, required, path, why, version, detail}` -- `version` the first
|
|
1006
1051
|
non-empty line of `--version`'s stdout (CRLF handled), `""` otherwise; `detail` status-dependent
|
|
1007
1052
|
free text (a `broken` row's last non-empty output line, `unauthenticated`'s
|
|
1008
1053
|
`run gh auth login [-- <stderr line>]`, `off-path`'s `found under a version manager, not on PATH`),
|
|
1009
1054
|
`""` when there is nothing to add.
|
|
1010
1055
|
|
|
1011
|
-
- **Executed, not just resolved.** `git`/`gh`/`node`/`claude` are resolved on
|
|
1012
|
-
(below) AND run (`<exe> --version`, a **15 s** timeout per spawn); `gh` additionally runs
|
|
1056
|
+
- **Executed, not just resolved.** `git`/`gh`/`node`/`claude`/`docker`/`cloudflared` are resolved on
|
|
1057
|
+
the refreshed PATH (below) AND run (`<exe> --version`, a **15 s** timeout per spawn); `gh` additionally runs
|
|
1013
1058
|
`gh auth status` (needs the network -- offline it exits non-zero and the row reads
|
|
1014
1059
|
`unauthenticated` with the stderr line saying why, since a real, validated token is what bullet 2
|
|
1015
1060
|
wants, not merely a cached one `gh auth token` would report on regardless). `chromium` is
|
|
@@ -1024,7 +1069,7 @@ free text (a `broken` row's last non-empty output line, `unauthenticated`'s
|
|
|
1024
1069
|
Linux the process PATH plus every version-manager bin directory (`fnm`/`nvm`/`volta`/`asdf`) is
|
|
1025
1070
|
searched as a fallback, so a tool invisible to a bare `which` reads `off-path`, never `missing`.
|
|
1026
1071
|
`searchPath` on the report names which: `"registry"` or `"process"`.
|
|
1027
|
-
- **The whole check answers in a few seconds.**
|
|
1072
|
+
- **The whole check answers in a few seconds.** The seven rows run concurrently; the worst case is
|
|
1028
1073
|
`gh`'s `--version` answering just under 15 s and then `auth status` timing out at 15 s too --
|
|
1029
1074
|
under the console's own 100 s edge-request budget behind Cloudflare Access.
|
|
1030
1075
|
|
|
@@ -1113,9 +1158,31 @@ Every refusal, the verified hand-off, and a failed hand-off are logged as
|
|
|
1113
1158
|
independent gates (the console's grant, and the machine owner's own `toolchain_upgrade` key in
|
|
1114
1159
|
`agent.toml`, off by default), upgrades with only three fixed command shapes and a validated package
|
|
1115
1160
|
id (the one other program a request can name, `cmd`, is limited to four tools whose `--version` the
|
|
1116
|
-
preflight already runs
|
|
1161
|
+
preflight already runs: an explicit list, so the preflight's `docker` and `cloudflared` rows are not
|
|
1162
|
+
among them), takes its elevation from the machine's own configuration rather than ever
|
|
1117
1163
|
elevating itself, and logs every run. See **`POST /v1/toolchain/upgrade`**, including what the Linux
|
|
1118
1164
|
sudoers rule it needs actually grants.
|
|
1165
|
+
- **The agent does not run an executable it finds in a request's working directory** (#699), in two
|
|
1166
|
+
layers, neither of which replaces the other. (1) git (the repo verbs), `gh`, `claude`, the browser and
|
|
1167
|
+
`winget`/`sudo`/`dpkg-query` are found first and spawned by their absolute path (git and `claude`,
|
|
1168
|
+
the two whose working directory a request chooses, have a relative PATH entry made absolute first:
|
|
1169
|
+
layer (2) does not cover a relative name that carries a directory, only a bare one), and `reg.exe` and
|
|
1170
|
+
`taskkill.exe` are built from `%SystemRoot%\System32`; the system launchers that never run in a
|
|
1171
|
+
request's directory are still bare names: `rundll32.exe` and `powershell.exe` on Windows, which layer (2)
|
|
1172
|
+
covers, and `xdg-open`, `xdg-mime`, `systemctl`, `loginctl` and `systemd-run` on Linux, which has no
|
|
1173
|
+
default working-directory search for a bare name. (The tunnel lint runs its interpreter in the
|
|
1174
|
+
directory of the operator-configured preflight script, which is the script it runs anyway.)
|
|
1175
|
+
(2) On Windows the agent process defines
|
|
1176
|
+
`NoDefaultCurrentDirectoryInExePath=1` in its own environment as the first thing `ac-agent` does (every
|
|
1177
|
+
command: `install`, `serve`, `status`, ...), unless that variable is already set. Windows' (and
|
|
1178
|
+
libuv's) search for a bare executable name then no longer looks in the spawn's working directory
|
|
1179
|
+
first, for any bare-name spawn anywhere in the process, and every child inherits the variable. The
|
|
1180
|
+
variable is the agent's own addition to its environment, so a child (git and its credential
|
|
1181
|
+
helpers, `ssh`) sees it too; it only turns the current-directory executable search off, which is
|
|
1182
|
+
also what a Windows shell that has the variable set does: a script a child runs (an npm script, say)
|
|
1183
|
+
that relies on a bare `build.bat` in its working directory, without `.\`, stops finding it. No agent
|
|
1184
|
+
code depends on its value, and it does nothing off Windows. A machine that already defines it with a
|
|
1185
|
+
value is left as it is (an empty value counts as unset and is replaced).
|
|
1119
1186
|
- **Every other route requires `Authorization: Bearer <token>`**, compared with a
|
|
1120
1187
|
constant-time equality check against a stored hash (never the token itself). Before pairing,
|
|
1121
1188
|
every such route answers `401 {"error": "not paired"}`; a present-but-wrong token answers
|
package/data/README.md
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
# `packages/agent/data/`
|
|
2
2
|
|
|
3
|
-
Vendored copies of source files owned by other repos
|
|
4
|
-
|
|
3
|
+
Vendored copies of source files owned by other repos, unmodified, embedded into the agent's bundle as
|
|
4
|
+
text (see `../text-plugin.mjs`).
|
|
5
5
|
|
|
6
6
|
## `Get-ToolchainInventory.ps1`
|
|
7
7
|
|
|
8
8
|
A copy of `artifact-console/data/Get-ToolchainInventory.ps1` from
|
|
9
9
|
[`Rackbops/artifacts-console`](https://github.com/Rackbops/artifacts-console) (the 1.x app), pinned
|
|
10
|
-
to commit `
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
that repo, `git -C <artifacts-console checkout> show 8e33836:artifact-console/data/Get-ToolchainInventory.ps1 | diff - <this repo>/packages/agent/data/Get-ToolchainInventory.ps1`
|
|
14
|
-
prints exactly them, and `8e33836`'s own sha256 is `76b6a712c27f34ea9dc4cec230fc7107fd3c88720e8f24c0c2c933f2a685fea0`.
|
|
10
|
+
to commit `5de373e` on that repo's `main`, byte-identical, with no local addition. From a checkout of
|
|
11
|
+
that repo, `git -C <artifacts-console checkout> show 5de373e:artifact-console/data/Get-ToolchainInventory.ps1 | diff - <this repo>/packages/agent/data/Get-ToolchainInventory.ps1`
|
|
12
|
+
prints nothing.
|
|
15
13
|
|
|
16
14
|
- 1,832 lines, 102,154 bytes
|
|
17
15
|
- LF line endings, no BOM, ASCII-only
|
|
@@ -21,20 +19,18 @@ The hash is platform-stable because both repos' `.gitattributes` normalize this
|
|
|
21
19
|
`text=auto eol=lf`, and `toolchain-inventory.test.ts` asserts the embedded text hashes to the value
|
|
22
20
|
above -- a re-vendored script whose bytes moved fails that test rather than silently drifting.
|
|
23
21
|
|
|
24
|
-
**Re-copy, never edit
|
|
22
|
+
**Re-copy, never edit.** The script is shipped and run as-is
|
|
25
23
|
(`packages/agent/src/scans/toolchain-inventory.ts`); a local edit here would break the byte-compare
|
|
26
|
-
against 1.x's own output, which is the entire point of vendoring rather than rewriting it. The
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
1.x's `v1-maintenance` branch carries the same row
|
|
30
|
-
|
|
31
|
-
section, so it is not a source to re-copy from.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
tests (`toolchain-inventory.test.ts`) -- they also pin the script's catalog names and schema-version
|
|
37
|
-
literal, so a re-vendor whose shape moved is caught there, not discovered on a card.
|
|
24
|
+
against 1.x's own output, which is the entire point of vendoring rather than rewriting it. The
|
|
25
|
+
`Claude Code CLI` catalog row (after `Docker`) and its `$refLinks` entry, which this agent's
|
|
26
|
+
inventory reports for Rackbops/artifact-console#673, come from 1.x `main` itself since
|
|
27
|
+
artifacts-console#755. 1.x's `v1-maintenance` branch carries the same row since
|
|
28
|
+
artifacts-console#750, but that branch's script is an older revision without the `## Containers`
|
|
29
|
+
section, so it is not a source to re-copy from. To pick up a newer 1.x revision: copy the file from
|
|
30
|
+
the new `main` commit, update the commit hash and the three numbers above, and re-run the
|
|
31
|
+
embedding/drift-guard tests (`toolchain-inventory.test.ts`) -- they pin the script's catalog names
|
|
32
|
+
(the `Claude Code CLI` row included) and schema-version literal, so a re-vendor whose shape moved is
|
|
33
|
+
caught there, not discovered on a card.
|
|
38
34
|
|
|
39
35
|
**Provenance note (Tooling#721 F3, landed 2026-09-29):** re-pinned from `7280ae6` to `8e33836`
|
|
40
36
|
(`git diff --stat 7280ae6 8e33836 -- artifact-console/data/Get-ToolchainInventory.ps1`: 456
|
|
@@ -53,3 +49,8 @@ re-vendor changes for a machine with no such WSL/docker setup (the realistic cas
|
|
|
53
49
|
today) is nothing. Confirmed by `toolchain-inventory.test.ts`'s existing suite passing unchanged (no
|
|
54
50
|
test needed updating for this sync) -- the script's catalog and schema-version literal, which that
|
|
55
51
|
suite pins directly, are both identical to before.
|
|
52
|
+
|
|
53
|
+
**Provenance note (2026-10-09):** re-pinned from `8e33836` plus the six local `Claude Code CLI`
|
|
54
|
+
lines (Rackbops/artifact-console#673) to `5de373e`, byte-identical, via artifacts-console#755 (the
|
|
55
|
+
1.x `main` twin of artifacts-console#750, for artifacts-console#754). `git hash-object` gives
|
|
56
|
+
`50ee5b9ad2cf7ae3722d438bb956daedc322df71` for both files, so the three numbers above are unchanged.
|
package/dist/ac-agent.mjs
CHANGED
|
@@ -47,7 +47,7 @@ function _mergeNamespaces(n, m) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
var name = "@rackbops/ac-agent";
|
|
50
|
-
var version = "2.0.0-alpha.
|
|
50
|
+
var version = "2.0.0-alpha.56";
|
|
51
51
|
var type = "module";
|
|
52
52
|
var description = "The per-machine host agent (installed with npx @rackbops/ac-agent): an HTTPS health server plus install/status/uninstall for a Windows logon task or a systemd user unit.";
|
|
53
53
|
var bin = {
|
|
@@ -59,7 +59,7 @@ var files = [
|
|
|
59
59
|
"data/"
|
|
60
60
|
];
|
|
61
61
|
var engines = {
|
|
62
|
-
node: ">=
|
|
62
|
+
node: ">=26"
|
|
63
63
|
};
|
|
64
64
|
var publishConfig = {
|
|
65
65
|
access: "public"
|
|
@@ -119,7 +119,9 @@ function expandVars(value, env) {
|
|
|
119
119
|
}
|
|
120
120
|
/** Where `reg.exe` lives on this machine (`%SystemRoot%\System32`, `C:\Windows` when the process
|
|
121
121
|
* environment carries no `SystemRoot`). Shared with `claude-desktop.ts` (#457), so the two registry
|
|
122
|
-
* readers never disagree about which binary they launch.
|
|
122
|
+
* readers never disagree about which binary they launch. Absolute, not `reg.exe`, because Windows looks
|
|
123
|
+
* for a bare name in the working directory first; `applySpawnGuard` (cli.ts, #699) now stops the whole
|
|
124
|
+
* process doing that, and this stays absolute so neither layer is the only protection. */
|
|
123
125
|
function regExePath(env) {
|
|
124
126
|
const systemRoot = env.SystemRoot ?? String.raw `C:\Windows`;
|
|
125
127
|
return path.win32.join(systemRoot, "System32", "reg.exe");
|
|
@@ -21540,8 +21542,8 @@ const GIT_TIMEOUT_MS = 120_000;
|
|
|
21540
21542
|
// should cost that request single-digit seconds, not the full network-call budget.
|
|
21541
21543
|
const ORIGIN_TIMEOUT_MS = 10_000;
|
|
21542
21544
|
const GIT_ENV = { GIT_TERMINAL_PROMPT: "0" };
|
|
21543
|
-
async function git(
|
|
21544
|
-
return run(
|
|
21545
|
+
async function git(exec, cwd, args, timeoutMs = GIT_TIMEOUT_MS) {
|
|
21546
|
+
return exec.run(exec.cmd, args, { cwd, timeoutMs, env: GIT_ENV });
|
|
21545
21547
|
}
|
|
21546
21548
|
/** 1.x `provenance.py:53-55`'s lesson: a git *worktree*'s marker files sit directly in its own
|
|
21547
21549
|
* git-dir the same way a plain clone's do, so an existence check (not an is-directory check)
|
|
@@ -21608,26 +21610,26 @@ function parseRemoteOwnerName(url) {
|
|
|
21608
21610
|
}
|
|
21609
21611
|
/** The checkout's `origin` remote URL, trimmed, or null when there is none or the call fails --
|
|
21610
21612
|
* never thrown. */
|
|
21611
|
-
async function originUrl(repoPath,
|
|
21612
|
-
const result = await git(
|
|
21613
|
+
async function originUrl(repoPath, exec) {
|
|
21614
|
+
const result = await git(exec, repoPath, ["remote", "get-url", "origin"], ORIGIN_TIMEOUT_MS);
|
|
21613
21615
|
if (result.code !== 0)
|
|
21614
21616
|
return null;
|
|
21615
21617
|
return result.stdout.trim();
|
|
21616
21618
|
}
|
|
21617
|
-
async function repoStatus(repoPath,
|
|
21618
|
-
const gitDirResult = await git(
|
|
21619
|
+
async function repoStatus(repoPath, exec, exists) {
|
|
21620
|
+
const gitDirResult = await git(exec, repoPath, ["rev-parse", "--absolute-git-dir"]);
|
|
21619
21621
|
if (gitDirResult.code !== 0)
|
|
21620
21622
|
return { ok: false, error: `git: ${gitDirResult.stderr}` };
|
|
21621
|
-
const headResult = await git(
|
|
21623
|
+
const headResult = await git(exec, repoPath, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
21622
21624
|
if (headResult.code !== 0)
|
|
21623
21625
|
return { ok: false, error: `git: ${headResult.stderr}` };
|
|
21624
|
-
const originHeadResult = await git(
|
|
21625
|
-
const upstreamResult = await git(
|
|
21626
|
+
const originHeadResult = await git(exec, repoPath, ["rev-parse", "--abbrev-ref", "origin/HEAD"]);
|
|
21627
|
+
const upstreamResult = await git(exec, repoPath, ["rev-parse", "--abbrev-ref", "@{upstream}"]);
|
|
21626
21628
|
const hasUpstream = upstreamResult.code === 0;
|
|
21627
21629
|
const countsResult = hasUpstream
|
|
21628
|
-
? await git(
|
|
21630
|
+
? await git(exec, repoPath, ["rev-list", "--left-right", "--count", "@{upstream}...HEAD"])
|
|
21629
21631
|
: { code: 1, stdout: ""};
|
|
21630
|
-
const porcelainResult = await git(
|
|
21632
|
+
const porcelainResult = await git(exec, repoPath, ["status", "--porcelain"]);
|
|
21631
21633
|
if (porcelainResult.code !== 0)
|
|
21632
21634
|
return { ok: false, error: `git: ${porcelainResult.stderr}` };
|
|
21633
21635
|
const { behind, ahead } = counts(countsResult.stdout.trim(), countsResult.code);
|
|
@@ -21642,7 +21644,7 @@ async function repoStatus(repoPath, run, exists) {
|
|
|
21642
21644
|
inProgress: inProgress(gitDirResult.stdout.trim(), exists),
|
|
21643
21645
|
};
|
|
21644
21646
|
}
|
|
21645
|
-
async function pullRepo(name, repoPath,
|
|
21647
|
+
async function pullRepo(name, repoPath, exec, options, exists) {
|
|
21646
21648
|
const skip = (reason, detail, behind = 0, ahead = 0) => ({
|
|
21647
21649
|
name,
|
|
21648
21650
|
path: repoPath,
|
|
@@ -21652,10 +21654,10 @@ async function pullRepo(name, repoPath, run, options, exists) {
|
|
|
21652
21654
|
behind,
|
|
21653
21655
|
ahead,
|
|
21654
21656
|
});
|
|
21655
|
-
const gitDirResult = await git(
|
|
21657
|
+
const gitDirResult = await git(exec, repoPath, ["rev-parse", "--absolute-git-dir"]);
|
|
21656
21658
|
if (gitDirResult.code !== 0)
|
|
21657
21659
|
return skip("not-a-repo", "no git checkout here");
|
|
21658
|
-
const fetchResult = await git(
|
|
21660
|
+
const fetchResult = await git(exec, repoPath, ["fetch", "--prune"]);
|
|
21659
21661
|
if (fetchResult.code !== 0) {
|
|
21660
21662
|
return {
|
|
21661
21663
|
name,
|
|
@@ -21670,20 +21672,20 @@ async function pullRepo(name, repoPath, run, options, exists) {
|
|
|
21670
21672
|
ahead: 0,
|
|
21671
21673
|
};
|
|
21672
21674
|
}
|
|
21673
|
-
const upstreamResult = await git(
|
|
21675
|
+
const upstreamResult = await git(exec, repoPath, ["rev-parse", "--abbrev-ref", "@{upstream}"]);
|
|
21674
21676
|
const hasUpstream = upstreamResult.code === 0;
|
|
21675
21677
|
const countsResult = hasUpstream
|
|
21676
|
-
? await git(
|
|
21678
|
+
? await git(exec, repoPath, ["rev-list", "--left-right", "--count", "@{upstream}...HEAD"])
|
|
21677
21679
|
: { code: 1, stdout: ""};
|
|
21678
21680
|
const { behind, ahead } = counts(countsResult.stdout.trim(), countsResult.code);
|
|
21679
|
-
const headResult = await git(
|
|
21681
|
+
const headResult = await git(exec, repoPath, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
21680
21682
|
const branch = headResult.stdout.trim();
|
|
21681
|
-
const originHeadResult = await git(
|
|
21683
|
+
const originHeadResult = await git(exec, repoPath, ["rev-parse", "--abbrev-ref", "origin/HEAD"]);
|
|
21682
21684
|
const defaultBr = defaultBranch(originHeadResult.stdout.trim(), originHeadResult.code);
|
|
21683
21685
|
if (inProgress(gitDirResult.stdout.trim(), exists)) {
|
|
21684
21686
|
return skip("in-progress", "a rebase/merge is underway", behind, ahead);
|
|
21685
21687
|
}
|
|
21686
|
-
const porcelainResult = await git(
|
|
21688
|
+
const porcelainResult = await git(exec, repoPath, ["status", "--porcelain"]);
|
|
21687
21689
|
if (porcelainResult.stdout.trim() !== "") {
|
|
21688
21690
|
return skip("dirty", "uncommitted changes", behind, ahead);
|
|
21689
21691
|
}
|
|
@@ -21720,7 +21722,7 @@ async function pullRepo(name, repoPath, run, options, exists) {
|
|
|
21720
21722
|
ahead: 0,
|
|
21721
21723
|
};
|
|
21722
21724
|
}
|
|
21723
|
-
const mergeResult = await git(
|
|
21725
|
+
const mergeResult = await git(exec, repoPath, ["merge", "--ff-only", "@{upstream}"]);
|
|
21724
21726
|
if (mergeResult.code !== 0) {
|
|
21725
21727
|
return skip("diverged",
|
|
21726
21728
|
// 1.x's check_repo (repo_pull.py:216) slices the same combined, stripped stdout+stderr text.
|
|
@@ -21737,6 +21739,82 @@ async function pullRepo(name, repoPath, run, options, exists) {
|
|
|
21737
21739
|
};
|
|
21738
21740
|
}
|
|
21739
21741
|
|
|
21742
|
+
/** The absolute path of the executable `findExecutable` found, for a verb that spawns it with a working
|
|
21743
|
+
* directory that is not the agent's own (a repo checkout, #699). `findExecutable` joins a PATH entry to
|
|
21744
|
+
* the tool name as it is, so a PATH with a relative entry (`.`, `bin`) yields `git.exe` or `bin\git.exe`:
|
|
21745
|
+
* existence was checked against the AGENT's working directory, but a spawn resolves a relative name
|
|
21746
|
+
* against its own `cwd`, where a checkout's planted file would be the one that runs. `path.resolve`
|
|
21747
|
+
* names the very file that was checked, and returns an absolute path unchanged. */
|
|
21748
|
+
function absoluteExecutablePath(found) {
|
|
21749
|
+
return path.resolve(found);
|
|
21750
|
+
}
|
|
21751
|
+
/** The text of the `502` a verb answers when the executable it needs cannot be run (#699): the tool is
|
|
21752
|
+
* `name` and the result of looking for it is `found`. A `.cmd`/`.bat` shim on PATH is named as such,
|
|
21753
|
+
* with what to install instead (`nativeBinary` completes "install ..."), and anything else is "not found
|
|
21754
|
+
* on PATH". `gh.ts`, the repo verbs and the toolchain upgrade all build their message with this, so the
|
|
21755
|
+
* wording a client matches on is one function's for those three (`claude.ts` and the preflight still
|
|
21756
|
+
* build their own inline copies of the same kind of text). */
|
|
21757
|
+
function notFoundError(name, found, nativeBinary = "the native binary") {
|
|
21758
|
+
if (!found.found && found.reason === "shim") {
|
|
21759
|
+
return `${name} is a .cmd shim; install ${nativeBinary}`;
|
|
21760
|
+
}
|
|
21761
|
+
return `${name} not found on PATH`;
|
|
21762
|
+
}
|
|
21763
|
+
/** Strips one pair of surrounding double quotes from a PATH entry ("C:\Program Files\Git\cmd" is
|
|
21764
|
+
* legal in a Windows PATH, and `CreateProcess` strips it before use) -- an entry left quoted
|
|
21765
|
+
* would otherwise never match a real file and read as `missing`. Harmless on POSIX, where a real
|
|
21766
|
+
* directory name wrapped in literal quotes is not a thing that happens. */
|
|
21767
|
+
function unquote(entry) {
|
|
21768
|
+
return entry.length >= 2 && entry.startsWith('"') && entry.endsWith('"')
|
|
21769
|
+
? entry.slice(1, -1)
|
|
21770
|
+
: entry;
|
|
21771
|
+
}
|
|
21772
|
+
function findExecutable(name, env, platform, fs) {
|
|
21773
|
+
const pathVar = env.PATH ?? env.Path ?? "";
|
|
21774
|
+
const sep = platform === "win32" ? ";" : ":";
|
|
21775
|
+
// A PATH entry that doesn't exist on disk is skipped by the exists() checks below, never thrown
|
|
21776
|
+
// -- there's nothing here to throw on a plain string split, and exists() is a pure boolean.
|
|
21777
|
+
const dirs = pathVar
|
|
21778
|
+
.split(sep)
|
|
21779
|
+
.filter((d) => d !== "")
|
|
21780
|
+
.map(unquote);
|
|
21781
|
+
// path.win32/path.posix explicitly, never the bare path.join/path.dirname -- those pick their
|
|
21782
|
+
// behavior from the OS this code is actually running on, not from the `platform` parameter, so
|
|
21783
|
+
// testing "linux" logic from a Windows dev box (or vice versa) would silently join with the
|
|
21784
|
+
// wrong separator otherwise.
|
|
21785
|
+
const join = platform === "win32" ? path.win32.join : path.posix.join;
|
|
21786
|
+
const probed = [];
|
|
21787
|
+
if (platform === "win32") {
|
|
21788
|
+
for (const dir of dirs) {
|
|
21789
|
+
const exePath = join(dir, `${name}.exe`);
|
|
21790
|
+
probed.push(exePath);
|
|
21791
|
+
if (fs.exists(exePath))
|
|
21792
|
+
return { found: true, path: exePath };
|
|
21793
|
+
const cmdPath = join(dir, `${name}.cmd`);
|
|
21794
|
+
if (fs.exists(cmdPath))
|
|
21795
|
+
return { found: false, reason: "shim", path: cmdPath };
|
|
21796
|
+
const batPath = join(dir, `${name}.bat`);
|
|
21797
|
+
if (fs.exists(batPath))
|
|
21798
|
+
return { found: false, reason: "shim", path: batPath };
|
|
21799
|
+
}
|
|
21800
|
+
return { found: false, reason: "not-found", probed };
|
|
21801
|
+
}
|
|
21802
|
+
for (const dir of dirs) {
|
|
21803
|
+
const candidate = join(dir, name);
|
|
21804
|
+
probed.push(candidate);
|
|
21805
|
+
if (fs.exists(candidate) && fs.isExecutable(candidate))
|
|
21806
|
+
return { found: true, path: candidate };
|
|
21807
|
+
}
|
|
21808
|
+
return { found: false, reason: "not-found", probed };
|
|
21809
|
+
}
|
|
21810
|
+
|
|
21811
|
+
var which = /*#__PURE__*/Object.freeze({
|
|
21812
|
+
__proto__: null,
|
|
21813
|
+
absoluteExecutablePath: absoluteExecutablePath,
|
|
21814
|
+
findExecutable: findExecutable,
|
|
21815
|
+
notFoundError: notFoundError
|
|
21816
|
+
});
|
|
21817
|
+
|
|
21740
21818
|
/** Whether `root/name`'s REAL (symlink/junction-resolved) parent is still the real `root` --
|
|
21741
21819
|
* the check a nominal path match can't make. `false` on any realpath failure: a path that can't
|
|
21742
21820
|
* be resolved is never treated as contained. */
|
|
@@ -21810,18 +21888,34 @@ function resolveRepo(root, name, fs) {
|
|
|
21810
21888
|
/** Annotates one already-listed entry with its resolved `ownerName` -- a real `git remote
|
|
21811
21889
|
* get-url` spawn per call, so this is only ever used from the HTTP handlers below, never from
|
|
21812
21890
|
* `listRepos` itself (memory-footprint's scan calls that directly and must stay git-free). */
|
|
21813
|
-
async function withOwnerName(entry,
|
|
21814
|
-
const url = await originUrl(entry.path,
|
|
21891
|
+
async function withOwnerName(entry, exec) {
|
|
21892
|
+
const url = await originUrl(entry.path, exec);
|
|
21815
21893
|
return { ...entry, ownerName: url === null ? null : parseRemoteOwnerName(url) };
|
|
21816
21894
|
}
|
|
21895
|
+
/** Where git is, resolved ONCE at the start of a request (#699), before any repo work and before any
|
|
21896
|
+
* working directory is used: every git spawn of the request then names this absolute path, never the
|
|
21897
|
+
* bare `git` that Windows would look for in the repo itself first. Not found is the verb's `502`, in
|
|
21898
|
+
* the wording `gh` uses.
|
|
21899
|
+
*
|
|
21900
|
+
* The path is made absolute (`absoluteExecutablePath`): a PATH with a relative entry would otherwise
|
|
21901
|
+
* hand the spawn `git.exe` or `bin\git.exe`, to be resolved against the repo. */
|
|
21902
|
+
async function resolveGit(deps) {
|
|
21903
|
+
const found = deps.which("git", { PATH: await deps.searchPath() });
|
|
21904
|
+
if (!found.found)
|
|
21905
|
+
return { ok: false, error: notFoundError("git", found) };
|
|
21906
|
+
return { ok: true, git: { run: deps.run, cmd: absoluteExecutablePath(found.path) } };
|
|
21907
|
+
}
|
|
21817
21908
|
function registerRepoVerbs(registry, deps) {
|
|
21818
21909
|
registry.register({
|
|
21819
21910
|
capability: "agent.git.read",
|
|
21820
21911
|
method: "GET",
|
|
21821
21912
|
path: "/repos",
|
|
21822
21913
|
handler: async (c) => {
|
|
21914
|
+
const git = await resolveGit(deps);
|
|
21915
|
+
if (!git.ok)
|
|
21916
|
+
return c.json({ ok: false, error: git.error }, 502);
|
|
21823
21917
|
const listed = listRepos(deps.root(), deps.fs);
|
|
21824
|
-
const repos = await Promise.all(listed.repos.map((entry) => withOwnerName(entry,
|
|
21918
|
+
const repos = await Promise.all(listed.repos.map((entry) => withOwnerName(entry, git.git)));
|
|
21825
21919
|
return c.json({ ...listed, repos });
|
|
21826
21920
|
},
|
|
21827
21921
|
});
|
|
@@ -21830,16 +21924,19 @@ function registerRepoVerbs(registry, deps) {
|
|
|
21830
21924
|
method: "GET",
|
|
21831
21925
|
path: "/repos/:name/status",
|
|
21832
21926
|
handler: async (c) => {
|
|
21927
|
+
const git = await resolveGit(deps);
|
|
21928
|
+
if (!git.ok)
|
|
21929
|
+
return c.json({ ok: false, error: git.error }, 502);
|
|
21833
21930
|
// Hono's param() is typed string | undefined for a dynamically-built path (it can't see
|
|
21834
21931
|
// the literal ":name" above at the type level); an actually-missing segment can't reach
|
|
21835
21932
|
// this handler at all, and the fallback empty string just fails resolveRepo's pattern.
|
|
21836
21933
|
const resolved = resolveRepo(deps.root(), c.req.param("name") ?? "", deps.fs);
|
|
21837
21934
|
if (resolved === null)
|
|
21838
21935
|
return c.json({ ok: false, error: "no such repo" }, 404);
|
|
21839
|
-
const status = await repoStatus(resolved.path,
|
|
21936
|
+
const status = await repoStatus(resolved.path, git.git, deps.fs.exists);
|
|
21840
21937
|
if (!status.ok)
|
|
21841
21938
|
return c.json(status, 502);
|
|
21842
|
-
const { ownerName } = await withOwnerName(resolved,
|
|
21939
|
+
const { ownerName } = await withOwnerName(resolved, git.git);
|
|
21843
21940
|
return c.json({ name: resolved.name, path: resolved.path, ownerName, ...status });
|
|
21844
21941
|
},
|
|
21845
21942
|
});
|
|
@@ -21848,6 +21945,9 @@ function registerRepoVerbs(registry, deps) {
|
|
|
21848
21945
|
method: "POST",
|
|
21849
21946
|
path: "/repos/:name/pull",
|
|
21850
21947
|
handler: async (c) => {
|
|
21948
|
+
const git = await resolveGit(deps);
|
|
21949
|
+
if (!git.ok)
|
|
21950
|
+
return c.json({ ok: false, error: git.error }, 502);
|
|
21851
21951
|
const resolved = resolveRepo(deps.root(), c.req.param("name") ?? "", deps.fs);
|
|
21852
21952
|
if (resolved === null)
|
|
21853
21953
|
return c.json({ ok: false, error: "no such repo" }, 404);
|
|
@@ -21861,7 +21961,7 @@ function registerRepoVerbs(registry, deps) {
|
|
|
21861
21961
|
catch {
|
|
21862
21962
|
// No body, or a non-JSON one -- dryRun keeps its documented default of false.
|
|
21863
21963
|
}
|
|
21864
|
-
const row = await pullRepo(resolved.name, resolved.path,
|
|
21964
|
+
const row = await pullRepo(resolved.name, resolved.path, git.git, { dryRun }, deps.fs.exists);
|
|
21865
21965
|
deps.logEvent(`pull ${row.name}: ${row.status} ${row.reason}`);
|
|
21866
21966
|
return c.json({ ok: true, row });
|
|
21867
21967
|
},
|
|
@@ -22939,15 +23039,26 @@ async function dispatch(argv, io) {
|
|
|
22939
23039
|
// apart from the fs-dep builders (`realExists`/`buildRepoFs`/`buildWhichFs`), which cli.test.ts builds
|
|
22940
23040
|
// against a stubbed fs (#439). ---
|
|
22941
23041
|
const TASK_NAME = "ac-agent";
|
|
22942
|
-
|
|
22943
|
-
|
|
22944
|
-
|
|
22945
|
-
|
|
22946
|
-
|
|
23042
|
+
/**
|
|
23043
|
+
* The oldest Node major the agent runs on (#827): the fleet's one Node line, the console image's
|
|
23044
|
+
* major. Both `package.json` files say `engines.node: ">=<NODE_FLOOR>"`, and node-floor.test.ts pins
|
|
23045
|
+
* all three to the Dockerfile's `FROM node:<major>...`. `npx` treats `engines` only as a warning, so
|
|
23046
|
+
* `checkEnvironment` is what actually refuses an older Node.
|
|
23047
|
+
*/
|
|
23048
|
+
const NODE_FLOOR = 26;
|
|
23049
|
+
/**
|
|
23050
|
+
* The runtime check `main` runs before any verb (so a machine left on an older Node fails loudly
|
|
23051
|
+
* instead of running untested, and `install` never half-installs): a Node major below `NODE_FLOOR`,
|
|
23052
|
+
* or a platform other than Windows or Linux, is refused with one stderr line and exit 2. Pure, so the
|
|
23053
|
+
* caller writes the line and exits; `null` means the agent may run.
|
|
23054
|
+
*/
|
|
23055
|
+
function checkEnvironment(nodeVersion = process.versions.node, platform = process.platform) {
|
|
23056
|
+
const major = Number.parseInt(nodeVersion.split(".")[0] ?? "0", 10);
|
|
23057
|
+
if (major < NODE_FLOOR) {
|
|
23058
|
+
return { code: 2, message: `ac-agent requires Node >=${NODE_FLOOR}, found ${nodeVersion}` };
|
|
22947
23059
|
}
|
|
22948
|
-
if (
|
|
22949
|
-
|
|
22950
|
-
return 2;
|
|
23060
|
+
if (platform !== "win32" && platform !== "linux") {
|
|
23061
|
+
return { code: 2, message: `ac-agent supports Windows and Linux, found ${platform}` };
|
|
22951
23062
|
}
|
|
22952
23063
|
return null;
|
|
22953
23064
|
}
|
|
@@ -23365,18 +23476,25 @@ async function realServe(overrides = {}) {
|
|
|
23365
23476
|
const registry$3 = createVerbRegistry();
|
|
23366
23477
|
const run = createRun();
|
|
23367
23478
|
const repoFs = buildRepoFs(fs);
|
|
23479
|
+
const whichFs = buildWhichFs(fs);
|
|
23480
|
+
// The optional second (env) parameter is #55's own addition: claude.ts/gh.ts's own deps type
|
|
23481
|
+
// `which` as one-argument (a required second parameter would not be assignable to that, tsc
|
|
23482
|
+
// 5.9 TS2322), while runPreflight's deps call it with the refreshed PATH on every probe.
|
|
23483
|
+
const which$1 = (name, env = process.env) => findExecutable(name, env, process.platform, whichFs);
|
|
23484
|
+
// The PATH to look for an executable on: the registry's on Windows (a tool installed after this agent
|
|
23485
|
+
// started is still found), the process PATH elsewhere. One closure for the repo verbs (git, #699) and
|
|
23486
|
+
// the toolchain upgrade, so "where is X" has a single answer ("reuse that resolution, never a second
|
|
23487
|
+
// list").
|
|
23488
|
+
const refreshedPath = async () => (await searchPath({ platform: process.platform, env: process.env, run })).path;
|
|
23368
23489
|
registerRepoVerbs(registry$3, {
|
|
23369
23490
|
root: () => loaded.config.reposRoot,
|
|
23370
23491
|
run,
|
|
23371
23492
|
fs: repoFs,
|
|
23372
23493
|
logEvent: (message) => log$1.write(message),
|
|
23494
|
+
which: which$1,
|
|
23495
|
+
searchPath: refreshedPath,
|
|
23373
23496
|
});
|
|
23374
23497
|
registerOpenVerb(registry$3, { spawn: createSpawn(), platform: process.platform });
|
|
23375
|
-
const whichFs = buildWhichFs(fs);
|
|
23376
|
-
// The optional second (env) parameter is #55's own addition: claude.ts/gh.ts's own deps type
|
|
23377
|
-
// `which` as one-argument (a required second parameter would not be assignable to that, tsc
|
|
23378
|
-
// 5.9 TS2322), while runPreflight's deps call it with the refreshed PATH on every probe.
|
|
23379
|
-
const which$1 = (name, env = process.env) => findExecutable(name, env, process.platform, whichFs);
|
|
23380
23498
|
// Named so both /v1/preflight (below, via createApp) and the publish verb's browser resolution
|
|
23381
23499
|
// (#53, immediately below) share the exact same search -- "reuse that resolution, never a second
|
|
23382
23500
|
// list" (E6-work/53-publish-mirror.md).
|
|
@@ -23493,7 +23611,7 @@ async function realServe(overrides = {}) {
|
|
|
23493
23611
|
loaded,
|
|
23494
23612
|
platform: process.platform,
|
|
23495
23613
|
which: which$1,
|
|
23496
|
-
searchPath:
|
|
23614
|
+
searchPath: refreshedPath,
|
|
23497
23615
|
run,
|
|
23498
23616
|
jobs,
|
|
23499
23617
|
userInfo: () => os.userInfo(),
|
|
@@ -23609,10 +23727,40 @@ async function realServe(overrides = {}) {
|
|
|
23609
23727
|
process.on("SIGTERM", shutdown);
|
|
23610
23728
|
process.on("SIGINT", shutdown);
|
|
23611
23729
|
}
|
|
23730
|
+
/** The environment variable that stops Windows from searching the current directory for an executable. */
|
|
23731
|
+
const NO_CWD_EXE_SEARCH_VAR = "NoDefaultCurrentDirectoryInExePath";
|
|
23732
|
+
/**
|
|
23733
|
+
* Make this process refuse the current-directory executable search (#699). On Windows, a spawn of a bare
|
|
23734
|
+
* name (`git`, `taskkill`, ...) is resolved by libuv's `search_path` (`src/win/process.c`), which looks in
|
|
23735
|
+
* the spawn's working directory first -- `<name>.com`, then `<name>.exe` -- whenever
|
|
23736
|
+
* `NeedCurrentDirectoryForExePathW("")` is true, i.e. unless the CALLING PROCESS's environment defines
|
|
23737
|
+
* `NoDefaultCurrentDirectoryInExePath` (the child's `env` option is not consulted). So a spawn whose cwd
|
|
23738
|
+
* is a repo checkout runs whatever the checkout holds. Setting the variable here covers every bare-name
|
|
23739
|
+
* spawn in this process, and the children inherit it (so a child's own `cmd.exe` stops running a bare
|
|
23740
|
+
* `build.bat` out of its working directory too). This is the general guard for that class (`taskkill.exe`
|
|
23741
|
+
* and `reg.exe` were already spawned by absolute path for it; `git` was the one that was missed); the
|
|
23742
|
+
* repo verbs still run git by absolute path, so neither layer is the only protection. It covers a BARE
|
|
23743
|
+
* name only: a relative name that carries a directory (`bin\git.exe`) is resolved against the spawn's
|
|
23744
|
+
* working directory either way, which is why `absoluteExecutablePath` exists. A no-op off Windows, and it
|
|
23745
|
+
* never changes a value that is set.
|
|
23746
|
+
* The empty string counts as unset: whether an empty value switches the search off is not something
|
|
23747
|
+
* this code relies on.
|
|
23748
|
+
*/
|
|
23749
|
+
function applySpawnGuard(env, platform) {
|
|
23750
|
+
if (platform !== "win32")
|
|
23751
|
+
return;
|
|
23752
|
+
const current = env[NO_CWD_EXE_SEARCH_VAR];
|
|
23753
|
+
if (current === undefined || current === "")
|
|
23754
|
+
env[NO_CWD_EXE_SEARCH_VAR] = "1";
|
|
23755
|
+
}
|
|
23612
23756
|
async function main(argv) {
|
|
23757
|
+
// Before anything can spawn: the very first statement, ahead of the environment check and of every
|
|
23758
|
+
// dynamic import below.
|
|
23759
|
+
applySpawnGuard(process.env, process.platform);
|
|
23613
23760
|
const envError = checkEnvironment();
|
|
23614
23761
|
if (envError !== null) {
|
|
23615
|
-
process.
|
|
23762
|
+
process.stderr.write(`${envError.message}\n`);
|
|
23763
|
+
process.exit(envError.code);
|
|
23616
23764
|
}
|
|
23617
23765
|
const io = {
|
|
23618
23766
|
install: async (args) => {
|
|
@@ -65144,7 +65292,8 @@ function killChild(child, spawnFn) {
|
|
|
65144
65292
|
if (process.platform === "win32" && child.pid !== undefined) {
|
|
65145
65293
|
try {
|
|
65146
65294
|
// By absolute path (as `regExePath` does for reg.exe): a bare name is searched for in the working
|
|
65147
|
-
// directory before PATH.
|
|
65295
|
+
// directory before PATH. `applySpawnGuard` (cli.ts, #699) now also stops the process doing that
|
|
65296
|
+
// for any bare name; this stays absolute so neither layer is the only protection.
|
|
65148
65297
|
const taskkill = path.win32.join(process.env.SystemRoot ?? process.env.windir ?? String.raw `C:\Windows`, "System32", "taskkill.exe");
|
|
65149
65298
|
const killer = spawnFn(taskkill, ["/pid", String(child.pid), "/t", "/f"], {
|
|
65150
65299
|
shell: false,
|
|
@@ -65252,7 +65401,8 @@ function registerClaudeVerbs(registry, deps) {
|
|
|
65252
65401
|
return c.json({ ok: false, error: "too many running jobs" }, 429);
|
|
65253
65402
|
}
|
|
65254
65403
|
const argv = [
|
|
65255
|
-
|
|
65404
|
+
// absolute: this spawn's cwd is the repo checkout (#699), see absoluteExecutablePath
|
|
65405
|
+
absoluteExecutablePath(which.path),
|
|
65256
65406
|
"-p",
|
|
65257
65407
|
validated.value.prompt,
|
|
65258
65408
|
"--permission-mode",
|
|
@@ -65560,10 +65710,7 @@ function registerGhVerb(registry, deps) {
|
|
|
65560
65710
|
}
|
|
65561
65711
|
const which = deps.which("gh");
|
|
65562
65712
|
if (!which.found) {
|
|
65563
|
-
|
|
65564
|
-
? "gh is a .cmd shim; install the native GitHub CLI binary"
|
|
65565
|
-
: "gh not found on PATH";
|
|
65566
|
-
return c.json({ ok: false, error }, 502);
|
|
65713
|
+
return c.json({ ok: false, error: notFoundError("gh", which, "the native GitHub CLI binary") }, 502);
|
|
65567
65714
|
}
|
|
65568
65715
|
const result = await deps.run(which.path, args, { timeoutMs: GH_TIMEOUT_MS, env: GH_ENV });
|
|
65569
65716
|
const stdout = truncateOutput(result.stdout);
|
|
@@ -65586,59 +65733,6 @@ var gh = /*#__PURE__*/Object.freeze({
|
|
|
65586
65733
|
registerGhVerb: registerGhVerb
|
|
65587
65734
|
});
|
|
65588
65735
|
|
|
65589
|
-
/** Strips one pair of surrounding double quotes from a PATH entry ("C:\Program Files\Git\cmd" is
|
|
65590
|
-
* legal in a Windows PATH, and `CreateProcess` strips it before use) -- an entry left quoted
|
|
65591
|
-
* would otherwise never match a real file and read as `missing`. Harmless on POSIX, where a real
|
|
65592
|
-
* directory name wrapped in literal quotes is not a thing that happens. */
|
|
65593
|
-
function unquote(entry) {
|
|
65594
|
-
return entry.length >= 2 && entry.startsWith('"') && entry.endsWith('"')
|
|
65595
|
-
? entry.slice(1, -1)
|
|
65596
|
-
: entry;
|
|
65597
|
-
}
|
|
65598
|
-
function findExecutable(name, env, platform, fs) {
|
|
65599
|
-
const pathVar = env.PATH ?? env.Path ?? "";
|
|
65600
|
-
const sep = platform === "win32" ? ";" : ":";
|
|
65601
|
-
// A PATH entry that doesn't exist on disk is skipped by the exists() checks below, never thrown
|
|
65602
|
-
// -- there's nothing here to throw on a plain string split, and exists() is a pure boolean.
|
|
65603
|
-
const dirs = pathVar
|
|
65604
|
-
.split(sep)
|
|
65605
|
-
.filter((d) => d !== "")
|
|
65606
|
-
.map(unquote);
|
|
65607
|
-
// path.win32/path.posix explicitly, never the bare path.join/path.dirname -- those pick their
|
|
65608
|
-
// behavior from the OS this code is actually running on, not from the `platform` parameter, so
|
|
65609
|
-
// testing "linux" logic from a Windows dev box (or vice versa) would silently join with the
|
|
65610
|
-
// wrong separator otherwise.
|
|
65611
|
-
const join = platform === "win32" ? path.win32.join : path.posix.join;
|
|
65612
|
-
const probed = [];
|
|
65613
|
-
if (platform === "win32") {
|
|
65614
|
-
for (const dir of dirs) {
|
|
65615
|
-
const exePath = join(dir, `${name}.exe`);
|
|
65616
|
-
probed.push(exePath);
|
|
65617
|
-
if (fs.exists(exePath))
|
|
65618
|
-
return { found: true, path: exePath };
|
|
65619
|
-
const cmdPath = join(dir, `${name}.cmd`);
|
|
65620
|
-
if (fs.exists(cmdPath))
|
|
65621
|
-
return { found: false, reason: "shim", path: cmdPath };
|
|
65622
|
-
const batPath = join(dir, `${name}.bat`);
|
|
65623
|
-
if (fs.exists(batPath))
|
|
65624
|
-
return { found: false, reason: "shim", path: batPath };
|
|
65625
|
-
}
|
|
65626
|
-
return { found: false, reason: "not-found", probed };
|
|
65627
|
-
}
|
|
65628
|
-
for (const dir of dirs) {
|
|
65629
|
-
const candidate = join(dir, name);
|
|
65630
|
-
probed.push(candidate);
|
|
65631
|
-
if (fs.exists(candidate) && fs.isExecutable(candidate))
|
|
65632
|
-
return { found: true, path: candidate };
|
|
65633
|
-
}
|
|
65634
|
-
return { found: false, reason: "not-found", probed };
|
|
65635
|
-
}
|
|
65636
|
-
|
|
65637
|
-
var which = /*#__PURE__*/Object.freeze({
|
|
65638
|
-
__proto__: null,
|
|
65639
|
-
findExecutable: findExecutable
|
|
65640
|
-
});
|
|
65641
|
-
|
|
65642
65736
|
/**
|
|
65643
65737
|
* The agent's in-memory job registry (#49, 49b): a port of the host's own
|
|
65644
65738
|
* `packages/host/src/tasks/registry.ts` (#42), extended with the fields a `claude -p` run needs
|
|
@@ -66851,7 +66945,7 @@ var scriptText = "<#\n.SYNOPSIS\n Inventory the language toolchains installed
|
|
|
66851
66945
|
* Runs 1.x's own `Get-ToolchainInventory.ps1` through `pwsh` (#52, 52b; 1.x had no Linux
|
|
66852
66946
|
* equivalent, `toolchain_sidecar.py`, `sched_spec.py:264-273`), kind `toolchain-inventory`,
|
|
66853
66947
|
* capability `agent.scan.toolchain-inventory`. Shipped and run, not rewritten (`data/README.md`):
|
|
66854
|
-
* the script is 1,832 lines (1.x `main` at `
|
|
66948
|
+
* the script is 1,832 lines (1.x `main` at `5de373e`, vendored byte for byte) whose output is
|
|
66855
66949
|
* the byte-compare target, so a TypeScript rewrite would have to re-derive every probe and
|
|
66856
66950
|
* formatting rule and still match it byte for byte.
|
|
66857
66951
|
*
|
|
@@ -67176,7 +67270,8 @@ var scan = /*#__PURE__*/Object.freeze({
|
|
|
67176
67270
|
*
|
|
67177
67271
|
* `why` strings are 1.x's own (`:116-157`), verbatim -- they are the *consequence* of a tool's
|
|
67178
67272
|
* absence, not a restatement of its name, and this plan reuses them rather than re-deriving new
|
|
67179
|
-
* wording that could quietly drift from what the 1.x Environment card has always said.
|
|
67273
|
+
* wording that could quietly drift from what the 1.x Environment card has always said. The
|
|
67274
|
+
* `docker` and `cloudflared` rows (#828) have no 1.x counterpart; theirs follow the same rule.
|
|
67180
67275
|
*/
|
|
67181
67276
|
const PREREQ_TABLE = [
|
|
67182
67277
|
{
|
|
@@ -67203,6 +67298,19 @@ const PREREQ_TABLE = [
|
|
|
67203
67298
|
required: true,
|
|
67204
67299
|
why: "the spawned `claude -p` coding tasks, and the context-pressure / subagent-cost scans -- every console action that runs a headless Claude fails without it",
|
|
67205
67300
|
},
|
|
67301
|
+
// #828: two of the fleet standard's tools; optional, so a machine without them is not a failed preflight.
|
|
67302
|
+
{
|
|
67303
|
+
cmd: "docker",
|
|
67304
|
+
label: "Docker",
|
|
67305
|
+
required: false,
|
|
67306
|
+
why: "the console host and every server stack run in it; without it a server role has nothing to run, and a dev box simply has no container tooling",
|
|
67307
|
+
},
|
|
67308
|
+
{
|
|
67309
|
+
cmd: "cloudflared",
|
|
67310
|
+
label: "cloudflared",
|
|
67311
|
+
required: false,
|
|
67312
|
+
why: "the tunnels that put the console and the other stacks behind Cloudflare Access; without it a server host is unreachable from outside the LAN",
|
|
67313
|
+
},
|
|
67206
67314
|
{
|
|
67207
67315
|
cmd: "chromium",
|
|
67208
67316
|
label: "PDF renderer (Chromium-family)",
|
|
@@ -67339,7 +67447,7 @@ async function checkRowUnsafe(entry, deps, refreshedEnv) {
|
|
|
67339
67447
|
return { ...base, status: "missing", path: "", version: "", detail: "" };
|
|
67340
67448
|
}
|
|
67341
67449
|
/** Wraps one row's whole resolution so a throwing `which`/`run`/`exists` becomes that row's own
|
|
67342
|
-
* `broken`, never a crash that takes the other
|
|
67450
|
+
* `broken`, never a crash that takes the other rows down with it (1.x
|
|
67343
67451
|
* `tests/test_readiness.py:263`'s "a probe that raises becomes a row, not a crash"). */
|
|
67344
67452
|
async function checkRow(entry, deps, refreshedEnv) {
|
|
67345
67453
|
try {
|
|
@@ -67372,7 +67480,7 @@ const CHROMIUM_ENTRY = PREREQ_TABLE.find((entry) => entry.cmd === "chromium");
|
|
|
67372
67480
|
* with `runPreflight`'s own `chromium` row) rather than a second candidate list: winPaths first, then
|
|
67373
67481
|
* `cmd`/`altCmds` on the refreshed PATH, then the same names under a version manager's bin dir. Never
|
|
67374
67482
|
* executes the browser (E4 §5 decision 19, unchanged here). Used by the publish-mirror verb (#53) so
|
|
67375
|
-
* finding a PDF renderer doesn't also run the rest of preflight (
|
|
67483
|
+
* finding a PDF renderer doesn't also run the rest of preflight (every executed row) on every publish.
|
|
67376
67484
|
*/
|
|
67377
67485
|
async function resolveChromiumPath(deps) {
|
|
67378
67486
|
if (!CHROMIUM_ENTRY)
|
|
@@ -75557,9 +75665,9 @@ var tunnel = /*#__PURE__*/Object.freeze({
|
|
|
75557
75665
|
* three literal shapes and one validated id. Every field is checked against a grammar before anything
|
|
75558
75666
|
* spawns; a request that does not match is a `400` naming the field. A valid id can still name a package
|
|
75559
75667
|
* the operator did not mean (the console decides which one), which is the accepted worst case. The one
|
|
75560
|
-
* other program the request can name is `cmd`, and only to read its version: it must be one of the
|
|
75561
|
-
*
|
|
75562
|
-
* the request's choosing.
|
|
75668
|
+
* other program the request can name is `cmd`, and only to read its version: it must be one of the four
|
|
75669
|
+
* tools in `VERSION_READABLE_CMDS` (each one the preflight already runs `--version` on), never an
|
|
75670
|
+
* executable of the request's choosing.
|
|
75563
75671
|
* - NO ELEVATION in the agent: it never tries to elevate itself. Linux needs a sudoers rule for the agent's
|
|
75564
75672
|
* user (`needs-sudoers`). On Windows the agent is a limited logon task: winget answers `0x8A150019` when
|
|
75565
75673
|
* the command itself needs administrator rights (`needs-elevation`, with the command to paste into an
|
|
@@ -75594,11 +75702,15 @@ const MANAGERS = ["winget", "apt", "native"];
|
|
|
75594
75702
|
// punctuation: no quote, ":", "(", ")", ">" or ",", the characters that would let one run's line imitate
|
|
75595
75703
|
// another's outcome. Spaces stay (the standard's keys have them: "GitHub CLI").
|
|
75596
75704
|
const TOOL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 ._+-]{0,63}$/;
|
|
75597
|
-
/** The commands whose `--version` this verb may run: the agent's own preflight
|
|
75598
|
-
* the preflight
|
|
75599
|
-
* 19). The Environment panel sends the `cmd` of a preflight row, so
|
|
75600
|
-
*
|
|
75601
|
-
|
|
75705
|
+
/** The commands whose `--version` this verb may run: four of the tools the agent's own preflight executes
|
|
75706
|
+
* (never `chromium`, which the preflight only resolves: a Chromium-family `--version` can open a window,
|
|
75707
|
+
* E4 §5 decision 19). The Environment panel sends the `cmd` of a preflight row, so a `cmd` outside this
|
|
75708
|
+
* list is either a row the panel only grades (`docker`, `cloudflared`, #828: upgrading either can restart
|
|
75709
|
+
* the containers or tunnels on the host, a host operation, not a panel button's) or a request to run a
|
|
75710
|
+
* program of the console's choosing. */
|
|
75711
|
+
// An explicit list, not derived from PREREQ_TABLE: adding a preflight row must never widen the `cmd`s this
|
|
75712
|
+
// verb accepts for an upgrade (#828). (`cmd` is not tied to `packageId`; see the README.)
|
|
75713
|
+
const VERSION_READABLE_CMDS = ["git", "gh", "node", "claude"];
|
|
75602
75714
|
// winget: a `Publisher.Name` id (`Git.Git`, `Microsoft.PowerShell`); must start alphanumeric, so it can
|
|
75603
75715
|
// never be read as an option, and must contain a ".".
|
|
75604
75716
|
const WINGET_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,127}$/;
|
|
@@ -75628,8 +75740,8 @@ function validateUpgradeRequest(body, platform) {
|
|
|
75628
75740
|
const cmd = field("cmd");
|
|
75629
75741
|
if (typeof cmd !== "string" || cmd === "")
|
|
75630
75742
|
return refuse$1("cmd: must be a non-empty string");
|
|
75631
|
-
// An allowlist, not a grammar: only the
|
|
75632
|
-
//
|
|
75743
|
+
// An allowlist, not a grammar: only the four upgradable tools pass, so no name, path or separator that
|
|
75744
|
+
// is not one of them can reach `which`.
|
|
75633
75745
|
if (!VERSION_READABLE_CMDS.includes(cmd)) {
|
|
75634
75746
|
return refuse$1(`cmd: not a tool this agent reads the version of (${VERSION_READABLE_CMDS.join(", ")})`);
|
|
75635
75747
|
}
|
|
@@ -75812,12 +75924,6 @@ const SPAWN_DIAGNOSTIC = /^\[(?:killed, but its output never closed|the command
|
|
|
75812
75924
|
// dpkg-query's own format syntax (a status column, a tab, the package name): not a template literal.
|
|
75813
75925
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: ${...} is dpkg-query's format syntax
|
|
75814
75926
|
const DPKG_SHOWFORMAT = "--showformat=${db:Status-Abbrev}\t${Package}\n";
|
|
75815
|
-
function notFoundError(name, found) {
|
|
75816
|
-
if (!found.found && found.reason === "shim") {
|
|
75817
|
-
return `${name} is a .cmd shim; install the native binary`;
|
|
75818
|
-
}
|
|
75819
|
-
return `${name} not found on PATH`;
|
|
75820
|
-
}
|
|
75821
75927
|
/** The most `<exe> --version` output a version read collects (`RunOptions.maxOutputLength`). */
|
|
75822
75928
|
const VERSION_MAX_OUTPUT = 65_536;
|
|
75823
75929
|
const VERSION_DEADLINE_MS = VERSION_TIMEOUT_MS + 5_000;
|
|
@@ -76536,4 +76642,4 @@ var updater = /*#__PURE__*/Object.freeze({
|
|
|
76536
76642
|
selfUpdateSupported: selfUpdateSupported
|
|
76537
76643
|
});
|
|
76538
76644
|
|
|
76539
|
-
export { AGENT_API_VERSION, buildRepoFs, buildWhichFs, dispatch, healthResultFrom, isEntryPoint, main, realExists, runningJobCount, toolchainUpgradeDeps };
|
|
76645
|
+
export { AGENT_API_VERSION, NODE_FLOOR, NO_CWD_EXE_SEARCH_VAR, applySpawnGuard, buildRepoFs, buildWhichFs, checkEnvironment, dispatch, healthResultFrom, isEntryPoint, main, realExists, runningJobCount, toolchainUpgradeDeps };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rackbops/ac-agent",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.56",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The per-machine host agent (installed with npx @rackbops/ac-agent): an HTTPS health server plus install/status/uninstall for a Windows logon task or a systemd user unit.",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"data/"
|
|
14
14
|
],
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": ">=
|
|
16
|
+
"node": ">=26"
|
|
17
17
|
},
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public"
|