hilos-agent 0.11.13 → 0.11.14
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/package.json +1 -1
- package/src/agent-result.mjs +5 -3
- package/src/coding-transport.mjs +440 -0
- package/src/handler.mjs +187 -886
- package/src/run.mjs +37 -87
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hilos-agent",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.14",
|
|
4
4
|
"description": "Run your own coding agent (Claude Code, Codex, Cursor, OpenCode, Hermes, or any command) as a teammate in a hilos room. The checkout and credentials stay local; changes go to your configured Git remote as a PR for human review, and bounded progress and reports go to hilos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/agent-result.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// 1298 — one writing contract and final-result envelope for hosted and local runs.
|
|
2
2
|
export const AGENT_REPLY_STYLE_RULE = [
|
|
3
3
|
"How to write the reply:",
|
|
4
|
-
"- Lead with the answer or outcome. Never restate the request.",
|
|
4
|
+
"- Lead with the answer or concrete outcome, in words a teammate outside the code can use. Never restate the request.",
|
|
5
5
|
"- Default to 1-3 short sentences, or at most three short bullets for separate outcomes.",
|
|
6
|
-
"-
|
|
7
|
-
"-
|
|
6
|
+
"- For instructions, put the action or command first and number sequential steps. For choices, recommend one first, then give the requested alternatives with short trade-offs.",
|
|
7
|
+
"- Include useful links once. Omit facts that do not change the answer or next decision: routine checks, file counts, and process history. No preamble, headings on short replies, sign-off, recap, or tangents.",
|
|
8
|
+
"- Keep blockers, material limitations, and real uncertainty visible. State errors plainly. Never invent causes, time estimates, checks, deployment, or merging.",
|
|
9
|
+
"- When blocked on a person, ask for the one missing decision: 'Which inbox should receive the form?' Otherwise, do not add a question. Do authorized work yourself; do not hand it back or ask permission again. When done, stop; no follow-up offers.",
|
|
8
10
|
"- Give requested documents, plans, code, research, and explanations the detail they need; keep their introduction short. Put code and commands in fenced blocks.",
|
|
9
11
|
"The room's voice may shape tone, but only a person's request for detail expands this default. Clarity beats a word limit.",
|
|
10
12
|
].join("\n");
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { minimalEnv, scrubHilosEnv } from "./cli.mjs";
|
|
2
|
+
import { commandArgv } from "./argv.mjs";
|
|
3
|
+
import { webToolEnv, attachTarget, detectVendor } from "./progress-emitter.mjs";
|
|
4
|
+
import { shouldGateClaudePermissions } from "./claude-permissions.mjs";
|
|
5
|
+
import { shouldGateCodexPermissions, codexMcpTransportUnavailable, codexSandboxFromArgs } from "./codex-mcp-session.mjs";
|
|
6
|
+
import { childMcpArgs, codexChildMcpArgs } from "./child-mcp.mjs";
|
|
7
|
+
import { repoEditAllowance } from "./permission-gate.mjs";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The environment for a coding/chat CLI run. runCli always strips HILOS_* on top
|
|
11
|
+
* of whatever this returns; here we additionally honor `codingEnv: "minimal"`
|
|
12
|
+
* (strict isolation — only what the tool needs to reach its own model provider).
|
|
13
|
+
* Returns undefined for the default "inherit" mode so runCli falls back to the
|
|
14
|
+
* scrubbed process.env.
|
|
15
|
+
*/
|
|
16
|
+
export function codingChildEnv(cfg) {
|
|
17
|
+
return cfg?.codingEnv === "minimal"
|
|
18
|
+
? minimalEnv(process.env, cfg.codingEnvAllow)
|
|
19
|
+
: undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Add only the selected CLI's public-web enablement to its already-isolated
|
|
23
|
+
* child environment. Today this changes OpenCode only; keeping it shared makes
|
|
24
|
+
* argv, ACP, HTTP, gated, and conversational runs follow one contract. */
|
|
25
|
+
export function codingChildWebEnv(cfg, vendor) {
|
|
26
|
+
return webToolEnv(vendor, codingChildEnv(cfg), {
|
|
27
|
+
enabled: cfg?.webSearch !== false,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Bind one OpenCode HTTP session to hilos's vendor-neutral permission tools.
|
|
33
|
+
* Both repo and direct-folder runs use this exact callback contract so neither
|
|
34
|
+
* path can accidentally become the ungated exception.
|
|
35
|
+
*/
|
|
36
|
+
function openCodePermissionCallbacks({
|
|
37
|
+
tool,
|
|
38
|
+
channelId,
|
|
39
|
+
threadRoot,
|
|
40
|
+
runId = null,
|
|
41
|
+
provider = "opencode",
|
|
42
|
+
// 0866 — when the server's get_permission_decision supports a long-poll
|
|
43
|
+
// hold, each read blocks up to this many ms and answers the moment a human
|
|
44
|
+
// decides, instead of the gate discovering the decision on its next 1s poll.
|
|
45
|
+
// Tests may use 0 for an immediate decision fixture.
|
|
46
|
+
decisionWaitMs = 0,
|
|
47
|
+
// 1256 — the checkout this run edits; an edit under it is allowed on the
|
|
48
|
+
// daemon without a card (the pull request is the gate). Empty means "no
|
|
49
|
+
// local allowance": every ask still reaches the room.
|
|
50
|
+
repoRoot = "",
|
|
51
|
+
gateRepoEdits = false,
|
|
52
|
+
}) {
|
|
53
|
+
return {
|
|
54
|
+
localAllow: (request) => repoEditAllowance(request, { repoRoot, gateRepoEdits }),
|
|
55
|
+
requestPermission: async (request) => {
|
|
56
|
+
const detail =
|
|
57
|
+
request.metadata && typeof request.metadata === "object"
|
|
58
|
+
? request.metadata
|
|
59
|
+
: {};
|
|
60
|
+
const title = [
|
|
61
|
+
detail.title,
|
|
62
|
+
detail.description,
|
|
63
|
+
detail.command,
|
|
64
|
+
request.resources?.[0],
|
|
65
|
+
].find((value) => typeof value === "string" && value.trim());
|
|
66
|
+
return tool("request_permission", {
|
|
67
|
+
channelId,
|
|
68
|
+
threadRootId: threadRoot,
|
|
69
|
+
...(runId ? { runId } : {}),
|
|
70
|
+
provider,
|
|
71
|
+
vendorSessionId: request.sessionId,
|
|
72
|
+
vendorRequestId: request.vendorRequestId,
|
|
73
|
+
action: request.action,
|
|
74
|
+
title: title || `${request.action} permission`,
|
|
75
|
+
resources: request.resources,
|
|
76
|
+
suggestedSave: request.suggestedSave,
|
|
77
|
+
metadata: detail,
|
|
78
|
+
...(request.source ? { source: request.source } : {}),
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
getPermissionDecision: async (handle, { request, failClosed = false }) => {
|
|
82
|
+
const requestId =
|
|
83
|
+
handle &&
|
|
84
|
+
typeof handle === "object" &&
|
|
85
|
+
typeof handle.requestId === "string"
|
|
86
|
+
? handle.requestId
|
|
87
|
+
: null;
|
|
88
|
+
if (!requestId) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"hilos returned a pending permission without a request id",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return tool("get_permission_decision", {
|
|
94
|
+
requestId,
|
|
95
|
+
provider,
|
|
96
|
+
vendorSessionId: request.sessionId,
|
|
97
|
+
vendorRequestId: request.vendorRequestId,
|
|
98
|
+
...(failClosed ? { failClosed: true } : {}),
|
|
99
|
+
// Never on a failClosed settlement — that call exists to act NOW.
|
|
100
|
+
...(decisionWaitMs > 0 && !failClosed ? { waitMs: decisionWaitMs } : {}),
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The local HTTP bridge can own only a local OpenCode server. An explicit
|
|
107
|
+
* `--attach` remains on OpenCode's CLI responder, which rejects unanswered asks
|
|
108
|
+
* fail closed; taking over a remote server requires a separate authenticated
|
|
109
|
+
* transport contract. Shared by repo and direct-folder runs. */
|
|
110
|
+
export function shouldUseRuntimePermissionBridge({
|
|
111
|
+
vendor,
|
|
112
|
+
runtimePermissions,
|
|
113
|
+
codeArgs,
|
|
114
|
+
codingCmd,
|
|
115
|
+
}) {
|
|
116
|
+
return (
|
|
117
|
+
vendor === "opencode" &&
|
|
118
|
+
runtimePermissions === true &&
|
|
119
|
+
!codeArgs.includes("--auto") &&
|
|
120
|
+
attachTarget(codingCmd) === null
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** ACP transport (0759; slice 1 opencode, slice 2 cursor). Only runs the
|
|
125
|
+
* workspace already gates with runtime permissions qualify, and only when the
|
|
126
|
+
* vendor's own auto-allow escape hatches are absent — over ACP, hilos must be
|
|
127
|
+
* the one answering asks, so a run configured to never ask has nothing to gate
|
|
128
|
+
* here.
|
|
129
|
+
*
|
|
130
|
+
* 0778 removed two limits that were never about correctness. `acpTransport`
|
|
131
|
+
* now defaults ON (config.mjs) for the vendors whose adapter is proven, and a
|
|
132
|
+
* RESUMED run no longer disqualifies: both live vendors advertise ACP's
|
|
133
|
+
* `loadSession` capability, so acp-session.mjs resumes the session and keeps
|
|
134
|
+
* raising cards. Setting `acpTransport: false` still opts a workspace out. */
|
|
135
|
+
export function shouldUseAcpTransport({
|
|
136
|
+
vendor,
|
|
137
|
+
acpTransport,
|
|
138
|
+
runtimePermissions,
|
|
139
|
+
codeArgs,
|
|
140
|
+
codingCmd,
|
|
141
|
+
}) {
|
|
142
|
+
if (acpTransport !== true) return false;
|
|
143
|
+
if (runtimePermissions !== true) return false;
|
|
144
|
+
if (vendor === "opencode") {
|
|
145
|
+
return shouldUseRuntimePermissionBridge({
|
|
146
|
+
vendor,
|
|
147
|
+
runtimePermissions,
|
|
148
|
+
codeArgs,
|
|
149
|
+
codingCmd,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (vendor === "cursor") {
|
|
153
|
+
// -f/--force is cursor's "allow everything" switch — the exact analog of
|
|
154
|
+
// opencode's --auto.
|
|
155
|
+
return !codeArgs.includes("-f") && !codeArgs.includes("--force");
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The env a gated claude run gets (0777).
|
|
162
|
+
*
|
|
163
|
+
* A permission card can legitimately wait on a person for minutes — a 150s wait
|
|
164
|
+
* was verified live — so the CLI's own MCP tool timeout must not cut the ask
|
|
165
|
+
* short before hilos's run deadline does. Everything else about the env is
|
|
166
|
+
* unchanged (runCli still strips HILOS_* itself).
|
|
167
|
+
*/
|
|
168
|
+
function claudeGateEnv(cfg) {
|
|
169
|
+
const base = codingChildEnv(cfg) || process.env;
|
|
170
|
+
const budget = Math.max(60_000, Number(cfg?.runTimeoutMs) || 0) + 60_000;
|
|
171
|
+
return { ...base, MCP_TOOL_TIMEOUT: String(budget) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Run claude_code through its permission-prompt seam (0777).
|
|
176
|
+
*
|
|
177
|
+
* Deliberately NOT a new transport: the ordinary argv run is untouched — same
|
|
178
|
+
* runCli, same resume args, same streaming — and the gate is two extra flags
|
|
179
|
+
* plus a loopback MCP server that lives exactly as long as the run. The prompt
|
|
180
|
+
* stays the final argument.
|
|
181
|
+
*/
|
|
182
|
+
async function runClaudeGatedCli({
|
|
183
|
+
deps,
|
|
184
|
+
cfg,
|
|
185
|
+
onGateDropped,
|
|
186
|
+
cmd,
|
|
187
|
+
codeArgs,
|
|
188
|
+
prompt,
|
|
189
|
+
cwd,
|
|
190
|
+
signal,
|
|
191
|
+
onData,
|
|
192
|
+
permissionCallbacks,
|
|
193
|
+
sessionId = "",
|
|
194
|
+
resolveSessionId,
|
|
195
|
+
beforeSpawn,
|
|
196
|
+
childMcp = null,
|
|
197
|
+
log = console,
|
|
198
|
+
}) {
|
|
199
|
+
const server = await deps.startClaudePermissionServer({
|
|
200
|
+
...permissionCallbacks,
|
|
201
|
+
// 1255 — one config file for the run: the gate's permission server and the
|
|
202
|
+
// run's own hilos seat, plus the allow entry that keeps the agent's room
|
|
203
|
+
// tools off the permission card.
|
|
204
|
+
...(childMcp ? { extraServers: childMcp.servers, allowedTools: childMcp.allowedTools } : {}),
|
|
205
|
+
sessionId,
|
|
206
|
+
// A FRESH run has no session id to hand over: claude reveals its own in the
|
|
207
|
+
// init frame of its stream, which the progress emitter is already folding.
|
|
208
|
+
// Pulling from that snapshot means an ask carries the CLI's real session id
|
|
209
|
+
// without a second parser — and hilos rejects an empty one outright.
|
|
210
|
+
resolveSessionId,
|
|
211
|
+
timeoutMs: cfg.runTimeoutMs,
|
|
212
|
+
signal,
|
|
213
|
+
log,
|
|
214
|
+
});
|
|
215
|
+
try {
|
|
216
|
+
return await deps.runCli({
|
|
217
|
+
cmd,
|
|
218
|
+
args: [...codeArgs, ...server.args, prompt],
|
|
219
|
+
cwd,
|
|
220
|
+
timeoutMs: cfg.runTimeoutMs,
|
|
221
|
+
label: "coding",
|
|
222
|
+
signal,
|
|
223
|
+
env: claudeGateEnv(cfg),
|
|
224
|
+
onData,
|
|
225
|
+
beforeSpawn,
|
|
226
|
+
// 0785 — a CLI that rejects the gate flags is retried without them. This
|
|
227
|
+
// fires BEFORE that ungated retry is spawned, so the room hears about the
|
|
228
|
+
// downgrade first rather than after the fact.
|
|
229
|
+
onPermissionGateDropped: onGateDropped,
|
|
230
|
+
});
|
|
231
|
+
} finally {
|
|
232
|
+
try {
|
|
233
|
+
await server.close();
|
|
234
|
+
} catch {
|
|
235
|
+
/* tearing the gate down must never fail a finished run */
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Will this codex run take the gated transport (0785)?
|
|
242
|
+
*
|
|
243
|
+
* Answerable before the run's argv exists, because the only input that can
|
|
244
|
+
* change the answer is the operator's OWN command — every flag the daemon
|
|
245
|
+
* appends later (model, dir, image, resume, stream) is ours and none of them is
|
|
246
|
+
* the bypass tier. Which matters because the PROMPT is written before the
|
|
247
|
+
* transport is chosen, and what the prompt may claim about images depends on it.
|
|
248
|
+
*/
|
|
249
|
+
export function codexRunIsGated(cfg, testCapabilities) {
|
|
250
|
+
return shouldGateCodexPermissions({
|
|
251
|
+
vendor: detectVendor(cfg?.codingCmd),
|
|
252
|
+
runtimePermissions: testCapabilities?.runtimePermissions,
|
|
253
|
+
codeArgs: commandArgv(String(cfg?.codingCmd || "")).slice(1),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Run codex through its gated transport, with an honest fallback (0785).
|
|
259
|
+
*
|
|
260
|
+
* A granted workspace ALWAYS gets `codex mcp-server` — that is what the grant
|
|
261
|
+
* means, and it is the only codex transport that can ask (`codex exec` has no
|
|
262
|
+
* approval channel at any flag combination, see codex-mcp-session.mjs). The one
|
|
263
|
+
* thing the grant can't conjure is the subcommand itself: a codex old enough
|
|
264
|
+
* not to have it never answers the MCP handshake, and before this the run just
|
|
265
|
+
* failed. Now it degrades to the plain exec argv — the ungated run every codex
|
|
266
|
+
* did before 0777, never worse.
|
|
267
|
+
*
|
|
268
|
+
* Two rules make that degrade safe. It happens ONLY on positive evidence that
|
|
269
|
+
* the subcommand is absent (codexMcpTransportUnavailable — every other
|
|
270
|
+
* pre-handshake failure is returned as the failed run it is, because a wrongly
|
|
271
|
+
* failed run is recoverable and a wrongly ungated one is not). And the room is
|
|
272
|
+
* told BEFORE the ungated child is spawned, so the warning arrives while the
|
|
273
|
+
* run can still be stopped.
|
|
274
|
+
*
|
|
275
|
+
* The exec argv is the run's real one (model, images, resume, stream flags),
|
|
276
|
+
* so the fallback is the same run the ungated lane would have made.
|
|
277
|
+
*/
|
|
278
|
+
async function runCodexGatedSession({
|
|
279
|
+
deps,
|
|
280
|
+
cfg,
|
|
281
|
+
onGateDropped,
|
|
282
|
+
cmd,
|
|
283
|
+
codeArgs,
|
|
284
|
+
prompt,
|
|
285
|
+
cwd,
|
|
286
|
+
model,
|
|
287
|
+
resumeThreadId = null,
|
|
288
|
+
signal,
|
|
289
|
+
onData,
|
|
290
|
+
onEvent,
|
|
291
|
+
beforeSpawn,
|
|
292
|
+
permissionCallbacks,
|
|
293
|
+
childMcp = null,
|
|
294
|
+
}) {
|
|
295
|
+
// 1255 — `codex mcp-server` takes the same global `-c` overrides `codex exec`
|
|
296
|
+
// does (verified on codex-cli 0.153.4: `codex mcp-server --help` lists
|
|
297
|
+
// `-c, --config <key=value>`), so the seat reaches both codex transports
|
|
298
|
+
// through one argv fragment.
|
|
299
|
+
const childMcpConfig = childMcp ? codexChildMcpArgs(childMcp.url) : [];
|
|
300
|
+
if (typeof beforeSpawn === "function" && (await beforeSpawn()) === false) {
|
|
301
|
+
return {
|
|
302
|
+
status: null,
|
|
303
|
+
stdout: "",
|
|
304
|
+
stderr: "",
|
|
305
|
+
aborted: true,
|
|
306
|
+
authorityLost: true,
|
|
307
|
+
error: new Error("execution authority lost before spawn"),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const run = await deps.runCodexMcpSession({
|
|
311
|
+
cmd,
|
|
312
|
+
...(childMcpConfig.length ? { serverArgs: ["mcp-server", ...childMcpConfig] } : {}),
|
|
313
|
+
cwd,
|
|
314
|
+
prompt,
|
|
315
|
+
sandbox: codexSandboxFromArgs(codeArgs),
|
|
316
|
+
// 0785 — the resolved tier (0783) or a hand-pinned id. The gated transport
|
|
317
|
+
// took the account default before this, so a preset was silently ignored
|
|
318
|
+
// exactly where the operator was most likely to have set one.
|
|
319
|
+
model: model || null,
|
|
320
|
+
webSearch: cfg.webSearch !== false,
|
|
321
|
+
resumeThreadId,
|
|
322
|
+
timeoutMs: cfg.runTimeoutMs,
|
|
323
|
+
signal,
|
|
324
|
+
env: scrubHilosEnv(codingChildEnv(cfg) || process.env),
|
|
325
|
+
onData,
|
|
326
|
+
onEvent,
|
|
327
|
+
beforeSpawn,
|
|
328
|
+
...permissionCallbacks,
|
|
329
|
+
});
|
|
330
|
+
if (!codexMcpTransportUnavailable(run)) return run;
|
|
331
|
+
console.log(
|
|
332
|
+
" code → this codex has no `mcp-server`; running WITHOUT the hilos permission gate",
|
|
333
|
+
);
|
|
334
|
+
// Warn FIRST — before a single ungated command can run.
|
|
335
|
+
if (typeof onGateDropped === "function") {
|
|
336
|
+
try {
|
|
337
|
+
await onGateDropped();
|
|
338
|
+
} catch {
|
|
339
|
+
/* telling the room must never break the run */
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const fallback = await deps.runCli({
|
|
343
|
+
cmd,
|
|
344
|
+
args: [...codeArgs, ...childMcpConfig, prompt],
|
|
345
|
+
cwd,
|
|
346
|
+
timeoutMs: cfg.runTimeoutMs,
|
|
347
|
+
label: "coding",
|
|
348
|
+
signal,
|
|
349
|
+
env: codingChildEnv(cfg),
|
|
350
|
+
onData,
|
|
351
|
+
beforeSpawn,
|
|
352
|
+
});
|
|
353
|
+
return { ...fallback, permissionGateDropped: true };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* One turn on the repo/folder transport ladder (1290). childMcp opens the
|
|
358
|
+
* turn's seat after its transport is known; it closes before onSettle runs,
|
|
359
|
+
* including on startup failure. Resume ids and the spawn/settlement hooks are
|
|
360
|
+
* the repo lane's explicit additions; permissionCallbacks supplies room context.
|
|
361
|
+
*
|
|
362
|
+
* @param {{
|
|
363
|
+
* deps: any, cfg: any, cmd: string, model?: string | null,
|
|
364
|
+
* runtimePermissions?: boolean, vendor: string, codeArgs: string[],
|
|
365
|
+
* cwd: string, prompt: string, permissionCallbacks: any,
|
|
366
|
+
* childMcp: (options: { standaloneConfig: boolean }) => Promise<any>,
|
|
367
|
+
* onData: any, onEvent?: any, signal?: AbortSignal,
|
|
368
|
+
* beforeSpawn?: () => boolean | Promise<boolean>,
|
|
369
|
+
* resume?: { sessionId: string | null }, runId?: string | null,
|
|
370
|
+
* onSettle?: (run: any) => void | Promise<void>,
|
|
371
|
+
* onGateDropped: () => void | Promise<void>,
|
|
372
|
+
* resolveSessionId?: () => string | null,
|
|
373
|
+
* }} options
|
|
374
|
+
*/
|
|
375
|
+
export async function runCodingTransport({
|
|
376
|
+
deps, cfg, cmd, model, runtimePermissions,
|
|
377
|
+
vendor, codeArgs, cwd, prompt, permissionCallbacks, childMcp,
|
|
378
|
+
onData, onEvent, signal, beforeSpawn, resume, runId,
|
|
379
|
+
onSettle, onGateDropped, resolveSessionId,
|
|
380
|
+
}) {
|
|
381
|
+
let seat = null;
|
|
382
|
+
let run;
|
|
383
|
+
try {
|
|
384
|
+
const useAcpTransport = shouldUseAcpTransport({
|
|
385
|
+
vendor, acpTransport: cfg.acpTransport, runtimePermissions,
|
|
386
|
+
codeArgs, codingCmd: cfg.codingCmd,
|
|
387
|
+
});
|
|
388
|
+
const useRuntimePermissionBridge = !useAcpTransport && shouldUseRuntimePermissionBridge({
|
|
389
|
+
vendor, runtimePermissions, codeArgs, codingCmd: cfg.codingCmd,
|
|
390
|
+
});
|
|
391
|
+
const gateCodexPermissions = !useAcpTransport && !useRuntimePermissionBridge &&
|
|
392
|
+
shouldGateCodexPermissions({ vendor, runtimePermissions, codeArgs });
|
|
393
|
+
const gateClaudePermissions = !useAcpTransport && !useRuntimePermissionBridge && !gateCodexPermissions &&
|
|
394
|
+
shouldGateClaudePermissions({ vendor, runtimePermissions, codeArgs });
|
|
395
|
+
seat = await childMcp({ standaloneConfig: vendor === "claude_code" && !gateClaudePermissions });
|
|
396
|
+
const callbacks = (extra = {}) => openCodePermissionCallbacks({ ...permissionCallbacks, runId, ...extra });
|
|
397
|
+
if (useAcpTransport) {
|
|
398
|
+
run = await deps.runAcpSession({
|
|
399
|
+
cmd, vendor, cwd, prompt,
|
|
400
|
+
...(resume ? { resumeSessionId: resume.sessionId } : {}),
|
|
401
|
+
timeoutMs: cfg.runTimeoutMs, signal,
|
|
402
|
+
env: scrubHilosEnv(codingChildWebEnv(cfg, vendor) || process.env),
|
|
403
|
+
onData, onEvent, beforeSpawn,
|
|
404
|
+
...callbacks({ provider: vendor }),
|
|
405
|
+
});
|
|
406
|
+
} else if (useRuntimePermissionBridge) {
|
|
407
|
+
run = await deps.runOpenCodeHttpSession({
|
|
408
|
+
cmd, args: codeArgs, cwd, prompt, timeoutMs: cfg.runTimeoutMs, signal,
|
|
409
|
+
env: scrubHilosEnv(codingChildWebEnv(cfg, vendor) || process.env),
|
|
410
|
+
onData, beforeSpawn, ...callbacks(),
|
|
411
|
+
});
|
|
412
|
+
} else if (gateCodexPermissions) {
|
|
413
|
+
run = await runCodexGatedSession({
|
|
414
|
+
deps, cfg, onGateDropped, cmd, codeArgs, cwd, prompt, model,
|
|
415
|
+
...(resume ? { resumeThreadId: resume.sessionId } : {}),
|
|
416
|
+
signal, onData, onEvent, beforeSpawn, childMcp: seat,
|
|
417
|
+
permissionCallbacks: callbacks({ provider: vendor, repoRoot: cwd, gateRepoEdits: cfg.gateRepoEdits === true }),
|
|
418
|
+
});
|
|
419
|
+
} else if (gateClaudePermissions) {
|
|
420
|
+
run = await runClaudeGatedCli({
|
|
421
|
+
deps, cfg, onGateDropped, cmd, codeArgs, cwd, prompt,
|
|
422
|
+
...(resume ? { sessionId: resume.sessionId ?? "" } : {}),
|
|
423
|
+
signal, onData, beforeSpawn, resolveSessionId, childMcp: seat,
|
|
424
|
+
permissionCallbacks: callbacks({ provider: vendor, repoRoot: cwd, gateRepoEdits: cfg.gateRepoEdits === true }),
|
|
425
|
+
});
|
|
426
|
+
} else {
|
|
427
|
+
run = await deps.runCli({
|
|
428
|
+
cmd, args: [...codeArgs, ...childMcpArgs(vendor, seat), prompt],
|
|
429
|
+
cwd, timeoutMs: cfg.runTimeoutMs, label: "coding", signal,
|
|
430
|
+
env: codingChildWebEnv(cfg, vendor), onData, beforeSpawn,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
// 0785 backstop; the callback's run-scoped latch keeps this notice singular.
|
|
434
|
+
if (run?.permissionGateDropped) await onGateDropped();
|
|
435
|
+
return run;
|
|
436
|
+
} finally {
|
|
437
|
+
try { await seat?.close(); } catch { /* teardown must never fail a finished run */ }
|
|
438
|
+
await onSettle?.(run);
|
|
439
|
+
}
|
|
440
|
+
}
|