@runuai/host 0.9.13 → 0.9.42
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 +22 -5
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +80 -30
- package/images/standard/Dockerfile +36 -10
- package/images/standard/README.md +63 -18
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +308 -38
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/lib/agent-cli.ts +69 -4
- package/lib/agent.ts +46 -7
- package/lib/agents/claude.ts +13 -8
- package/lib/agents/codex.ts +11 -6
- package/lib/agents/cursor.ts +39 -29
- package/lib/agents/durable-proc.ts +20 -27
- package/lib/agents/factory.ts +9 -25
- package/lib/agents/grok.ts +43 -30
- package/lib/agents/kimi.ts +44 -29
- package/lib/agents/opencode.ts +43 -31
- package/lib/agents/proc.ts +149 -114
- package/lib/agents/transport.ts +62 -50
- package/lib/agents/types.ts +6 -4
- package/lib/apple-runtime-recycle.ts +236 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/browser-testing.ts +233 -93
- package/lib/codex-auth.ts +40 -6
- package/lib/command-db.ts +20 -0
- package/lib/container-runtime.ts +1338 -0
- package/lib/db.ts +1 -0
- package/lib/docker-exec.ts +87 -5
- package/lib/engine-accounts.ts +68 -5
- package/lib/engine-login.ts +1952 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +155 -0
- package/lib/env.ts +4 -0
- package/lib/git-diff.ts +98 -32
- package/lib/git-identity.ts +199 -87
- package/lib/github-tokens.ts +202 -91
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-logs.ts +962 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +49 -0
- package/lib/managed-runtime.ts +3644 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-gateway.ts +450 -23
- package/lib/orchestrator.ts +3070 -223
- package/lib/preview-sidecar.ts +57 -13
- package/lib/release-manifest.ts +708 -0
- package/lib/release-trust.ts +28 -0
- package/lib/runtime-activation-tail.ts +232 -0
- package/lib/runtime-archive.ts +1086 -0
- package/lib/runtime-authority.ts +79 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-state.ts +232 -12
- package/lib/skills.ts +24 -3
- package/lib/ssh.ts +18 -0
- package/lib/standard-image.ts +1104 -141
- package/lib/stopped-task-status-queue.ts +44 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-diff.ts +66 -46
- package/lib/task-environment/apple-container.ts +757 -0
- package/lib/task-environment/docker.ts +945 -0
- package/lib/task-environment/index.ts +364 -0
- package/lib/task-environment/legacy-adoption.ts +443 -0
- package/lib/task-environment/registry.ts +58 -0
- package/lib/task-environment/types.ts +408 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +585 -0
- package/lib/tunnel-registry.ts +135 -19
- package/lib/tunnel-runtime.ts +235 -0
- package/package.json +1 -1
- package/scripts/agent/_common.sh +123 -3
- package/scripts/agent/task-down.sh +146 -38
- package/scripts/agent/task-status.sh +19 -3
- package/scripts/agent/task-up.sh +1463 -109
- package/scripts/install/darwin.ts +848 -50
- package/scripts/install/linux.ts +838 -35
- package/scripts/install/types.ts +43 -0
- package/scripts/install/util.ts +215 -8
- package/scripts/install/win.ts +12 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/cli.ts +1464 -72
- package/src/event-outbox.ts +83 -4
- package/src/index.ts +871 -50
- package/src/main.ts +1398 -255
- package/src/paths.ts +17 -1
- package/src/protocol.ts +695 -1
- package/src/runtime-bootstrap.ts +165 -0
- package/src/ui/server.ts +46 -10
- package/src/ui/types.ts +37 -0
package/lib/agents/grok.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GrokSession — a real AgentSession backed by xAI's **Grok** CLI, run inside
|
|
3
|
-
* the task
|
|
3
|
+
* the task environment:
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* ~/.grok/bin/grok -p "<prompt>" \
|
|
6
6
|
* --output-format streaming-json --permission-mode bypassPermissions \
|
|
7
7
|
* -m grok-4.5 --system-prompt-override "<briefing>" [-r <sessionId>]
|
|
8
8
|
*
|
|
9
9
|
* Like Kimi (and unlike Claude's persistent stdin / Codex's app-server), Grok
|
|
10
10
|
* headless mode is **one-shot per turn**: `-p` runs one prompt to completion,
|
|
11
11
|
* streams newline-delimited JSON, and exits. Each `send()` spawns a fresh
|
|
12
|
-
*
|
|
12
|
+
* provider process; sends are **serialized** so a second message can't race a
|
|
13
13
|
* running turn. Continuity is `-r <sessionId>` — captured from the turn-end
|
|
14
14
|
* event (per-agent isolation, since each session tracks its own id).
|
|
15
15
|
*
|
|
@@ -28,11 +28,20 @@
|
|
|
28
28
|
* it in-container. Cloud never sees it (ADR-015). `--system-prompt-override`
|
|
29
29
|
* carries the channel briefing (no fold-into-turn-1 needed).
|
|
30
30
|
*/
|
|
31
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
32
31
|
import { existsSync } from "node:fs";
|
|
33
32
|
import { homedir } from "node:os";
|
|
34
33
|
import { join } from "node:path";
|
|
35
34
|
|
|
35
|
+
import { requireContainerRuntimeOperational } from "../runtime-guard";
|
|
36
|
+
import type {
|
|
37
|
+
TaskEnvironmentAgentSessionSurface,
|
|
38
|
+
TaskEnvironmentSessionRequest,
|
|
39
|
+
} from "../task-environment/types";
|
|
40
|
+
import {
|
|
41
|
+
AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
|
42
|
+
spawnEnvironmentProcess,
|
|
43
|
+
type AgentEnvironmentProcess,
|
|
44
|
+
} from "./proc";
|
|
36
45
|
import { register } from "./registry";
|
|
37
46
|
import type {
|
|
38
47
|
AgentEvent,
|
|
@@ -94,15 +103,15 @@ export function mapGrokLine(line: string): MappedGrokLine {
|
|
|
94
103
|
// Session
|
|
95
104
|
// ---------------------------------------------------------------------------
|
|
96
105
|
|
|
97
|
-
export type Spawner = (
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
export type Spawner = (
|
|
107
|
+
request: TaskEnvironmentSessionRequest,
|
|
108
|
+
) => AgentEnvironmentProcess;
|
|
100
109
|
|
|
101
110
|
export class GrokSession implements AgentSession {
|
|
102
111
|
readonly agentId: string;
|
|
103
112
|
readonly kind: AgentKind = "grok";
|
|
104
113
|
|
|
105
|
-
private readonly
|
|
114
|
+
private readonly environment: TaskEnvironmentAgentSessionSurface;
|
|
106
115
|
private readonly model?: string;
|
|
107
116
|
private readonly systemPreamble: string;
|
|
108
117
|
private readonly agentEnv: Record<string, string>;
|
|
@@ -110,24 +119,25 @@ export class GrokSession implements AgentSession {
|
|
|
110
119
|
|
|
111
120
|
private readonly handlers = new Set<AgentEventHandler>();
|
|
112
121
|
private sessionId: string | null = null;
|
|
113
|
-
private current:
|
|
122
|
+
private current: AgentEnvironmentProcess | null = null;
|
|
114
123
|
private queue: Promise<void> = Promise.resolve();
|
|
115
124
|
private closed = false;
|
|
116
125
|
|
|
117
126
|
constructor(args: {
|
|
118
127
|
taskId: string;
|
|
119
128
|
agent: RosterAgent;
|
|
120
|
-
|
|
129
|
+
environment: TaskEnvironmentAgentSessionSurface;
|
|
121
130
|
systemPreamble: string;
|
|
122
131
|
agentEnv?: Record<string, string>;
|
|
123
132
|
spawner?: Spawner;
|
|
124
133
|
}) {
|
|
125
134
|
this.agentId = args.agent.id;
|
|
126
|
-
this.
|
|
135
|
+
this.environment = args.environment;
|
|
127
136
|
this.model = args.agent.model;
|
|
128
137
|
this.systemPreamble = args.systemPreamble;
|
|
129
138
|
this.agentEnv = args.agentEnv ?? {};
|
|
130
|
-
this.spawner =
|
|
139
|
+
this.spawner =
|
|
140
|
+
args.spawner ?? ((request) => spawnEnvironmentProcess(this.environment, request));
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
onEvent(handler: AgentEventHandler): () => void {
|
|
@@ -142,22 +152,18 @@ export class GrokSession implements AgentSession {
|
|
|
142
152
|
|
|
143
153
|
async send(text: string): Promise<void> {
|
|
144
154
|
if (this.closed) return;
|
|
145
|
-
|
|
146
|
-
|
|
155
|
+
const turn = this.queue.catch(() => {}).then(() => this.runTurn(text));
|
|
156
|
+
this.queue = turn;
|
|
157
|
+
return turn;
|
|
147
158
|
}
|
|
148
159
|
|
|
149
|
-
private
|
|
150
|
-
const args = ["exec", "-i", "-u", "node"];
|
|
160
|
+
private buildRequest(prompt: string): TaskEnvironmentSessionRequest {
|
|
151
161
|
// Host-resident API key (pasted in the engines panel, ADR-015): forward
|
|
152
162
|
// name-only — docker takes the value from the client env, keeping the
|
|
153
163
|
// secret out of argv. Subscription logins need nothing here (task-up
|
|
154
164
|
// copies ~/.grok into the container instead).
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
args.push("-e", `${k}=${v}`);
|
|
158
|
-
}
|
|
159
|
-
args.push(
|
|
160
|
-
this.containerName,
|
|
165
|
+
const inheritEnv = process.env.XAI_API_KEY ? ["XAI_API_KEY"] : [];
|
|
166
|
+
const argv = [
|
|
161
167
|
GROK_BIN,
|
|
162
168
|
"-p",
|
|
163
169
|
prompt,
|
|
@@ -166,19 +172,26 @@ export class GrokSession implements AgentSession {
|
|
|
166
172
|
// The container is the isolation boundary — auto-run tools.
|
|
167
173
|
"--permission-mode",
|
|
168
174
|
"bypassPermissions",
|
|
169
|
-
|
|
170
|
-
if (this.model)
|
|
175
|
+
];
|
|
176
|
+
if (this.model) argv.push("-m", this.model);
|
|
171
177
|
if (this.systemPreamble.trim().length > 0) {
|
|
172
|
-
|
|
178
|
+
argv.push("--system-prompt-override", this.systemPreamble);
|
|
173
179
|
}
|
|
174
180
|
// Resume the same conversation across turns (captured from the last end).
|
|
175
|
-
if (this.sessionId)
|
|
176
|
-
return
|
|
181
|
+
if (this.sessionId) argv.push("-r", this.sessionId);
|
|
182
|
+
return {
|
|
183
|
+
argv: argv as [string, ...string[]],
|
|
184
|
+
user: "node",
|
|
185
|
+
inheritEnv,
|
|
186
|
+
env: this.agentEnv,
|
|
187
|
+
maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
|
188
|
+
};
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
private async runTurn(text: string): Promise<void> {
|
|
180
192
|
if (this.closed) return;
|
|
181
|
-
|
|
193
|
+
requireContainerRuntimeOperational();
|
|
194
|
+
const child = this.spawner(this.buildRequest(text));
|
|
182
195
|
this.current = child;
|
|
183
196
|
|
|
184
197
|
let acc = "";
|
|
@@ -256,6 +269,6 @@ register({
|
|
|
256
269
|
supportedEfforts: () => [...GROK_EFFORTS],
|
|
257
270
|
available: () =>
|
|
258
271
|
existsSync(grokAuthPath()) || Boolean(process.env.XAI_API_KEY),
|
|
259
|
-
create: async ({ taskId, agent,
|
|
260
|
-
new GrokSession({ taskId, agent,
|
|
272
|
+
create: async ({ taskId, agent, environment, systemPreamble, agentEnv }) =>
|
|
273
|
+
new GrokSession({ taskId, agent, environment, systemPreamble, agentEnv }),
|
|
261
274
|
});
|
package/lib/agents/kimi.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* KimiSession — a real AgentSession backed by Moonshot's **Kimi Code** CLI,
|
|
3
|
-
* run inside the task
|
|
3
|
+
* run inside the task environment:
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* ~/.kimi-code/bin/kimi \
|
|
6
6
|
* -p "<prompt>" -m kimi-code/k3 --output-format stream-json [-r <sessionId>]
|
|
7
7
|
*
|
|
8
8
|
* Unlike Claude (persistent stdin session) and Codex (`app-server` JSON-RPC),
|
|
9
9
|
* Kimi Code is **one-shot per turn**: `-p` runs a single prompt to completion,
|
|
10
10
|
* streams newline-delimited JSON to stdout, and exits. Multi-turn continuity is
|
|
11
11
|
* `-r <sessionId>` (the session lives in Kimi's cloud + the container's
|
|
12
|
-
* ~/.kimi-code). So each `send()` spawns a fresh
|
|
12
|
+
* ~/.kimi-code). So each `send()` spawns a fresh provider process; sends are
|
|
13
13
|
* serialized so a second message can't race a running turn.
|
|
14
14
|
*
|
|
15
15
|
* Stream-json vocabulary (reverse-engineered — OpenAI-chat-style role lines):
|
|
@@ -24,12 +24,21 @@
|
|
|
24
24
|
* `-p` runs non-interactively and auto-runs tools (the container is the
|
|
25
25
|
* isolation boundary), so there are no permission prompts to resolve.
|
|
26
26
|
*/
|
|
27
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
28
27
|
import { existsSync } from "node:fs";
|
|
29
28
|
import { homedir } from "node:os";
|
|
30
29
|
import { join } from "node:path";
|
|
31
30
|
|
|
31
|
+
import { requireContainerRuntimeOperational } from "../runtime-guard";
|
|
32
|
+
import type {
|
|
33
|
+
TaskEnvironmentAgentSessionSurface,
|
|
34
|
+
TaskEnvironmentSessionRequest,
|
|
35
|
+
} from "../task-environment/types";
|
|
32
36
|
import { newId } from "../ulid";
|
|
37
|
+
import {
|
|
38
|
+
AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
|
39
|
+
spawnEnvironmentProcess,
|
|
40
|
+
type AgentEnvironmentProcess,
|
|
41
|
+
} from "./proc";
|
|
33
42
|
import { register } from "./registry";
|
|
34
43
|
import type {
|
|
35
44
|
AgentEvent,
|
|
@@ -40,7 +49,7 @@ import type {
|
|
|
40
49
|
} from "./types";
|
|
41
50
|
|
|
42
51
|
/** Installed by the image's install.sh (code.kimi.com); absolute so a
|
|
43
|
-
* non-login
|
|
52
|
+
* non-login provider process doesn't depend on PATH. */
|
|
44
53
|
const KIMI_BIN = "/home/node/.kimi-code/bin/kimi";
|
|
45
54
|
|
|
46
55
|
// Model ids from `~/.kimi-code/config.toml` (`kimi -m <id>`). k3 is the
|
|
@@ -143,15 +152,15 @@ export function mapKimiLine(line: string): MappedLine {
|
|
|
143
152
|
// ---------------------------------------------------------------------------
|
|
144
153
|
|
|
145
154
|
/** Injectable spawn seam (tests provide a fake). */
|
|
146
|
-
export type Spawner = (
|
|
147
|
-
|
|
148
|
-
|
|
155
|
+
export type Spawner = (
|
|
156
|
+
request: TaskEnvironmentSessionRequest,
|
|
157
|
+
) => AgentEnvironmentProcess;
|
|
149
158
|
|
|
150
159
|
export class KimiSession implements AgentSession {
|
|
151
160
|
readonly agentId: string;
|
|
152
161
|
readonly kind: AgentKind = "kimi";
|
|
153
162
|
|
|
154
|
-
private readonly
|
|
163
|
+
private readonly environment: TaskEnvironmentAgentSessionSurface;
|
|
155
164
|
private readonly model?: string;
|
|
156
165
|
private readonly systemPreamble: string;
|
|
157
166
|
private readonly agentEnv: Record<string, string>;
|
|
@@ -159,7 +168,7 @@ export class KimiSession implements AgentSession {
|
|
|
159
168
|
|
|
160
169
|
private readonly handlers = new Set<AgentEventHandler>();
|
|
161
170
|
private sessionId: string | null = null;
|
|
162
|
-
private current:
|
|
171
|
+
private current: AgentEnvironmentProcess | null = null;
|
|
163
172
|
private queue: Promise<void> = Promise.resolve();
|
|
164
173
|
private sentPreamble = false;
|
|
165
174
|
private closed = false;
|
|
@@ -167,17 +176,18 @@ export class KimiSession implements AgentSession {
|
|
|
167
176
|
constructor(args: {
|
|
168
177
|
taskId: string;
|
|
169
178
|
agent: RosterAgent;
|
|
170
|
-
|
|
179
|
+
environment: TaskEnvironmentAgentSessionSurface;
|
|
171
180
|
systemPreamble: string;
|
|
172
181
|
agentEnv?: Record<string, string>;
|
|
173
182
|
spawner?: Spawner;
|
|
174
183
|
}) {
|
|
175
184
|
this.agentId = args.agent.id;
|
|
176
|
-
this.
|
|
185
|
+
this.environment = args.environment;
|
|
177
186
|
this.model = args.agent.model;
|
|
178
187
|
this.systemPreamble = args.systemPreamble;
|
|
179
188
|
this.agentEnv = args.agentEnv ?? {};
|
|
180
|
-
this.spawner =
|
|
189
|
+
this.spawner =
|
|
190
|
+
args.spawner ?? ((request) => spawnEnvironmentProcess(this.environment, request));
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
onEvent(handler: AgentEventHandler): () => void {
|
|
@@ -194,32 +204,37 @@ export class KimiSession implements AgentSession {
|
|
|
194
204
|
* running one instead of spawning a racing `kimi -p`. */
|
|
195
205
|
async send(text: string): Promise<void> {
|
|
196
206
|
if (this.closed) return;
|
|
197
|
-
|
|
198
|
-
|
|
207
|
+
const turn = this.queue.catch(() => {}).then(() => this.runTurn(text));
|
|
208
|
+
this.queue = turn;
|
|
209
|
+
return turn;
|
|
199
210
|
}
|
|
200
211
|
|
|
201
|
-
private
|
|
202
|
-
const
|
|
203
|
-
for (const [k, v] of Object.entries(this.agentEnv)) {
|
|
204
|
-
args.push("-e", `${k}=${v}`);
|
|
205
|
-
}
|
|
206
|
-
args.push(
|
|
207
|
-
this.containerName,
|
|
212
|
+
private buildRequest(prompt: string): TaskEnvironmentSessionRequest {
|
|
213
|
+
const argv = [
|
|
208
214
|
KIMI_BIN,
|
|
209
215
|
"-p",
|
|
210
216
|
prompt,
|
|
211
217
|
"--output-format",
|
|
212
218
|
"stream-json",
|
|
213
|
-
|
|
214
|
-
if (this.model)
|
|
219
|
+
];
|
|
220
|
+
if (this.model) argv.push("-m", this.model);
|
|
215
221
|
// Continue the same Kimi session across turns (captured from the first
|
|
216
222
|
// turn's resume_hint). First turn has none → a fresh session.
|
|
217
|
-
if (this.sessionId)
|
|
218
|
-
return
|
|
223
|
+
if (this.sessionId) argv.push("-r", this.sessionId);
|
|
224
|
+
return {
|
|
225
|
+
argv: argv as [string, ...string[]],
|
|
226
|
+
user: "node",
|
|
227
|
+
env: this.agentEnv,
|
|
228
|
+
maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
|
229
|
+
};
|
|
219
230
|
}
|
|
220
231
|
|
|
221
232
|
private async runTurn(text: string): Promise<void> {
|
|
222
233
|
if (this.closed) return;
|
|
234
|
+
// A turn may have spent minutes queued behind its predecessor. Admission
|
|
235
|
+
// at send() time is stale; this synchronous check immediately precedes the
|
|
236
|
+
// provider process launch and leaves the first-turn preamble unconsumed on fail.
|
|
237
|
+
requireContainerRuntimeOperational();
|
|
223
238
|
// Kimi's `-p` has no system-prompt flag, so fold the channel briefing into
|
|
224
239
|
// the FIRST turn's prompt (later turns carry it via the resumed session).
|
|
225
240
|
const prompt =
|
|
@@ -228,7 +243,7 @@ export class KimiSession implements AgentSession {
|
|
|
228
243
|
: text;
|
|
229
244
|
this.sentPreamble = true;
|
|
230
245
|
|
|
231
|
-
const child = this.spawner(this.
|
|
246
|
+
const child = this.spawner(this.buildRequest(prompt));
|
|
232
247
|
this.current = child;
|
|
233
248
|
|
|
234
249
|
let buf = "";
|
|
@@ -308,6 +323,6 @@ register({
|
|
|
308
323
|
// Gated on the operator's Kimi Code subscription credential (copied into
|
|
309
324
|
// containers at task-up). No login → the engine isn't advertised.
|
|
310
325
|
available: () => existsSync(kimiCredPath()),
|
|
311
|
-
create: async ({ taskId, agent,
|
|
312
|
-
new KimiSession({ taskId, agent,
|
|
326
|
+
create: async ({ taskId, agent, environment, systemPreamble, agentEnv }) =>
|
|
327
|
+
new KimiSession({ taskId, agent, environment, systemPreamble, agentEnv }),
|
|
313
328
|
});
|
package/lib/agents/opencode.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpencodeSession — a real AgentSession backed by the OpenCode CLI's
|
|
3
|
-
* non-interactive `run` mode, executed inside the task
|
|
3
|
+
* non-interactive `run` mode, executed inside the task environment (ADR-077):
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* ~/.opencode/bin/opencode run \
|
|
6
6
|
* --format json --auto --model <provider/model> [--variant <effort>] \
|
|
7
7
|
* [--session <id>] "<prompt>"
|
|
8
8
|
*
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* `run` executes a single prompt to completion, streams newline-delimited JSON
|
|
11
11
|
* events to stdout, and exits. Multi-turn continuity is `--session <id>` (the
|
|
12
12
|
* id is captured from the first turn's event stream). Each `send()` spawns a
|
|
13
|
-
* fresh
|
|
13
|
+
* fresh provider process; sends are serialized so a second message can't race a
|
|
14
14
|
* running turn.
|
|
15
15
|
*
|
|
16
16
|
* `--format json` vocabulary (one object per line, `type` field):
|
|
@@ -29,12 +29,21 @@
|
|
|
29
29
|
* there are no permission prompts to resolve. ADR-076: `OPENCODE_DATA_DIR` in
|
|
30
30
|
* agentEnv selects which account's data dir (auth) this exec uses.
|
|
31
31
|
*/
|
|
32
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
33
32
|
import { existsSync } from "node:fs";
|
|
34
33
|
import { homedir } from "node:os";
|
|
35
34
|
import { join } from "node:path";
|
|
36
35
|
|
|
36
|
+
import { requireContainerRuntimeOperational } from "../runtime-guard";
|
|
37
|
+
import type {
|
|
38
|
+
TaskEnvironmentAgentSessionSurface,
|
|
39
|
+
TaskEnvironmentSessionRequest,
|
|
40
|
+
} from "../task-environment/types";
|
|
37
41
|
import { newId } from "../ulid";
|
|
42
|
+
import {
|
|
43
|
+
AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
|
44
|
+
spawnEnvironmentProcess,
|
|
45
|
+
type AgentEnvironmentProcess,
|
|
46
|
+
} from "./proc";
|
|
38
47
|
import { isRateLimitMessage } from "./rate-limit";
|
|
39
48
|
import { register } from "./registry";
|
|
40
49
|
import type {
|
|
@@ -47,7 +56,7 @@ import type {
|
|
|
47
56
|
} from "./types";
|
|
48
57
|
|
|
49
58
|
/** Installed by the image's opencode installer (opencode.ai/install); absolute
|
|
50
|
-
* so a non-login
|
|
59
|
+
* so a non-login provider process doesn't depend on PATH. */
|
|
51
60
|
const OPENCODE_BIN = "/home/node/.opencode/bin/opencode";
|
|
52
61
|
|
|
53
62
|
// OpenCode is multi-provider: models are `provider/model` strings. This is a
|
|
@@ -257,9 +266,9 @@ export function mapOpencodeLine(line: string): MappedOpencodeLine {
|
|
|
257
266
|
// ---------------------------------------------------------------------------
|
|
258
267
|
|
|
259
268
|
/** Injectable spawn seam (tests provide a fake). */
|
|
260
|
-
export type Spawner = (
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
export type Spawner = (
|
|
270
|
+
request: TaskEnvironmentSessionRequest,
|
|
271
|
+
) => AgentEnvironmentProcess;
|
|
263
272
|
|
|
264
273
|
/** Sum this turn's step usage into a running total. */
|
|
265
274
|
function addUsage(acc: AgentUsage, u: AgentUsage): AgentUsage {
|
|
@@ -278,7 +287,7 @@ export class OpencodeSession implements AgentSession {
|
|
|
278
287
|
readonly agentId: string;
|
|
279
288
|
readonly kind: AgentKind = "opencode";
|
|
280
289
|
|
|
281
|
-
private readonly
|
|
290
|
+
private readonly environment: TaskEnvironmentAgentSessionSurface;
|
|
282
291
|
private readonly model?: string;
|
|
283
292
|
private readonly effort?: string;
|
|
284
293
|
private readonly systemPreamble: string;
|
|
@@ -287,7 +296,7 @@ export class OpencodeSession implements AgentSession {
|
|
|
287
296
|
|
|
288
297
|
private readonly handlers = new Set<AgentEventHandler>();
|
|
289
298
|
private sessionId: string | null = null;
|
|
290
|
-
private current:
|
|
299
|
+
private current: AgentEnvironmentProcess | null = null;
|
|
291
300
|
private queue: Promise<void> = Promise.resolve();
|
|
292
301
|
private sentPreamble = false;
|
|
293
302
|
private closed = false;
|
|
@@ -295,18 +304,19 @@ export class OpencodeSession implements AgentSession {
|
|
|
295
304
|
constructor(args: {
|
|
296
305
|
taskId: string;
|
|
297
306
|
agent: RosterAgent;
|
|
298
|
-
|
|
307
|
+
environment: TaskEnvironmentAgentSessionSurface;
|
|
299
308
|
systemPreamble: string;
|
|
300
309
|
agentEnv?: Record<string, string>;
|
|
301
310
|
spawner?: Spawner;
|
|
302
311
|
}) {
|
|
303
312
|
this.agentId = args.agent.id;
|
|
304
|
-
this.
|
|
313
|
+
this.environment = args.environment;
|
|
305
314
|
this.model = args.agent.model;
|
|
306
315
|
this.effort = args.agent.effort;
|
|
307
316
|
this.systemPreamble = args.systemPreamble;
|
|
308
317
|
this.agentEnv = args.agentEnv ?? {};
|
|
309
|
-
this.spawner =
|
|
318
|
+
this.spawner =
|
|
319
|
+
args.spawner ?? ((request) => spawnEnvironmentProcess(this.environment, request));
|
|
310
320
|
}
|
|
311
321
|
|
|
312
322
|
onEvent(handler: AgentEventHandler): () => void {
|
|
@@ -323,17 +333,13 @@ export class OpencodeSession implements AgentSession {
|
|
|
323
333
|
* running one instead of spawning a racing `opencode run`. */
|
|
324
334
|
async send(text: string): Promise<void> {
|
|
325
335
|
if (this.closed) return;
|
|
326
|
-
|
|
327
|
-
|
|
336
|
+
const turn = this.queue.catch(() => {}).then(() => this.runTurn(text));
|
|
337
|
+
this.queue = turn;
|
|
338
|
+
return turn;
|
|
328
339
|
}
|
|
329
340
|
|
|
330
|
-
private
|
|
331
|
-
const
|
|
332
|
-
for (const [k, v] of Object.entries(this.agentEnv)) {
|
|
333
|
-
args.push("-e", `${k}=${v}`);
|
|
334
|
-
}
|
|
335
|
-
args.push(
|
|
336
|
-
this.containerName,
|
|
341
|
+
private buildRequest(prompt: string): TaskEnvironmentSessionRequest {
|
|
342
|
+
const argv = [
|
|
337
343
|
OPENCODE_BIN,
|
|
338
344
|
"run",
|
|
339
345
|
"--format",
|
|
@@ -341,19 +347,25 @@ export class OpencodeSession implements AgentSession {
|
|
|
341
347
|
// Auto-approve permissions not explicitly denied — the container is the
|
|
342
348
|
// sandbox, and headless has no interactive approver.
|
|
343
349
|
"--auto",
|
|
344
|
-
|
|
345
|
-
if (this.model)
|
|
346
|
-
if (this.effort)
|
|
350
|
+
];
|
|
351
|
+
if (this.model) argv.push("--model", this.model);
|
|
352
|
+
if (this.effort) argv.push("--variant", this.effort);
|
|
347
353
|
// Continue the same OpenCode session across turns (captured from the first
|
|
348
354
|
// turn's stream). First turn has none → a fresh session.
|
|
349
|
-
if (this.sessionId)
|
|
355
|
+
if (this.sessionId) argv.push("--session", this.sessionId);
|
|
350
356
|
// The prompt is positional; put it LAST so it's never mistaken for a flag.
|
|
351
|
-
|
|
352
|
-
return
|
|
357
|
+
argv.push(prompt);
|
|
358
|
+
return {
|
|
359
|
+
argv: argv as [string, ...string[]],
|
|
360
|
+
user: "node",
|
|
361
|
+
env: this.agentEnv,
|
|
362
|
+
maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
|
363
|
+
};
|
|
353
364
|
}
|
|
354
365
|
|
|
355
366
|
private async runTurn(text: string): Promise<void> {
|
|
356
367
|
if (this.closed) return;
|
|
368
|
+
requireContainerRuntimeOperational();
|
|
357
369
|
// `run` has no system-prompt flag, so fold the channel briefing into the
|
|
358
370
|
// FIRST turn's prompt (later turns carry it via the resumed session).
|
|
359
371
|
const prompt =
|
|
@@ -362,7 +374,7 @@ export class OpencodeSession implements AgentSession {
|
|
|
362
374
|
: text;
|
|
363
375
|
this.sentPreamble = true;
|
|
364
376
|
|
|
365
|
-
const child = this.spawner(this.
|
|
377
|
+
const child = this.spawner(this.buildRequest(prompt));
|
|
366
378
|
this.current = child;
|
|
367
379
|
|
|
368
380
|
let turnUsage: AgentUsage = {};
|
|
@@ -467,6 +479,6 @@ register({
|
|
|
467
479
|
// Gated on the operator's OpenCode auth (copied into containers at task-up).
|
|
468
480
|
// No auth → the engine isn't advertised.
|
|
469
481
|
available: () => existsSync(opencodeAuthPath()),
|
|
470
|
-
create: async ({ taskId, agent,
|
|
471
|
-
new OpencodeSession({ taskId, agent,
|
|
482
|
+
create: async ({ taskId, agent, environment, systemPreamble, agentEnv }) =>
|
|
483
|
+
new OpencodeSession({ taskId, agent, environment, systemPreamble, agentEnv }),
|
|
472
484
|
});
|