@runuai/host 0.9.147 → 0.9.149
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/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +312 -2
- package/images/standard/Dockerfile +44 -6
- package/images/standard/container/uai-init +85 -2
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/lib/agent-cli.ts +18 -1
- package/lib/agent.ts +99 -34
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/codex.ts +137 -13
- package/lib/agents/cursor.ts +15 -1
- package/lib/agents/durable-proc.ts +2 -0
- package/lib/agents/grok.ts +8 -0
- package/lib/agents/kimi.ts +1 -0
- package/lib/agents/machine-durable.ts +17 -0
- package/lib/agents/opencode.ts +1 -0
- package/lib/agents/proc.ts +6 -1
- package/lib/agents/session-state.ts +107 -0
- package/lib/agents/transport.ts +125 -58
- package/lib/agents/types.ts +3 -3
- package/lib/browser-testing.ts +6 -0
- package/lib/codex-auth.ts +2 -0
- package/lib/credential-store.ts +141 -6
- package/lib/engine-accounts.ts +1 -0
- package/lib/env-file.ts +7 -0
- package/lib/env-schema.ts +841 -51
- package/lib/git-diff.ts +1 -0
- package/lib/git-identity.ts +2 -0
- package/lib/github-tokens.ts +1 -0
- package/lib/host-env.ts +98 -18
- package/lib/lease.ts +11 -3
- package/lib/machine-gateway-tunnel.ts +195 -17
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/managed-runtime.ts +270 -51
- package/lib/mcp-gateway.ts +777 -26
- package/lib/orchestrator.ts +1681 -138
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-loader.ts +237 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/runtime-archive.ts +79 -16
- package/lib/runtime-authority.ts +44 -0
- package/lib/runtime-state.ts +175 -10
- package/lib/secrets-proxy/address.ts +72 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +60 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +141 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +574 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/shared-files-s3.ts +3 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +51 -3
- package/lib/standard-image.ts +50 -12
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +193 -0
- package/lib/task-environment/apple-container.ts +45 -11
- package/lib/task-environment/docker.ts +51 -12
- package/lib/task-environment/index.ts +6 -2
- package/lib/task-environment/machine-task-up.ts +258 -25
- package/lib/task-environment/machine.ts +80 -22
- package/lib/task-environment/registry.ts +3 -2
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +92 -16
- package/lib/task-environment/types.ts +63 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +44 -0
- package/lib/task-gateway-state.ts +497 -0
- package/lib/transcript.ts +1 -0
- package/lib/ulid.ts +10 -1
- package/package.json +13 -1
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/task-up.sh +476 -27
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/src/cli.ts +26 -3
- package/src/index.ts +273 -19
- package/src/main.ts +941 -48
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +281 -0
- package/src/project-env-wording.ts +47 -0
- package/src/protocol.ts +2239 -15
package/lib/agent.ts
CHANGED
|
@@ -32,14 +32,24 @@ import { containerRuntimeStandardAssetsRoot } from "./container-runtime";
|
|
|
32
32
|
import { getDecryptedForProject } from "./host-env";
|
|
33
33
|
import { PreviewPortRuntimesSchema } from "./preview-ports";
|
|
34
34
|
import { getHostTask } from "./runtime-state";
|
|
35
|
+
import { activeSecretsProxy } from "./secrets-proxy/host";
|
|
36
|
+
import { closeTaskUpTrustSteps, finishTaskUpTrustSteps, writeTaskUpTrustSteps } from "./secrets-proxy/task-trust";
|
|
35
37
|
import { removeTaskIdentity, writeTaskIdentity } from "./ssh";
|
|
36
|
-
import
|
|
38
|
+
import { taskGatewayHost } from "./mcp-gateway";
|
|
39
|
+
import type { ProjectEnvMode } from "./task-environment/types";
|
|
40
|
+
import { isProjectEnvSchemaObservation, type ProjectEnvSchemaObservation, type TaskDownInput, type TaskLaunchInput } from "../src/protocol";
|
|
37
41
|
|
|
38
42
|
export type { TaskLaunchInput } from "../src/protocol";
|
|
39
43
|
|
|
40
44
|
export interface TaskUpCredentials {
|
|
41
45
|
/** Path to the task owner's private, ephemeral Git credential cache. */
|
|
42
46
|
githubCredentialSocket?: string;
|
|
47
|
+
/** Fresh ADR-153 generation credential. The raw bytes are forwarded only
|
|
48
|
+
* over stdin and are never serialized into a task row, argv, or env. */
|
|
49
|
+
taskGateway?: {
|
|
50
|
+
generation: string;
|
|
51
|
+
secret: Uint8Array;
|
|
52
|
+
};
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
// ---------------------------------------------------------------------------
|
|
@@ -86,6 +96,13 @@ const TaskUpData = z.object({
|
|
|
86
96
|
previewPorts: PreviewPortRuntimesSchema.optional(),
|
|
87
97
|
/** Degraded start: uai-init failed twice; surfaced as a feed system note. */
|
|
88
98
|
initWarning: z.string().optional(),
|
|
99
|
+
/** ADR-153: whether this start put the secrets proxy's CA, and its pins,
|
|
100
|
+
* into the task. Absent from a task-up that predates them. */
|
|
101
|
+
secretsProxyTrust: z.enum(["installed", "failed", "skipped"]).optional(),
|
|
102
|
+
secretsProxyPins: z.enum(["installed", "failed", "skipped"]).optional(),
|
|
103
|
+
/** ADR-153 §4: each project's schema observation, as the runner stored it
|
|
104
|
+
* before anything in the task started (a task machine reports its own). */
|
|
105
|
+
envSchemas: z.array(z.custom<ProjectEnvSchemaObservation>(isProjectEnvSchemaObservation)).optional(),
|
|
89
106
|
});
|
|
90
107
|
export type TaskUpResult = z.infer<typeof TaskUpData>;
|
|
91
108
|
|
|
@@ -303,46 +320,94 @@ export const agent = {
|
|
|
303
320
|
async taskUp(
|
|
304
321
|
input: TaskLaunchInput,
|
|
305
322
|
credentials: TaskUpCredentials = {},
|
|
323
|
+
envMode: ProjectEnvMode = "baked",
|
|
306
324
|
): Promise<TaskUpResult> {
|
|
307
325
|
const commandDbPath = createTaskUpCommandDb(input);
|
|
308
326
|
// Materialize the creator's per-user SSH key for commit signing and the
|
|
309
327
|
// explicit no-GitHub-connection transport fallback (ADR-029).
|
|
310
328
|
const identityDir = writeTaskIdentity(input.task.id, input.task.ownerUserId);
|
|
311
|
-
// Per-(project, key) env values must NEVER enter task-up's host process
|
|
312
|
-
// environment. Besides ordinary app names, projects can declare names such
|
|
313
|
-
// as BASH_ENV, PATH, GIT_CONFIG_*, or UAI_*; exposing those before the host
|
|
314
|
-
// clone would let container-owned configuration influence host commands or
|
|
315
|
-
// read another credential boundary. Send one JSON object over stdin
|
|
316
|
-
// instead. task-up feeds it directly to Compose as an in-memory override,
|
|
317
|
-
// so values reach only the task container and are never written to disk.
|
|
318
|
-
// On a key collision the LEAD project (position 0) wins: iterate in
|
|
319
|
-
// descending position order so position-0 assignments apply last.
|
|
320
|
-
const projectEnv: Record<string, string> = {};
|
|
321
|
-
const orderedProjects = [...input.projects].sort(
|
|
322
|
-
(a, b) => b.position - a.position,
|
|
323
|
-
);
|
|
324
|
-
for (const project of orderedProjects) {
|
|
325
|
-
Object.assign(projectEnv, getDecryptedForProject(project.id));
|
|
326
|
-
}
|
|
327
|
-
const extraEnv: Record<string, string> = {};
|
|
328
|
-
if (identityDir) extraEnv.UAI_TASK_IDENTITY_DIR = identityDir;
|
|
329
|
-
// Only a non-secret socket path crosses into Bash. The token itself was
|
|
330
|
-
// seeded over stdin and never enters argv, env, project data, Compose, or
|
|
331
|
-
// the task container.
|
|
332
|
-
if (credentials.githubCredentialSocket) {
|
|
333
|
-
extraEnv.UAI_GITHUB_CREDENTIAL_SOCKET =
|
|
334
|
-
credentials.githubCredentialSocket;
|
|
335
|
-
}
|
|
336
329
|
try {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
)
|
|
330
|
+
// Per-(project, key) env values must NEVER enter task-up's host process
|
|
331
|
+
// environment. Besides ordinary app names, projects can declare names such
|
|
332
|
+
// as BASH_ENV, PATH, GIT_CONFIG_*, or UAI_*; exposing those before the host
|
|
333
|
+
// clone would let container-owned configuration influence host commands or
|
|
334
|
+
// read another credential boundary. Send one JSON object over stdin
|
|
335
|
+
// instead. task-up feeds it directly to Compose as an in-memory override,
|
|
336
|
+
// so values reach only the task container and are never written to disk.
|
|
337
|
+
// On a key collision the LEAD project (position 0) wins: iterate in
|
|
338
|
+
// descending position order so position-0 assignments apply last.
|
|
339
|
+
const projectEnv: Record<string, string> = {};
|
|
340
|
+
if (envMode === "baked") {
|
|
341
|
+
const orderedProjects = [...input.projects].sort(
|
|
342
|
+
(a, b) => b.position - a.position,
|
|
343
|
+
);
|
|
344
|
+
for (const project of orderedProjects) {
|
|
345
|
+
Object.assign(projectEnv, getDecryptedForProject(project.id));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const extraEnv: Record<string, string> = {
|
|
349
|
+
UAI_PROJECT_ENV_MODE: envMode,
|
|
350
|
+
};
|
|
351
|
+
if (identityDir) extraEnv.UAI_TASK_IDENTITY_DIR = identityDir;
|
|
352
|
+
// Only a non-secret socket path crosses into Bash. The token itself was
|
|
353
|
+
// seeded over stdin and never enters argv, env, project data, Compose, or
|
|
354
|
+
// the task container.
|
|
355
|
+
if (credentials.githubCredentialSocket) {
|
|
356
|
+
extraEnv.UAI_GITHUB_CREDENTIAL_SOCKET =
|
|
357
|
+
credentials.githubCredentialSocket;
|
|
358
|
+
}
|
|
359
|
+
let stdinPayload: string;
|
|
360
|
+
if (envMode === "loader") {
|
|
361
|
+
const gateway = credentials.taskGateway;
|
|
362
|
+
if (!gateway || gateway.secret.byteLength !== 32) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
"loader task is missing its gateway generation credential",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
extraEnv.UAI_TASK_GATEWAY_GENERATION = gateway.generation;
|
|
368
|
+
extraEnv.UAI_TASK_GATEWAY_HOST = await taskGatewayHost();
|
|
369
|
+
// ADR-153: what task-up installs before anything runs in the task.
|
|
370
|
+
// Public material only; absent under the kill switch.
|
|
371
|
+
// The start holds the task's root writes until its result is
|
|
372
|
+
// recorded, so no older writer (a recovery, a re-pin) lands over
|
|
373
|
+
// them while uai-init runs.
|
|
374
|
+
const secretsProxy = activeSecretsProxy();
|
|
375
|
+
if (secretsProxy !== null) {
|
|
376
|
+
// task-up reports each step's outcome into it as it goes, so the
|
|
377
|
+
// task's readiness is exact even while uai-init already runs.
|
|
378
|
+
const stepsDir = await writeTaskUpTrustSteps(env.dataDir, input.task.id, secretsProxy.runtime);
|
|
379
|
+
if (stepsDir !== null) extraEnv.UAI_SECRETS_PROXY_DIR = stepsDir;
|
|
380
|
+
}
|
|
381
|
+
const secretCopy = Buffer.from(gateway.secret);
|
|
382
|
+
try {
|
|
383
|
+
stdinPayload = secretCopy.toString("base64");
|
|
384
|
+
} finally {
|
|
385
|
+
secretCopy.fill(0);
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
stdinPayload = JSON.stringify(projectEnv);
|
|
389
|
+
}
|
|
390
|
+
let result: TaskUpResult;
|
|
391
|
+
try {
|
|
392
|
+
result = await runAgent(
|
|
393
|
+
"task-up.sh",
|
|
394
|
+
[input.task.id],
|
|
395
|
+
TaskUpData,
|
|
396
|
+
commandDbPath,
|
|
397
|
+
extraEnv,
|
|
398
|
+
stdinPayload,
|
|
399
|
+
);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
finishTaskUpTrustSteps(env.dataDir, input.task.id, null);
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
// What this start actually installed. A start that did not get the CA
|
|
405
|
+
// in is retried by the trust watcher once the task runs.
|
|
406
|
+
finishTaskUpTrustSteps(env.dataDir, input.task.id, result);
|
|
407
|
+
if (result.secretsProxyTrust === "failed") activeSecretsProxy()?.runtime.requestTrustDelivery();
|
|
408
|
+
return result;
|
|
345
409
|
} finally {
|
|
410
|
+
closeTaskUpTrustSteps(env.dataDir, input.task.id);
|
|
346
411
|
removeCommandDb(commandDbPath);
|
|
347
412
|
removeTaskIdentity(input.task.id);
|
|
348
413
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility for the v0.9.147 -> v0.9.148 durable-session key rollout.
|
|
3
|
+
*
|
|
4
|
+
* Older hosts could not receive live roster edits and persisted only the
|
|
5
|
+
* execution-policy key: null for crew, or one of the Secretary policy
|
|
6
|
+
* constants. v0.9.148 adds kind/model/effort to that key. Treat the old row as
|
|
7
|
+
* compatible with the richer desired key only when its policy is unchanged;
|
|
8
|
+
* the transition is deliberately one-way so future/unknown keys remain exact.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const LEGACY_SECRETARY_POLICY_KEYS = new Set([
|
|
12
|
+
"secretary-normal-tools-v1",
|
|
13
|
+
"secretary-communicator-v1",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function rosterLaunchPolicy(key: string | null): string | null | undefined {
|
|
17
|
+
if (key === null) return undefined;
|
|
18
|
+
try {
|
|
19
|
+
const parsed: unknown = JSON.parse(key);
|
|
20
|
+
if (
|
|
21
|
+
!Array.isArray(parsed) ||
|
|
22
|
+
parsed.length !== 5 ||
|
|
23
|
+
parsed[0] !== "roster-launch-v1" ||
|
|
24
|
+
typeof parsed[1] !== "string" ||
|
|
25
|
+
typeof parsed[2] !== "string" ||
|
|
26
|
+
typeof parsed[3] !== "string" ||
|
|
27
|
+
typeof parsed[4] !== "string"
|
|
28
|
+
) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return parsed[4] === "" ? null : parsed[4];
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function attachCompatibilityKeysMatch(
|
|
38
|
+
stored: string | null | undefined,
|
|
39
|
+
desired: string | null | undefined,
|
|
40
|
+
): boolean {
|
|
41
|
+
const storedKey = stored ?? null;
|
|
42
|
+
const desiredKey = desired ?? null;
|
|
43
|
+
if (storedKey === desiredKey) return true;
|
|
44
|
+
|
|
45
|
+
const desiredPolicy = rosterLaunchPolicy(desiredKey);
|
|
46
|
+
if (storedKey === null) return desiredPolicy === null;
|
|
47
|
+
return (
|
|
48
|
+
LEGACY_SECRETARY_POLICY_KEYS.has(storedKey) &&
|
|
49
|
+
desiredPolicy === storedKey
|
|
50
|
+
);
|
|
51
|
+
}
|
package/lib/agents/codex.ts
CHANGED
|
@@ -32,12 +32,13 @@ import { existsSync } from "node:fs";
|
|
|
32
32
|
import { homedir } from "node:os";
|
|
33
33
|
import { join } from "node:path";
|
|
34
34
|
|
|
35
|
-
import { and, eq } from "drizzle-orm";
|
|
35
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
36
36
|
|
|
37
37
|
import { getDb, schema } from "../db";
|
|
38
38
|
import { requireContainerRuntimeOperational } from "../runtime-guard";
|
|
39
39
|
import type { TaskEnvironmentAgentSessionSurface } from "../task-environment/types";
|
|
40
40
|
import { newId } from "../ulid";
|
|
41
|
+
import { attachCompatibilityKeysMatch } from "./attach-compatibility";
|
|
41
42
|
import { createAgentTransport, type LineTransport } from "./transport";
|
|
42
43
|
import { isRateLimitMessage } from "./rate-limit";
|
|
43
44
|
import { register } from "./registry";
|
|
@@ -233,11 +234,21 @@ const delay = (ms: number): Promise<void> =>
|
|
|
233
234
|
// Thread persistence (ADR-078) — resume the SAME thread across a host restart.
|
|
234
235
|
// ---------------------------------------------------------------------------
|
|
235
236
|
|
|
236
|
-
/** The Codex thread id persisted for (task, agent), or null
|
|
237
|
-
function readCodexThreadId(
|
|
237
|
+
/** The compatible Codex thread id persisted for (task, agent), or null. */
|
|
238
|
+
function readCodexThreadId(
|
|
239
|
+
taskId: string,
|
|
240
|
+
agentId: string,
|
|
241
|
+
attachCompatibilityKey: string | undefined,
|
|
242
|
+
): string | null {
|
|
238
243
|
try {
|
|
239
244
|
const row = getDb()
|
|
240
|
-
.select({
|
|
245
|
+
.select({
|
|
246
|
+
id: schema.hostAgentSessions.codexThreadId,
|
|
247
|
+
kind: schema.hostAgentSessions.kind,
|
|
248
|
+
sessionDir: schema.hostAgentSessions.sessionDir,
|
|
249
|
+
attachCompatibilityKey:
|
|
250
|
+
schema.hostAgentSessions.attachCompatibilityKey,
|
|
251
|
+
})
|
|
241
252
|
.from(schema.hostAgentSessions)
|
|
242
253
|
.where(
|
|
243
254
|
and(
|
|
@@ -246,6 +257,31 @@ function readCodexThreadId(taskId: string, agentId: string): string | null {
|
|
|
246
257
|
),
|
|
247
258
|
)
|
|
248
259
|
.get();
|
|
260
|
+
if (
|
|
261
|
+
row?.kind !== "codex" ||
|
|
262
|
+
!attachCompatibilityKeysMatch(
|
|
263
|
+
row.attachCompatibilityKey,
|
|
264
|
+
attachCompatibilityKey,
|
|
265
|
+
)
|
|
266
|
+
) {
|
|
267
|
+
// Transport will promote the row to the new launch key while preserving
|
|
268
|
+
// codexThreadId. Clear the incompatible id first so a crash between that
|
|
269
|
+
// upsert and thread/started cannot make the next host resume it.
|
|
270
|
+
if (row?.id && row.sessionDir) {
|
|
271
|
+
getDb()
|
|
272
|
+
.update(schema.hostAgentSessions)
|
|
273
|
+
.set({ codexThreadId: null, updatedAt: Date.now() })
|
|
274
|
+
.where(
|
|
275
|
+
and(
|
|
276
|
+
eq(schema.hostAgentSessions.taskId, taskId),
|
|
277
|
+
eq(schema.hostAgentSessions.agentId, agentId),
|
|
278
|
+
eq(schema.hostAgentSessions.sessionDir, row.sessionDir),
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
.run();
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
249
285
|
return row?.id ?? null;
|
|
250
286
|
} catch {
|
|
251
287
|
return null;
|
|
@@ -258,15 +294,57 @@ function readCodexThreadId(taskId: string, agentId: string): string | null {
|
|
|
258
294
|
* thread (today's behavior). The transport's session-row upsert never clears
|
|
259
295
|
* this column, so it survives the kill+respawn of a host restart.
|
|
260
296
|
*/
|
|
261
|
-
function saveCodexThreadId(
|
|
297
|
+
function saveCodexThreadId(
|
|
298
|
+
taskId: string,
|
|
299
|
+
agentId: string,
|
|
300
|
+
threadId: string,
|
|
301
|
+
sessionDir: string | undefined,
|
|
302
|
+
attachCompatibilityKey: string | undefined,
|
|
303
|
+
): void {
|
|
304
|
+
if (!sessionDir) return;
|
|
262
305
|
try {
|
|
263
|
-
getDb()
|
|
306
|
+
const db = getDb();
|
|
307
|
+
const row = db
|
|
308
|
+
.select({
|
|
309
|
+
kind: schema.hostAgentSessions.kind,
|
|
310
|
+
sessionDir: schema.hostAgentSessions.sessionDir,
|
|
311
|
+
attachCompatibilityKey:
|
|
312
|
+
schema.hostAgentSessions.attachCompatibilityKey,
|
|
313
|
+
})
|
|
314
|
+
.from(schema.hostAgentSessions)
|
|
315
|
+
.where(
|
|
316
|
+
and(
|
|
317
|
+
eq(schema.hostAgentSessions.taskId, taskId),
|
|
318
|
+
eq(schema.hostAgentSessions.agentId, agentId),
|
|
319
|
+
),
|
|
320
|
+
)
|
|
321
|
+
.get();
|
|
322
|
+
const persistedKey = row?.attachCompatibilityKey ?? null;
|
|
323
|
+
if (
|
|
324
|
+
row?.kind !== "codex" ||
|
|
325
|
+
row.sessionDir !== sessionDir ||
|
|
326
|
+
!attachCompatibilityKeysMatch(
|
|
327
|
+
row.attachCompatibilityKey,
|
|
328
|
+
attachCompatibilityKey,
|
|
329
|
+
)
|
|
330
|
+
) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
db
|
|
264
334
|
.update(schema.hostAgentSessions)
|
|
265
335
|
.set({ codexThreadId: threadId, updatedAt: Date.now() })
|
|
266
336
|
.where(
|
|
267
337
|
and(
|
|
268
338
|
eq(schema.hostAgentSessions.taskId, taskId),
|
|
269
339
|
eq(schema.hostAgentSessions.agentId, agentId),
|
|
340
|
+
eq(schema.hostAgentSessions.kind, "codex"),
|
|
341
|
+
eq(schema.hostAgentSessions.sessionDir, sessionDir),
|
|
342
|
+
persistedKey === null
|
|
343
|
+
? isNull(schema.hostAgentSessions.attachCompatibilityKey)
|
|
344
|
+
: eq(
|
|
345
|
+
schema.hostAgentSessions.attachCompatibilityKey,
|
|
346
|
+
persistedKey,
|
|
347
|
+
),
|
|
270
348
|
),
|
|
271
349
|
)
|
|
272
350
|
.run();
|
|
@@ -302,6 +380,7 @@ export class CodexSession implements AgentSession {
|
|
|
302
380
|
|
|
303
381
|
private threadId: string | null = null;
|
|
304
382
|
private readonly ready: Promise<void>;
|
|
383
|
+
private readonly attachCompatibilityKey: string | undefined;
|
|
305
384
|
|
|
306
385
|
constructor(args: {
|
|
307
386
|
taskId: string;
|
|
@@ -309,15 +388,21 @@ export class CodexSession implements AgentSession {
|
|
|
309
388
|
environment: TaskEnvironmentAgentSessionSurface;
|
|
310
389
|
systemPreamble: string;
|
|
311
390
|
agentEnv?: Record<string, string>;
|
|
391
|
+
attachCompatibilityKey?: string;
|
|
312
392
|
/** ADR-128: `-c key=value` overrides a delegating adapter prepends. */
|
|
313
393
|
cliConfigOverrides?: string[];
|
|
314
394
|
}) {
|
|
315
395
|
this.agentId = args.agent.id;
|
|
316
396
|
this.systemPreamble = args.systemPreamble;
|
|
317
397
|
this.taskId = args.taskId;
|
|
398
|
+
this.attachCompatibilityKey = args.attachCompatibilityKey;
|
|
318
399
|
// ADR-078: read BEFORE createAgentTransport (whose session-row upsert
|
|
319
400
|
// preserves this column). Non-null → a host restart; resume the thread.
|
|
320
|
-
this.resumeThreadId = readCodexThreadId(
|
|
401
|
+
this.resumeThreadId = readCodexThreadId(
|
|
402
|
+
args.taskId,
|
|
403
|
+
args.agent.id,
|
|
404
|
+
args.attachCompatibilityKey,
|
|
405
|
+
);
|
|
321
406
|
|
|
322
407
|
// The agent's model / effort (when set) are selected via config overrides
|
|
323
408
|
// (`-c model=<model>`, `-c model_reasoning_effort=<effort>`) on the
|
|
@@ -333,7 +418,15 @@ export class CodexSession implements AgentSession {
|
|
|
333
418
|
for (const override of args.cliConfigOverrides ?? []) {
|
|
334
419
|
codexArgs.push("-c", override);
|
|
335
420
|
}
|
|
336
|
-
|
|
421
|
+
// These are two independent features in Codex 0.157. Append after every
|
|
422
|
+
// user override so task env can never be frozen into an on-disk snapshot.
|
|
423
|
+
codexArgs.push(
|
|
424
|
+
"--disable",
|
|
425
|
+
"shell_snapshot",
|
|
426
|
+
"--disable",
|
|
427
|
+
"shell_snapshot_v2",
|
|
428
|
+
"app-server",
|
|
429
|
+
);
|
|
337
430
|
// ADR-061: the CLI is owned by an in-container runner (durable across
|
|
338
431
|
// host restarts); legacy pipes behind UAI_DURABLE_SESSIONS=0. Codex
|
|
339
432
|
// re-handshakes per host process (initialize + thread/start live in this
|
|
@@ -349,6 +442,7 @@ export class CodexSession implements AgentSession {
|
|
|
349
442
|
passEnv: [],
|
|
350
443
|
explicitEnv: args.agentEnv ?? {},
|
|
351
444
|
allowAttach: false,
|
|
445
|
+
attachCompatibilityKey: args.attachCompatibilityKey,
|
|
352
446
|
kind: "codex",
|
|
353
447
|
debugLabel: `codex:${this.agentId}`,
|
|
354
448
|
});
|
|
@@ -387,6 +481,10 @@ export class CodexSession implements AgentSession {
|
|
|
387
481
|
// -- JSON-RPC plumbing ----------------------------------------------------
|
|
388
482
|
|
|
389
483
|
private onLine(line: string): void {
|
|
484
|
+
// close() severs this generation before the durable runner necessarily
|
|
485
|
+
// exits. Ignore buffered notifications so a late thread/started cannot
|
|
486
|
+
// restore the resume id that a live roster edit just cleared.
|
|
487
|
+
if (this.closed) return;
|
|
390
488
|
let msg: unknown;
|
|
391
489
|
try {
|
|
392
490
|
msg = JSON.parse(line);
|
|
@@ -439,7 +537,7 @@ export class CodexSession implements AgentSession {
|
|
|
439
537
|
const thread = msg.params.thread;
|
|
440
538
|
if (isObj(thread) && typeof thread.id === "string") {
|
|
441
539
|
this.threadId = thread.id;
|
|
442
|
-
|
|
540
|
+
this.persistThreadId(thread.id);
|
|
443
541
|
}
|
|
444
542
|
}
|
|
445
543
|
for (const ev of mapCodexNotification(method, msg.params)) {
|
|
@@ -568,7 +666,7 @@ export class CodexSession implements AgentSession {
|
|
|
568
666
|
if (isObj(result) && isObj(result.thread)) {
|
|
569
667
|
const id = str(result.thread.id) || this.threadId;
|
|
570
668
|
this.threadId = id;
|
|
571
|
-
if (id)
|
|
669
|
+
if (id) this.persistThreadId(id);
|
|
572
670
|
}
|
|
573
671
|
}
|
|
574
672
|
|
|
@@ -594,7 +692,7 @@ export class CodexSession implements AgentSession {
|
|
|
594
692
|
? str(result.thread.id)
|
|
595
693
|
: threadId;
|
|
596
694
|
this.threadId = id;
|
|
597
|
-
|
|
695
|
+
this.persistThreadId(id);
|
|
598
696
|
return true;
|
|
599
697
|
} catch (err) {
|
|
600
698
|
const code = (err as { rpcCode?: number }).rpcCode;
|
|
@@ -615,6 +713,16 @@ export class CodexSession implements AgentSession {
|
|
|
615
713
|
|
|
616
714
|
// -- AgentSession ---------------------------------------------------------
|
|
617
715
|
|
|
716
|
+
private persistThreadId(threadId: string): void {
|
|
717
|
+
saveCodexThreadId(
|
|
718
|
+
this.taskId,
|
|
719
|
+
this.agentId,
|
|
720
|
+
threadId,
|
|
721
|
+
this.proc.durableSessionDir,
|
|
722
|
+
this.attachCompatibilityKey,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
|
|
618
726
|
onEvent(handler: AgentEventHandler): () => void {
|
|
619
727
|
this.handlers.add(handler);
|
|
620
728
|
return () => this.handlers.delete(handler);
|
|
@@ -704,6 +812,22 @@ register({
|
|
|
704
812
|
// provider block rides it, and this destructuring silently dropped it on
|
|
705
813
|
// the first live gpt-5-mini smoke (2026-09-12, host 0.9.136 — codex still
|
|
706
814
|
// dialed api.openai.com).
|
|
707
|
-
create: async ({
|
|
708
|
-
|
|
815
|
+
create: async ({
|
|
816
|
+
taskId,
|
|
817
|
+
agent,
|
|
818
|
+
environment,
|
|
819
|
+
systemPreamble,
|
|
820
|
+
agentEnv,
|
|
821
|
+
attachCompatibilityKey,
|
|
822
|
+
cliConfigOverrides,
|
|
823
|
+
}) =>
|
|
824
|
+
new CodexSession({
|
|
825
|
+
taskId,
|
|
826
|
+
agent,
|
|
827
|
+
environment,
|
|
828
|
+
systemPreamble,
|
|
829
|
+
agentEnv,
|
|
830
|
+
attachCompatibilityKey,
|
|
831
|
+
cliConfigOverrides,
|
|
832
|
+
}),
|
|
709
833
|
});
|
package/lib/agents/cursor.ts
CHANGED
|
@@ -50,6 +50,13 @@ import type {
|
|
|
50
50
|
|
|
51
51
|
const CURSOR_BIN = "/home/node/.local/bin/cursor-agent";
|
|
52
52
|
|
|
53
|
+
// Cursor replays its captured sandbox environment immediately before it
|
|
54
|
+
// evaluates each shell command. In loader tasks this restore hook is the last
|
|
55
|
+
// step in that replay, so the command sees the current project environment
|
|
56
|
+
// rather than Cursor's stale launch snapshot.
|
|
57
|
+
export const CURSOR_SANDBOX_ENV_RESTORE =
|
|
58
|
+
". /usr/local/share/uai/env-hook.sh; __uai_env_sync";
|
|
59
|
+
|
|
53
60
|
// A curated slice of Cursor's ~32 models (full ids from `cursor-agent
|
|
54
61
|
// --list-models`). `auto` lets Cursor pick; the rest are the frontier. The UI
|
|
55
62
|
// also accepts any typed id (unverified round-trip). UPDATE as Cursor's lineup
|
|
@@ -233,8 +240,15 @@ export class CursorSession implements AgentSession {
|
|
|
233
240
|
if (this.sessionId) argv.push("--resume", this.sessionId);
|
|
234
241
|
return {
|
|
235
242
|
argv: argv as [string, ...string[]],
|
|
243
|
+
projectEnvProfile: "task",
|
|
236
244
|
user: "node",
|
|
237
|
-
env: {
|
|
245
|
+
env: {
|
|
246
|
+
...this.agentEnv,
|
|
247
|
+
CURSOR_API_KEY: this.apiKey,
|
|
248
|
+
...(this.environment.descriptor.envMode === "loader"
|
|
249
|
+
? { __CURSOR_SANDBOX_ENV_RESTORE: CURSOR_SANDBOX_ENV_RESTORE }
|
|
250
|
+
: {}),
|
|
251
|
+
},
|
|
238
252
|
maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
|
239
253
|
};
|
|
240
254
|
}
|
|
@@ -72,6 +72,7 @@ interface RunnerMeta {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export class DurableProcess {
|
|
75
|
+
readonly durableSessionDir: string;
|
|
75
76
|
private readonly dir: string;
|
|
76
77
|
private readonly outboxPath: string;
|
|
77
78
|
private readonly inboxPath: string;
|
|
@@ -95,6 +96,7 @@ export class DurableProcess {
|
|
|
95
96
|
private readonly debug: string | null;
|
|
96
97
|
|
|
97
98
|
constructor(opts: DurableProcessOptions) {
|
|
99
|
+
this.durableSessionDir = opts.hostSessionDir;
|
|
98
100
|
this.dir = opts.hostSessionDir;
|
|
99
101
|
this.outboxPath = join(this.dir, "outbox.jsonl");
|
|
100
102
|
this.inboxPath = join(this.dir, "inbox.jsonl");
|
package/lib/agents/grok.ts
CHANGED
|
@@ -52,6 +52,7 @@ import type {
|
|
|
52
52
|
} from "./types";
|
|
53
53
|
|
|
54
54
|
const GROK_BIN = "/home/node/.grok/bin/grok";
|
|
55
|
+
export const GROK_LOADER_LOGIN_ENV = "0";
|
|
55
56
|
|
|
56
57
|
// `grok models` lists what the account can run. grok-4.5 is the current
|
|
57
58
|
// default; UPDATE WHEN xAI CHANGES the lineup.
|
|
@@ -181,6 +182,7 @@ export class GrokSession implements AgentSession {
|
|
|
181
182
|
if (this.sessionId) argv.push("-r", this.sessionId);
|
|
182
183
|
return {
|
|
183
184
|
argv: argv as [string, ...string[]],
|
|
185
|
+
projectEnvProfile: "task",
|
|
184
186
|
user: "node",
|
|
185
187
|
inheritEnv,
|
|
186
188
|
env: {
|
|
@@ -193,6 +195,12 @@ export class GrokSession implements AgentSession {
|
|
|
193
195
|
// the same reasoning as --permission-mode bypassPermissions above
|
|
194
196
|
// (live 2026-09-08, first Grok agents with MCP servers).
|
|
195
197
|
GROK_FOLDER_TRUST: "0",
|
|
198
|
+
// grok 1.0.5 otherwise captures a login-shell environment once and
|
|
199
|
+
// reuses that static state for LocalNonPersistent commands. Loader
|
|
200
|
+
// tasks refresh per command, so disable that stale snapshot path.
|
|
201
|
+
...(this.environment.descriptor.envMode === "loader"
|
|
202
|
+
? { GROK_LOGIN_ENV: GROK_LOADER_LOGIN_ENV }
|
|
203
|
+
: {}),
|
|
196
204
|
},
|
|
197
205
|
maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
|
198
206
|
};
|
package/lib/agents/kimi.ts
CHANGED
|
@@ -223,6 +223,7 @@ export class KimiSession implements AgentSession {
|
|
|
223
223
|
if (this.sessionId) argv.push("-r", this.sessionId);
|
|
224
224
|
return {
|
|
225
225
|
argv: argv as [string, ...string[]],
|
|
226
|
+
projectEnvProfile: "task",
|
|
226
227
|
user: "node",
|
|
227
228
|
env: this.agentEnv,
|
|
228
229
|
maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
|
@@ -59,6 +59,7 @@ async function execStep(
|
|
|
59
59
|
): Promise<{ exitCode: number | null; stdout: string; stderr: string }> {
|
|
60
60
|
const result = await environment.exec({
|
|
61
61
|
argv,
|
|
62
|
+
projectEnvProfile: "maintenance",
|
|
62
63
|
...(stdin ? { stdin } : {}),
|
|
63
64
|
timeoutMs: STEP_TIMEOUT_MS,
|
|
64
65
|
maxOutputBytes: STEP_OUTPUT_BYTES,
|
|
@@ -129,6 +130,7 @@ export interface MachineDurableOptions {
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
export class MachineDurableProcess implements LineTransport {
|
|
133
|
+
readonly durableSessionDir: string;
|
|
132
134
|
readonly #environment: MachineSessionEnvironment;
|
|
133
135
|
readonly #sessionDir: string;
|
|
134
136
|
readonly #outboxPath: string;
|
|
@@ -158,6 +160,7 @@ export class MachineDurableProcess implements LineTransport {
|
|
|
158
160
|
readonly #debug: string | null;
|
|
159
161
|
|
|
160
162
|
constructor(opts: MachineDurableOptions) {
|
|
163
|
+
this.durableSessionDir = opts.sessionDir;
|
|
161
164
|
this.#environment = opts.environment;
|
|
162
165
|
this.#sessionDir = opts.sessionDir;
|
|
163
166
|
this.#outboxPath = posix.join(opts.sessionDir, "outbox.jsonl");
|
|
@@ -226,6 +229,7 @@ export class MachineDurableProcess implements LineTransport {
|
|
|
226
229
|
String(this.#offset + 1),
|
|
227
230
|
this.#outboxPath,
|
|
228
231
|
],
|
|
232
|
+
projectEnvProfile: "maintenance",
|
|
229
233
|
maxOutputBytes: 8 * 1024 * 1024,
|
|
230
234
|
});
|
|
231
235
|
} catch {
|
|
@@ -563,3 +567,16 @@ export async function stopMachinePredecessor(
|
|
|
563
567
|
// Dir gone / runner dead — nothing to stop.
|
|
564
568
|
}
|
|
565
569
|
}
|
|
570
|
+
|
|
571
|
+
/** Removal/recovery variant: failure is not success because no later attach
|
|
572
|
+
* will provide the transport layer's usual predecessor-stop backstop. */
|
|
573
|
+
export async function stopMachinePredecessorStrict(
|
|
574
|
+
environment: MachineSessionEnvironment,
|
|
575
|
+
sessionDir: string,
|
|
576
|
+
): Promise<void> {
|
|
577
|
+
await appendEnvironmentFile(
|
|
578
|
+
environment,
|
|
579
|
+
posix.join(sessionDir, "inbox.jsonl"),
|
|
580
|
+
'{"__uai":"stop"}\n',
|
|
581
|
+
);
|
|
582
|
+
}
|
package/lib/agents/opencode.ts
CHANGED
|
@@ -357,6 +357,7 @@ export class OpencodeSession implements AgentSession {
|
|
|
357
357
|
argv.push(prompt);
|
|
358
358
|
return {
|
|
359
359
|
argv: argv as [string, ...string[]],
|
|
360
|
+
projectEnvProfile: "task",
|
|
360
361
|
user: "node",
|
|
361
362
|
env: this.agentEnv,
|
|
362
363
|
maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
package/lib/agents/proc.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TaskEnvironmentProcess,
|
|
8
8
|
TaskEnvironmentSessionRequest,
|
|
9
9
|
} from "../task-environment/types";
|
|
10
|
+
import { agentCliArgv } from "../agent-cli";
|
|
10
11
|
|
|
11
12
|
export type LineHandler = (line: string) => void;
|
|
12
13
|
export type ExitHandler = (code: number | null) => void;
|
|
@@ -168,6 +169,10 @@ export function spawnEnvironmentProcess(
|
|
|
168
169
|
environment: TaskEnvironmentAgentSessionSurface,
|
|
169
170
|
request: TaskEnvironmentSessionRequest,
|
|
170
171
|
): AgentEnvironmentProcess {
|
|
172
|
+
const launchRequest: TaskEnvironmentSessionRequest = {
|
|
173
|
+
...request,
|
|
174
|
+
argv: agentCliArgv(environment, request.argv),
|
|
175
|
+
};
|
|
171
176
|
class EnvironmentProcess extends EventEmitter implements AgentEnvironmentProcess {
|
|
172
177
|
readonly stdout = new EventEmitter();
|
|
173
178
|
readonly stderr = new EventEmitter();
|
|
@@ -176,7 +181,7 @@ export function spawnEnvironmentProcess(
|
|
|
176
181
|
|
|
177
182
|
constructor() {
|
|
178
183
|
super();
|
|
179
|
-
this.processPromise = environment.spawnSession(
|
|
184
|
+
this.processPromise = environment.spawnSession(launchRequest);
|
|
180
185
|
void this.consume();
|
|
181
186
|
}
|
|
182
187
|
|