hilos-agent 0.9.5 → 0.10.1
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 +53 -4
- package/bin/hilos-agent.mjs +66 -0
- package/package.json +5 -2
- package/src/cli.mjs +29 -1
- package/src/codex-mcp-session.mjs +3 -1
- package/src/config.mjs +29 -0
- package/src/handler.mjs +52 -21
- package/src/progress-emitter.mjs +16 -32
- package/src/run.mjs +18 -0
- package/src/web-tools.mjs +188 -0
- package/src/webmcp-bridge.mjs +393 -0
- package/src/webmcp-init.js +281 -0
package/README.md
CHANGED
|
@@ -50,6 +50,7 @@ Running from elsewhere, or want to map several repos explicitly? Use a config:
|
|
|
50
50
|
"codingCmd": "claude -p --permission-mode acceptEdits", // safe default; see Permissions / autonomy. or "codex exec", "cursor-agent -p --output-format text --trust", "opencode run", "agy -p", any command
|
|
51
51
|
"codingModel": "", // Codex tier ("most-capable" | "balanced" | "fastest"), resolved against this account's own model list; "" = the tool's default
|
|
52
52
|
"chatCmd": "", // FAST command for chat replies + the plan-ack. Empty = derived from codingCmd's tool (codex daemons chat with codex, etc.); set to override
|
|
53
|
+
"webSearch": true, // native public-web tools are on by default; false stops Hilos enablement/instructions
|
|
53
54
|
"defaultBranch": "main",
|
|
54
55
|
"gate": false, // default: open a PR directly. true = approve-before-push
|
|
55
56
|
"heartbeatMs": 180000, // long runs post one "still working…" thread reply this often (0 = off, min 15s)
|
|
@@ -71,10 +72,19 @@ a stderr tail) instead of claiming "no changes". Chat replies use the faster
|
|
|
71
72
|
`chatCmd` (when unset, derived from `codingCmd`'s tool — a Claude daemon chats
|
|
72
73
|
with Haiku, a Codex daemon with `codex exec`, and so on) bounded by
|
|
73
74
|
`chatTimeoutMs`. The chat-vs-code pass only classifies; the separate chat
|
|
74
|
-
responder keeps the CLI's normal tools.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
responder keeps the CLI's normal tools. Public web is on by default and stays
|
|
76
|
+
native to the selected CLI: Hilos pre-approves Claude Code's read-only
|
|
77
|
+
`WebSearch`/`WebFetch`, enables Codex search, turns on OpenCode's official Exa
|
|
78
|
+
search switch, preserves Hermes's full web-capable coding toolset, and keeps
|
|
79
|
+
Cursor and Antigravity's built-ins. Generated chat replies use Cursor `ask`
|
|
80
|
+
with safe auto-review, OpenCode
|
|
81
|
+
`plan`, Antigravity `plan`, Codex's read-only sandbox, or Hermes `safe,web`
|
|
82
|
+
where available; coding runs keep their normal tool/approval policy. Run
|
|
83
|
+
`hilos-agent web doctor` to inspect the configured contract. Set
|
|
84
|
+
`"webSearch": false`, `HILOS_WEB_SEARCH=0`, or `--no-web-search` to stop Hilos
|
|
85
|
+
from enabling or requesting web use; the CLI's own global/project tool policy
|
|
86
|
+
still applies. Custom commands retain their own tools without guessed flags.
|
|
87
|
+
The responsive surface needs a hilos server new enough to expose
|
|
78
88
|
`edit_message`; older servers just skip the live edits.
|
|
79
89
|
|
|
80
90
|
```sh
|
|
@@ -82,6 +92,45 @@ hilos-agent # watch every channel the agent is in
|
|
|
82
92
|
hilos-agent --channel <id> # scope to one channel
|
|
83
93
|
```
|
|
84
94
|
|
|
95
|
+
## WebMCP site tools
|
|
96
|
+
|
|
97
|
+
The local daemon can give its coding and chat agents a narrow bridge to
|
|
98
|
+
third-party WebMCP sites. It is off until you name exact origins and exact read
|
|
99
|
+
tools in the machine's config:
|
|
100
|
+
|
|
101
|
+
```jsonc
|
|
102
|
+
{
|
|
103
|
+
"webMcp": {
|
|
104
|
+
"origins": {
|
|
105
|
+
"https://docs.example.com": {
|
|
106
|
+
"readTools": ["search_docs", "read_reference"]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The site does not authorize itself: `readOnlyHint` is informational, while this
|
|
114
|
+
person-owned list decides what may run. Site descriptions are omitted, schema
|
|
115
|
+
prose is stripped, results are bounded and labeled untrusted, and cookies stay
|
|
116
|
+
inside a separate browser profile. The bundled browser bridge currently needs
|
|
117
|
+
Node.js 24 or newer; the rest of the daemon keeps its existing Node.js support.
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
hilos-agent webmcp doctor
|
|
121
|
+
hilos-agent webmcp login https://docs.example.com # person signs in in the opened browser
|
|
122
|
+
hilos-agent webmcp open https://docs.example.com/reference
|
|
123
|
+
hilos-agent webmcp tools
|
|
124
|
+
hilos-agent webmcp call search_docs '{"query":"WebMCP"}'
|
|
125
|
+
hilos-agent webmcp close
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The daemon adds this capability and its citation rules to agent prompts only
|
|
129
|
+
when the config is valid. Unlisted tools — including writes — are refused with
|
|
130
|
+
`human_approval_required`; there is no approval flag the agent can set. WebMCP
|
|
131
|
+
can never approve or merge hilos work. See the complete contract in
|
|
132
|
+
[WebMCP in hilos](https://hilos.sh/docs/webmcp).
|
|
133
|
+
|
|
85
134
|
## How it works
|
|
86
135
|
|
|
87
136
|
- **Trigger** — an `@mention` of your agent in a channel that's linked to a repo,
|
package/bin/hilos-agent.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// hilos-agent --join <blob> connect with a copy-paste link from hilos
|
|
8
8
|
// hilos-agent --join-stdin read a private link without exposing it in argv
|
|
9
9
|
// hilos-agent init write a starter config (~/.hilos/agent.json)
|
|
10
|
+
// hilos-agent webmcp doctor verify the local site-tool bridge
|
|
10
11
|
// hilos-agent run with the resolved config (default)
|
|
11
12
|
// hilos-agent run same as above, explicit
|
|
12
13
|
//
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
// codingCmd in hilos-agent.json to change it and the daemon picks it up on its
|
|
18
19
|
// next poll — no restart needed.
|
|
19
20
|
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
20
22
|
import { readFileSync } from "node:fs";
|
|
21
23
|
import { fileURLToPath } from "node:url";
|
|
22
24
|
|
|
@@ -24,6 +26,9 @@ import { resolveConfig, decodeJoin, writeStarterConfig, GLOBAL_CONFIG } from "..
|
|
|
24
26
|
import { readPrivateJoin } from "../src/join-input.mjs";
|
|
25
27
|
import { run } from "../src/run.mjs";
|
|
26
28
|
import { hookMain, hooksMain } from "../src/hook.mjs";
|
|
29
|
+
import { runWebMcpCommand } from "../src/webmcp-bridge.mjs";
|
|
30
|
+
import { detectVendor, fastChatCmd, webCapability } from "../src/progress-emitter.mjs";
|
|
31
|
+
import { commandArgv } from "../src/argv.mjs";
|
|
27
32
|
|
|
28
33
|
function packageVersion() {
|
|
29
34
|
const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
@@ -44,6 +49,8 @@ function parseArgs(argv) {
|
|
|
44
49
|
else if (a === "--coding-cmd") flags.codingCmd = argv[++i];
|
|
45
50
|
else if (a === "--coding-model") flags.codingModel = argv[++i];
|
|
46
51
|
else if (a === "--chat-cmd") flags.chatCmd = argv[++i];
|
|
52
|
+
else if (a === "--no-web-search") flags.webSearch = false;
|
|
53
|
+
else if (a === "--web-search") flags.webSearch = true;
|
|
47
54
|
else if (a === "--once") flags.once = true;
|
|
48
55
|
else if (a === "--backfill") flags.backfill = true;
|
|
49
56
|
else if (a === "--no-gate") flags.gate = false;
|
|
@@ -67,6 +74,13 @@ const HELP = `hilos-agent — your coding agent as a teammate in hilos
|
|
|
67
74
|
hilos-agent --join <blob> connect using a link copied from hilos
|
|
68
75
|
hilos-agent --join-stdin paste the private link at a no-echo prompt
|
|
69
76
|
hilos-agent init write a starter config to ~/.hilos/agent.json
|
|
77
|
+
hilos-agent webmcp doctor verify the local WebMCP browser bridge
|
|
78
|
+
hilos-agent webmcp login <url> open the isolated profile for person sign-in
|
|
79
|
+
hilos-agent webmcp open <url> open a person-allowlisted site for an agent
|
|
80
|
+
hilos-agent webmcp tools list registered, person-allowlisted read tools
|
|
81
|
+
hilos-agent webmcp call <name> '<json object>'
|
|
82
|
+
hilos-agent webmcp close close the isolated browser session
|
|
83
|
+
hilos-agent web doctor report this CLI's native public-web capability
|
|
70
84
|
hilos-agent run the daemon (watch @mentions, propose diffs)
|
|
71
85
|
hilos-agent hooks install stream this repo's Codex, Claude, and Cursor
|
|
72
86
|
sessions to hilos and continue replies in the same
|
|
@@ -90,6 +104,7 @@ Options:
|
|
|
90
104
|
--chat-cmd <cmd> fast command for chat replies + the plan-ack (default:
|
|
91
105
|
derived from the coding command, so a Codex or Cursor
|
|
92
106
|
daemon chats with its own tool)
|
|
107
|
+
--no-web-search stop hilos from enabling/requesting native public web
|
|
93
108
|
--once one poll then exit (cron-friendly)
|
|
94
109
|
--backfill also act on mentions that predate startup
|
|
95
110
|
--no-gate propose only; don't wait for approval / push
|
|
@@ -150,6 +165,57 @@ async function main() {
|
|
|
150
165
|
return;
|
|
151
166
|
}
|
|
152
167
|
|
|
168
|
+
if (cmd === "webmcp") {
|
|
169
|
+
const cliFlags = { ...flags };
|
|
170
|
+
delete cliFlags.help;
|
|
171
|
+
const cfg = resolveConfig({ flags: cliFlags });
|
|
172
|
+
const operation = positional[1] || "doctor";
|
|
173
|
+
const result = await runWebMcpCommand(cfg, operation, positional.slice(2));
|
|
174
|
+
console.log(JSON.stringify(result, null, 2));
|
|
175
|
+
if (!result.ok) process.exitCode = 1;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (cmd === "web") {
|
|
180
|
+
const operation = positional[1] || "doctor";
|
|
181
|
+
if (operation !== "doctor") {
|
|
182
|
+
throw new Error(`Unknown web command: ${operation}. Try \`hilos-agent web doctor\`.`);
|
|
183
|
+
}
|
|
184
|
+
const cliFlags = { ...flags };
|
|
185
|
+
delete cliFlags.help;
|
|
186
|
+
const cfg = resolveConfig({ flags: cliFlags });
|
|
187
|
+
const codingCommand = cfg.codingCmd;
|
|
188
|
+
const chatCommand = cfg.chatCmd || fastChatCmd(detectVendor(codingCommand)) || codingCommand;
|
|
189
|
+
const describe = (command) => {
|
|
190
|
+
const argv = commandArgv(command);
|
|
191
|
+
const vendor = detectVendor(command);
|
|
192
|
+
const capability = webCapability(vendor, {
|
|
193
|
+
enabled: cfg.webSearch !== false,
|
|
194
|
+
args: argv.slice(1),
|
|
195
|
+
});
|
|
196
|
+
const binary = argv[0] || "";
|
|
197
|
+
const probed = binary
|
|
198
|
+
? spawnSync(binary, ["--version"], { encoding: "utf8", timeout: 5_000 })
|
|
199
|
+
: null;
|
|
200
|
+
const binaryAvailable = Boolean(binary) && !probed?.error;
|
|
201
|
+
const version = String(probed?.stdout || probed?.stderr || "").trim().split("\n")[0] || null;
|
|
202
|
+
return { vendor, binary, binaryAvailable, version, command, ...capability };
|
|
203
|
+
};
|
|
204
|
+
const chat = describe(chatCommand);
|
|
205
|
+
const code = codingCommand === chatCommand ? chat : describe(codingCommand);
|
|
206
|
+
console.log(JSON.stringify({
|
|
207
|
+
ok: [chat, code].every((lane) => lane.status === "enabled" && lane.binaryAvailable),
|
|
208
|
+
chat,
|
|
209
|
+
code,
|
|
210
|
+
note: !chat.binaryAvailable || !code.binaryAvailable
|
|
211
|
+
? "One or more selected CLI binaries are not installed or not on PATH."
|
|
212
|
+
: chat.verified && code.verified
|
|
213
|
+
? "Configured by hilos-agent; this does not spend a model call or test provider credentials."
|
|
214
|
+
: "Custom commands keep their own tool configuration; hilos-agent does not guess flags.",
|
|
215
|
+
}, null, 2));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
153
219
|
// run (default) — when --join is passed without init, connect straight away.
|
|
154
220
|
const cliFlags = { ...flags };
|
|
155
221
|
delete cliFlags.join;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hilos-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Run your own coding agent (Claude Code / Codex / Cursor) as an autonomous teammate in a hilos channel. Picks up @mentions in channels and threads, makes the change, and opens a PR for review — your code and credentials never leave your machine. (Approve-before-push is available via gate:true.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,5 +32,8 @@
|
|
|
32
32
|
"cursor",
|
|
33
33
|
"coding-agent"
|
|
34
34
|
],
|
|
35
|
-
"license": "MIT"
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"agent-browser": "^0.35.0"
|
|
38
|
+
}
|
|
36
39
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -332,6 +332,13 @@ function runCliOnce(opts) {
|
|
|
332
332
|
// inside runCli (not a wrapper) so every call site, present and future, gets it.
|
|
333
333
|
const UNKNOWN_TRUST_RE = /unknown option '--trust'/;
|
|
334
334
|
|
|
335
|
+
// Compat retry (0974): --auto-review is the current Cursor CLI's safe,
|
|
336
|
+
// server-classified approval mode, which lets read-only Web calls run in the
|
|
337
|
+
// conversational `ask` profile. Older Cursor CLIs do not know that flag. On
|
|
338
|
+
// the exact commander error, retry without it so upgrading Hilos cannot make
|
|
339
|
+
// an otherwise-working older local CLI fail before it sees the prompt.
|
|
340
|
+
const UNKNOWN_CURSOR_AUTO_REVIEW_RE = /unknown option '--auto-review'/;
|
|
341
|
+
|
|
335
342
|
// Compat retry (0777): the permission gate adds `--permission-prompt-tool` (and
|
|
336
343
|
// the `--mcp-config` that serves it) to claude runs. The flag is real but
|
|
337
344
|
// undocumented on 2.1.233, so a Claude Code old enough not to know it would
|
|
@@ -373,7 +380,28 @@ export async function runCli(opts) {
|
|
|
373
380
|
args.includes("--trust") &&
|
|
374
381
|
UNKNOWN_TRUST_RE.test(first.stderr || "")
|
|
375
382
|
) {
|
|
376
|
-
|
|
383
|
+
const withoutTrust = args.filter((a) => a !== "--trust");
|
|
384
|
+
const retried = await runCliOnce({ ...opts, args: withoutTrust });
|
|
385
|
+
if (
|
|
386
|
+
retried.status !== 0 &&
|
|
387
|
+
!retried.aborted &&
|
|
388
|
+
withoutTrust.includes("--auto-review") &&
|
|
389
|
+
UNKNOWN_CURSOR_AUTO_REVIEW_RE.test(retried.stderr || "")
|
|
390
|
+
) {
|
|
391
|
+
return runCliOnce({
|
|
392
|
+
...opts,
|
|
393
|
+
args: withoutTrust.filter((a) => a !== "--auto-review"),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return retried;
|
|
397
|
+
}
|
|
398
|
+
if (
|
|
399
|
+
first.status !== 0 &&
|
|
400
|
+
!first.aborted &&
|
|
401
|
+
args.includes("--auto-review") &&
|
|
402
|
+
UNKNOWN_CURSOR_AUTO_REVIEW_RE.test(first.stderr || "")
|
|
403
|
+
) {
|
|
404
|
+
return runCliOnce({ ...opts, args: args.filter((a) => a !== "--auto-review") });
|
|
377
405
|
}
|
|
378
406
|
if (
|
|
379
407
|
first.status !== 0 &&
|
|
@@ -149,6 +149,7 @@ export function mapCodexDecision(reply, availableDecisions) {
|
|
|
149
149
|
* prompt?: string,
|
|
150
150
|
* env?: Record<string, string | undefined>,
|
|
151
151
|
* model?: string | null,
|
|
152
|
+
* webSearch?: boolean,
|
|
152
153
|
* sandbox?: string,
|
|
153
154
|
* approvalPolicy?: string,
|
|
154
155
|
* resumeThreadId?: string | null,
|
|
@@ -173,6 +174,7 @@ export async function runCodexMcpSession({
|
|
|
173
174
|
prompt = "",
|
|
174
175
|
env,
|
|
175
176
|
model = null,
|
|
177
|
+
webSearch = true,
|
|
176
178
|
sandbox = "workspace-write",
|
|
177
179
|
approvalPolicy = "untrusted",
|
|
178
180
|
resumeThreadId = null,
|
|
@@ -464,7 +466,7 @@ export async function runCodexMcpSession({
|
|
|
464
466
|
// The gated transport cannot inherit `codex exec` argv. Carry the
|
|
465
467
|
// same public web-search capability through the MCP tool's config;
|
|
466
468
|
// permission policy and sandboxing remain unchanged.
|
|
467
|
-
config: { tools: { web_search: true } },
|
|
469
|
+
...(webSearch ? { config: { tools: { web_search: true } } } : {}),
|
|
468
470
|
...(model ? { model } : {}),
|
|
469
471
|
},
|
|
470
472
|
};
|
package/src/config.mjs
CHANGED
|
@@ -53,6 +53,16 @@ const DEFAULTS = {
|
|
|
53
53
|
// and preview (default) vs production. Shape:
|
|
54
54
|
// { "<channelId>": { provider: "vercel" | "netlify", prod: false } }.
|
|
55
55
|
deploy: {},
|
|
56
|
+
// Local WebMCP consumer (0959). Disabled until the person names exact site
|
|
57
|
+
// origins and exact read-only tools. A site's readOnlyHint is never an
|
|
58
|
+
// authorization signal. Shape:
|
|
59
|
+
// { origins: { "https://example.com": { readTools: ["search_docs"] } },
|
|
60
|
+
// profile?: "/absolute/private/browser/profile" }
|
|
61
|
+
webMcp: null,
|
|
62
|
+
// Public web is a baseline agent capability. Supported local CLIs use their
|
|
63
|
+
// own search/fetch tools; false stops Hilos from enabling or requesting it.
|
|
64
|
+
// The CLI's own global/project policy remains the ultimate authority.
|
|
65
|
+
webSearch: true,
|
|
56
66
|
// acceptEdits lets the CLI make file edits without prompting (bias to action);
|
|
57
67
|
// it still won't run arbitrary commands. Override in hilos-agent.json if you
|
|
58
68
|
// want a stricter (or `--dangerously-skip-permissions`) command.
|
|
@@ -167,6 +177,12 @@ export function resolveConfig({ flags = {}, join: joinPayload } = {}) {
|
|
|
167
177
|
codingCmd: process.env.CODING_CMD,
|
|
168
178
|
codingModel: process.env.HILOS_CODING_MODEL,
|
|
169
179
|
chatCmd: process.env.HILOS_CHAT_CMD,
|
|
180
|
+
webSearch:
|
|
181
|
+
/^(1|on|true)$/i.test(process.env.HILOS_WEB_SEARCH || "")
|
|
182
|
+
? true
|
|
183
|
+
: /^(0|off|false)$/i.test(process.env.HILOS_WEB_SEARCH || "")
|
|
184
|
+
? false
|
|
185
|
+
: undefined,
|
|
170
186
|
heartbeatMs: process.env.HILOS_HEARTBEAT_MS ? Number(process.env.HILOS_HEARTBEAT_MS) : undefined,
|
|
171
187
|
progressMs: process.env.HILOS_PROGRESS_MS ? Number(process.env.HILOS_PROGRESS_MS) : undefined,
|
|
172
188
|
chatTimeoutMs: process.env.HILOS_CHAT_TIMEOUT_MS ? Number(process.env.HILOS_CHAT_TIMEOUT_MS) : undefined,
|
|
@@ -251,6 +267,8 @@ const LIVE_FIELDS = [
|
|
|
251
267
|
// like `repos`, so a partial edit doesn't drop the defaults.
|
|
252
268
|
"folders",
|
|
253
269
|
"deploy",
|
|
270
|
+
"webMcp",
|
|
271
|
+
"webSearch",
|
|
254
272
|
];
|
|
255
273
|
|
|
256
274
|
/**
|
|
@@ -293,6 +311,14 @@ export function reloadConfig(prev) {
|
|
|
293
311
|
// the normal on state. An explicit launch flag remains pinned when the file
|
|
294
312
|
// was never changed, preserving the 0576 precedence rule above.
|
|
295
313
|
if (changed("replyBridge")) next.replyBridge = file.replyBridge !== false;
|
|
314
|
+
// WebMCP is an authorization allowlist. Removing it must revoke the browser
|
|
315
|
+
// bridge on the next poll rather than preserving stale origins/tools until a
|
|
316
|
+
// restart. An explicit null and a deleted edited key both mean disabled.
|
|
317
|
+
if (changed("webMcp")) next.webMcp = file.webMcp ?? null;
|
|
318
|
+
// Native web is safe-on by default. Removing an edited `webSearch:false`
|
|
319
|
+
// returns to that default on the next poll instead of pinning a stale opt-out
|
|
320
|
+
// until restart; the environment override below still wins.
|
|
321
|
+
if (changed("webSearch")) next.webSearch = file.webSearch !== false;
|
|
296
322
|
// The environment stays the operator's override on reload, in both
|
|
297
323
|
// directions — a machine that opted out with =0 must not be opted back in by
|
|
298
324
|
// a file edit (0792).
|
|
@@ -303,6 +329,8 @@ export function reloadConfig(prev) {
|
|
|
303
329
|
if (process.env.CODING_CMD) next.codingCmd = process.env.CODING_CMD;
|
|
304
330
|
if (process.env.HILOS_CODING_MODEL) next.codingModel = process.env.HILOS_CODING_MODEL;
|
|
305
331
|
if (process.env.HILOS_CHAT_CMD) next.chatCmd = process.env.HILOS_CHAT_CMD;
|
|
332
|
+
if (/^(1|on|true)$/i.test(process.env.HILOS_WEB_SEARCH || "")) next.webSearch = true;
|
|
333
|
+
if (/^(0|off|false)$/i.test(process.env.HILOS_WEB_SEARCH || "")) next.webSearch = false;
|
|
306
334
|
if (process.env.HILOS_HEARTBEAT_MS) next.heartbeatMs = Number(process.env.HILOS_HEARTBEAT_MS);
|
|
307
335
|
if (process.env.HILOS_PROGRESS_MS) next.progressMs = Number(process.env.HILOS_PROGRESS_MS);
|
|
308
336
|
if (process.env.HILOS_CHAT_TIMEOUT_MS) next.chatTimeoutMs = Number(process.env.HILOS_CHAT_TIMEOUT_MS);
|
|
@@ -327,6 +355,7 @@ export function writeStarterConfig(path, partial = {}) {
|
|
|
327
355
|
repos: partial.repos || { "owner/name": "/absolute/path/to/checkout" },
|
|
328
356
|
codingCmd: partial.codingCmd || DEFAULTS.codingCmd,
|
|
329
357
|
...(partial.codingModel ? { codingModel: partial.codingModel } : {}),
|
|
358
|
+
webSearch: partial.webSearch !== false,
|
|
330
359
|
defaultBranch: DEFAULTS.defaultBranch,
|
|
331
360
|
// false = open a PR directly (bias to action); true = approve-before-push.
|
|
332
361
|
gate: false,
|
package/src/handler.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
import { makeStreamParser, createUsageFold } from "./agent-events.mjs";
|
|
41
41
|
import {
|
|
42
42
|
detectVendor,
|
|
43
|
+
chatWebArgs,
|
|
43
44
|
codeStreamArgs,
|
|
44
45
|
codeWebArgs,
|
|
45
46
|
codeDirArgs,
|
|
@@ -49,6 +50,8 @@ import {
|
|
|
49
50
|
attachTarget,
|
|
50
51
|
createProgressEmitter,
|
|
51
52
|
fastChatCmd,
|
|
53
|
+
nativeWebPrompt,
|
|
54
|
+
webToolEnv,
|
|
52
55
|
} from "./progress-emitter.mjs";
|
|
53
56
|
import { createTranscriptTap } from "./transcript.mjs";
|
|
54
57
|
import { imagePromptNote, renderAttachmentLine } from "./attachments.mjs";
|
|
@@ -83,6 +86,7 @@ import {
|
|
|
83
86
|
shouldGateCodexPermissions,
|
|
84
87
|
} from "./codex-mcp-session.mjs";
|
|
85
88
|
import { createUngatedRunNotice } from "./permission-gate.mjs";
|
|
89
|
+
import { webMcpAgentPrompt } from "./webmcp-bridge.mjs";
|
|
86
90
|
|
|
87
91
|
/**
|
|
88
92
|
* The environment for a coding/chat CLI run. runCli always strips HILOS_* on top
|
|
@@ -97,6 +101,15 @@ function codingChildEnv(cfg) {
|
|
|
97
101
|
: undefined;
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
/** Add only the selected CLI's public-web enablement to its already-isolated
|
|
105
|
+
* child environment. Today this changes OpenCode only; keeping it shared makes
|
|
106
|
+
* argv, ACP, HTTP, gated, and conversational runs follow one contract. */
|
|
107
|
+
function codingChildWebEnv(cfg, vendor) {
|
|
108
|
+
return webToolEnv(vendor, codingChildEnv(cfg), {
|
|
109
|
+
enabled: cfg?.webSearch !== false,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
100
113
|
/**
|
|
101
114
|
* The fast chat command for this config: an explicit chatCmd wins, else the
|
|
102
115
|
* coding vendor's verified non-interactive print mode (fastChatCmd), else the
|
|
@@ -693,6 +706,7 @@ async function runCodexGatedSession({
|
|
|
693
706
|
// took the account default before this, so a preset was silently ignored
|
|
694
707
|
// exactly where the operator was most likely to have set one.
|
|
695
708
|
model: model || null,
|
|
709
|
+
webSearch: cfg.webSearch !== false,
|
|
696
710
|
resumeThreadId,
|
|
697
711
|
timeoutMs: cfg.runTimeoutMs,
|
|
698
712
|
signal,
|
|
@@ -1263,6 +1277,15 @@ function memoryPreamble(workspaceMemory) {
|
|
|
1263
1277
|
return m ? `Workspace context (the project's soul):\n${m}\n\n` : "";
|
|
1264
1278
|
}
|
|
1265
1279
|
|
|
1280
|
+
/** Shared context for a model that may act, not the read-only intent router. */
|
|
1281
|
+
function agentPreamble(workspaceMemory, cfg) {
|
|
1282
|
+
return (
|
|
1283
|
+
memoryPreamble(workspaceMemory) +
|
|
1284
|
+
nativeWebPrompt(cfg?.webSearch !== false) +
|
|
1285
|
+
webMcpAgentPrompt(cfg)
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1266
1289
|
/**
|
|
1267
1290
|
* Recent conversation the agent should reply within: the thread it was pinged in
|
|
1268
1291
|
* (threads are where conversations live), else the channel tail. Returns the raw
|
|
@@ -1368,7 +1391,7 @@ async function respondConversationally({ message, channelId, tool, me, cfg, repo
|
|
|
1368
1391
|
`concisely and directly as a single chat message — no preamble, no headings. ` +
|
|
1369
1392
|
`${repoLine}` +
|
|
1370
1393
|
`${dmJudgmentBlock(implicitDm)}\n\n` +
|
|
1371
|
-
`${
|
|
1394
|
+
`${agentPreamble(workspaceMemory, cfg)}` +
|
|
1372
1395
|
`Conversation so far:\n${transcript}`;
|
|
1373
1396
|
|
|
1374
1397
|
// Chat uses the FAST one-shot command (the coding vendor's own print mode when
|
|
@@ -1376,6 +1399,10 @@ async function respondConversationally({ message, channelId, tool, me, cfg, repo
|
|
|
1376
1399
|
// and a stalled model can't dead-air the channel for the full coding timeout.
|
|
1377
1400
|
const cmd = chatCmdFor(cfg);
|
|
1378
1401
|
const parts = cmd.split(" ").filter(Boolean);
|
|
1402
|
+
const chatVendor = detectVendor(cmd);
|
|
1403
|
+
const webArgs = chatWebArgs(chatVendor, parts.slice(1), {
|
|
1404
|
+
enabled: cfg.webSearch !== false,
|
|
1405
|
+
});
|
|
1379
1406
|
console.log(` chat → running \`${cmd}\` (output appears when it finishes)…`);
|
|
1380
1407
|
|
|
1381
1408
|
// If the reply is slow, post ONE "still thinking…" ping and then edit it into
|
|
@@ -1406,11 +1433,11 @@ async function respondConversationally({ message, channelId, tool, me, cfg, repo
|
|
|
1406
1433
|
try {
|
|
1407
1434
|
run = await runCli({
|
|
1408
1435
|
cmd: parts[0],
|
|
1409
|
-
args: [...parts.slice(1), prompt],
|
|
1436
|
+
args: [...parts.slice(1), ...webArgs, prompt],
|
|
1410
1437
|
timeoutMs: cfg.chatTimeoutMs || cfg.runTimeoutMs,
|
|
1411
1438
|
label: "thinking",
|
|
1412
1439
|
signal,
|
|
1413
|
-
env:
|
|
1440
|
+
env: codingChildWebEnv(cfg, chatVendor),
|
|
1414
1441
|
});
|
|
1415
1442
|
} finally {
|
|
1416
1443
|
beatStopped = true;
|
|
@@ -2090,7 +2117,9 @@ async function handleFolderTask({ message, channelId, tool, me, caps, cfg, deps,
|
|
|
2090
2117
|
// 0779: [] for every vendor without a verified image flag — their argv is
|
|
2091
2118
|
// byte-identical to before, and the prompt note still names the files.
|
|
2092
2119
|
const imageArgs = codeImageArgs(vendor, localImages);
|
|
2093
|
-
const webArgs = codeWebArgs(vendor, parts.slice(1)
|
|
2120
|
+
const webArgs = codeWebArgs(vendor, parts.slice(1), {
|
|
2121
|
+
enabled: cfg.webSearch !== false,
|
|
2122
|
+
});
|
|
2094
2123
|
const codeArgs = [
|
|
2095
2124
|
...parts.slice(1),
|
|
2096
2125
|
...webArgs,
|
|
@@ -2175,10 +2204,10 @@ async function handleFolderTask({ message, channelId, tool, me, caps, cfg, deps,
|
|
|
2175
2204
|
cmd: parts[0],
|
|
2176
2205
|
vendor,
|
|
2177
2206
|
cwd: folderPath,
|
|
2178
|
-
prompt:
|
|
2207
|
+
prompt: agentPreamble(workspaceMemory, cfg) + promptText,
|
|
2179
2208
|
timeoutMs: cfg.runTimeoutMs,
|
|
2180
2209
|
signal,
|
|
2181
|
-
env: scrubHilosEnv(
|
|
2210
|
+
env: scrubHilosEnv(codingChildWebEnv(cfg, vendor) || process.env),
|
|
2182
2211
|
onData: handleCliData,
|
|
2183
2212
|
onEvent: handleCliEvent,
|
|
2184
2213
|
...openCodePermissionCallbacks({
|
|
@@ -2194,10 +2223,10 @@ async function handleFolderTask({ message, channelId, tool, me, caps, cfg, deps,
|
|
|
2194
2223
|
cmd: parts[0],
|
|
2195
2224
|
args: codeArgs,
|
|
2196
2225
|
cwd: folderPath,
|
|
2197
|
-
prompt:
|
|
2226
|
+
prompt: agentPreamble(workspaceMemory, cfg) + promptText,
|
|
2198
2227
|
timeoutMs: cfg.runTimeoutMs,
|
|
2199
2228
|
signal,
|
|
2200
|
-
env: scrubHilosEnv(
|
|
2229
|
+
env: scrubHilosEnv(codingChildWebEnv(cfg, vendor) || process.env),
|
|
2201
2230
|
onData: handleCliData,
|
|
2202
2231
|
...openCodePermissionCallbacks({
|
|
2203
2232
|
tool,
|
|
@@ -2214,7 +2243,7 @@ async function handleFolderTask({ message, channelId, tool, me, caps, cfg, deps,
|
|
|
2214
2243
|
cmd: parts[0],
|
|
2215
2244
|
codeArgs,
|
|
2216
2245
|
cwd: folderPath,
|
|
2217
|
-
prompt:
|
|
2246
|
+
prompt: agentPreamble(workspaceMemory, cfg) + promptText,
|
|
2218
2247
|
model: resolvedModelId,
|
|
2219
2248
|
signal,
|
|
2220
2249
|
onData: handleCliData,
|
|
@@ -2234,7 +2263,7 @@ async function handleFolderTask({ message, channelId, tool, me, caps, cfg, deps,
|
|
|
2234
2263
|
onGateDropped: () => noticeUngatedRun(parts[0]),
|
|
2235
2264
|
cmd: parts[0],
|
|
2236
2265
|
codeArgs,
|
|
2237
|
-
prompt:
|
|
2266
|
+
prompt: agentPreamble(workspaceMemory, cfg) + promptText,
|
|
2238
2267
|
cwd: folderPath,
|
|
2239
2268
|
signal,
|
|
2240
2269
|
onData: handleCliData,
|
|
@@ -2252,13 +2281,13 @@ async function handleFolderTask({ message, channelId, tool, me, caps, cfg, deps,
|
|
|
2252
2281
|
cmd: parts[0],
|
|
2253
2282
|
args: [
|
|
2254
2283
|
...codeArgs,
|
|
2255
|
-
|
|
2284
|
+
agentPreamble(workspaceMemory, cfg) + promptText,
|
|
2256
2285
|
],
|
|
2257
2286
|
cwd: folderPath,
|
|
2258
2287
|
timeoutMs: cfg.runTimeoutMs,
|
|
2259
2288
|
label: "coding",
|
|
2260
2289
|
signal,
|
|
2261
|
-
env:
|
|
2290
|
+
env: codingChildWebEnv(cfg, vendor),
|
|
2262
2291
|
onData: handleCliData,
|
|
2263
2292
|
});
|
|
2264
2293
|
}
|
|
@@ -3431,7 +3460,9 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
3431
3460
|
// `exec`, so it has to sit ahead of it. [] for every vendor without a
|
|
3432
3461
|
// verified flag, leaving their argv byte-identical to before.
|
|
3433
3462
|
const imageArgs = codeImageArgs(vendor, localImages);
|
|
3434
|
-
const webArgs = codeWebArgs(vendor, parts.slice(1)
|
|
3463
|
+
const webArgs = codeWebArgs(vendor, parts.slice(1), {
|
|
3464
|
+
enabled: cfg.webSearch !== false,
|
|
3465
|
+
});
|
|
3435
3466
|
const codeArgs = streamOn
|
|
3436
3467
|
? [...parts.slice(1), ...webArgs, ...modelArgs, ...dirArgs, ...imageArgs, ...resumeArgs, ...streamArgs]
|
|
3437
3468
|
: [...parts.slice(1), ...webArgs, ...modelArgs, ...dirArgs, ...imageArgs, ...resumeArgs];
|
|
@@ -3526,13 +3557,13 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
3526
3557
|
cmd: parts[0],
|
|
3527
3558
|
vendor,
|
|
3528
3559
|
cwd: repoPath,
|
|
3529
|
-
prompt:
|
|
3560
|
+
prompt: agentPreamble(workspaceMemory, cfg) + promptText,
|
|
3530
3561
|
// 0778: approvals AND continuity. `resume:false` (the never-worse
|
|
3531
3562
|
// retry) drops it exactly like buildResumeArgs does.
|
|
3532
3563
|
resumeSessionId: resume ? resumeSessionId : null,
|
|
3533
3564
|
timeoutMs: cfg.runTimeoutMs,
|
|
3534
3565
|
signal,
|
|
3535
|
-
env: scrubHilosEnv(
|
|
3566
|
+
env: scrubHilosEnv(codingChildWebEnv(cfg, vendor) || process.env),
|
|
3536
3567
|
onData: handleCliData,
|
|
3537
3568
|
onEvent: handleCliEvent,
|
|
3538
3569
|
...openCodePermissionCallbacks({
|
|
@@ -3549,13 +3580,13 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
3549
3580
|
cmd: parts[0],
|
|
3550
3581
|
args: codeArgs,
|
|
3551
3582
|
cwd: repoPath,
|
|
3552
|
-
prompt:
|
|
3583
|
+
prompt: agentPreamble(workspaceMemory, cfg) + promptText,
|
|
3553
3584
|
timeoutMs: cfg.runTimeoutMs,
|
|
3554
3585
|
signal,
|
|
3555
3586
|
// A model running inside the server must never inherit the daemon's
|
|
3556
3587
|
// hilos bearer token. The bridge's random loopback password is added
|
|
3557
3588
|
// after this scrub and dies with the process group.
|
|
3558
|
-
env: scrubHilosEnv(
|
|
3589
|
+
env: scrubHilosEnv(codingChildWebEnv(cfg, vendor) || process.env),
|
|
3559
3590
|
onData: handleCliData,
|
|
3560
3591
|
...openCodePermissionCallbacks({
|
|
3561
3592
|
tool,
|
|
@@ -3573,7 +3604,7 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
3573
3604
|
cmd: parts[0],
|
|
3574
3605
|
codeArgs,
|
|
3575
3606
|
cwd: repoPath,
|
|
3576
|
-
prompt:
|
|
3607
|
+
prompt: agentPreamble(workspaceMemory, cfg) + promptText,
|
|
3577
3608
|
model: resolvedModelId,
|
|
3578
3609
|
// Codex's own resume over this transport. `resume:false` (the
|
|
3579
3610
|
// never-worse retry) drops it exactly like buildResumeArgs does.
|
|
@@ -3599,7 +3630,7 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
3599
3630
|
// codeArgs already carries this run's resume flags, so a gated
|
|
3600
3631
|
// iterate resumes AND raises cards — the two never traded off.
|
|
3601
3632
|
codeArgs,
|
|
3602
|
-
prompt:
|
|
3633
|
+
prompt: agentPreamble(workspaceMemory, cfg) + promptText,
|
|
3603
3634
|
cwd: repoPath,
|
|
3604
3635
|
signal,
|
|
3605
3636
|
onData: handleCliData,
|
|
@@ -3621,12 +3652,12 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
3621
3652
|
// why nothing above the unit tests could ever drive it (0787).
|
|
3622
3653
|
run = await deps.runCli({
|
|
3623
3654
|
cmd: parts[0],
|
|
3624
|
-
args: [...codeArgs,
|
|
3655
|
+
args: [...codeArgs, agentPreamble(workspaceMemory, cfg) + promptText],
|
|
3625
3656
|
cwd: repoPath,
|
|
3626
3657
|
timeoutMs: cfg.runTimeoutMs,
|
|
3627
3658
|
label: "coding",
|
|
3628
3659
|
signal,
|
|
3629
|
-
env:
|
|
3660
|
+
env: codingChildWebEnv(cfg, vendor),
|
|
3630
3661
|
onData: handleCliData,
|
|
3631
3662
|
});
|
|
3632
3663
|
}
|
package/src/progress-emitter.mjs
CHANGED
|
@@ -23,6 +23,13 @@ import {
|
|
|
23
23
|
sanitizeText,
|
|
24
24
|
} from "./agent-events.mjs";
|
|
25
25
|
import { commandArgv } from "./argv.mjs";
|
|
26
|
+
export {
|
|
27
|
+
chatWebArgs,
|
|
28
|
+
codeWebArgs,
|
|
29
|
+
nativeWebPrompt,
|
|
30
|
+
webCapability,
|
|
31
|
+
webToolEnv,
|
|
32
|
+
} from "./web-tools.mjs";
|
|
26
33
|
|
|
27
34
|
/**
|
|
28
35
|
* Which parser/stream-flags a coding command wants, from its FIRST token.
|
|
@@ -57,8 +64,9 @@ export function detectVendor(codingCmd) {
|
|
|
57
64
|
* Code installed just to answer chat (0521). Every command is the vendor's
|
|
58
65
|
* verified non-interactive print mode; the daemon appends the prompt as the last
|
|
59
66
|
* arg. codex carries --skip-git-repo-check because chat (and the read-only
|
|
60
|
-
* review sandbox) can run outside a git checkout
|
|
61
|
-
*
|
|
67
|
+
* review sandbox) can run outside a git checkout; the responder adds its
|
|
68
|
+
* read-only/web profile separately so this same command stays cheap for the
|
|
69
|
+
* no-tools classifier and plan acknowledgement. cursor carries
|
|
62
70
|
* --output-format text (explicit, so a CLI default change can never post raw
|
|
63
71
|
* JSONL into the channel) and --trust (its Jan-2026 workspace-trust gate fails
|
|
64
72
|
* headless runs at spawn in untrusted directories — 0572; pre-2026 CLIs reject
|
|
@@ -71,13 +79,14 @@ export function detectVendor(codingCmd) {
|
|
|
71
79
|
*/
|
|
72
80
|
export function fastChatCmd(vendor) {
|
|
73
81
|
if (vendor === "claude_code") return "claude -p --model haiku";
|
|
74
|
-
if (vendor === "codex")
|
|
75
|
-
return "codex exec --skip-git-repo-check -c tools.web_search=true";
|
|
76
|
-
}
|
|
82
|
+
if (vendor === "codex") return "codex exec --skip-git-repo-check";
|
|
77
83
|
if (vendor === "cursor") return "cursor-agent -p --output-format text --trust";
|
|
78
84
|
if (vendor === "opencode") return "opencode run";
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
// Antigravity's -p consumes the NEXT argv as its prompt, so every option must
|
|
86
|
+
// sit before it. Hermes's -q has the same shape. Keep their complete generated
|
|
87
|
+
// read-only/web profile in the base command rather than appending after it.
|
|
88
|
+
if (vendor === "antigravity") return "agy --mode plan -p";
|
|
89
|
+
if (vendor === "hermes") return "hermes chat --toolsets safe,web -q";
|
|
81
90
|
return "";
|
|
82
91
|
}
|
|
83
92
|
|
|
@@ -110,31 +119,6 @@ export function codeStreamArgs(vendor) {
|
|
|
110
119
|
return [];
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
/**
|
|
114
|
-
* Make Codex's built-in public web search available to code runs. This is a
|
|
115
|
-
* capability flag, not an instruction to browse; Codex decides whether the
|
|
116
|
-
* task needs it. An operator's explicit true/false override wins unchanged.
|
|
117
|
-
* Other vendors already expose their own web tools and receive no guessed
|
|
118
|
-
* flags.
|
|
119
|
-
* @param {'claude_code'|'codex'|'cursor'|'opencode'|'antigravity'|'hermes'|'unknown'} vendor
|
|
120
|
-
* @param {string[]} baseArgs
|
|
121
|
-
* @returns {string[]}
|
|
122
|
-
*/
|
|
123
|
-
export function codeWebArgs(vendor, baseArgs = []) {
|
|
124
|
-
if (vendor !== "codex") return [];
|
|
125
|
-
const hasOverride = baseArgs.some((arg, index) => {
|
|
126
|
-
if (/^tools\.web_search=/.test(arg)) return true;
|
|
127
|
-
if (
|
|
128
|
-
(baseArgs[index - 1] === "-c" || baseArgs[index - 1] === "--config") &&
|
|
129
|
-
/^tools\.web_search=/.test(arg)
|
|
130
|
-
) {
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
return /^--config=tools\.web_search=/.test(arg);
|
|
134
|
-
});
|
|
135
|
-
return hasOverride ? [] : ["-c", "tools.web_search=true"];
|
|
136
|
-
}
|
|
137
|
-
|
|
138
122
|
/**
|
|
139
123
|
* Extra args that hand the code run an IMAGE, appended to the code run's argv
|
|
140
124
|
* (0779). Verified against the installed binaries, per the 0521 rule — never
|