hilos-agent 0.10.1 → 0.11.2
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 +21 -0
- package/README.md +54 -10
- package/bin/hilos-agent.mjs +87 -14
- package/package.json +7 -5
- package/src/acp-session.mjs +16 -1
- package/src/cli.mjs +34 -1
- package/src/codex-mcp-session.mjs +36 -0
- package/src/config.mjs +39 -5
- package/src/conflicts.mjs +156 -0
- package/src/cursor-store.mjs +43 -8
- package/src/handler.mjs +1197 -127
- package/src/iterate-claim-recovery.mjs +574 -0
- package/src/local-harness-events.mjs +370 -0
- package/src/local-harness-transport.mjs +157 -0
- package/src/mcp.mjs +411 -34
- package/src/model-resolve.mjs +3 -1
- package/src/opencode-session.mjs +22 -2
- package/src/progress-emitter.mjs +1 -1
- package/src/queue.mjs +70 -3
- package/src/redact.mjs +360 -7
- package/src/run-inputs.mjs +211 -0
- package/src/run.mjs +462 -76
- package/src/thread-pr.mjs +26 -4
- package/src/webmcp-bridge.mjs +77 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pablo Stanley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -113,8 +113,11 @@ tools in the machine's config:
|
|
|
113
113
|
The site does not authorize itself: `readOnlyHint` is informational, while this
|
|
114
114
|
person-owned list decides what may run. Site descriptions are omitted, schema
|
|
115
115
|
prose is stripped, results are bounded and labeled untrusted, and cookies stay
|
|
116
|
-
inside a separate browser profile. The
|
|
117
|
-
Node.js
|
|
116
|
+
inside a separate browser profile. The optional browser bridge needs Node.js 24
|
|
117
|
+
or newer. On Node.js 20 or 22 WebMCP stays unavailable, while the rest of the
|
|
118
|
+
daemon keeps working regardless of whether that optional package was omitted by
|
|
119
|
+
the installer. Run `hilos-agent webmcp doctor` after upgrading Node.js and
|
|
120
|
+
reinstalling the package.
|
|
118
121
|
|
|
119
122
|
```sh
|
|
120
123
|
hilos-agent webmcp doctor
|
|
@@ -128,7 +131,9 @@ hilos-agent webmcp close
|
|
|
128
131
|
The daemon adds this capability and its citation rules to agent prompts only
|
|
129
132
|
when the config is valid. Unlisted tools — including writes — are refused with
|
|
130
133
|
`human_approval_required`; there is no approval flag the agent can set. WebMCP
|
|
131
|
-
can never approve or merge hilos work.
|
|
134
|
+
can never approve or merge hilos work. This wrapper is a policy boundary for
|
|
135
|
+
the bridge, not a sandbox around the coding CLI, which still has the machine
|
|
136
|
+
access its operator granted. See the complete contract in
|
|
132
137
|
[WebMCP in hilos](https://hilos.sh/docs/webmcp).
|
|
133
138
|
|
|
134
139
|
## How it works
|
|
@@ -158,7 +163,16 @@ can never approve or merge hilos work. See the complete contract in
|
|
|
158
163
|
PR thread is executed, not described. The daemon relays the request to hilos
|
|
159
164
|
with the id of the message that asked; hilos verifies the person's role and
|
|
160
165
|
that their message really asks for it, then acts with the workspace's GitHub
|
|
161
|
-
App.
|
|
166
|
+
App. If GitHub reports a same-repository merge conflict, the daemon keeps the
|
|
167
|
+
same turn alive: it fetches the base branch, starts a real two-parent merge on
|
|
168
|
+
the existing PR branch, names the conflicted files to the coding agent,
|
|
169
|
+
verifies that no unmerged entries or conflict markers remain, and pushes the
|
|
170
|
+
repaired head for fresh human review. It never opens a replacement PR or
|
|
171
|
+
silently merges after repair. Fork PRs stop with an explicit writable-branch
|
|
172
|
+
boundary. The daemon never merges on its own judgment and holds no merge
|
|
173
|
+
rights. With `gate:true`, this existing-branch repair still pushes so the PR
|
|
174
|
+
can return for fresh review. It is the only approve-before-push exception;
|
|
175
|
+
the repaired head remains unmerged and needs a new approval.
|
|
162
176
|
- **Open a PR** (default) — it commits, pushes with *your* `git`/`gh`, opens a PR,
|
|
163
177
|
and posts a report card with the link. Review on the card: **Approve** merges,
|
|
164
178
|
**Reject** closes, **Request changes** re-works.
|
|
@@ -300,10 +314,9 @@ safest first:
|
|
|
300
314
|
|
|
301
315
|
The default stays `acceptEdits`. Reach for `--dangerously-skip-permissions` when
|
|
302
316
|
you want a truly hands-off teammate, and keep `gate:true` if you'd rather review
|
|
303
|
-
before anything is pushed.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
vendor-neutral hilos substrate.
|
|
317
|
+
before anything is pushed. When the workspace grants runtime approvals,
|
|
318
|
+
OpenCode, Cursor, Claude Code, and Codex all pause on the same durable hilos
|
|
319
|
+
permission card. A refusal or broken decision path fails closed.
|
|
307
320
|
|
|
308
321
|
## Hooks — keep a raw Codex, Claude Code, or Cursor session in the room
|
|
309
322
|
|
|
@@ -396,6 +409,16 @@ hilos's GitHub App only for workspace owners/admins). Want a human checkpoint
|
|
|
396
409
|
before anything is pushed? Set `"gate": true`. Keep your token in the config file
|
|
397
410
|
or `HILOS_TOKEN`, never in shared shell history.
|
|
398
411
|
|
|
412
|
+
### Durable activity, not raw output
|
|
413
|
+
|
|
414
|
+
On a current hilos server, daemon-launched Claude Code, Codex, Cursor, and
|
|
415
|
+
OpenCode repo runs automatically keep the same bounded activity facts used by
|
|
416
|
+
the live card—phases, tool/file activity, notes, reported usage, and completion—
|
|
417
|
+
in hilos's runtime-neutral event record. Writes are batched, retry-safe,
|
|
418
|
+
bounded to two one-second attempts, re-authorized and redacted on the server,
|
|
419
|
+
and never allowed to fail the coding run. Raw stdout is not sent by this path.
|
|
420
|
+
An older server simply leaves it off.
|
|
421
|
+
|
|
399
422
|
### Run transcripts are opt-in
|
|
400
423
|
|
|
401
424
|
The room gets what a teammate needs to see: a plan, live progress, a report, a
|
|
@@ -421,8 +444,29 @@ restart — and **deleting** the key counts as off, not as "leave it as it was".
|
|
|
421
444
|
|
|
422
445
|
## Flags
|
|
423
446
|
|
|
424
|
-
`--join <blob>` · `--channel <id>` · `--config <path>` ·
|
|
425
|
-
`--
|
|
447
|
+
`--join <blob>` · `--join-stdin` · `--channel <id>` · `--config <path>` ·
|
|
448
|
+
`--url <endpoint>` · `--token <token>` ·
|
|
449
|
+
`--coding-cmd <cmd>` · `--coding-model <tier>` · `--chat-cmd <cmd>` ·
|
|
450
|
+
`--web-search` · `--no-web-search` · `--once` · `--backfill` · `--gate` ·
|
|
451
|
+
`--no-gate` · `--no-reply-bridge` · `--version` · `--help`. Hook installation
|
|
452
|
+
also accepts `--global` or one of `--claude`, `--codex`, and `--cursor`.
|
|
453
|
+
|
|
454
|
+
`--token` remains only for compatibility with older scripts. Command-line
|
|
455
|
+
arguments can be visible to other processes on the machine, so use
|
|
456
|
+
`--join-stdin`, `HILOS_TOKEN`, or the private config file for credentials.
|
|
457
|
+
Likewise, prefer `--join-stdin` over the legacy `--join <blob>` form for a new
|
|
458
|
+
connection.
|
|
426
459
|
|
|
427
460
|
Env: `HILOS_TOKEN`, `HILOS_URL`, `HILOS_CHANNEL`, `CODING_CMD`, `HILOS_ONCE=1`,
|
|
428
461
|
`HILOS_BACKFILL=1`, `HILOS_UPLOAD_TRANSCRIPTS=1|0`.
|
|
462
|
+
|
|
463
|
+
## Releasing
|
|
464
|
+
|
|
465
|
+
Bump `version` in `package.json`, merge, wait for main CI, then tag that commit
|
|
466
|
+
`hilos-agent-v<version>` and push the tag. The `npm-publish` workflow requires
|
|
467
|
+
that exact version, a commit contained in `main`, and a successful `verify`
|
|
468
|
+
check. It packs and clean-installs the artifact on Node.js 20, 22, and 24 before
|
|
469
|
+
publishing, then installs the registry copy and checks its version and help.
|
|
470
|
+
A manual workflow dispatch is always a dry run and cannot publish. The current
|
|
471
|
+
release path needs the `NPM_TOKEN` repository secret until npm trusted
|
|
472
|
+
publishing is configured (1094, 1211).
|
package/bin/hilos-agent.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// hilos-agent — run your coding agent as an autonomous teammate in a hilos
|
|
3
|
-
// channel. Picks up @mentions
|
|
4
|
-
//
|
|
3
|
+
// channel. Picks up @mentions and opens a PR by default; --gate adds a human
|
|
4
|
+
// approval checkpoint before the push. Your code + credentials never leave
|
|
5
|
+
// your machine.
|
|
5
6
|
//
|
|
6
7
|
// Usage:
|
|
7
8
|
// hilos-agent --join <blob> connect with a copy-paste link from hilos
|
|
@@ -35,43 +36,107 @@ function packageVersion() {
|
|
|
35
36
|
return JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function requiredOptionValue(argv, index, option) {
|
|
40
|
+
const value = argv[index + 1];
|
|
41
|
+
if (value === undefined || value.startsWith("-")) {
|
|
42
|
+
throw new Error(`Option ${option} requires a value.`);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateCommand(cmd, positional, { skipShape = false } = {}) {
|
|
48
|
+
const commands = new Set(["run", "init", "hook", "hooks", "webmcp", "web", "help", "version"]);
|
|
49
|
+
if (!commands.has(cmd)) {
|
|
50
|
+
throw new Error(`Unknown command: ${cmd}. Try \`hilos-agent --help\`.`);
|
|
51
|
+
}
|
|
52
|
+
if (skipShape) return;
|
|
53
|
+
|
|
54
|
+
if (["run", "init", "hook", "help", "version"].includes(cmd) && positional.length > 1) {
|
|
55
|
+
throw new Error(`Unexpected argument for ${cmd}: ${positional[1]}. Try \`hilos-agent --help\`.`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (cmd === "hooks") {
|
|
59
|
+
const subcommand = positional[1];
|
|
60
|
+
if (!subcommand) throw new Error("A hooks command is required: install or print.");
|
|
61
|
+
if (!new Set(["install", "print"]).has(subcommand)) {
|
|
62
|
+
throw new Error(`Unknown hooks command: ${subcommand}. Use install or print.`);
|
|
63
|
+
}
|
|
64
|
+
if (positional.length > 2) {
|
|
65
|
+
throw new Error(`Unexpected argument for hooks ${subcommand}: ${positional[2]}.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (cmd === "web") {
|
|
70
|
+
const subcommand = positional[1] || "doctor";
|
|
71
|
+
if (subcommand !== "doctor") {
|
|
72
|
+
throw new Error(`Unknown web command: ${subcommand}. Try \`hilos-agent web doctor\`.`);
|
|
73
|
+
}
|
|
74
|
+
if (positional.length > 2) {
|
|
75
|
+
throw new Error(`Unexpected argument for web doctor: ${positional[2]}.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (cmd === "webmcp") {
|
|
80
|
+
const operation = positional[1] || "doctor";
|
|
81
|
+
const allowed = new Set(["doctor", "login", "open", "tools", "call", "close"]);
|
|
82
|
+
if (!allowed.has(operation)) {
|
|
83
|
+
throw new Error(`Unknown webmcp command: ${operation}. Use doctor, login, open, tools, call, or close.`);
|
|
84
|
+
}
|
|
85
|
+
const argumentCount = positional.length - 2;
|
|
86
|
+
const validCount = operation === "login" || operation === "open"
|
|
87
|
+
? argumentCount === 1
|
|
88
|
+
: operation === "call"
|
|
89
|
+
? argumentCount === 1 || argumentCount === 2
|
|
90
|
+
: argumentCount === 0;
|
|
91
|
+
if (!validCount) {
|
|
92
|
+
throw new Error(`Invalid arguments for webmcp ${operation}. Try \`hilos-agent --help\`.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
38
97
|
function parseArgs(argv) {
|
|
39
98
|
const flags = {};
|
|
40
99
|
const positional = [];
|
|
41
100
|
for (let i = 0; i < argv.length; i++) {
|
|
42
101
|
const a = argv[i];
|
|
43
|
-
if (a === "--join") flags.join = argv
|
|
102
|
+
if (a === "--join") flags.join = requiredOptionValue(argv, i++, a);
|
|
44
103
|
else if (a === "--join-stdin") flags.joinStdin = true;
|
|
45
|
-
else if (a === "--config") flags.config = argv
|
|
46
|
-
else if (a === "--channel") flags.channelId = argv
|
|
47
|
-
else if (a === "--url") flags.url = argv
|
|
48
|
-
else if (a === "--token") flags.token = argv
|
|
49
|
-
else if (a === "--coding-cmd") flags.codingCmd = argv
|
|
50
|
-
else if (a === "--coding-model") flags.codingModel = argv
|
|
51
|
-
else if (a === "--chat-cmd") flags.chatCmd = argv
|
|
104
|
+
else if (a === "--config") flags.config = requiredOptionValue(argv, i++, a);
|
|
105
|
+
else if (a === "--channel") flags.channelId = requiredOptionValue(argv, i++, a);
|
|
106
|
+
else if (a === "--url") flags.url = requiredOptionValue(argv, i++, a);
|
|
107
|
+
else if (a === "--token") flags.token = requiredOptionValue(argv, i++, a);
|
|
108
|
+
else if (a === "--coding-cmd") flags.codingCmd = requiredOptionValue(argv, i++, a);
|
|
109
|
+
else if (a === "--coding-model") flags.codingModel = requiredOptionValue(argv, i++, a);
|
|
110
|
+
else if (a === "--chat-cmd") flags.chatCmd = requiredOptionValue(argv, i++, a);
|
|
52
111
|
else if (a === "--no-web-search") flags.webSearch = false;
|
|
53
112
|
else if (a === "--web-search") flags.webSearch = true;
|
|
54
113
|
else if (a === "--once") flags.once = true;
|
|
55
114
|
else if (a === "--backfill") flags.backfill = true;
|
|
115
|
+
else if (a === "--gate") flags.gate = true;
|
|
56
116
|
else if (a === "--no-gate") flags.gate = false;
|
|
57
117
|
else if (a === "--no-reply-bridge") flags.replyBridge = false;
|
|
58
118
|
else if (a === "--global") flags.global = true;
|
|
59
119
|
else if (a === "--claude") flags.hookClient = "claude";
|
|
60
120
|
else if (a === "--codex") flags.hookClient = "codex";
|
|
61
121
|
else if (a === "--cursor") flags.hookClient = "cursor";
|
|
62
|
-
else if (a === "--vendor") flags.vendor = argv
|
|
122
|
+
else if (a === "--vendor") flags.vendor = requiredOptionValue(argv, i++, a);
|
|
63
123
|
else if (a === "--scope-managed") flags.scopeManaged = true;
|
|
124
|
+
// Marker embedded in self-contained hook commands. It is intentionally
|
|
125
|
+
// internal: hook.mjs uses it to recognize and replace managed installs.
|
|
64
126
|
else if (a === "--managed-runtime") flags.managedRuntime = true;
|
|
65
127
|
else if (a === "-h" || a === "--help") flags.help = true;
|
|
66
128
|
else if (a === "-v" || a === "--version") flags.version = true;
|
|
129
|
+
else if (a.startsWith("-")) throw new Error(`Unknown option: ${a}. Try \`hilos-agent --help\`.`);
|
|
67
130
|
else positional.push(a);
|
|
68
131
|
}
|
|
69
|
-
|
|
132
|
+
const cmd = positional[0] || "run";
|
|
133
|
+
validateCommand(cmd, positional, { skipShape: flags.help || flags.version });
|
|
134
|
+
return { cmd, flags, positional };
|
|
70
135
|
}
|
|
71
136
|
|
|
72
137
|
const HELP = `hilos-agent — your coding agent as a teammate in hilos
|
|
73
138
|
|
|
74
|
-
hilos-agent --join <blob>
|
|
139
|
+
hilos-agent --join <blob> legacy argv-compatible connect link
|
|
75
140
|
hilos-agent --join-stdin paste the private link at a no-echo prompt
|
|
76
141
|
hilos-agent init write a starter config to ~/.hilos/agent.json
|
|
77
142
|
hilos-agent webmcp doctor verify the local WebMCP browser bridge
|
|
@@ -82,16 +147,21 @@ const HELP = `hilos-agent — your coding agent as a teammate in hilos
|
|
|
82
147
|
hilos-agent webmcp close close the isolated browser session
|
|
83
148
|
hilos-agent web doctor report this CLI's native public-web capability
|
|
84
149
|
hilos-agent run the daemon (watch @mentions, propose diffs)
|
|
150
|
+
hilos-agent run same as above, explicit
|
|
85
151
|
hilos-agent hooks install stream this repo's Codex, Claude, and Cursor
|
|
86
152
|
sessions to hilos and continue replies in the same
|
|
87
153
|
local session. Installs all three hook formats;
|
|
88
154
|
use --codex, --claude, or --cursor to choose, and
|
|
89
155
|
--global for every repo. HILOS_HOOKS=off pauses
|
|
90
156
|
streaming; HILOS_REPLY_BRIDGE=off pauses pickup.
|
|
157
|
+
hilos-agent hooks print preview the hook configuration without writing
|
|
91
158
|
|
|
92
159
|
Options:
|
|
93
160
|
--channel <id> watch only one channel (per-channel override)
|
|
94
161
|
--config <path> use a specific config file
|
|
162
|
+
--url <endpoint> override the MCP endpoint (or use HILOS_URL/config)
|
|
163
|
+
--token <token> legacy token override; argv may be visible to other local
|
|
164
|
+
processes. Prefer --join-stdin or HILOS_TOKEN.
|
|
95
165
|
--coding-cmd <cmd> the coding agent to run — claude -p, codex exec,
|
|
96
166
|
cursor-agent -p --trust, opencode run, agy -p, hermes -z,
|
|
97
167
|
or any command
|
|
@@ -104,10 +174,12 @@ Options:
|
|
|
104
174
|
--chat-cmd <cmd> fast command for chat replies + the plan-ack (default:
|
|
105
175
|
derived from the coding command, so a Codex or Cursor
|
|
106
176
|
daemon chats with its own tool)
|
|
177
|
+
--web-search allow hilos to request native public web tools (default)
|
|
107
178
|
--no-web-search stop hilos from enabling/requesting native public web
|
|
108
179
|
--once one poll then exit (cron-friendly)
|
|
109
180
|
--backfill also act on mentions that predate startup
|
|
110
|
-
--
|
|
181
|
+
--gate wait for approval in hilos before pushing a branch
|
|
182
|
+
--no-gate open a PR directly without pre-push approval (default)
|
|
111
183
|
--no-reply-bridge don't resume local sessions from replies in bound threads
|
|
112
184
|
-v, --version print the installed version
|
|
113
185
|
-h, --help this help
|
|
@@ -158,6 +230,7 @@ async function main() {
|
|
|
158
230
|
const starter = { ...(joinPayload || {}) };
|
|
159
231
|
if (flags.codingCmd) starter.codingCmd = flags.codingCmd;
|
|
160
232
|
if (flags.codingModel) starter.codingModel = flags.codingModel;
|
|
233
|
+
if (flags.gate !== undefined) starter.gate = flags.gate;
|
|
161
234
|
const path = writeStarterConfig(joinPayload ? GLOBAL_CONFIG : flags.config, starter);
|
|
162
235
|
console.log(`Wrote ${path}.`);
|
|
163
236
|
console.log(joinPayload ? "Token + endpoint set from your link." : "Fill in token + repos, then run `hilos-agent`.");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hilos-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Run your own coding agent (Claude Code
|
|
3
|
+
"version": "0.11.2",
|
|
4
|
+
"description": "Run your own coding agent (Claude Code, Codex, Cursor, OpenCode, Hermes, or any command) as a teammate in a hilos room. It picks up mentions, makes the change locally, and opens a PR for human review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"hilos-agent": "bin/hilos-agent.mjs"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=20"
|
|
16
16
|
},
|
|
17
17
|
"homepage": "https://hilos.sh",
|
|
18
18
|
"repository": {
|
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
"claude-code",
|
|
31
31
|
"codex",
|
|
32
32
|
"cursor",
|
|
33
|
+
"opencode",
|
|
34
|
+
"hermes-agent",
|
|
33
35
|
"coding-agent"
|
|
34
36
|
],
|
|
35
37
|
"license": "MIT",
|
|
36
|
-
"
|
|
37
|
-
"agent-browser": "^0.
|
|
38
|
+
"optionalDependencies": {
|
|
39
|
+
"agent-browser": "^0.36.0"
|
|
38
40
|
}
|
|
39
41
|
}
|
package/src/acp-session.mjs
CHANGED
|
@@ -191,6 +191,7 @@ export function createNdjsonParser() {
|
|
|
191
191
|
* requestPermission?: (request: object, context: object) => Promise<unknown>,
|
|
192
192
|
* getPermissionDecision?: (handle: unknown, context: object) => Promise<unknown>,
|
|
193
193
|
* mcpServers?: object[],
|
|
194
|
+
* beforeSpawn?: () => boolean | Promise<boolean>,
|
|
194
195
|
* spawnImpl?: (cmd: string, args: string[], options: object) => import("node:child_process").ChildProcess,
|
|
195
196
|
* setTimer?: typeof setTimeout,
|
|
196
197
|
* clearTimer?: typeof clearTimeout,
|
|
@@ -217,6 +218,7 @@ export async function runAcpSession({
|
|
|
217
218
|
/** Session to continue (0778); null starts a fresh one. */
|
|
218
219
|
resumeSessionId = null,
|
|
219
220
|
mcpServers = [],
|
|
221
|
+
beforeSpawn,
|
|
220
222
|
spawnImpl = spawn,
|
|
221
223
|
setTimer = setTimeout,
|
|
222
224
|
clearTimer = clearTimeout,
|
|
@@ -264,6 +266,7 @@ export async function runAcpSession({
|
|
|
264
266
|
let currentMessageId = null;
|
|
265
267
|
let messageBuffer = "";
|
|
266
268
|
const eventMapper = createAcpEventMapper();
|
|
269
|
+
let authorityLost = false;
|
|
267
270
|
|
|
268
271
|
const emitOutput = (text) => {
|
|
269
272
|
try {
|
|
@@ -411,6 +414,17 @@ export async function runAcpSession({
|
|
|
411
414
|
}
|
|
412
415
|
|
|
413
416
|
try {
|
|
417
|
+
if (typeof beforeSpawn === "function") {
|
|
418
|
+
try {
|
|
419
|
+
if ((await beforeSpawn()) === false) {
|
|
420
|
+
authorityLost = true;
|
|
421
|
+
throw new Error("execution authority lost before ACP spawn");
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
authorityLost = true;
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
414
428
|
child = spawnImpl(cmd, acpArgs, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
415
429
|
const spawned = new Promise((resolve, reject) => {
|
|
416
430
|
child.once("spawn", resolve);
|
|
@@ -508,13 +522,14 @@ export async function runAcpSession({
|
|
|
508
522
|
};
|
|
509
523
|
} catch (error) {
|
|
510
524
|
flushMessage();
|
|
511
|
-
const aborted = abortKind === "cancelled";
|
|
525
|
+
const aborted = abortKind === "cancelled" || authorityLost;
|
|
512
526
|
const timedOut = abortKind === "timeout";
|
|
513
527
|
return {
|
|
514
528
|
status: null,
|
|
515
529
|
stdout: stdout.trimEnd(),
|
|
516
530
|
stderr: stderr.trimEnd(),
|
|
517
531
|
...(aborted ? { aborted: true } : {}),
|
|
532
|
+
...(authorityLost ? { authorityLost: true } : {}),
|
|
518
533
|
...(sessionId ? { sessionId } : {}),
|
|
519
534
|
error:
|
|
520
535
|
error instanceof Error && !timedOut
|
package/src/cli.mjs
CHANGED
|
@@ -179,6 +179,9 @@ const MAX_CAPTURE_BYTES = 50 * 1024 * 1024;
|
|
|
179
179
|
* BEFORE the ungated compat retry is spawned when a CLI rejects the 0777
|
|
180
180
|
* permission flags (0785), so the caller can warn its room while the run can
|
|
181
181
|
* still be stopped. A throw here never fails the run.
|
|
182
|
+
* @property {() => (boolean | Promise<boolean>)} [beforeSpawn] - fail-closed
|
|
183
|
+
* authority check, awaited immediately before each child-process attempt.
|
|
184
|
+
* Returning false or throwing prevents that spawn and marks the run aborted.
|
|
182
185
|
*/
|
|
183
186
|
|
|
184
187
|
/**
|
|
@@ -191,7 +194,7 @@ const MAX_CAPTURE_BYTES = 50 * 1024 * 1024;
|
|
|
191
194
|
*
|
|
192
195
|
* @param {RunCliOptions} opts
|
|
193
196
|
*/
|
|
194
|
-
function runCliOnce(opts) {
|
|
197
|
+
async function runCliOnce(opts) {
|
|
195
198
|
const {
|
|
196
199
|
cmd,
|
|
197
200
|
args = [],
|
|
@@ -204,7 +207,37 @@ function runCliOnce(opts) {
|
|
|
204
207
|
signal,
|
|
205
208
|
onData,
|
|
206
209
|
env,
|
|
210
|
+
beforeSpawn,
|
|
207
211
|
} = opts || {};
|
|
212
|
+
// This guard belongs at the process boundary, after all potentially-slow
|
|
213
|
+
// caller setup and once per compatibility retry. Checking in runCli's caller
|
|
214
|
+
// leaves a race before the first child and lets its internal retries escape.
|
|
215
|
+
if (signal?.aborted) {
|
|
216
|
+
return { status: null, stdout: "", stderr: "", aborted: true, error: new Error("cancelled") };
|
|
217
|
+
}
|
|
218
|
+
if (typeof beforeSpawn === "function") {
|
|
219
|
+
try {
|
|
220
|
+
if ((await beforeSpawn()) === false) {
|
|
221
|
+
return {
|
|
222
|
+
status: null,
|
|
223
|
+
stdout: "",
|
|
224
|
+
stderr: "",
|
|
225
|
+
aborted: true,
|
|
226
|
+
authorityLost: true,
|
|
227
|
+
error: new Error("execution authority lost before spawn"),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return {
|
|
232
|
+
status: null,
|
|
233
|
+
stdout: "",
|
|
234
|
+
stderr: "",
|
|
235
|
+
aborted: true,
|
|
236
|
+
authorityLost: true,
|
|
237
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
208
241
|
return new Promise((resolve) => {
|
|
209
242
|
// Already cancelled before we even start.
|
|
210
243
|
if (signal?.aborted) {
|
|
@@ -160,6 +160,7 @@ export function mapCodexDecision(reply, availableDecisions) {
|
|
|
160
160
|
* onEvent?: (event: object) => void,
|
|
161
161
|
* requestPermission?: (request: object, context: object) => Promise<unknown>,
|
|
162
162
|
* getPermissionDecision?: (handle: unknown, context: object) => Promise<unknown>,
|
|
163
|
+
* beforeSpawn?: () => boolean | Promise<boolean>,
|
|
163
164
|
* spawnImpl?: typeof spawn,
|
|
164
165
|
* sleep?: (ms: number) => Promise<void>,
|
|
165
166
|
* now?: () => number,
|
|
@@ -185,6 +186,7 @@ export async function runCodexMcpSession({
|
|
|
185
186
|
onEvent,
|
|
186
187
|
requestPermission,
|
|
187
188
|
getPermissionDecision,
|
|
189
|
+
beforeSpawn,
|
|
188
190
|
spawnImpl = spawn,
|
|
189
191
|
sleep,
|
|
190
192
|
now = () => Date.now(),
|
|
@@ -199,6 +201,40 @@ export async function runCodexMcpSession({
|
|
|
199
201
|
if (signal?.aborted) onOuterAbort();
|
|
200
202
|
else signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
201
203
|
|
|
204
|
+
if (typeof beforeSpawn === "function") {
|
|
205
|
+
try {
|
|
206
|
+
if ((await beforeSpawn()) === false) {
|
|
207
|
+
signal?.removeEventListener("abort", onOuterAbort);
|
|
208
|
+
return {
|
|
209
|
+
status: null,
|
|
210
|
+
stdout: "",
|
|
211
|
+
stderr: "",
|
|
212
|
+
error: new Error("execution authority lost before Codex MCP spawn"),
|
|
213
|
+
sessionId: null,
|
|
214
|
+
initialized: false,
|
|
215
|
+
sawFrame: false,
|
|
216
|
+
exitCode: null,
|
|
217
|
+
aborted: true,
|
|
218
|
+
authorityLost: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
signal?.removeEventListener("abort", onOuterAbort);
|
|
223
|
+
return {
|
|
224
|
+
status: null,
|
|
225
|
+
stdout: "",
|
|
226
|
+
stderr: "",
|
|
227
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
228
|
+
sessionId: null,
|
|
229
|
+
initialized: false,
|
|
230
|
+
sawFrame: false,
|
|
231
|
+
exitCode: null,
|
|
232
|
+
aborted: true,
|
|
233
|
+
authorityLost: true,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
202
238
|
const child = spawnImpl(cmd, serverArgs, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
203
239
|
const parser = createNdjsonParser();
|
|
204
240
|
const pending = new Map();
|
package/src/config.mjs
CHANGED
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
// overlaid by env vars and CLI flags / a --join blob. The join blob carries the
|
|
3
3
|
// MCP url + token (+ optional channel) so a user can paste one command.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
chmodSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
6
15
|
import { homedir } from "node:os";
|
|
7
|
-
import {
|
|
16
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
8
17
|
|
|
9
18
|
export const GLOBAL_CONFIG = join(homedir(), ".hilos", "agent.json");
|
|
10
19
|
export const LOCAL_CONFIG = "hilos-agent.json";
|
|
@@ -347,7 +356,16 @@ export function reloadConfig(prev) {
|
|
|
347
356
|
/** Write a starter config file (used by `hilos-agent init`). */
|
|
348
357
|
export function writeStarterConfig(path, partial = {}) {
|
|
349
358
|
const target = path || GLOBAL_CONFIG;
|
|
350
|
-
|
|
359
|
+
const targetDir = dirname(target);
|
|
360
|
+
const directoryExisted = existsSync(targetDir);
|
|
361
|
+
mkdirSync(targetDir, { recursive: true, mode: 0o700 });
|
|
362
|
+
// A newly created credentials directory must be private. Also repair the
|
|
363
|
+
// default ~/.hilos directory if an older install created it permissively,
|
|
364
|
+
// without unexpectedly chmod'ing an arbitrary existing directory supplied
|
|
365
|
+
// through --config (which may be a repository root).
|
|
366
|
+
if (!directoryExisted || resolve(targetDir) === resolve(dirname(GLOBAL_CONFIG))) {
|
|
367
|
+
chmodSync(targetDir, 0o700);
|
|
368
|
+
}
|
|
351
369
|
const starter = {
|
|
352
370
|
url: partial.url || DEFAULTS.url,
|
|
353
371
|
token: partial.token || "",
|
|
@@ -358,8 +376,24 @@ export function writeStarterConfig(path, partial = {}) {
|
|
|
358
376
|
webSearch: partial.webSearch !== false,
|
|
359
377
|
defaultBranch: DEFAULTS.defaultBranch,
|
|
360
378
|
// false = open a PR directly (bias to action); true = approve-before-push.
|
|
361
|
-
gate:
|
|
379
|
+
gate: partial.gate === true,
|
|
362
380
|
};
|
|
363
|
-
|
|
381
|
+
// Write beside the destination and rename it into place so a crash cannot
|
|
382
|
+
// leave a truncated token file. The temporary starts private, and replacing
|
|
383
|
+
// an older 0644 file also tightens the final path to 0600 in one rename.
|
|
384
|
+
const temporary = join(
|
|
385
|
+
targetDir,
|
|
386
|
+
`.${basename(target)}.${process.pid}.${randomUUID()}.tmp`,
|
|
387
|
+
);
|
|
388
|
+
try {
|
|
389
|
+
writeFileSync(temporary, JSON.stringify(starter, null, 2) + "\n", {
|
|
390
|
+
flag: "wx",
|
|
391
|
+
mode: 0o600,
|
|
392
|
+
});
|
|
393
|
+
renameSync(temporary, target);
|
|
394
|
+
chmodSync(target, 0o600);
|
|
395
|
+
} finally {
|
|
396
|
+
rmSync(temporary, { force: true });
|
|
397
|
+
}
|
|
364
398
|
return target;
|
|
365
399
|
}
|