@runuai/host 0.9.148 → 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/codex.ts +9 -1
- package/lib/agents/cursor.ts +15 -1
- package/lib/agents/grok.ts +8 -0
- package/lib/agents/kimi.ts +1 -0
- package/lib/agents/machine-durable.ts +2 -0
- package/lib/agents/opencode.ts +1 -0
- package/lib/agents/proc.ts +6 -1
- package/lib/agents/transport.ts +9 -5
- 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 +391 -69
- 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/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 +939 -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 +2219 -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
|
}
|
package/lib/agents/codex.ts
CHANGED
|
@@ -418,7 +418,15 @@ export class CodexSession implements AgentSession {
|
|
|
418
418
|
for (const override of args.cliConfigOverrides ?? []) {
|
|
419
419
|
codexArgs.push("-c", override);
|
|
420
420
|
}
|
|
421
|
-
|
|
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
|
+
);
|
|
422
430
|
// ADR-061: the CLI is owned by an in-container runner (durable across
|
|
423
431
|
// host restarts); legacy pipes behind UAI_DURABLE_SESSIONS=0. Codex
|
|
424
432
|
// re-handshakes per host process (initialize + thread/start live in this
|
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
|
}
|
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,
|
|
@@ -228,6 +229,7 @@ export class MachineDurableProcess implements LineTransport {
|
|
|
228
229
|
String(this.#offset + 1),
|
|
229
230
|
this.#outboxPath,
|
|
230
231
|
],
|
|
232
|
+
projectEnvProfile: "maintenance",
|
|
231
233
|
maxOutputBytes: 8 * 1024 * 1024,
|
|
232
234
|
});
|
|
233
235
|
} catch {
|
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
|
|
package/lib/agents/transport.ts
CHANGED
|
@@ -55,6 +55,7 @@ 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";
|
|
59
60
|
import { attachCompatibilityKeysMatch } from "./attach-compatibility";
|
|
60
61
|
|
|
@@ -326,15 +327,16 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
326
327
|
onCloseRequested: markClosed,
|
|
327
328
|
debugLabel: opts.debugLabel,
|
|
328
329
|
launch: async () => {
|
|
330
|
+
const cliArgv = agentCliArgv(opts.environment, [opts.cli, ...opts.cliArgs]);
|
|
329
331
|
const result = await opts.environment.launchDetachedSession({
|
|
330
332
|
argv: [
|
|
331
333
|
"node",
|
|
332
334
|
runnerInEnvironment,
|
|
333
335
|
environmentSessionDir,
|
|
334
336
|
"--",
|
|
335
|
-
|
|
336
|
-
...opts.cliArgs,
|
|
337
|
+
...cliArgv,
|
|
337
338
|
],
|
|
339
|
+
projectEnvProfile: "task",
|
|
338
340
|
inheritEnv: opts.passEnv ?? [],
|
|
339
341
|
env: opts.explicitEnv ?? {},
|
|
340
342
|
launchTimeoutMs: 30_000,
|
|
@@ -484,15 +486,16 @@ function machineDurableTransport(opts: AgentTransportOptions): LineTransport {
|
|
|
484
486
|
}
|
|
485
487
|
const runnerPath = await deployRunner(environment, sessionDir);
|
|
486
488
|
await publishMachineCurrentSession(environment, sessionDir);
|
|
489
|
+
const cliArgv = agentCliArgv(environment, [opts.cli, ...opts.cliArgs]);
|
|
487
490
|
const result = await environment.launchDetachedSession({
|
|
488
491
|
argv: [
|
|
489
492
|
"node",
|
|
490
493
|
runnerPath,
|
|
491
494
|
sessionDir,
|
|
492
495
|
"--",
|
|
493
|
-
|
|
494
|
-
...opts.cliArgs,
|
|
496
|
+
...cliArgv,
|
|
495
497
|
],
|
|
498
|
+
projectEnvProfile: "task",
|
|
496
499
|
inheritEnv: opts.passEnv ?? [],
|
|
497
500
|
env: opts.explicitEnv ?? {},
|
|
498
501
|
launchTimeoutMs: 30_000,
|
|
@@ -538,7 +541,8 @@ function directEnvironmentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
538
541
|
return new EnvironmentLineProcess({
|
|
539
542
|
environment: opts.environment,
|
|
540
543
|
request: {
|
|
541
|
-
argv: [opts.cli, ...opts.cliArgs],
|
|
544
|
+
argv: agentCliArgv(opts.environment, [opts.cli, ...opts.cliArgs]),
|
|
545
|
+
projectEnvProfile: "task",
|
|
542
546
|
inheritEnv: opts.passEnv ?? [],
|
|
543
547
|
env: opts.explicitEnv ?? {},
|
|
544
548
|
maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
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 });
|