@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.
Files changed (97) hide show
  1. package/README.md +22 -5
  2. package/db/migrations/0014_host_inventory_event_index.sql +1 -0
  3. package/db/migrations/0015_host_settings.sql +9 -0
  4. package/db/migrations/0016_task_environment.sql +2 -0
  5. package/db/migrations/meta/_journal.json +21 -0
  6. package/db/schema.ts +80 -30
  7. package/images/standard/Dockerfile +36 -10
  8. package/images/standard/README.md +63 -18
  9. package/images/standard/container/corepack-version +1 -0
  10. package/images/standard/container/uai-init +308 -38
  11. package/images/standard/container/uai-materialize-runtimes +1527 -0
  12. package/lib/agent-cli.ts +69 -4
  13. package/lib/agent.ts +46 -7
  14. package/lib/agents/claude.ts +13 -8
  15. package/lib/agents/codex.ts +11 -6
  16. package/lib/agents/cursor.ts +39 -29
  17. package/lib/agents/durable-proc.ts +20 -27
  18. package/lib/agents/factory.ts +9 -25
  19. package/lib/agents/grok.ts +43 -30
  20. package/lib/agents/kimi.ts +44 -29
  21. package/lib/agents/opencode.ts +43 -31
  22. package/lib/agents/proc.ts +149 -114
  23. package/lib/agents/transport.ts +62 -50
  24. package/lib/agents/types.ts +6 -4
  25. package/lib/apple-runtime-recycle.ts +236 -0
  26. package/lib/apple-uninstall-teardown.ts +224 -0
  27. package/lib/browser-testing.ts +233 -93
  28. package/lib/codex-auth.ts +40 -6
  29. package/lib/command-db.ts +20 -0
  30. package/lib/container-runtime.ts +1338 -0
  31. package/lib/db.ts +1 -0
  32. package/lib/docker-exec.ts +87 -5
  33. package/lib/engine-accounts.ts +68 -5
  34. package/lib/engine-login.ts +1952 -0
  35. package/lib/enrollment-state.ts +251 -0
  36. package/lib/env-file.ts +155 -0
  37. package/lib/env.ts +4 -0
  38. package/lib/git-diff.ts +98 -32
  39. package/lib/git-identity.ts +199 -87
  40. package/lib/github-tokens.ts +202 -91
  41. package/lib/host-cloud-url.ts +62 -0
  42. package/lib/host-config.ts +279 -0
  43. package/lib/host-logs.ts +962 -0
  44. package/lib/keyed-promise-tail.ts +23 -0
  45. package/lib/legacy-runtime-v1.fixture.ts +627 -0
  46. package/lib/managed-activation-watcher.ts +72 -0
  47. package/lib/managed-install-owner-watcher.ts +55 -0
  48. package/lib/managed-operation-drain.ts +49 -0
  49. package/lib/managed-runtime.ts +3644 -0
  50. package/lib/managed-update-scheduler.ts +125 -0
  51. package/lib/mcp-gateway.ts +450 -23
  52. package/lib/orchestrator.ts +3070 -223
  53. package/lib/preview-sidecar.ts +57 -13
  54. package/lib/release-manifest.ts +708 -0
  55. package/lib/release-trust.ts +28 -0
  56. package/lib/runtime-activation-tail.ts +232 -0
  57. package/lib/runtime-archive.ts +1086 -0
  58. package/lib/runtime-authority.ts +79 -0
  59. package/lib/runtime-guard.ts +36 -0
  60. package/lib/runtime-provider-state.ts +169 -0
  61. package/lib/runtime-state.ts +232 -12
  62. package/lib/skills.ts +24 -3
  63. package/lib/ssh.ts +18 -0
  64. package/lib/standard-image.ts +1104 -141
  65. package/lib/stopped-task-status-queue.ts +44 -0
  66. package/lib/task-container-cli.ts +269 -0
  67. package/lib/task-diff.ts +66 -46
  68. package/lib/task-environment/apple-container.ts +757 -0
  69. package/lib/task-environment/docker.ts +945 -0
  70. package/lib/task-environment/index.ts +364 -0
  71. package/lib/task-environment/legacy-adoption.ts +443 -0
  72. package/lib/task-environment/registry.ts +58 -0
  73. package/lib/task-environment/types.ts +408 -0
  74. package/lib/task-identity.ts +19 -0
  75. package/lib/task-inventory.ts +585 -0
  76. package/lib/tunnel-registry.ts +135 -19
  77. package/lib/tunnel-runtime.ts +235 -0
  78. package/package.json +1 -1
  79. package/scripts/agent/_common.sh +123 -3
  80. package/scripts/agent/task-down.sh +146 -38
  81. package/scripts/agent/task-status.sh +19 -3
  82. package/scripts/agent/task-up.sh +1463 -109
  83. package/scripts/install/darwin.ts +848 -50
  84. package/scripts/install/linux.ts +838 -35
  85. package/scripts/install/types.ts +43 -0
  86. package/scripts/install/util.ts +215 -8
  87. package/scripts/install/win.ts +12 -0
  88. package/src/apple-tunnel-route.ts +104 -0
  89. package/src/cli.ts +1464 -72
  90. package/src/event-outbox.ts +83 -4
  91. package/src/index.ts +871 -50
  92. package/src/main.ts +1398 -255
  93. package/src/paths.ts +17 -1
  94. package/src/protocol.ts +695 -1
  95. package/src/runtime-bootstrap.ts +165 -0
  96. package/src/ui/server.ts +46 -10
  97. package/src/ui/types.ts +37 -0
