@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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable agent-session state used by live roster reconciliation.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator normally owns a live AgentSession and closes it through the
|
|
5
|
+
* adapter. After a host restart, however, a removed agent can exist only as a
|
|
6
|
+
* host_agent_sessions row. These helpers stop that persisted runner
|
|
7
|
+
* without deleting its row or generation directory, and clear Codex's durable
|
|
8
|
+
* thread id when launch settings change.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { promises as fsp } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { and, eq } from "drizzle-orm";
|
|
15
|
+
|
|
16
|
+
import { getDb, schema } from "../db";
|
|
17
|
+
import type { TaskEnvironmentHandle } from "../task-environment/types";
|
|
18
|
+
import { isEnvironmentTransportProvider } from "../task-environment/types";
|
|
19
|
+
import {
|
|
20
|
+
stopMachinePredecessorStrict,
|
|
21
|
+
type MachineSessionEnvironment,
|
|
22
|
+
} from "./machine-durable";
|
|
23
|
+
|
|
24
|
+
/** Durable-session ids persisted for one task, including rows marked closed.
|
|
25
|
+
* A crash can land after markClosed but before its stop reaches the runner. */
|
|
26
|
+
export function persistedAgentIds(taskId: string): string[] {
|
|
27
|
+
return getDb()
|
|
28
|
+
.select({ agentId: schema.hostAgentSessions.agentId })
|
|
29
|
+
.from(schema.hostAgentSessions)
|
|
30
|
+
.where(eq(schema.hostAgentSessions.taskId, taskId))
|
|
31
|
+
.all()
|
|
32
|
+
.map((row) => row.agentId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A changed model/engine/effort starts a fresh Codex thread. The channel
|
|
37
|
+
* transcript is the durable memory; resuming a thread created under the old
|
|
38
|
+
* launch settings can make the replacement keep using the old model.
|
|
39
|
+
*/
|
|
40
|
+
export function clearPersistedCodexThread(
|
|
41
|
+
taskId: string,
|
|
42
|
+
agentId: string,
|
|
43
|
+
): void {
|
|
44
|
+
getDb()
|
|
45
|
+
.update(schema.hostAgentSessions)
|
|
46
|
+
.set({ codexThreadId: null, updatedAt: Date.now() })
|
|
47
|
+
.where(
|
|
48
|
+
and(
|
|
49
|
+
eq(schema.hostAgentSessions.taskId, taskId),
|
|
50
|
+
eq(schema.hostAgentSessions.agentId, agentId),
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
.run();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Stop a persisted runner that has no in-memory AgentSession (boot recovery).
|
|
58
|
+
* The session directory and DB row deliberately remain for diagnostics.
|
|
59
|
+
*/
|
|
60
|
+
export async function stopPersistedAgentSession(
|
|
61
|
+
taskId: string,
|
|
62
|
+
agentId: string,
|
|
63
|
+
environment: TaskEnvironmentHandle<unknown>,
|
|
64
|
+
): Promise<string | null> {
|
|
65
|
+
const db = getDb();
|
|
66
|
+
const row = db
|
|
67
|
+
.select()
|
|
68
|
+
.from(schema.hostAgentSessions)
|
|
69
|
+
.where(
|
|
70
|
+
and(
|
|
71
|
+
eq(schema.hostAgentSessions.taskId, taskId),
|
|
72
|
+
eq(schema.hostAgentSessions.agentId, agentId),
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
.get();
|
|
76
|
+
if (!row) return null;
|
|
77
|
+
|
|
78
|
+
if (isEnvironmentTransportProvider(environment.descriptor.locator.provider)) {
|
|
79
|
+
await stopMachinePredecessorStrict(
|
|
80
|
+
environment as MachineSessionEnvironment,
|
|
81
|
+
row.sessionDir,
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
try {
|
|
85
|
+
await fsp.appendFile(
|
|
86
|
+
join(row.sessionDir, "inbox.jsonl"),
|
|
87
|
+
'{"__uai":"stop"}\n',
|
|
88
|
+
);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// A removed generation directory proves there is no runner left to read
|
|
91
|
+
// it. Permission, I/O, and other failures are not proof: retry them.
|
|
92
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
db.update(schema.hostAgentSessions)
|
|
97
|
+
.set({ status: "closed", updatedAt: Date.now() })
|
|
98
|
+
.where(
|
|
99
|
+
and(
|
|
100
|
+
eq(schema.hostAgentSessions.taskId, taskId),
|
|
101
|
+
eq(schema.hostAgentSessions.agentId, agentId),
|
|
102
|
+
eq(schema.hostAgentSessions.sessionDir, row.sessionDir),
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
.run();
|
|
106
|
+
return row.sessionDir;
|
|
107
|
+
}
|
package/lib/agents/transport.ts
CHANGED
|
@@ -33,9 +33,9 @@ import {
|
|
|
33
33
|
} from "node:fs";
|
|
34
34
|
import { basename, dirname, join } from "node:path";
|
|
35
35
|
|
|
36
|
-
import { and, eq } from "drizzle-orm";
|
|
36
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
37
37
|
|
|
38
|
-
import { getDb, schema } from "../db";
|
|
38
|
+
import { getDb, schema, type Db } from "../db";
|
|
39
39
|
import { taskWorkspaceDir } from "../env";
|
|
40
40
|
import { machineSessionAuthorityEnv } from "../runtime-authority";
|
|
41
41
|
import { requireContainerRuntimeOperational } from "../runtime-guard";
|
|
@@ -55,10 +55,14 @@ import {
|
|
|
55
55
|
type ExitHandler,
|
|
56
56
|
type LineHandler,
|
|
57
57
|
} from "./proc";
|
|
58
|
+
import { agentCliArgv } from "../agent-cli";
|
|
58
59
|
import { isEnvironmentTransportProvider } from "../task-environment/types";
|
|
60
|
+
import { attachCompatibilityKeysMatch } from "./attach-compatibility";
|
|
59
61
|
|
|
60
62
|
/** The shared surface adapters program against (LineProcess's shape). */
|
|
61
63
|
export interface LineTransport {
|
|
64
|
+
/** Exact durable row generation owned by this transport, when durable. */
|
|
65
|
+
readonly durableSessionDir?: string;
|
|
62
66
|
onLine(handler: LineHandler): void;
|
|
63
67
|
onExit(handler: ExitHandler): void;
|
|
64
68
|
writeLine(value: unknown): void;
|
|
@@ -84,7 +88,7 @@ export interface AgentTransportOptions {
|
|
|
84
88
|
* protocol state can't outlive the host process (codex handshake).
|
|
85
89
|
*/
|
|
86
90
|
allowAttach: boolean;
|
|
87
|
-
/** Host-owned launch-
|
|
91
|
+
/** Host-owned launch-settings generation. Both absent remains compatible. */
|
|
88
92
|
attachCompatibilityKey?: string;
|
|
89
93
|
kind: string;
|
|
90
94
|
debugLabel?: string;
|
|
@@ -120,6 +124,73 @@ function durableEnabled(): boolean {
|
|
|
120
124
|
return process.env.UAI_DURABLE_SESSIONS !== "0";
|
|
121
125
|
}
|
|
122
126
|
|
|
127
|
+
/** Durable rows are keyed by task + agent for lookup, but callbacks belong to
|
|
128
|
+
* one exact generation. A delayed offset/exit from the predecessor must never
|
|
129
|
+
* mutate the replacement row after its upsert. */
|
|
130
|
+
function durableRowCallbacks(
|
|
131
|
+
db: Db,
|
|
132
|
+
taskId: string,
|
|
133
|
+
agentId: string,
|
|
134
|
+
sessionDir: string,
|
|
135
|
+
): {
|
|
136
|
+
persistOffset(offset: number): void;
|
|
137
|
+
markClosed(): void;
|
|
138
|
+
} {
|
|
139
|
+
const ownedRow = () =>
|
|
140
|
+
and(
|
|
141
|
+
eq(schema.hostAgentSessions.taskId, taskId),
|
|
142
|
+
eq(schema.hostAgentSessions.agentId, agentId),
|
|
143
|
+
eq(schema.hostAgentSessions.sessionDir, sessionDir),
|
|
144
|
+
);
|
|
145
|
+
return {
|
|
146
|
+
persistOffset(offset: number): void {
|
|
147
|
+
db.update(schema.hostAgentSessions)
|
|
148
|
+
.set({ outboxOffset: offset, updatedAt: Date.now() })
|
|
149
|
+
.where(ownedRow())
|
|
150
|
+
.run();
|
|
151
|
+
},
|
|
152
|
+
markClosed(): void {
|
|
153
|
+
db.update(schema.hostAgentSessions)
|
|
154
|
+
.set({ status: "closed", updatedAt: Date.now() })
|
|
155
|
+
.where(ownedRow())
|
|
156
|
+
.run();
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Claim a compatible durable row and promote a one-release legacy key to the
|
|
162
|
+
* rich launch fingerprint. The exact generation/key fence prevents a stale
|
|
163
|
+
* host process from rewriting a replacement row after reading its snapshot. */
|
|
164
|
+
function claimCompatibleAttachRow(
|
|
165
|
+
db: Db,
|
|
166
|
+
taskId: string,
|
|
167
|
+
agentId: string,
|
|
168
|
+
sessionDir: string,
|
|
169
|
+
stored: string | null | undefined,
|
|
170
|
+
desired: string | null | undefined,
|
|
171
|
+
): boolean {
|
|
172
|
+
if (!attachCompatibilityKeysMatch(stored, desired)) return false;
|
|
173
|
+
const storedKey = stored ?? null;
|
|
174
|
+
const desiredKey = desired ?? null;
|
|
175
|
+
if (storedKey === desiredKey) return true;
|
|
176
|
+
if (desiredKey === null) return false;
|
|
177
|
+
const result = db
|
|
178
|
+
.update(schema.hostAgentSessions)
|
|
179
|
+
.set({ attachCompatibilityKey: desiredKey, updatedAt: Date.now() })
|
|
180
|
+
.where(
|
|
181
|
+
and(
|
|
182
|
+
eq(schema.hostAgentSessions.taskId, taskId),
|
|
183
|
+
eq(schema.hostAgentSessions.agentId, agentId),
|
|
184
|
+
eq(schema.hostAgentSessions.sessionDir, sessionDir),
|
|
185
|
+
storedKey === null
|
|
186
|
+
? isNull(schema.hostAgentSessions.attachCompatibilityKey)
|
|
187
|
+
: eq(schema.hostAgentSessions.attachCompatibilityKey, storedKey),
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
.run();
|
|
191
|
+
return result.changes === 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
123
194
|
export function createAgentTransport(opts: AgentTransportOptions): LineTransport {
|
|
124
195
|
// ADR-121: machine-backed sessions ride the environment's own transport —
|
|
125
196
|
// the container-runtime preflight is a docker/apple concern a machine task
|
|
@@ -161,29 +232,6 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
161
232
|
)
|
|
162
233
|
.get();
|
|
163
234
|
|
|
164
|
-
const persistOffset = (offset: number): void => {
|
|
165
|
-
db.update(schema.hostAgentSessions)
|
|
166
|
-
.set({ outboxOffset: offset, updatedAt: Date.now() })
|
|
167
|
-
.where(
|
|
168
|
-
and(
|
|
169
|
-
eq(schema.hostAgentSessions.taskId, opts.taskId),
|
|
170
|
-
eq(schema.hostAgentSessions.agentId, opts.agentId),
|
|
171
|
-
),
|
|
172
|
-
)
|
|
173
|
-
.run();
|
|
174
|
-
};
|
|
175
|
-
const markClosed = (): void => {
|
|
176
|
-
db.update(schema.hostAgentSessions)
|
|
177
|
-
.set({ status: "closed", updatedAt: Date.now() })
|
|
178
|
-
.where(
|
|
179
|
-
and(
|
|
180
|
-
eq(schema.hostAgentSessions.taskId, opts.taskId),
|
|
181
|
-
eq(schema.hostAgentSessions.agentId, opts.agentId),
|
|
182
|
-
),
|
|
183
|
-
)
|
|
184
|
-
.run();
|
|
185
|
-
};
|
|
186
|
-
|
|
187
235
|
const tailKey = `${opts.taskId}:${opts.agentId}`;
|
|
188
236
|
|
|
189
237
|
// ---- Attach: a previous host process left this agent's runner alive. ----
|
|
@@ -192,10 +240,23 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
192
240
|
row &&
|
|
193
241
|
row.status === "running" &&
|
|
194
242
|
row.containerName === opts.environment.durableIdentity &&
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
243
|
+
row.kind === opts.kind &&
|
|
244
|
+
heartbeatFresh(row.sessionDir) &&
|
|
245
|
+
claimCompatibleAttachRow(
|
|
246
|
+
db,
|
|
247
|
+
opts.taskId,
|
|
248
|
+
opts.agentId,
|
|
249
|
+
row.sessionDir,
|
|
250
|
+
row.attachCompatibilityKey,
|
|
251
|
+
opts.attachCompatibilityKey,
|
|
252
|
+
)
|
|
198
253
|
) {
|
|
254
|
+
const { persistOffset, markClosed } = durableRowCallbacks(
|
|
255
|
+
db,
|
|
256
|
+
opts.taskId,
|
|
257
|
+
opts.agentId,
|
|
258
|
+
row.sessionDir,
|
|
259
|
+
);
|
|
199
260
|
const proc = new DurableProcess({
|
|
200
261
|
hostSessionDir: row.sessionDir,
|
|
201
262
|
initialOutboxOffset: row.outboxOffset,
|
|
@@ -240,6 +301,12 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
240
301
|
opts.agentId,
|
|
241
302
|
basename(sessionDir),
|
|
242
303
|
);
|
|
304
|
+
const { persistOffset, markClosed } = durableRowCallbacks(
|
|
305
|
+
db,
|
|
306
|
+
opts.taskId,
|
|
307
|
+
opts.agentId,
|
|
308
|
+
sessionDir,
|
|
309
|
+
);
|
|
243
310
|
const runnerInEnvironment = join(environmentSessionDir, "runner.mjs");
|
|
244
311
|
try {
|
|
245
312
|
copyFileSync(runnerScriptPath(), runnerInSession);
|
|
@@ -260,15 +327,16 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
260
327
|
onCloseRequested: markClosed,
|
|
261
328
|
debugLabel: opts.debugLabel,
|
|
262
329
|
launch: async () => {
|
|
330
|
+
const cliArgv = agentCliArgv(opts.environment, [opts.cli, ...opts.cliArgs]);
|
|
263
331
|
const result = await opts.environment.launchDetachedSession({
|
|
264
332
|
argv: [
|
|
265
333
|
"node",
|
|
266
334
|
runnerInEnvironment,
|
|
267
335
|
environmentSessionDir,
|
|
268
336
|
"--",
|
|
269
|
-
|
|
270
|
-
...opts.cliArgs,
|
|
337
|
+
...cliArgv,
|
|
271
338
|
],
|
|
339
|
+
projectEnvProfile: "task",
|
|
272
340
|
inheritEnv: opts.passEnv ?? [],
|
|
273
341
|
env: opts.explicitEnv ?? {},
|
|
274
342
|
launchTimeoutMs: 30_000,
|
|
@@ -350,28 +418,6 @@ function machineDurableTransport(opts: AgentTransportOptions): LineTransport {
|
|
|
350
418
|
),
|
|
351
419
|
)
|
|
352
420
|
.get();
|
|
353
|
-
const persistOffset = (offset: number): void => {
|
|
354
|
-
db.update(schema.hostAgentSessions)
|
|
355
|
-
.set({ outboxOffset: offset, updatedAt: Date.now() })
|
|
356
|
-
.where(
|
|
357
|
-
and(
|
|
358
|
-
eq(schema.hostAgentSessions.taskId, opts.taskId),
|
|
359
|
-
eq(schema.hostAgentSessions.agentId, opts.agentId),
|
|
360
|
-
),
|
|
361
|
-
)
|
|
362
|
-
.run();
|
|
363
|
-
};
|
|
364
|
-
const markClosed = (): void => {
|
|
365
|
-
db.update(schema.hostAgentSessions)
|
|
366
|
-
.set({ status: "closed", updatedAt: Date.now() })
|
|
367
|
-
.where(
|
|
368
|
-
and(
|
|
369
|
-
eq(schema.hostAgentSessions.taskId, opts.taskId),
|
|
370
|
-
eq(schema.hostAgentSessions.agentId, opts.agentId),
|
|
371
|
-
),
|
|
372
|
-
)
|
|
373
|
-
.run();
|
|
374
|
-
};
|
|
375
421
|
const tailKey = `${opts.taskId}:${opts.agentId}`;
|
|
376
422
|
|
|
377
423
|
// ---- Attach: a previous host process left this machine's runner alive. --
|
|
@@ -382,9 +428,22 @@ function machineDurableTransport(opts: AgentTransportOptions): LineTransport {
|
|
|
382
428
|
row &&
|
|
383
429
|
row.status === "running" &&
|
|
384
430
|
row.containerName === environment.durableIdentity &&
|
|
385
|
-
|
|
386
|
-
|
|
431
|
+
row.kind === opts.kind &&
|
|
432
|
+
claimCompatibleAttachRow(
|
|
433
|
+
db,
|
|
434
|
+
opts.taskId,
|
|
435
|
+
opts.agentId,
|
|
436
|
+
row.sessionDir,
|
|
437
|
+
row.attachCompatibilityKey,
|
|
438
|
+
opts.attachCompatibilityKey,
|
|
439
|
+
)
|
|
387
440
|
) {
|
|
441
|
+
const { persistOffset, markClosed } = durableRowCallbacks(
|
|
442
|
+
db,
|
|
443
|
+
opts.taskId,
|
|
444
|
+
opts.agentId,
|
|
445
|
+
row.sessionDir,
|
|
446
|
+
);
|
|
388
447
|
const proc = new MachineDurableProcess({
|
|
389
448
|
environment,
|
|
390
449
|
sessionDir: row.sessionDir,
|
|
@@ -406,6 +465,12 @@ function machineDurableTransport(opts: AgentTransportOptions): LineTransport {
|
|
|
406
465
|
environment.descriptor.workspacePath,
|
|
407
466
|
opts.agentId,
|
|
408
467
|
);
|
|
468
|
+
const { persistOffset, markClosed } = durableRowCallbacks(
|
|
469
|
+
db,
|
|
470
|
+
opts.taskId,
|
|
471
|
+
opts.agentId,
|
|
472
|
+
sessionDir,
|
|
473
|
+
);
|
|
409
474
|
const proc = new MachineDurableProcess({
|
|
410
475
|
environment,
|
|
411
476
|
sessionDir,
|
|
@@ -421,15 +486,16 @@ function machineDurableTransport(opts: AgentTransportOptions): LineTransport {
|
|
|
421
486
|
}
|
|
422
487
|
const runnerPath = await deployRunner(environment, sessionDir);
|
|
423
488
|
await publishMachineCurrentSession(environment, sessionDir);
|
|
489
|
+
const cliArgv = agentCliArgv(environment, [opts.cli, ...opts.cliArgs]);
|
|
424
490
|
const result = await environment.launchDetachedSession({
|
|
425
491
|
argv: [
|
|
426
492
|
"node",
|
|
427
493
|
runnerPath,
|
|
428
494
|
sessionDir,
|
|
429
495
|
"--",
|
|
430
|
-
|
|
431
|
-
...opts.cliArgs,
|
|
496
|
+
...cliArgv,
|
|
432
497
|
],
|
|
498
|
+
projectEnvProfile: "task",
|
|
433
499
|
inheritEnv: opts.passEnv ?? [],
|
|
434
500
|
env: opts.explicitEnv ?? {},
|
|
435
501
|
launchTimeoutMs: 30_000,
|
|
@@ -475,7 +541,8 @@ function directEnvironmentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
475
541
|
return new EnvironmentLineProcess({
|
|
476
542
|
environment: opts.environment,
|
|
477
543
|
request: {
|
|
478
|
-
argv: [opts.cli, ...opts.cliArgs],
|
|
544
|
+
argv: agentCliArgv(opts.environment, [opts.cli, ...opts.cliArgs]),
|
|
545
|
+
projectEnvProfile: "task",
|
|
479
546
|
inheritEnv: opts.passEnv ?? [],
|
|
480
547
|
env: opts.explicitEnv ?? {},
|
|
481
548
|
maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
package/lib/agents/types.ts
CHANGED
|
@@ -180,9 +180,9 @@ export interface AgentSessionFactory {
|
|
|
180
180
|
/** Host-derived restricted execution policy (ADR-083). Never supplied by
|
|
181
181
|
* the browser or stored on the roster entry. */
|
|
182
182
|
executionProfile?: "communicator";
|
|
183
|
-
/** Durable runners are keyed by task+agent.
|
|
184
|
-
*
|
|
185
|
-
* a process launched with incompatible
|
|
183
|
+
/** Durable runners are keyed by task+agent. Host-owned launch settings
|
|
184
|
+
* supply a compatibility key so restart recovery replaces, rather than
|
|
185
|
+
* attaches, a process launched with incompatible model/effort/policy. */
|
|
186
186
|
attachCompatibilityKey?: string;
|
|
187
187
|
/** ADR-048: extra per-agent process environment (e.g. this agent's own
|
|
188
188
|
* UAI_TASK_TOKEN so its `uai` CLI carries only its own permissions). */
|
package/lib/browser-testing.ts
CHANGED
|
@@ -1157,6 +1157,7 @@ export async function setupBrowserTesting(
|
|
|
1157
1157
|
for (const step of VIEWER_STEPS) {
|
|
1158
1158
|
if (environment) {
|
|
1159
1159
|
await environment.launchDetachedSession({
|
|
1160
|
+
projectEnvProfile: "maintenance",
|
|
1160
1161
|
argv: ["sh", "-c", step],
|
|
1161
1162
|
env: authorityEnv,
|
|
1162
1163
|
maxOutputBytes: 64 * 1024,
|
|
@@ -1194,6 +1195,7 @@ export async function setupBrowserTesting(
|
|
|
1194
1195
|
const installed = environment
|
|
1195
1196
|
? await (async () => {
|
|
1196
1197
|
const chown = await environment.exec({
|
|
1198
|
+
projectEnvProfile: "maintenance",
|
|
1197
1199
|
argv: ["/usr/bin/chown", "node:node", "/opt/pw-browsers"],
|
|
1198
1200
|
user: "root",
|
|
1199
1201
|
env: authorityEnv,
|
|
@@ -1208,6 +1210,7 @@ export async function setupBrowserTesting(
|
|
|
1208
1210
|
);
|
|
1209
1211
|
}
|
|
1210
1212
|
return environment.exec({
|
|
1213
|
+
projectEnvProfile: "maintenance",
|
|
1211
1214
|
argv: ["sh", "-lc", PREWARM_CMD],
|
|
1212
1215
|
cwd: "/workspace",
|
|
1213
1216
|
env: authorityEnv,
|
|
@@ -1309,6 +1312,7 @@ export async function setupBrowserTesting(
|
|
|
1309
1312
|
] as [string, ...string[]];
|
|
1310
1313
|
const wrote = environment
|
|
1311
1314
|
? await environment.exec({
|
|
1315
|
+
projectEnvProfile: "maintenance",
|
|
1312
1316
|
argv: writeArgv,
|
|
1313
1317
|
cwd: "/workspace",
|
|
1314
1318
|
env: writeEnv,
|
|
@@ -1397,6 +1401,7 @@ export async function setupBrowserTesting(
|
|
|
1397
1401
|
] as [string, ...string[]];
|
|
1398
1402
|
const registered = environment
|
|
1399
1403
|
? await environment.exec({
|
|
1404
|
+
projectEnvProfile: "maintenance",
|
|
1400
1405
|
argv: grokArgv,
|
|
1401
1406
|
cwd: "/workspace",
|
|
1402
1407
|
env: authorityEnv,
|
|
@@ -1469,6 +1474,7 @@ export async function setupBrowserTesting(
|
|
|
1469
1474
|
};
|
|
1470
1475
|
if (environment) {
|
|
1471
1476
|
await environment.exec({
|
|
1477
|
+
projectEnvProfile: "maintenance",
|
|
1472
1478
|
argv: ["/bin/sh", "-c", guardedAptStart],
|
|
1473
1479
|
user: "root",
|
|
1474
1480
|
cwd: "/workspace",
|
package/lib/codex-auth.ts
CHANGED
|
@@ -261,6 +261,7 @@ export async function injectCodexIntoMachine(
|
|
|
261
261
|
environment: {
|
|
262
262
|
exec(request: {
|
|
263
263
|
argv: readonly [string, ...string[]];
|
|
264
|
+
projectEnvProfile: "maintenance" | "task";
|
|
264
265
|
timeoutMs: number;
|
|
265
266
|
maxOutputBytes: number;
|
|
266
267
|
}): Promise<{ exitCode: number | null; stderr: Uint8Array }>;
|
|
@@ -277,6 +278,7 @@ export async function injectCodexIntoMachine(
|
|
|
277
278
|
if (!exists(join(dir, "auth.json"))) return true; // nothing to inject
|
|
278
279
|
try {
|
|
279
280
|
const made = await environment.exec({
|
|
281
|
+
projectEnvProfile: "maintenance",
|
|
280
282
|
argv: ["/bin/mkdir", "-p", "/home/node/.codex"],
|
|
281
283
|
timeoutMs: EXEC_TIMEOUT_MS,
|
|
282
284
|
maxOutputBytes: 64 * 1024,
|
package/lib/credential-store.ts
CHANGED
|
@@ -36,12 +36,57 @@ export interface StoredParameter {
|
|
|
36
36
|
value: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export interface StoredParameterVersion extends StoredParameter {
|
|
40
|
+
version: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ImmutableParameterPutResult = "created" | "matched";
|
|
44
|
+
|
|
45
|
+
/** The immutable name exists, but its authenticated identity is different. */
|
|
46
|
+
export class ImmutableParameterConflictError extends Error {
|
|
47
|
+
constructor(message = "immutable parameter identity conflicts with the existing value") {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "ImmutableParameterConflictError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A failed/ambiguous create could not yet be proved to be our prior write. */
|
|
54
|
+
export class ImmutableParameterUnverifiedError extends Error {
|
|
55
|
+
constructor(message = "immutable parameter create could not be verified") {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "ImmutableParameterUnverifiedError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
export interface CredentialStore {
|
|
40
62
|
readonly kind: "aws-ssm";
|
|
41
63
|
/** Create or overwrite one credential. Throws on failure. */
|
|
42
64
|
put(name: string, value: string): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Create one immutable SecureString with its identity tags in the same AWS
|
|
67
|
+
* request. A retry never overwrites or reads the value: metadata proves that
|
|
68
|
+
* an already-existing name is the exact prior write.
|
|
69
|
+
*
|
|
70
|
+
* Optional for custom/older stores. Callers that require create-once
|
|
71
|
+
* semantics must fail closed when it is absent.
|
|
72
|
+
*/
|
|
73
|
+
putImmutable?(
|
|
74
|
+
name: string,
|
|
75
|
+
value: string,
|
|
76
|
+
tags: Record<string, string>,
|
|
77
|
+
): Promise<ImmutableParameterPutResult>;
|
|
43
78
|
/** The credential's plaintext, or null when it does not exist. */
|
|
44
79
|
get(name: string): Promise<string | null>;
|
|
80
|
+
/**
|
|
81
|
+
* One exact immutable parameter version. Project-env replica migration
|
|
82
|
+
* uses this for legacy value references: reading the latest value and then
|
|
83
|
+
* assuming its version would allow a late old writer to substitute a new
|
|
84
|
+
* secret for the head the cloud actually named.
|
|
85
|
+
*
|
|
86
|
+
* Optional for older/custom stores. Callers that require a pinned read
|
|
87
|
+
* must fail closed when it is absent.
|
|
88
|
+
*/
|
|
89
|
+
getVersion?(name: string, version: number): Promise<StoredParameterVersion | null>;
|
|
45
90
|
/** Idempotent: a missing parameter is not an error. */
|
|
46
91
|
delete(name: string): Promise<void>;
|
|
47
92
|
/** Every credential under `path` (recursive), decrypted. Throws on failure. */
|
|
@@ -162,6 +207,19 @@ function isAccessDenied(result: CliResult): boolean {
|
|
|
162
207
|
return /AccessDenied/.test(result.stderr);
|
|
163
208
|
}
|
|
164
209
|
|
|
210
|
+
function checkedTagEntries(tags: Record<string, string>): Array<[string, string]> {
|
|
211
|
+
const entries = Object.entries(tags);
|
|
212
|
+
for (const [key, value] of entries) {
|
|
213
|
+
if (
|
|
214
|
+
!/^[A-Za-z0-9_.:/=+@-]{1,128}$/.test(key) ||
|
|
215
|
+
!/^[A-Za-z0-9_.:/=+@ -]{0,256}$/.test(value)
|
|
216
|
+
) {
|
|
217
|
+
throw new Error(`invalid tag ${key}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return entries;
|
|
221
|
+
}
|
|
222
|
+
|
|
165
223
|
/** Summarize a CLI failure WITHOUT echoing stdout (which could carry a value). */
|
|
166
224
|
function failure(op: string, result: CliResult): Error {
|
|
167
225
|
const line =
|
|
@@ -207,15 +265,59 @@ export class AwsSsmCredentialStore implements CredentialStore {
|
|
|
207
265
|
if (result.status !== 0) throw failure("put-parameter", result);
|
|
208
266
|
}
|
|
209
267
|
|
|
210
|
-
async
|
|
268
|
+
async putImmutable(
|
|
269
|
+
name: string,
|
|
270
|
+
value: string,
|
|
271
|
+
tags: Record<string, string>,
|
|
272
|
+
): Promise<ImmutableParameterPutResult> {
|
|
211
273
|
if (!isParameterName(name)) throw new Error(`invalid parameter name: ${name}`);
|
|
212
|
-
const entries =
|
|
213
|
-
if (entries.length === 0)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
274
|
+
const entries = checkedTagEntries(tags);
|
|
275
|
+
if (entries.length === 0) {
|
|
276
|
+
throw new Error("immutable parameter identity tags are required");
|
|
277
|
+
}
|
|
278
|
+
const input = JSON.stringify({
|
|
279
|
+
Name: name,
|
|
280
|
+
Value: value,
|
|
281
|
+
Type: "SecureString",
|
|
282
|
+
KeyId: this.kmsKeyId,
|
|
283
|
+
Tier: "Advanced",
|
|
284
|
+
Overwrite: false,
|
|
285
|
+
Tags: entries.map(([Key, Value]) => ({ Key, Value })),
|
|
286
|
+
});
|
|
287
|
+
const result = await withPrivateInputFile(input, (path) =>
|
|
288
|
+
this.run(["ssm", "put-parameter", "--cli-input-json", `file://${path}`]),
|
|
289
|
+
);
|
|
290
|
+
if (result.status === 0) return "created";
|
|
291
|
+
|
|
292
|
+
// The CLI can lose the successful response, and a retry then receives
|
|
293
|
+
// ParameterAlreadyExists. The sign-in-box identity is intentionally
|
|
294
|
+
// write-only, so authenticate the prior write using metadata only.
|
|
295
|
+
let observed: Record<string, string>;
|
|
296
|
+
try {
|
|
297
|
+
observed = await this.listTags(name);
|
|
298
|
+
} catch {
|
|
299
|
+
throw new ImmutableParameterUnverifiedError();
|
|
300
|
+
}
|
|
301
|
+
let missing = false;
|
|
302
|
+
for (const [key, expected] of entries) {
|
|
303
|
+
const actual = observed[key];
|
|
304
|
+
if (actual === undefined) {
|
|
305
|
+
missing = true;
|
|
306
|
+
} else if (actual !== expected) {
|
|
307
|
+
throw new ImmutableParameterConflictError();
|
|
217
308
|
}
|
|
218
309
|
}
|
|
310
|
+
if (!missing) return "matched";
|
|
311
|
+
|
|
312
|
+
// Tags are eventually consistent with the create. Missing/partial tags
|
|
313
|
+
// are therefore retryable, never permission to add tags or overwrite.
|
|
314
|
+
throw new ImmutableParameterUnverifiedError();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async tag(name: string, tags: Record<string, string>): Promise<void> {
|
|
318
|
+
if (!isParameterName(name)) throw new Error(`invalid parameter name: ${name}`);
|
|
319
|
+
const entries = checkedTagEntries(tags);
|
|
320
|
+
if (entries.length === 0) return;
|
|
219
321
|
const result = await this.run([
|
|
220
322
|
"ssm",
|
|
221
323
|
"add-tags-to-resource",
|
|
@@ -248,6 +350,39 @@ export class AwsSsmCredentialStore implements CredentialStore {
|
|
|
248
350
|
return value;
|
|
249
351
|
}
|
|
250
352
|
|
|
353
|
+
async getVersion(
|
|
354
|
+
name: string,
|
|
355
|
+
version: number,
|
|
356
|
+
): Promise<StoredParameterVersion | null> {
|
|
357
|
+
if (!isParameterName(name)) throw new Error(`invalid parameter name: ${name}`);
|
|
358
|
+
if (!Number.isSafeInteger(version) || version < 1) {
|
|
359
|
+
throw new Error(`invalid parameter version: ${version}`);
|
|
360
|
+
}
|
|
361
|
+
const result = await this.run([
|
|
362
|
+
"ssm",
|
|
363
|
+
"get-parameter",
|
|
364
|
+
"--name",
|
|
365
|
+
`${name}:${version}`,
|
|
366
|
+
"--with-decryption",
|
|
367
|
+
]);
|
|
368
|
+
if (result.status !== 0) {
|
|
369
|
+
if (isNotFound(result)) return null;
|
|
370
|
+
throw failure("get-parameter", result);
|
|
371
|
+
}
|
|
372
|
+
const parameter = (JSON.parse(result.stdout) as {
|
|
373
|
+
Parameter?: { Name?: unknown; Value?: unknown; Version?: unknown };
|
|
374
|
+
}).Parameter;
|
|
375
|
+
if (
|
|
376
|
+
parameter === undefined ||
|
|
377
|
+
parameter.Name !== name ||
|
|
378
|
+
typeof parameter.Value !== "string" ||
|
|
379
|
+
parameter.Version !== version
|
|
380
|
+
) {
|
|
381
|
+
throw new Error("aws ssm get-parameter: malformed versioned response");
|
|
382
|
+
}
|
|
383
|
+
return { name, value: parameter.Value, version };
|
|
384
|
+
}
|
|
385
|
+
|
|
251
386
|
async delete(name: string): Promise<void> {
|
|
252
387
|
if (!isParameterName(name)) throw new Error(`invalid parameter name: ${name}`);
|
|
253
388
|
const result = await this.run(["ssm", "delete-parameter", "--name", name]);
|
package/lib/engine-accounts.ts
CHANGED
|
@@ -654,6 +654,7 @@ async function copyDirIntoRaw(
|
|
|
654
654
|
if (environment) {
|
|
655
655
|
const run = async (argv: [string, ...string[]], step: string) => {
|
|
656
656
|
const result = await environment.exec({
|
|
657
|
+
projectEnvProfile: "maintenance",
|
|
657
658
|
argv,
|
|
658
659
|
user: "root",
|
|
659
660
|
env: RUNTIME_AUTHORITY_ENV,
|
package/lib/env-file.ts
CHANGED
|
@@ -133,6 +133,13 @@ export function restoreEnvFileSnapshotAtomic(
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/** Write `body` to `file` so a crash or power loss leaves either the old
|
|
137
|
+
* file or the new one, never a torn or empty one: fsync the data, rename,
|
|
138
|
+
* then fsync the directory. 0600, parent created 0700. */
|
|
139
|
+
export function writeDurableFileAtomic(file: string, body: string): void {
|
|
140
|
+
replaceEnvFileAtomic(file, body);
|
|
141
|
+
}
|
|
142
|
+
|
|
136
143
|
function replaceEnvFileAtomic(file: string, body: string): void {
|
|
137
144
|
const parent = dirname(file);
|
|
138
145
|
mkdirSync(parent, { recursive: true, mode: 0o700 });
|