@runuai/host 0.9.70 → 0.9.72
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/lib/agent-cli.ts +36 -0
- package/lib/agents/machine-durable.ts +492 -0
- package/lib/agents/transport.ts +186 -2
- package/lib/orchestrator.ts +49 -4
- package/lib/runtime-state.ts +6 -0
- package/lib/task-environment/index.ts +64 -2
- package/lib/task-environment/legacy-adoption.ts +6 -0
- package/lib/task-environment/machine-task-up.ts +333 -0
- package/lib/task-environment/machine.ts +50 -19
- package/lib/transcript.ts +83 -5
- package/package.json +1 -1
- package/src/index.ts +117 -11
package/lib/agents/transport.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import {
|
|
25
25
|
copyFileSync,
|
|
26
|
+
readFileSync,
|
|
26
27
|
mkdirSync,
|
|
27
28
|
renameSync,
|
|
28
29
|
rmSync,
|
|
@@ -39,6 +40,14 @@ import { taskWorkspaceDir } from "../env";
|
|
|
39
40
|
import { requireContainerRuntimeOperational } from "../runtime-guard";
|
|
40
41
|
import type { TaskEnvironmentAgentSessionSurface } from "../task-environment/types";
|
|
41
42
|
import { DurableProcess, runnerScriptPath } from "./durable-proc";
|
|
43
|
+
import {
|
|
44
|
+
MachineDurableProcess,
|
|
45
|
+
deployRunner,
|
|
46
|
+
machineSessionDir,
|
|
47
|
+
publishMachineCurrentSession,
|
|
48
|
+
stopMachinePredecessor,
|
|
49
|
+
type MachineSessionEnvironment,
|
|
50
|
+
} from "./machine-durable";
|
|
42
51
|
import {
|
|
43
52
|
AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
|
44
53
|
EnvironmentLineProcess,
|
|
@@ -93,9 +102,13 @@ const ATTACH_HEARTBEAT_FRESH_MS = 20_000;
|
|
|
93
102
|
* the feed, multiplicity growing by one per errored turn. Enforcing the
|
|
94
103
|
* invariant here covers every replacement path, including future ones.
|
|
95
104
|
*/
|
|
96
|
-
|
|
105
|
+
interface DetachableTail extends LineTransport {
|
|
106
|
+
detach(): void;
|
|
107
|
+
}
|
|
97
108
|
|
|
98
|
-
|
|
109
|
+
const liveTails = new Map<string, DetachableTail>();
|
|
110
|
+
|
|
111
|
+
function claimTail<T extends DetachableTail>(key: string, proc: T): T {
|
|
99
112
|
liveTails.get(key)?.detach();
|
|
100
113
|
liveTails.set(key, proc);
|
|
101
114
|
return proc;
|
|
@@ -106,6 +119,17 @@ function durableEnabled(): boolean {
|
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
export function createAgentTransport(opts: AgentTransportOptions): LineTransport {
|
|
122
|
+
// ADR-121: machine-backed sessions ride the environment's own transport —
|
|
123
|
+
// the container-runtime preflight is a docker/apple concern a machine task
|
|
124
|
+
// must not trip over, and the host-FS durable flow below polls files a
|
|
125
|
+
// machine does not share. Machine durability uses the ssh-tail backend.
|
|
126
|
+
if (opts.environment.descriptor.locator.provider === "machine") {
|
|
127
|
+
if (!durableEnabled()) {
|
|
128
|
+
clearCurrentSession(opts.taskId, opts.agentId);
|
|
129
|
+
return directEnvironmentTransport(opts);
|
|
130
|
+
}
|
|
131
|
+
return machineDurableTransport(opts);
|
|
132
|
+
}
|
|
109
133
|
// Session creation can happen after channel/task lifecycle queues drain, well
|
|
110
134
|
// after the command-level runtime preflight. Recheck at the actual attach or
|
|
111
135
|
// spawn boundary so a cached ready verdict cannot launch container work.
|
|
@@ -277,6 +301,166 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
277
301
|
return claimTail(tailKey, proc);
|
|
278
302
|
}
|
|
279
303
|
|
|
304
|
+
/**
|
|
305
|
+
* ADR-121: durable machine sessions. Same DB row + attach/replace semantics
|
|
306
|
+
* as the container flow; the session files live in the MACHINE's workspace
|
|
307
|
+
* and are reached over the environment transport (MachineDurableProcess).
|
|
308
|
+
* Degrades to direct pipes when the handle lacks the wider ops or the runner
|
|
309
|
+
* asset is missing — sessions work, they just don't survive host restarts.
|
|
310
|
+
*/
|
|
311
|
+
function machineDurableTransport(opts: AgentTransportOptions): LineTransport {
|
|
312
|
+
const surface = opts.environment;
|
|
313
|
+
const capable =
|
|
314
|
+
"exec" in surface && "spawnSession" in surface && "writeWorkspaceFile" in surface;
|
|
315
|
+
let runnerSource: Buffer | null = null;
|
|
316
|
+
if (capable) {
|
|
317
|
+
try {
|
|
318
|
+
runnerSource = readFileSync(runnerScriptPath());
|
|
319
|
+
} catch (err) {
|
|
320
|
+
console.warn(
|
|
321
|
+
`[transport] runner unavailable (${err instanceof Error ? err.message : err}) — falling back to direct pipes for ${opts.agentId}`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!capable || runnerSource === null) {
|
|
326
|
+
clearCurrentSession(opts.taskId, opts.agentId);
|
|
327
|
+
return directEnvironmentTransport(opts);
|
|
328
|
+
}
|
|
329
|
+
const environment = surface as unknown as MachineSessionEnvironment &
|
|
330
|
+
TaskEnvironmentAgentSessionSurface;
|
|
331
|
+
|
|
332
|
+
const db = getDb();
|
|
333
|
+
const row = db
|
|
334
|
+
.select()
|
|
335
|
+
.from(schema.hostAgentSessions)
|
|
336
|
+
.where(
|
|
337
|
+
and(
|
|
338
|
+
eq(schema.hostAgentSessions.taskId, opts.taskId),
|
|
339
|
+
eq(schema.hostAgentSessions.agentId, opts.agentId),
|
|
340
|
+
),
|
|
341
|
+
)
|
|
342
|
+
.get();
|
|
343
|
+
const persistOffset = (offset: number): void => {
|
|
344
|
+
db.update(schema.hostAgentSessions)
|
|
345
|
+
.set({ outboxOffset: offset, updatedAt: Date.now() })
|
|
346
|
+
.where(
|
|
347
|
+
and(
|
|
348
|
+
eq(schema.hostAgentSessions.taskId, opts.taskId),
|
|
349
|
+
eq(schema.hostAgentSessions.agentId, opts.agentId),
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
.run();
|
|
353
|
+
};
|
|
354
|
+
const markClosed = (): void => {
|
|
355
|
+
db.update(schema.hostAgentSessions)
|
|
356
|
+
.set({ status: "closed", updatedAt: Date.now() })
|
|
357
|
+
.where(
|
|
358
|
+
and(
|
|
359
|
+
eq(schema.hostAgentSessions.taskId, opts.taskId),
|
|
360
|
+
eq(schema.hostAgentSessions.agentId, opts.agentId),
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
.run();
|
|
364
|
+
};
|
|
365
|
+
const tailKey = `${opts.taskId}:${opts.agentId}`;
|
|
366
|
+
|
|
367
|
+
// ---- Attach: a previous host process left this machine's runner alive. --
|
|
368
|
+
// Freshness cannot be a sync stat over ssh; MachineDurableProcess probes
|
|
369
|
+
// the heartbeat immediately on attach and finishes fast when it is stale.
|
|
370
|
+
if (
|
|
371
|
+
opts.allowAttach &&
|
|
372
|
+
row &&
|
|
373
|
+
row.status === "running" &&
|
|
374
|
+
row.containerName === environment.durableIdentity &&
|
|
375
|
+
(row.attachCompatibilityKey ?? null) ===
|
|
376
|
+
(opts.attachCompatibilityKey ?? null)
|
|
377
|
+
) {
|
|
378
|
+
const proc = new MachineDurableProcess({
|
|
379
|
+
environment,
|
|
380
|
+
sessionDir: row.sessionDir,
|
|
381
|
+
initialOutboxOffset: row.outboxOffset,
|
|
382
|
+
onOffsetAdvance: persistOffset,
|
|
383
|
+
onCloseRequested: markClosed,
|
|
384
|
+
debugLabel: opts.debugLabel,
|
|
385
|
+
});
|
|
386
|
+
proc.onExit(markClosed);
|
|
387
|
+
return claimTail(tailKey, proc);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ---- Spawn a fresh runner, asking any predecessor to stop. --------------
|
|
391
|
+
const staleMachineSession =
|
|
392
|
+
row && row.containerName === environment.durableIdentity
|
|
393
|
+
? row.sessionDir
|
|
394
|
+
: null;
|
|
395
|
+
const sessionDir = machineSessionDir(
|
|
396
|
+
environment.descriptor.workspacePath,
|
|
397
|
+
opts.agentId,
|
|
398
|
+
);
|
|
399
|
+
const proc = new MachineDurableProcess({
|
|
400
|
+
environment,
|
|
401
|
+
sessionDir,
|
|
402
|
+
onOffsetAdvance: persistOffset,
|
|
403
|
+
onCloseRequested: markClosed,
|
|
404
|
+
debugLabel: opts.debugLabel,
|
|
405
|
+
launch: async () => {
|
|
406
|
+
// Unconditional stop of a same-machine predecessor (see the container
|
|
407
|
+
// flow): a runner falsely marked closed may still be alive, and a stop
|
|
408
|
+
// appended to a dead session's inbox is harmless.
|
|
409
|
+
if (staleMachineSession) {
|
|
410
|
+
await stopMachinePredecessor(environment, staleMachineSession);
|
|
411
|
+
}
|
|
412
|
+
const runnerPath = await deployRunner(environment, sessionDir);
|
|
413
|
+
await publishMachineCurrentSession(environment, sessionDir);
|
|
414
|
+
const result = await environment.launchDetachedSession({
|
|
415
|
+
argv: [
|
|
416
|
+
"node",
|
|
417
|
+
runnerPath,
|
|
418
|
+
sessionDir,
|
|
419
|
+
"--",
|
|
420
|
+
opts.cli,
|
|
421
|
+
...opts.cliArgs,
|
|
422
|
+
],
|
|
423
|
+
inheritEnv: opts.passEnv ?? [],
|
|
424
|
+
env: opts.explicitEnv ?? {},
|
|
425
|
+
launchTimeoutMs: 30_000,
|
|
426
|
+
maxOutputBytes: 256 * 1024,
|
|
427
|
+
});
|
|
428
|
+
return { exitCode: result.exitCode, stderr: result.stderr };
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const now = Date.now();
|
|
433
|
+
db.insert(schema.hostAgentSessions)
|
|
434
|
+
.values({
|
|
435
|
+
taskId: opts.taskId,
|
|
436
|
+
agentId: opts.agentId,
|
|
437
|
+
sessionDir,
|
|
438
|
+
containerName: environment.durableIdentity,
|
|
439
|
+
kind: opts.kind,
|
|
440
|
+
attachCompatibilityKey: opts.attachCompatibilityKey ?? null,
|
|
441
|
+
outboxOffset: 0,
|
|
442
|
+
status: "running",
|
|
443
|
+
createdAt: now,
|
|
444
|
+
updatedAt: now,
|
|
445
|
+
})
|
|
446
|
+
.onConflictDoUpdate({
|
|
447
|
+
target: [schema.hostAgentSessions.taskId, schema.hostAgentSessions.agentId],
|
|
448
|
+
set: {
|
|
449
|
+
sessionDir,
|
|
450
|
+
containerName: environment.durableIdentity,
|
|
451
|
+
kind: opts.kind,
|
|
452
|
+
attachCompatibilityKey: opts.attachCompatibilityKey ?? null,
|
|
453
|
+
outboxOffset: 0,
|
|
454
|
+
status: "running",
|
|
455
|
+
updatedAt: now,
|
|
456
|
+
},
|
|
457
|
+
})
|
|
458
|
+
.run();
|
|
459
|
+
|
|
460
|
+
proc.onExit(markClosed);
|
|
461
|
+
return claimTail(tailKey, proc);
|
|
462
|
+
}
|
|
463
|
+
|
|
280
464
|
function directEnvironmentTransport(opts: AgentTransportOptions): LineTransport {
|
|
281
465
|
return new EnvironmentLineProcess({
|
|
282
466
|
environment: opts.environment,
|
package/lib/orchestrator.ts
CHANGED
|
@@ -96,6 +96,7 @@ import {
|
|
|
96
96
|
apiUrlFromCloudUrl,
|
|
97
97
|
loadTaskCliSecret,
|
|
98
98
|
writeAgentCli,
|
|
99
|
+
writeAgentCliViaEnvironment,
|
|
99
100
|
} from "./agent-cli";
|
|
100
101
|
import {
|
|
101
102
|
DEFAULT_CODEX_HOME,
|
|
@@ -1370,6 +1371,13 @@ export class Orchestrator {
|
|
|
1370
1371
|
|
|
1371
1372
|
for (const agent of missing) channel.spawning.add(agent.id);
|
|
1372
1373
|
try {
|
|
1374
|
+
// ADR-121: machine workspaces are only reachable through the
|
|
1375
|
+
// environment handle — the docker-exec cold-start default would
|
|
1376
|
+
// dial a container that does not exist. Settle the handle first.
|
|
1377
|
+
if (task.environmentProvider === "machine") {
|
|
1378
|
+
await this.agentEnvironment(channel).catch(() => {});
|
|
1379
|
+
if (!this.isActiveChannel(channel)) return;
|
|
1380
|
+
}
|
|
1373
1381
|
// Same per-agent materialisation the initial start does. Browser setup
|
|
1374
1382
|
// is awaited before EVERY missing-session spawn: that reasserts configs
|
|
1375
1383
|
// clobbered by resume/auth injection and covers roster generation.
|
|
@@ -1402,8 +1410,8 @@ export class Orchestrator {
|
|
|
1402
1410
|
|
|
1403
1411
|
const apiUrl = apiUrlFromCloudUrl(env.UAI_CLOUD_URL);
|
|
1404
1412
|
const cliSecret = loadTaskCliSecret(channel.taskId);
|
|
1405
|
-
const cliWritten =
|
|
1406
|
-
|
|
1413
|
+
const cliWritten = await materializeAgentCli(
|
|
1414
|
+
task,
|
|
1407
1415
|
roster,
|
|
1408
1416
|
apiUrl,
|
|
1409
1417
|
channel.mode === "secretary",
|
|
@@ -1698,6 +1706,14 @@ export class Orchestrator {
|
|
|
1698
1706
|
// browser/MCP configuration, or runner creation can execute in Docker.
|
|
1699
1707
|
await agentClisReady;
|
|
1700
1708
|
if (!this.isActiveChannel(channel)) return false;
|
|
1709
|
+
// ADR-121: machine workspaces are only reachable through the environment
|
|
1710
|
+
// handle. Settle it before any materialization step below, so identity,
|
|
1711
|
+
// skills, browser, and MCP writes all ride the provider surface instead
|
|
1712
|
+
// of dialing a compose container that does not exist.
|
|
1713
|
+
if (task.environmentProvider === "machine") {
|
|
1714
|
+
await this.agentEnvironment(channel).catch(() => {});
|
|
1715
|
+
if (!this.isActiveChannel(channel)) return false;
|
|
1716
|
+
}
|
|
1701
1717
|
// Freeze the generation before any slow docker work. A roster add while
|
|
1702
1718
|
// setup awaits must reconcile under its own engine-aware browser pass,
|
|
1703
1719
|
// not slip into this factory loop under the old generation's config.
|
|
@@ -1777,8 +1793,8 @@ export class Orchestrator {
|
|
|
1777
1793
|
// are actually enforced. Best-effort, host-side.
|
|
1778
1794
|
const apiUrl = apiUrlFromCloudUrl(env.UAI_CLOUD_URL);
|
|
1779
1795
|
const cliSecret = loadTaskCliSecret(channel.taskId);
|
|
1780
|
-
const cliWritten =
|
|
1781
|
-
|
|
1796
|
+
const cliWritten = await materializeAgentCli(
|
|
1797
|
+
task,
|
|
1782
1798
|
channel.roster,
|
|
1783
1799
|
apiUrl,
|
|
1784
1800
|
channel.mode === "secretary",
|
|
@@ -5619,6 +5635,35 @@ async function quarantineWritableAppleRuntimeContainers(options: {
|
|
|
5619
5635
|
|
|
5620
5636
|
/** Dispatch one durable row through its provider. Generic boot orchestration
|
|
5621
5637
|
* owns DB/lifecycle sequencing but never derives a Compose/container identity. */
|
|
5638
|
+
/**
|
|
5639
|
+
* ADR-121: materialize the in-task uai CLI through whichever surface owns the
|
|
5640
|
+
* workspace — host FS for container tasks, the environment transport for
|
|
5641
|
+
* machine tasks (their workspace is not host-reachable). Best-effort like the
|
|
5642
|
+
* host-side writer.
|
|
5643
|
+
*/
|
|
5644
|
+
async function materializeAgentCli(
|
|
5645
|
+
task: typeof schema.hostTasks.$inferSelect,
|
|
5646
|
+
roster: RosterAgent[],
|
|
5647
|
+
apiUrl: string | null,
|
|
5648
|
+
allowPermissionless: boolean,
|
|
5649
|
+
): Promise<boolean> {
|
|
5650
|
+
if (task.environmentProvider === "machine") {
|
|
5651
|
+
try {
|
|
5652
|
+
const environment = await reconstructPersistedTaskEnvironment(task);
|
|
5653
|
+
if (environment === null) return false;
|
|
5654
|
+
return await writeAgentCliViaEnvironment(
|
|
5655
|
+
environment,
|
|
5656
|
+
roster,
|
|
5657
|
+
apiUrl,
|
|
5658
|
+
allowPermissionless,
|
|
5659
|
+
);
|
|
5660
|
+
} catch {
|
|
5661
|
+
return false;
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
return writeAgentCli(task.taskId, roster, apiUrl, allowPermissionless);
|
|
5665
|
+
}
|
|
5666
|
+
|
|
5622
5667
|
async function recoverPersistedTaskEnvironment(
|
|
5623
5668
|
task: typeof schema.hostTasks.$inferSelect,
|
|
5624
5669
|
maintenanceReady: Promise<void>,
|
package/lib/runtime-state.ts
CHANGED
|
@@ -31,6 +31,11 @@ export function getHostTask(taskId: string): HostTask | null {
|
|
|
31
31
|
|
|
32
32
|
/** Remove a provisional first-seen row when admission loses a lifecycle gate. */
|
|
33
33
|
export function deleteHostTask(taskId: string): void {
|
|
34
|
+
// Row deletions are rare and load-bearing; a silent one cost a live
|
|
35
|
+
// machine-task forensic session (2026-08-27). Name the caller.
|
|
36
|
+
console.warn(
|
|
37
|
+
`[runtime-state] deleting host task row ${taskId}\n${(new Error().stack ?? "").split("\n").slice(2, 5).join("\n")}`,
|
|
38
|
+
);
|
|
34
39
|
getDb()
|
|
35
40
|
.delete(schema.hostTasks)
|
|
36
41
|
.where(eq(schema.hostTasks.taskId, taskId))
|
|
@@ -41,6 +46,7 @@ export function deleteHostTask(taskId: string): void {
|
|
|
41
46
|
* happen before this transaction; a SQLite failure leaves every task-owned
|
|
42
47
|
* row intact rather than acknowledging a partial metadata deletion. */
|
|
43
48
|
export function purgeHostTaskState(taskId: string): void {
|
|
49
|
+
console.warn(`[runtime-state] purging all host state for task ${taskId}`);
|
|
44
50
|
getDb().transaction((tx) => {
|
|
45
51
|
tx.delete(schema.hostAgentSessions)
|
|
46
52
|
.where(eq(schema.hostAgentSessions.taskId, taskId))
|
|
@@ -22,6 +22,15 @@ import {
|
|
|
22
22
|
type DockerTaskEnvironmentRecoveryDriver,
|
|
23
23
|
type DockerMachineIdentity,
|
|
24
24
|
} from "./docker";
|
|
25
|
+
import { createAwsMachineProvider } from "../machine-provider-aws";
|
|
26
|
+
import { createLocalMachineProvider } from "../machine-provider-local";
|
|
27
|
+
import { ensureMachineKeyPair } from "../machine-keys";
|
|
28
|
+
import { requestAccessToken } from "../github-tokens";
|
|
29
|
+
import {
|
|
30
|
+
createMachineTaskEnvironmentProvider,
|
|
31
|
+
MACHINE_TASK_ENVIRONMENT_PROVIDER,
|
|
32
|
+
} from "./machine";
|
|
33
|
+
import { createMachineHostTaskEnvironmentProvider } from "./machine-task-up";
|
|
25
34
|
import { TaskEnvironmentRegistry } from "./registry";
|
|
26
35
|
import {
|
|
27
36
|
parseTaskEnvironmentLocator,
|
|
@@ -113,6 +122,53 @@ const appleProvider = createAppleContainerTaskEnvironmentProvider<
|
|
|
113
122
|
|
|
114
123
|
registry.register(appleProvider);
|
|
115
124
|
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// ADR-121: machine-backed tasks (ephemeral VM per task). Opt-in via
|
|
127
|
+
// UAI_MACHINE_TASKS=1 — the guinea-pig flag; selection stays coarse (whole
|
|
128
|
+
// host) until machine-backing becomes a per-task property.
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
function machineTasksEnabled(): boolean {
|
|
132
|
+
return process.env.UAI_MACHINE_TASKS === "1";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function machineBackend() {
|
|
136
|
+
if (process.env.UAI_MACHINE_PROVIDER === "aws") {
|
|
137
|
+
const region = process.env.UAI_AWS_REGION;
|
|
138
|
+
if (!region) {
|
|
139
|
+
throw new Error("UAI_MACHINE_PROVIDER=aws requires UAI_AWS_REGION");
|
|
140
|
+
}
|
|
141
|
+
return createAwsMachineProvider({
|
|
142
|
+
region,
|
|
143
|
+
subnetId: process.env.UAI_AWS_SUBNET_ID,
|
|
144
|
+
securityGroupId: process.env.UAI_AWS_SECURITY_GROUP_ID,
|
|
145
|
+
iamInstanceProfileArn: process.env.UAI_AWS_INSTANCE_PROFILE_ARN,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return createLocalMachineProvider();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const machineEnvironmentProvider = createMachineTaskEnvironmentProvider({
|
|
152
|
+
machines: machineBackend(),
|
|
153
|
+
taskControlDir: (taskId) => taskDir(taskId),
|
|
154
|
+
mintKeyPair: ensureMachineKeyPair,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const machineProvider = createMachineHostTaskEnvironmentProvider({
|
|
158
|
+
environment: machineEnvironmentProvider,
|
|
159
|
+
taskControlDir: (taskId) => taskDir(taskId),
|
|
160
|
+
machineImage: () => process.env.UAI_MACHINE_IMAGE ?? "uai-machine:dev",
|
|
161
|
+
machineCpus: () => Number(process.env.UAI_MACHINE_CPUS ?? "2"),
|
|
162
|
+
machineMemoryMiB: () => Number(process.env.UAI_MACHINE_MEMORY_MIB ?? "4096"),
|
|
163
|
+
// Local machines on macOS need loopback-published ssh (bridge IPs are not
|
|
164
|
+
// host-routable there); cloud machines and Linux dial the address directly.
|
|
165
|
+
publishSsh: () =>
|
|
166
|
+
process.env.UAI_MACHINE_PROVIDER !== "aws" && process.platform === "darwin",
|
|
167
|
+
githubToken: (userId) => requestAccessToken(userId),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
registry.register(machineProvider);
|
|
171
|
+
|
|
116
172
|
/**
|
|
117
173
|
* Bind the Docker provider's private recovery implementation. Kept as a
|
|
118
174
|
* replaceable module seam so HMR can reload the orchestrator without leaving
|
|
@@ -129,8 +185,9 @@ export function provisionTaskEnvironment(
|
|
|
129
185
|
credentials: TaskUpCredentials = {},
|
|
130
186
|
onPrepared?: (locator: TaskEnvironmentLocator) => Promise<void>,
|
|
131
187
|
): Promise<TaskEnvironmentProvisioned<TaskUpResult, TaskDownResult>> {
|
|
132
|
-
const provider =
|
|
133
|
-
|
|
188
|
+
const provider = machineTasksEnabled()
|
|
189
|
+
? machineProvider
|
|
190
|
+
: requireMachineIdentity().backend === "apple-container"
|
|
134
191
|
? appleProvider
|
|
135
192
|
: dockerProvider;
|
|
136
193
|
return provider.provision({
|
|
@@ -253,6 +310,9 @@ export function persistedTaskEnvironmentsMatchMachine(
|
|
|
253
310
|
};
|
|
254
311
|
}
|
|
255
312
|
if (locator === null) continue;
|
|
313
|
+
// ADR-121: machine locators name an ephemeral VM, not a container
|
|
314
|
+
// daemon — no container-runtime identity claim to verify.
|
|
315
|
+
if (locator.provider === MACHINE_TASK_ENVIRONMENT_PROVIDER) continue;
|
|
256
316
|
try {
|
|
257
317
|
const persisted = locatorMachineIdentity(locator);
|
|
258
318
|
if (
|
|
@@ -321,6 +381,8 @@ export function hostTaskEnvironmentsAllowMachineSelection(
|
|
|
321
381
|
if (locator.provider !== task.environmentProvider) {
|
|
322
382
|
throw new Error("task environment provider does not match its locator");
|
|
323
383
|
}
|
|
384
|
+
// ADR-121: machine rows make no container-runtime claim.
|
|
385
|
+
if (locator.provider === MACHINE_TASK_ENVIRONMENT_PROVIDER) continue;
|
|
324
386
|
const persisted = locatorMachineIdentity(locator);
|
|
325
387
|
if (
|
|
326
388
|
persisted.backend !== machine.backend ||
|
|
@@ -166,6 +166,12 @@ function legacyCandidate(
|
|
|
166
166
|
// 2026-08-15): a crash-preserved prepared apple locator made adoption —
|
|
167
167
|
// and with it runtime selection — fail closed, so the task could not
|
|
168
168
|
// even be settled.
|
|
169
|
+
// ADR-121: machine-backed rows are not container-runtime residents at
|
|
170
|
+
// all — their locator names an ephemeral VM, not a docker/apple daemon.
|
|
171
|
+
// They must never wedge container-runtime selection (live 2026-08-27:
|
|
172
|
+
// the first machine task quarantined the whole VM runtime on reconnect,
|
|
173
|
+
// collapsing every host command into a retry storm).
|
|
174
|
+
if (locator.provider === "machine") return null;
|
|
169
175
|
let persistedMachine: HostMachineIdentity;
|
|
170
176
|
if (locator.provider === "docker-compose") {
|
|
171
177
|
persistedMachine = parseDockerTaskEnvironmentLocator(locator).machine;
|