@@ -1,125 +1,139 @@
1
- /**
2
- * LineProcess — a child process whose stdout is consumed as
3
- * newline-delimited text (one JSON value per line).
4
- *
5
- * Both real agent adapters (ClaudeSession, CodexSession) drive a CLI
6
- * running *inside the task container* (ADR-010) via
7
- * `docker exec -i task-<id>-app-1 <cli> …`. That CLI speaks
8
- * newline-delimited JSON over stdio — Claude's stream-json, Codex's
9
- * JSON-RPC. This class owns the mechanics shared by both: spawn,
10
- * line-buffer stdout, capture stderr, write lines to stdin, tear down.
11
- *
12
- * It is deliberately protocol-agnostic — it knows nothing about
13
- * stream-json or JSON-RPC. The adapters layer that on top.
14
- */
1
+ /** Provider-backed process adapters shared by the agent protocols. */
15
2
 
16
- import { spawn, type ChildProcess } from "node:child_process";
17
-
18
- export interface LineProcessOptions {
19
- /** Executable — almost always "docker". */
20
- command: string;
21
- /** Args — e.g. ["exec", "-i", "task-…-app-1", "claude", …]. */
22
- args: string[];
23
- /**
24
- * When set *and* `UAI_DEBUG_AGENTS` is truthy in the environment,
25
- * spawn / raw stdio / exit are logged to the server console with
26
- * this label — the way to see what an agent CLI is actually doing.
27
- */
28
- debugLabel?: string;
29
- }
3
+ import { EventEmitter } from "node:events";
4
+
5
+ import type {
6
+ TaskEnvironmentAgentSessionSurface,
7
+ TaskEnvironmentProcess,
8
+ TaskEnvironmentSessionRequest,
9
+ } from "../task-environment/types";
30
10
 
31
11
  export type LineHandler = (line: string) => void;
32
12
  export type ExitHandler = (code: number | null) => void;
33
13
 
34
14
  /** How much trailing stderr to retain for error reporting. */
35
15
  const STDERR_CAP = 8 * 1024;
16
+ /** Maximum unread stdout+stderr retained by a provider for an agent process. */
17
+ export const AGENT_SESSION_OUTPUT_BUFFER_BYTES = 1024 * 1024;
36
18
 
