@rackbops/ac-agent 2.0.0-alpha.8
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/LICENSE +201 -0
- package/README.md +307 -0
- package/dist/ac-agent.mjs +74208 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 roshne
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# @rackbops/ac-agent
|
|
2
|
+
|
|
3
|
+
The per-machine host agent for [artifact-console](https://github.com/Rackbops/artifact-console): a
|
|
4
|
+
small HTTPS server, paired to a console over a token, that lets it reach one dev machine's
|
|
5
|
+
`claude`/`git`/`gh` and file system through an allowlisted verb API. Console-initiated pairing (a
|
|
6
|
+
one-time code exchanged for a per-machine token, #48), `repos` list/status/pull and `open` (#49,
|
|
7
|
+
49a), and `exec/claude` (with the job registry and its SSE log) plus `exec/gh` (#49, 49b) are all
|
|
8
|
+
live; `scan/:kind` (49c) lands in a later issue. Every verb also requires a per-plugin capability
|
|
9
|
+
grant, pushed by the console and enforced here (#50) -- see **Verbs** and **Security model** below.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx @rackbops/ac-agent install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Registers a Windows Task Scheduler task (`\ClaudeLocal\ac-agent`, a logon trigger for the current
|
|
18
|
+
user) or, on Linux, a systemd `--user` unit (`ac-agent.service`, `enable --now`), copies the running
|
|
19
|
+
bundle into the state dir (never the `npx` cache — that can be garbage-collected out from under a
|
|
20
|
+
long-running service), writes `agent.toml` if one doesn't already exist, and generates a self-signed
|
|
21
|
+
cert if one doesn't already exist. Prints the resolved config/state paths, the cert's fingerprint,
|
|
22
|
+
and the firewall rule's status.
|
|
23
|
+
|
|
24
|
+
Options: `--port <n>` (default `8788`), `--name <machine>` (default `os.hostname()`), `--repos-root
|
|
25
|
+
<abs>` (default `<homedir>/repos`; the fleet's roots differ per box — Windows and Linux checkouts
|
|
26
|
+
don't share a layout, so every real install passes this explicitly).
|
|
27
|
+
|
|
28
|
+
### The firewall step
|
|
29
|
+
|
|
30
|
+
`install` tries to add an inbound rule (`New-NetFirewallRule`, Windows only) so a LAN caller can
|
|
31
|
+
reach the port. Creating a firewall rule needs administrator rights; when the process isn't
|
|
32
|
+
elevated, `install` prints the exact one-liner to run in an elevated PowerShell and `status` reports
|
|
33
|
+
`firewall: missing` until that's done. This is expected on a fresh install run from an ordinary
|
|
34
|
+
shell — it is not a failure to work around.
|
|
35
|
+
|
|
36
|
+
### Windows: no console window
|
|
37
|
+
|
|
38
|
+
The task's action is `wscript.exe //B "<launch.js>"` against a small generated JScript launcher
|
|
39
|
+
(written into the state dir alongside the bundle copy), not a bare `node` invocation and not
|
|
40
|
+
`conhost.exe --headless` (an earlier design that turned out to run nothing at all — verified live,
|
|
41
|
+
`conhost.exe` isn't a generic headless-launch wrapper). `launch.js` runs
|
|
42
|
+
`WScript.CreateObject("WScript.Shell").Run("<node> <bundle> serve --config-dir <abs> --state-dir
|
|
43
|
+
<abs>", 0, true)` in a loop (the two dirs `install` itself resolved, baked in the same way
|
|
44
|
+
`<node>`/`<bundle>` already are — see **Config and state directories** below), retrying on a crash.
|
|
45
|
+
Window style `0` keeps node's console from ever appearing (Node has no `nodew.exe`, the
|
|
46
|
+
`pythonw.exe`-equivalent 1.x used), and waiting (`true`, not fire-and-forget) keeps `wscript.exe`
|
|
47
|
+
itself alive as the task's tracked process for the agent's entire lifetime, so the task reads
|
|
48
|
+
Running while serving and a crash propagates as a non-zero exit, visible afterward as
|
|
49
|
+
`LastTaskResult`. **`RestartCount 3` does not actually restart a crashed agent** — verified live (two
|
|
50
|
+
scratch tasks with these exact settings, one killed by pid and one exiting on its own, sampled every
|
|
51
|
+
30 s for 150 s): Task Scheduler's restart-on-failure does not act on the action's exit code for this
|
|
52
|
+
action shape. The real crash-restart mechanism is `launch.js`'s own retry loop (#238): a non-zero
|
|
53
|
+
exit sleeps a minute and runs node again, up to 3 retries, since the exit code is already in
|
|
54
|
+
`wscript.exe`'s own hand; a zero exit (a deliberate shutdown) ends the loop instead. `stop`/`uninstall`
|
|
55
|
+
stop the task first — ending the loop — before killing the orphaned node by pid, the reverse of the
|
|
56
|
+
order that would otherwise let the loop see a bare kill as a crash and resurrect the agent a minute
|
|
57
|
+
after `stop`. Verified live: killing node by pid left the task Running with a fresh node pid
|
|
58
|
+
listening within about a minute; `stop` afterward left the task `Ready` with `LastTaskResult 267014`
|
|
59
|
+
(0x41306, terminated by user) and nothing listening 150 s later. The launcher is JScript rather than
|
|
60
|
+
VBScript specifically because
|
|
61
|
+
VBScript is a Windows feature-on-demand Microsoft is phasing out (Insider builds first); JScript and
|
|
62
|
+
Windows Script Host itself carry no such notice, so the same mechanism ships without a deprecated
|
|
63
|
+
component in the loop. (A PowerShell hidden launcher — `Start-Process -WindowStyle Hidden -PassThru
|
|
64
|
+
-Wait` — was also tried and verified working, but was dropped in favor of keeping Windows Script
|
|
65
|
+
Host as the only script host involved.) One thing this package cannot verify by itself — no CI
|
|
66
|
+
runner can register a real logon task — is named as unverified rather than assumed: whether any
|
|
67
|
+
window is visible at a real logon.
|
|
68
|
+
|
|
69
|
+
## Status
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
npx @rackbops/ac-agent status
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Prints, as JSON: the config path and whether it came from a file or defaults, the state dir, the
|
|
76
|
+
Task Scheduler/systemd service state, whether a pairing token is stored (`paired: true/false`), the
|
|
77
|
+
cert's fingerprint, the result of a live `GET /v1/health` call against itself (using its own pinned
|
|
78
|
+
cert, not a system CA — see **Security model** below), and the currently-held grants
|
|
79
|
+
(`{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).
|
|
81
|
+
|
|
82
|
+
## Uninstall
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
npx @rackbops/ac-agent uninstall [--purge]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Stops and unregisters the task/unit, removes the copied bundle, cert, key, token hash, pending-pair
|
|
89
|
+
file, grants and lock file. Keeps `agent.toml` unless `--purge` is given, so a re-install remembers
|
|
90
|
+
the machine's chosen port/name.
|
|
91
|
+
|
|
92
|
+
## serve / stop / start
|
|
93
|
+
|
|
94
|
+
`serve` is what the registered task/unit actually runs — it isn't meant to be invoked directly
|
|
95
|
+
except for debugging. `stop`/`start` control the registered service (`Stop-`/`Start-ScheduledTask`
|
|
96
|
+
or `systemctl --user stop/start`) without touching the registration itself.
|
|
97
|
+
|
|
98
|
+
## Config and state directories
|
|
99
|
+
|
|
100
|
+
| | Windows | Linux |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| Config (`agent.toml`) | `%APPDATA%\ac-agent\` | `~/.config/ac-agent/` |
|
|
103
|
+
| State (cert, key, token hash, grants, log, pid, the installed bundle copy) | `%LOCALAPPDATA%\ac-agent\` | `~/.local/state/ac-agent/` |
|
|
104
|
+
|
|
105
|
+
Both are overridable with `AC_AGENT_CONFIG_DIR` / `AC_AGENT_STATE_DIR` — **absolute paths only**; a
|
|
106
|
+
relative override throws, naming the value. `install` resolves these once, in its own process, and
|
|
107
|
+
bakes the resolved absolute paths into the registered task/unit as `serve --config-dir <abs>
|
|
108
|
+
--state-dir <abs>`, the same way it already baked in the node/bundle paths — rather than relying on
|
|
109
|
+
`serve` to re-resolve them from its own environment when the task/unit actually runs. This matters
|
|
110
|
+
because a Task Scheduler process (and a systemd `--user` unit) is a separate process tree that does
|
|
111
|
+
not see the installing shell's variables — transient or persisted — so before this, an install under
|
|
112
|
+
the overrides silently served from the real platform defaults instead, with a different certificate
|
|
113
|
+
and the default port (E4 finding 13, fixed in #241). A manual `serve` with no flags still honours
|
|
114
|
+
`AC_AGENT_CONFIG_DIR`/`AC_AGENT_STATE_DIR` as before.
|
|
115
|
+
|
|
116
|
+
`agent.toml`:
|
|
117
|
+
|
|
118
|
+
```toml
|
|
119
|
+
port = 8788
|
|
120
|
+
bind = "0.0.0.0"
|
|
121
|
+
machine = "melody"
|
|
122
|
+
repos_root = "S:/Repos"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Unknown keys are ignored, so a future field never breaks an older agent reading a newer file.
|
|
126
|
+
`repos_root` is overridable live with `AC_AGENT_REPOS_ROOT` the same way `port` is with
|
|
127
|
+
`AC_AGENT_PORT` — absolute paths only; a relative override throws, naming the value.
|
|
128
|
+
|
|
129
|
+
## Verbs
|
|
130
|
+
|
|
131
|
+
Everything under `/v1/*` except `/v1/health` and `/v1/pair` requires pairing (see **Security
|
|
132
|
+
model**). The verb registry fills `GET /v1/health`'s `capabilities` field with the sorted, unique
|
|
133
|
+
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"]`.
|
|
135
|
+
|
|
136
|
+
**Every verb also requires two headers, checked in this order** (#50), after the bearer check
|
|
137
|
+
passes: `X-AC-Plugin` (the calling plugin's id) missing or shaped wrong is `400 {ok: false, error:
|
|
138
|
+
"missing X-AC-Plugin"}` / `"invalid X-AC-Plugin"`; `X-AC-Grants-Revision` missing is `400 {ok:
|
|
139
|
+
false, error: "missing X-AC-Grants-Revision"}`; a value that doesn't match this agent's own current
|
|
140
|
+
revision — including a never-pushed `null` — is `409 {ok: false, error: "grants out of date",
|
|
141
|
+
revision: <this agent's own, or null>}`, since a stale console must self-heal before its refusal
|
|
142
|
+
means anything; only once the agent's own copy is known-current does an ungranted capability get a
|
|
143
|
+
real refusal, `403 {ok: false, error: "<capability> is not granted to <plugin> on <machine>",
|
|
144
|
+
capability, plugin}`. See **Security model** for how a grant reaches the agent in the first place.
|
|
145
|
+
|
|
146
|
+
- **`GET /v1/repos`** (`agent.git.read`) → `{ok, root, repos: [{name, path}]}`: every immediate
|
|
147
|
+
child of `repos_root` with a `.git` entry (a file for a worktree, a directory for a clone), sorted
|
|
148
|
+
by name. A missing or unreadable root answers `{ok: true, repos: [], reason: "root missing"}`,
|
|
149
|
+
never a 500.
|
|
150
|
+
- **`GET /v1/repos/:name/status`** (`agent.git.read`) → `{ok, name, path, branch, defaultBranch,
|
|
151
|
+
upstream, ahead, behind, dirty, inProgress}`, read-only (no fetch — `pull` is what refreshes the
|
|
152
|
+
refs `status` reads).
|
|
153
|
+
- **`POST /v1/repos/:name/pull`** (`agent.git.pull`), body `{dryRun?: boolean}` → `{ok: true, row}`
|
|
154
|
+
where `row.status` is `updated | would | current | skipped | error` and `row.reason` names which
|
|
155
|
+
of the six 1.x-derived guards applied (`dirty`, `branch`, `ahead`, `in-progress`, `no-upstream`,
|
|
156
|
+
`diverged`) when it skipped, or `not-a-repo` / `fetch-failed` / `dry-run`. Fast-forward only
|
|
157
|
+
(`git merge --ff-only`) — a diverged branch is reported, never resolved. Every git call runs with
|
|
158
|
+
`GIT_TERMINAL_PROMPT=0`, so a credential prompt refuses instead of hanging a request until its
|
|
159
|
+
timeout.
|
|
160
|
+
- **`:name` is an allowlist, not a path** — it must match a machine-generated repo name pattern, must
|
|
161
|
+
not be `.`/`..`, and must actually be one of the entries `GET /v1/repos` would list; anything else
|
|
162
|
+
is `404 {"error": "no such repo"}` before any git call runs.
|
|
163
|
+
- **`POST /v1/open`** (`agent.desktop.open`), body `{url}` → `{ok: true}`. Only a `https:` or
|
|
164
|
+
`claude:` URL is accepted (the console's own deep-link shape,
|
|
165
|
+
`claude://code/new?q=<prompt>&folder=<path>`); anything else is `400` before any process spawns.
|
|
166
|
+
The `200` attests only that the launcher (`rundll32.exe url.dll,FileProtocolHandler` on Windows,
|
|
167
|
+
`xdg-open` on Linux) started — never a shell string, always argv — not that a browser or Claude
|
|
168
|
+
Desktop actually opened; that's visible to the person at the desktop, not to the caller.
|
|
169
|
+
- **`POST /v1/scan/<kind>`** (`agent.scan.<kind>`, one capability per kind), body `{}` — a pure
|
|
170
|
+
dispatch shell (#49, 49c): this package ships no kinds itself, the four #52 scans
|
|
171
|
+
(`context-pressure`, `subagent-cost`, `memory-footprint`, `toolchain-inventory`) register into it,
|
|
172
|
+
so until then every `<kind>` is `404 {"error": "no such verb"}`, the same as any other unregistered
|
|
173
|
+
route — there is no separate `GET /v1/scan` list, since the kinds a machine implements are already
|
|
174
|
+
visible as `agent.scan.<kind>` entries in `GET /v1/health`'s `capabilities`. One run per kind at a
|
|
175
|
+
time: a second `POST` while one is still in flight is `409 {ok: false, error: "scan already
|
|
176
|
+
running"}` (the #52 scans read whole `~/.claude` trees and must not stack); a kind's own `run()`
|
|
177
|
+
gets an `AbortSignal` that fires at its `timeoutMs` (120s default) — a run that doesn't honour it
|
|
178
|
+
promptly answers the client `504 {ok: false, error: "scan timed out after <n> ms"}` while the run
|
|
179
|
+
keeps going in the background (a later `POST` for that kind still gets `409`, now naming "timed
|
|
180
|
+
out, still finishing", until the run actually settles); one that never settles at all is abandoned
|
|
181
|
+
five timeouts later, freeing the kind up rather than locking it for the agent's whole lifetime. A
|
|
182
|
+
successful run answers `200 {ok: true, kind, generatedAt, durationMs, payload}`; a rejected one,
|
|
183
|
+
`502 {ok: false, error: "<message>"}`.
|
|
184
|
+
|
|
185
|
+
### `POST /v1/exec/claude` and the job registry
|
|
186
|
+
|
|
187
|
+
- **`POST /v1/exec/claude`** (`agent.exec.claude`), body `{repo, prompt, permissionMode?, model?}` →
|
|
188
|
+
`202 {ok: true, jobId}`. `repo` resolves through the same allowlist as `GET /v1/repos/:name/status`
|
|
189
|
+
(a 404 before anything spawns); `prompt` is a non-empty string, at most 8 KiB (the Windows
|
|
190
|
+
`CreateProcess` command-line limit is 32,767 characters total, and quoting can double a prompt's
|
|
191
|
+
own backslashes and quotes — 8 KiB leaves headroom for the fixed flags and the repo path in the
|
|
192
|
+
worst case) with no control character other than `\n`/`\t`; `permissionMode` is one of `plan`
|
|
193
|
+
(the default) / `acceptEdits` / `bypassPermissions`; `model` is an optional
|
|
194
|
+
`^[A-Za-z0-9._-]{1,64}$` string. Any other shape is `400` before anything spawns. `claude` runs as
|
|
195
|
+
`claude -p <prompt> --permission-mode <mode> --add-dir <repo path> [--model <model>]`, `cwd` the
|
|
196
|
+
resolved repo. **At most 4 `claude` jobs run at once** — a 5th concurrent request is
|
|
197
|
+
`429 {ok: false, error: "too many running jobs"}`, checked before spawning, not after.
|
|
198
|
+
- **`GET /v1/jobs`** (`agent.exec.claude`) → `{ok: true, jobs: [view...]}`, newest first.
|
|
199
|
+
**`GET /v1/jobs/:id`** → `{ok: true, job: view}` or `404` for an unknown id. A job view is
|
|
200
|
+
`{id, kind: "claude", label, status: "running"|"done"|"failed", startedAt, endedAt?,
|
|
201
|
+
exitCode?: number|null, repo, permissionMode}`. **`POST /v1/jobs/clear-finished`** →
|
|
202
|
+
`{ok: true, cleared}`, dropping every done/failed job (running ones are kept).
|
|
203
|
+
- **`GET /v1/jobs/:id/events`** (`agent.exec.claude`) streams a job's log as Server-Sent Events: a
|
|
204
|
+
`404` for an unknown id up front, otherwise a finished job replays its whole buffer and closes,
|
|
205
|
+
and a running job replays its buffer then streams new lines live as they arrive, closing once the
|
|
206
|
+
job finishes. Two frame shapes only: `event: line` with `data` the JSON-encoded line text and `id`
|
|
207
|
+
the line's absolute position in the job's log (so a reconnecting client can tell whether it missed
|
|
208
|
+
lines to eviction); `event: done` with `data` `{"status": "done"|"failed", "exitCode":
|
|
209
|
+
number|null}`. These are the exact event names and shapes the host's own `/api/tasks/:id/log`
|
|
210
|
+
emits (`packages/host/src/tasks/route.ts`) with `exitCode` added, since the console's shell code
|
|
211
|
+
already knows how to consume them.
|
|
212
|
+
|
|
213
|
+
### `POST /v1/exec/gh`
|
|
214
|
+
|
|
215
|
+
- **`POST /v1/exec/gh`** (`agent.exec.gh`), body `{args: string[]}` → `200 {ok: true, code, stdout,
|
|
216
|
+
stderr, truncated}` (stdout/stderr each capped at 1 MiB, `truncated: true` when either was cut). A
|
|
217
|
+
non-zero `gh` exit is still a `200` — the caller reads `code`. At most 32 arguments, each at most
|
|
218
|
+
4 KiB with no control characters, except a `--body` value (`--body <text>` or `--body=<text>`),
|
|
219
|
+
which may be up to 64 KiB and contain `\n`/`\t` (a PR/issue comment body).
|
|
220
|
+
- **Every argument is checked against a fixed allowlist before anything spawns** — an unlisted
|
|
221
|
+
subcommand, flag, or malformed positional is `400 {ok: false, error: "<token and why>"}`, logged as
|
|
222
|
+
`gh refused: <error>`, additive-only (nothing here is ever loosened without a deliberate change to
|
|
223
|
+
the table):
|
|
224
|
+
|
|
225
|
+
| Subcommand | Positional | Flags |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| `issue view` | one `^[0-9]{1,9}$` | `--repo`, `--json`, `--jq` |
|
|
228
|
+
| `issue list` | none | `--repo`, `--json`, `--jq`, `--limit` (1-200), `--label`, `--author`, `--search`, `--state` (`open`\|`closed`\|`all`) |
|
|
229
|
+
| `issue comment` | one `^[0-9]{1,9}$` | `--repo`, `--body` |
|
|
230
|
+
| `pr view` | one `^[0-9]{1,9}$` | `--repo`, `--json`, `--jq` |
|
|
231
|
+
| `pr list` | none | `--repo`, `--json`, `--jq`, `--limit` (1-200), `--label`, `--author`, `--search`, `--state` (`open`\|`closed`\|`merged`\|`all`) |
|
|
232
|
+
| `pr checks` | one `^[0-9]{1,9}$` | `--repo`, `--json`, `--jq` |
|
|
233
|
+
| `pr comment` | one `^[0-9]{1,9}$` | `--repo`, `--body` |
|
|
234
|
+
| `release view` | one `^v?[0-9][A-Za-z0-9._-]{0,63}$` | `--repo` |
|
|
235
|
+
| `api` | one `^/?(repos\|orgs\|users\|rate_limit)(/(?!\.{1,2}(?:/\|$))[A-Za-z0-9._-]+)*$`, no query string | `--jq`, `--method` (`GET` only) |
|
|
236
|
+
|
|
237
|
+
`--repo` must be `^(?!\.{1,2}(?:/|$))[A-Za-z0-9._-]+/(?!\.{1,2}(?:/|$))[A-Za-z0-9._-]+$` (a bare
|
|
238
|
+
`.`/`..` half is refused the same way `resolveRepo` already refuses a bare `.`/`..` repo name — a
|
|
239
|
+
real owner or repo name may still contain a dot, e.g. `my.repo`); `--json` must be `^[A-Za-z,]+$`.
|
|
240
|
+
Both this and the `api` row's path pattern above build the same `.`/`..`-excluding segment rule
|
|
241
|
+
from one shared source (`SAFE_SEGMENT_SOURCE` in `gh-allowlist.ts`), not two independent copies.
|
|
242
|
+
`api` is **read-only by construction**: no request-body flags at all (`-f`, `-F`, `--input`, or `--method`
|
|
243
|
+
with anything but `GET` are refused), `search` is deliberately not in the path allowlist since a
|
|
244
|
+
real search needs a `q=` query string this table never permits, and a bare `.`/`..` path segment
|
|
245
|
+
is refused too — the segment class allows a literal `.` for a real repo/org name (`my.repo`), so
|
|
246
|
+
without this a `/repos/../search/issues` would otherwise satisfy the pattern and reach exactly
|
|
247
|
+
the excluded path space. `--flag=value` is split
|
|
248
|
+
before matching, and a bare `-`/`--` is always refused. A flag or subcommand not in this table —
|
|
249
|
+
`repo delete`, `issue delete`, `--body-file`, `--editor`, `--web`, `--hostname`, anything else — is
|
|
250
|
+
refused the same way, before `gh` is ever resolved or spawned.
|
|
251
|
+
|
|
252
|
+
### The `.cmd`/`.bat` shim note (`claude` and `gh`)
|
|
253
|
+
|
|
254
|
+
Both `exec/claude` and `exec/gh` resolve their binary by walking `PATH` themselves rather than
|
|
255
|
+
letting a shell do it, because neither ever sets `shell: true` (request input reaching a shell is
|
|
256
|
+
exactly what this agent refuses to do). Node itself cannot spawn a `.cmd`/`.bat` shim without
|
|
257
|
+
`shell: true` (`EINVAL`, since the CVE-2024-27980 fix) — so a `claude` or `gh` install that only put
|
|
258
|
+
a shim on `PATH` (common for a global npm install on Windows) answers `502` with a message naming
|
|
259
|
+
the shim specifically (`"claude is a .cmd shim; install the native Claude Code binary"` /
|
|
260
|
+
`"gh is a .cmd shim; install the native GitHub CLI binary"`), distinct from a bare
|
|
261
|
+
`"<name> not found on PATH"` — the operator's fix differs (install the native binary vs. install it
|
|
262
|
+
at all). Windows PATH resolution stops at the **first** directory with any matching extension, so a
|
|
263
|
+
shim earlier on `PATH` is reported even if a native binary exists further down — that is what a real
|
|
264
|
+
invocation would actually run, not "helpful" behavior a real shell wouldn't show either.
|
|
265
|
+
|
|
266
|
+
## Security model
|
|
267
|
+
|
|
268
|
+
- **HTTPS only**, with a self-signed certificate generated once at install (RSA 2048, SHA-256,
|
|
269
|
+
10-year validity; SAN covers the machine's hostname, every non-internal IPv4 address, and
|
|
270
|
+
`127.0.0.1`).
|
|
271
|
+
- **`GET /v1/health` and `POST /v1/pair` are the only unauthenticated routes.** `/v1/health` answers
|
|
272
|
+
`{ok, version, apiVersion, machine, paired, capabilities, grantsRevision}` — enough for a caller to
|
|
273
|
+
discover what it's talking to before pairing, nothing that requires trust. `grantsRevision` is
|
|
274
|
+
`null` before this agent has ever received a `PUT /v1/grants` (or if `grants.json` went missing).
|
|
275
|
+
`/v1/pair` authenticates by a one-time code instead of a bearer token, since pairing is what mints
|
|
276
|
+
the token in the first place.
|
|
277
|
+
- **Every other route requires `Authorization: Bearer <token>`**, compared with a
|
|
278
|
+
constant-time equality check against a stored hash (never the token itself). Before pairing,
|
|
279
|
+
every such route answers `401 {"error": "not paired"}`; a present-but-wrong token answers
|
|
280
|
+
`401 {"error": "unauthorized"}`.
|
|
281
|
+
- **Grants: pushed by the console, enforced here, self-healing (#50).** The console's
|
|
282
|
+
`machine_grants` table is the source of truth; every change is pushed via `PUT /v1/grants` (bearer-
|
|
283
|
+
gated, refuses a caller carrying `X-AC-Plugin` — that header is how a real verb's own guard tells a
|
|
284
|
+
plugin's request apart from the console's own, so a plugin can never reach this route), persisted
|
|
285
|
+
to `grants.json`, and read back on every `serve` start. A push that fails at change time (agent
|
|
286
|
+
down, network) is not the end of the story: the very next verb request from that plugin fails its
|
|
287
|
+
revision check (`409`, see **Verbs**), the console pushes again and retries once, and the request
|
|
288
|
+
succeeds or is genuinely refused — so a grant change reaches the agent on its next request either
|
|
289
|
+
way, without restarting either side.
|
|
290
|
+
- **A caller must pin the certificate's fingerprint, not trust a system CA** — there is no CA here,
|
|
291
|
+
by design, and a pairing console holds only the fingerprint an operator typed at pairing time, never
|
|
292
|
+
the certificate's PEM. The correct client shape is therefore an explicit compare, not
|
|
293
|
+
`checkServerIdentity`: open the TLS socket with `rejectUnauthorized: false` (there is no chain to
|
|
294
|
+
validate against), then on connect compare `getPeerCertificate().fingerprint256` — the same
|
|
295
|
+
colon-separated uppercase hex SHA-256 this package's own `status`/install output prints — against
|
|
296
|
+
the pinned value yourself, and destroy the socket on any mismatch. This package's own
|
|
297
|
+
`server.integration.test.ts` exercises exactly that shape against a real TLS socket, both for a
|
|
298
|
+
correct pin and a wrong one.
|
|
299
|
+
**`rejectUnauthorized: false` is not a pin by itself, and pairing it with `checkServerIdentity`
|
|
300
|
+
doesn't make it one:** verified directly against this Node version that `checkServerIdentity` is
|
|
301
|
+
never invoked at all once `rejectUnauthorized` is false — the whole verification pathway is
|
|
302
|
+
skipped, so a "pin" layered on top of it is dead code, not an additional check. (Holding the
|
|
303
|
+
actual certificate PEM in hand — which this package's own CLI does, reading its own on-disk
|
|
304
|
+
`cert.pem` for its `status` self-check — is a different, legitimate case: `ca: <PEM>` with the
|
|
305
|
+
default `rejectUnauthorized: true` validates the chain and keeps `checkServerIdentity` live. A
|
|
306
|
+
pairing console never has that PEM, only the fingerprint, so it needs the explicit-compare shape
|
|
307
|
+
above.)
|