37
- export class LineProcess {
38
- private readonly child: ChildProcess;
19
+ /**
20
+ * Line-oriented adapter over the provider-neutral environment
21
+ * stream. Startup may be asynchronous, so writes are serialized behind the
22
+ * provider's spawn promise without exposing a container command to callers.
23
+ */
24
+ export class EnvironmentLineProcess {
25
+ private readonly processPromise: Promise<TaskEnvironmentProcess>;
26
+ private process: TaskEnvironmentProcess | null = null;
39
27
  private stdoutBuf = "";
40
28
  private stderrBuf = "";
41
29
  private readonly lineHandlers = new Set<LineHandler>();
42
30
  private readonly exitHandlers = new Set<ExitHandler>();
31
+ private inputChain: Promise<void> = Promise.resolve();
43
32
  private closed = false;
33
+ private finished = false;
34
+ private exitCode: number | null = null;
44
35
  private readonly debug: string | null;
45
36
 
46
- constructor(opts: LineProcessOptions) {
37
+ constructor(opts: {
38
+ environment: TaskEnvironmentAgentSessionSurface;
39
+ request: TaskEnvironmentSessionRequest;
40
+ debugLabel?: string;
41
+ }) {
47
42
  this.debug =
48
43
  opts.debugLabel && process.env.UAI_DEBUG_AGENTS ? opts.debugLabel : null;
49
- if (this.debug) {
50
- this.log(`spawn: ${opts.command} ${opts.args.join(" ")}`);
51
- }
52
-
53
- this.child = spawn(opts.command, opts.args, {
54
- stdio: ["pipe", "pipe", "pipe"],
55
- });
56
-
57
- this.child.stdout?.setEncoding("utf8");
58
- this.child.stdout?.on("data", (chunk: string) => this.onStdout(chunk));
44
+ if (this.debug) this.log(`spawn: ${opts.request.argv.join(" ")}`);
45
+ this.processPromise = opts.environment.spawnSession(opts.request);
46
+ void this.consume();
47
+ }
59
48
 
60
- this.child.stderr?.setEncoding("utf8");
61
- this.child.stderr?.on("data", (chunk: string) => {
62
- // Keep only the tail — stderr can be noisy, we want the last words.
63
- this.stderrBuf = (this.stderrBuf + chunk).slice(-STDERR_CAP);
64
- if (this.debug) this.log(`stderr: ${chunk.trimEnd()}`);
65
- });
49
+ private log(message: string): void {
50
+ console.error(`[uai-agent ${this.debug}] ${message}`);
51
+ }
66
52
 
67
- this.child.on("exit", (code) => this.finish(code));
68
- this.child.on("error", (err) => {
69
- if (this.debug) this.log(`spawn error: ${err.message}`);
53
+ private async consume(): Promise<void> {
54
+ try {
55
+ const process = await this.processPromise;
56
+ this.process = process;
57
+ const stdout = this.consumeStdout(process.stdout);
58
+ const stderr = this.consumeStderr(process.stderr);
59
+ const exit = await process.completion;
60
+ await Promise.allSettled([stdout, stderr]);
61
+ this.finish(exit.exitCode);
62
+ } catch (error) {
63
+ this.stderrBuf = (
64
+ this.stderrBuf +
65
+ (error instanceof Error ? error.message : String(error))
66
+ ).slice(-STDERR_CAP);
70
67
  this.finish(null);
71
- });
68
+ }
72
69
  }
73
70
 
74
- private log(msg: string): void {
75
- console.error(`[uai-agent ${this.debug}] ${msg}`);
71
+ private async consumeStdout(stream: AsyncIterable<Uint8Array>): Promise<void> {
72
+ for await (const chunk of stream) this.onStdout(Buffer.from(chunk).toString("utf8"));
76
73
  }
77
74
 
78
- private finish(code: number | null): void {
79
- if (this.closed) return;
80
- this.closed = true;
81
- if (this.debug) this.log(`exit: code ${code}`);
82
- for (const h of this.exitHandlers) h(code);
75
+ private async consumeStderr(stream: AsyncIterable<Uint8Array>): Promise<void> {
76
+ for await (const chunk of stream) {
77
+ const text = Buffer.from(chunk).toString("utf8");
78
+ this.stderrBuf = (this.stderrBuf + text).slice(-STDERR_CAP);
79
+ if (this.debug) this.log(`stderr: ${text.trimEnd()}`);
80
+ }
83
81
  }
84
82
 
85
- /** Split the rolling buffer on newlines; emit each complete line. */
86
83
  private onStdout(chunk: string): void {
87
84
  this.stdoutBuf += chunk;
88
- let nl: number;
89
- while ((nl = this.stdoutBuf.indexOf("\n")) >= 0) {
90
- const line = this.stdoutBuf.slice(0, nl).trim();
91
- this.stdoutBuf = this.stdoutBuf.slice(nl + 1);
92
- if (line.length === 0) continue;
85
+ let newline: number;
86
+ while ((newline = this.stdoutBuf.indexOf("\n")) >= 0) {
87
+ const line = this.stdoutBuf.slice(0, newline).trim();
88
+ this.stdoutBuf = this.stdoutBuf.slice(newline + 1);
89
+ if (!line) continue;
93
90
  if (this.debug) this.log(`<- ${line.slice(0, 1000)}`);
94
- for (const h of this.lineHandlers) {
91
+ for (const handler of this.lineHandlers) {
95
92
  try {
96
- h(line);
93
+ handler(line);
97
94
  } catch {
98
- // A broken handler must not wedge the read loop.
95
+ // A broken protocol handler must not wedge stream consumption.
99
96
  }
100
97
  }
101
98
  }
102
99
  }
103
100
 
104
- /** Subscribe to complete stdout lines. */
101
+ private finish(code: number | null): void {
102
+ if (this.finished) return;
103
+ this.finished = true;
104
+ this.closed = true;
105
+ this.exitCode = code;
106
+ if (this.debug) this.log(`exit: code ${code}`);
107
+ for (const handler of this.exitHandlers) handler(code);
108
+ }
109
+
105
110
  onLine(handler: LineHandler): void {
106
111
  this.lineHandlers.add(handler);
107
112
  }
108
113
 
109
- /** Subscribe to process exit. `code` is null on spawn error. */
110
114
  onExit(handler: ExitHandler): void {
111
115
  this.exitHandlers.add(handler);
116
+ if (this.finished) queueMicrotask(() => handler(this.exitCode));
112
117
  }
113
118
 
114
- /** Serialise `value` as JSON and write it as one newline-delimited line. */
115
119
  writeLine(value: unknown): void {
116
120
  if (this.closed) return;
117
121
  const json = JSON.stringify(value);
118
122
  if (this.debug) this.log(`-> ${json.slice(0, 1000)}`);
119
- this.child.stdin?.write(`${json}\n`);
123
+ this.inputChain = this.inputChain
124
+ .then(async () => {
125
+ const process = await this.processPromise;
126
+ await process.write(Buffer.from(`${json}\n`));
127
+ })
128
+ .catch((error: unknown) => {
129
+ this.stderrBuf = (
130
+ this.stderrBuf +
131
+ (error instanceof Error ? error.message : String(error))
132
+ ).slice(-STDERR_CAP);
133
+ this.finish(null);
134
+ });
120
135
  }
121
136
 
122
- /** Last few KB of stderr — surfaced in error events. */
123
137
  get stderrTail(): string {
124
138
  return this.stderrBuf;
125
139
  }
@@ -128,54 +142,75 @@ export class LineProcess {
128
142
  return this.closed;
129
143
  }
130
144
 
131
- /** Terminate the process. Idempotent. */
132
145
  async close(): Promise<void> {
133
- if (this.closed) {
134
- // Already exited — nothing to kill.
135
- return;
136
- }
146
+ if (this.closed) return;
137
147
  try {
138
- this.child.stdin?.end();
148
+ const process = this.process ?? (await this.processPromise);
149
+ await process.closeInput().catch(() => {});
150
+ await process.terminate("SIGTERM").catch(() => {});
139
151
  } catch {
140
- // stdin may already be gone.
152
+ // Startup already failed; consume() owns the terminal notification.
141
153
  }
142
- this.child.kill("SIGTERM");
143
154
  }
144
155
  }
145
156
 
146
- /**
147
- * Build the `docker exec` argv that runs `cli …cliArgs` inside a task
148
- * container. `-i` keeps stdin open for the bidirectional session.
149
- */
150
- export function dockerExecArgs(
151
- containerName: string,
152
- cli: string,
153
- cliArgs: string[],
154
- /**
155
- * Env var names to forward from the host-agent process into the
156
- * `docker exec` (as `-e NAME`, value taken from the host-agent's own
157
- * environment). Used to pass host-resident AI credentials — e.g.
158
- * `CLAUDE_CODE_OAUTH_TOKEN` for headless Claude subscription auth — into
159
- * the container without baking them into the image or sending them to the
160
- * cloud (ADR-015). Only vars actually set on the host are forwarded.
161
- */
162
- passEnv: string[] = [],
163
- /**
164
- * Explicit per-exec env values (`-e NAME=value`) — used for per-agent values
165
- * that don't live in the host's own env, e.g. the agent's task-scoped
166
- * `UAI_TASK_TOKEN` (ADR-048). Values are argv, never a shell string.
167
- */
168
- explicitEnv: Record<string, string> = {},
169
- ): { command: string; args: string[] } {
170
- const envArgs: string[] = [];
171
- for (const name of passEnv) {
172
- if (process.env[name]) envArgs.push("-e", name);
173
- }
174
- for (const [name, value] of Object.entries(explicitEnv)) {
175
- envArgs.push("-e", `${name}=${value}`);
157
+ /** Minimal ChildProcess-shaped view retained by the one-shot adapters. */
158
+ export interface AgentEnvironmentProcess {
159
+ readonly stdout: EventEmitter;
160
+ readonly stderr: EventEmitter;
161
+ on(event: "exit", listener: (code: number | null) => void): this;
162
+ on(event: "error", listener: (error: Error) => void): this;
163
+ kill(signal?: "SIGTERM" | "SIGKILL"): boolean;
164
+ }
165
+
166
+ /** Bridge a provider stream into the event shape used by one-shot adapters. */
167
+ export function spawnEnvironmentProcess(
168
+ environment: TaskEnvironmentAgentSessionSurface,
169
+ request: TaskEnvironmentSessionRequest,
170
+ ): AgentEnvironmentProcess {
171
+ class EnvironmentProcess extends EventEmitter implements AgentEnvironmentProcess {
172
+ readonly stdout = new EventEmitter();
173
+ readonly stderr = new EventEmitter();
174
+ private process: TaskEnvironmentProcess | null = null;
175
+ private readonly processPromise: Promise<TaskEnvironmentProcess>;
176
+
177
+ constructor() {
178
+ super();
179
+ this.processPromise = environment.spawnSession(request);
180
+ void this.consume();
181
+ }
182
+
183
+ private async consume(): Promise<void> {
184
+ try {
185
+ const process = await this.processPromise;
186
+ this.process = process;
187
+ const stdout = pumpEnvironmentStream(process.stdout, this.stdout);
188
+ const stderr = pumpEnvironmentStream(process.stderr, this.stderr);
189
+ const exit = await process.completion;
190
+ await Promise.allSettled([stdout, stderr]);
191
+ this.emit("exit", exit.exitCode);
192
+ } catch (error) {
193
+ this.emit(
194
+ "error",
195
+ error instanceof Error ? error : new Error(String(error)),
196
+ );
197
+ }
198
+ }
199
+
200
+ kill(signal: "SIGTERM" | "SIGKILL" = "SIGTERM"): boolean {
201
+ const process = this.process;
202
+ if (process) void process.terminate(signal).catch(() => {});
203
+ else void this.processPromise.then((pending) => pending.terminate(signal)).catch(() => {});
204
+ return true;
205
+ }
176
206
  }
177
- return {
178
- command: "docker",
179
- args: ["exec", "-i", ...envArgs, containerName, cli, ...cliArgs],
180
- };
207
+
208
+ return new EnvironmentProcess();
209
+ }
210
+
211
+ async function pumpEnvironmentStream(
212
+ stream: AsyncIterable<Uint8Array>,
213
+ target: EventEmitter,
214
+ ): Promise<void> {
215
+ for await (const chunk of stream) target.emit("data", Buffer.from(chunk));
181
216
  }
@@ -2,7 +2,7 @@
2
2
  * Agent transport selection (ADR-061). Adapters call createAgentTransport()
3
3
  * where they used to build a LineProcess directly; it returns either:
4
4
  *
5
- * - legacy pipes (`docker exec -i <cli>`, LineProcess) when
5
+ * - provider-owned interactive pipes when
6
6
  * UAI_DURABLE_SESSIONS=0, or
7
7
  * - the durable path: an in-container runner owning the CLI, driven through
8
8
  * inbox/outbox files on the workspace mount (DurableProcess).
@@ -17,9 +17,8 @@
17
17
  * via its inbox first.
18
18
  *
19
19
  * The runner script is COPIED into each session dir (not bind-mounted):
20
- * every existing container already sees the workspace at its identical host
21
- * path, so durable sessions work for containers created before this
22
- * feature, and each spawn ships the runner version matching this host.
20
+ * the task environment sees the workspace at its provider path, so each
21
+ * durable spawn ships the runner version matching this host.
23
22
  */
24
23
 
25
24
  import {
@@ -37,8 +36,15 @@ import { and, eq } from "drizzle-orm";
37
36
 
38
37
  import { getDb, schema } from "../db";
39
38
  import { taskWorkspaceDir } from "../env";
39
+ import { requireContainerRuntimeOperational } from "../runtime-guard";
40
+ import type { TaskEnvironmentAgentSessionSurface } from "../task-environment/types";
40
41
  import { DurableProcess, runnerScriptPath } from "./durable-proc";
41
- import { LineProcess, dockerExecArgs, type ExitHandler, type LineHandler } from "./proc";
42
+ import {
43
+ AGENT_SESSION_OUTPUT_BUFFER_BYTES,
44
+ EnvironmentLineProcess,
45
+ type ExitHandler,
46
+ type LineHandler,
47
+ } from "./proc";
42
48
 
43
49
  /** The shared surface adapters program against (LineProcess's shape). */
44
50
  export interface LineTransport {
@@ -53,13 +59,13 @@ export interface LineTransport {
53
59
  export interface AgentTransportOptions {
54
60
  taskId: string;
55
61
  agentId: string;
56
- containerName: string;
57
- /** CLI + args exactly as they'd follow `docker exec -i <container>`. */
62
+ environment: TaskEnvironmentAgentSessionSurface;
63
+ /** CLI + literal argv as observed inside the task environment. */
58
64
  cli: string;
59
65
  cliArgs: string[];
60
- /** Env names forwarded from the host's own env (`-e NAME`). */
66
+ /** Env names forwarded from the host's own environment. */
61
67
  passEnv?: string[];
62
- /** Explicit per-exec env values (`-e NAME=value`). */
68
+ /** Explicit per-session environment values. */
63
69
  explicitEnv?: Record<string, string>;
64
70
  /**
65
71
  * Whether a live runner from a previous host process may be attached to.
@@ -100,16 +106,13 @@ function durableEnabled(): boolean {
100
106
  }
101
107
 
102
108
  export function createAgentTransport(opts: AgentTransportOptions): LineTransport {
109
+ // Session creation can happen after channel/task lifecycle queues drain, well
110
+ // after the command-level runtime preflight. Recheck at the actual attach or
111
+ // spawn boundary so a cached ready verdict cannot launch container work.
112
+ requireContainerRuntimeOperational();
103
113
  if (!durableEnabled()) {
104
114
  clearCurrentSession(opts.taskId, opts.agentId);
105
- const { command, args } = dockerExecArgs(
106
- opts.containerName,
107
- opts.cli,
108
- opts.cliArgs,
109
- opts.passEnv ?? [],
110
- opts.explicitEnv ?? {},
111
- );
112
- return new LineProcess({ command, args, debugLabel: opts.debugLabel });
115
+ return directEnvironmentTransport(opts);
113
116
  }
114
117
 
115
118
  const db = getDb();
@@ -154,6 +157,7 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
154
157
  opts.allowAttach &&
155
158
  row &&
156
159
  row.status === "running" &&
160
+ row.containerName === opts.environment.durableIdentity &&
157
161
  (row.attachCompatibilityKey ?? null) ===
158
162
  (opts.attachCompatibilityKey ?? null) &&
159
163
  heartbeatFresh(row.sessionDir)
@@ -188,13 +192,21 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
188
192
  ".uai",
189
193
  "sessions",
190
194
  opts.agentId,
191
- String(Date.now()),
195
+ `${String(Date.now())}-${process.pid}`,
192
196
  );
193
197
  mkdirSync(sessionDir, { recursive: true });
194
198
  // Ship this host's runner with the session: works in containers created
195
199
  // before this feature (the workspace is mounted at its host path) and
196
200
  // never skews against the host version.
197
201
  const runnerInSession = join(sessionDir, "runner.mjs");
202
+ const environmentSessionDir = join(
203
+ opts.environment.descriptor.workspacePath,
204
+ ".uai",
205
+ "sessions",
206
+ opts.agentId,
207
+ basename(sessionDir),
208
+ );
209
+ const runnerInEnvironment = join(environmentSessionDir, "runner.mjs");
198
210
  try {
199
211
  copyFileSync(runnerScriptPath(), runnerInSession);
200
212
  } catch (err) {
@@ -205,22 +217,7 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
205
217
  `[transport] runner unavailable (${err instanceof Error ? err.message : err}) — falling back to direct pipes for ${opts.agentId}`,
206
218
  );
207
219
  clearCurrentSession(opts.taskId, opts.agentId);
208
- const { command, args } = dockerExecArgs(
209
- opts.containerName,
210
- opts.cli,
211
- opts.cliArgs,
212
- opts.passEnv ?? [],
213
- opts.explicitEnv ?? {},
214
- );
215
- return new LineProcess({ command, args, debugLabel: opts.debugLabel });
216
- }
217
-
218
- const envArgs: string[] = [];
219
- for (const name of opts.passEnv ?? []) {
220
- if (process.env[name]) envArgs.push("-e", name);
221
- }
222
- for (const [name, value] of Object.entries(opts.explicitEnv ?? {})) {
223
- envArgs.push("-e", `${name}=${value}`);
220
+ return directEnvironmentTransport(opts);
224
221
  }
225
222
 
226
223
  const proc = new DurableProcess({
@@ -228,20 +225,22 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
228
225
  onOffsetAdvance: persistOffset,
229
226
  onCloseRequested: markClosed,
230
227
  debugLabel: opts.debugLabel,
231
- spawnCommand: {
232
- command: "docker",
233
- args: [
234
- "exec",
235
- "-d",
236
- ...envArgs,
237
- opts.containerName,
238
- "node",
239
- runnerInSession, // identical path in-container (workspace self-mount)
240
- sessionDir,
241
- "--",
242
- opts.cli,
243
- ...opts.cliArgs,
244
- ],
228
+ launch: async () => {
229
+ const result = await opts.environment.launchDetachedSession({
230
+ argv: [
231
+ "node",
232
+ runnerInEnvironment,
233
+ environmentSessionDir,
234
+ "--",
235
+ opts.cli,
236
+ ...opts.cliArgs,
237
+ ],
238
+ inheritEnv: opts.passEnv ?? [],
239
+ env: opts.explicitEnv ?? {},
240
+ launchTimeoutMs: 30_000,
241
+ maxOutputBytes: 256 * 1024,
242
+ });
243
+ return { exitCode: result.exitCode, stderr: result.stderr };
245
244
  },
246
245
  });
247
246
 
@@ -251,7 +250,7 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
251
250
  taskId: opts.taskId,
252
251
  agentId: opts.agentId,
253
252
  sessionDir,
254
- containerName: opts.containerName,
253
+ containerName: opts.environment.durableIdentity,
255
254
  kind: opts.kind,
256
255
  attachCompatibilityKey: opts.attachCompatibilityKey ?? null,
257
256
  outboxOffset: 0,
@@ -263,7 +262,7 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
263
262
  target: [schema.hostAgentSessions.taskId, schema.hostAgentSessions.agentId],
264
263
  set: {
265
264
  sessionDir,
266
- containerName: opts.containerName,
265
+ containerName: opts.environment.durableIdentity,
267
266
  kind: opts.kind,
268
267
  attachCompatibilityKey: opts.attachCompatibilityKey ?? null,
269
268
  outboxOffset: 0,
@@ -278,6 +277,19 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
278
277
  return claimTail(tailKey, proc);
279
278
  }
280
279
 
280
+ function directEnvironmentTransport(opts: AgentTransportOptions): LineTransport {
281
+ return new EnvironmentLineProcess({
282
+ environment: opts.environment,
283
+ request: {
284
+ argv: [opts.cli, ...opts.cliArgs],
285
+ inheritEnv: opts.passEnv ?? [],
286
+ env: opts.explicitEnv ?? {},
287
+ maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
288
+ },
289
+ debugLabel: opts.debugLabel,
290
+ });
291
+ }
292
+
281
293
  /**
282
294
  * Make the opaque generation directory discoverable from inside the task.
283
295
  * The relative link resolves on both sides of the workspace bind mount. The
@@ -13,6 +13,8 @@
13
13
 
14
14
  import { z } from "zod";
15
15
 
16
+ import type { TaskEnvironmentAgentSessionSurface } from "../task-environment/types";
17
+
16
18
  /** The host-advertised adapter kind ("claude" | "codex" | "gemini" | …). */
17
19
  export const AgentKind = z
18
20
  .string()
@@ -168,8 +170,8 @@ export interface AgentSessionFactory {
168
170
  create(args: {
169
171
  taskId: string;
170
172
  agent: RosterAgent;
171
- /** Container the task runs in (real adapters `docker exec` into it). */
172
- containerName: string;
173
+ /** Provider-owned process/session surface for this task environment. */
174
+ environment: TaskEnvironmentAgentSessionSurface;
173
175
  /** Initial briefing — project.defaultPrompt — sent on session start. */
174
176
  systemPreamble: string;
175
177
  /** Host-derived restricted execution policy (ADR-083). Never supplied by
@@ -179,8 +181,8 @@ export interface AgentSessionFactory {
179
181
  * supplies a new key so restart recovery replaces, rather than attaches,
180
182
  * a process launched with incompatible argv/preamble/tooling. */
181
183
  attachCompatibilityKey?: string;
182
- /** ADR-048: extra per-agent env for the `docker exec` (e.g. this agent's
183
- * own UAI_TASK_TOKEN so its `uai` CLI carries only its own permissions). */
184
+ /** ADR-048: extra per-agent process environment (e.g. this agent's own
185
+ * UAI_TASK_TOKEN so its `uai` CLI carries only its own permissions). */
184
186
  agentEnv?: Record<string, string>;
185
187
  }): Promise<AgentSession>;
186
188
  }