@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.
Files changed (98) hide show
  1. package/db/migrations/0024_task_env_mode.sql +9 -0
  2. package/db/migrations/0025_project_env_authority.sql +147 -0
  3. package/db/migrations/0026_project_env_replica.sql +150 -0
  4. package/db/migrations/meta/_journal.json +21 -0
  5. package/db/schema.ts +312 -2
  6. package/images/standard/Dockerfile +44 -6
  7. package/images/standard/container/uai-init +85 -2
  8. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  9. package/lib/agent-cli.ts +18 -1
  10. package/lib/agent.ts +99 -34
  11. package/lib/agents/attach-compatibility.ts +51 -0
  12. package/lib/agents/codex.ts +137 -13
  13. package/lib/agents/cursor.ts +15 -1
  14. package/lib/agents/durable-proc.ts +2 -0
  15. package/lib/agents/grok.ts +8 -0
  16. package/lib/agents/kimi.ts +1 -0
  17. package/lib/agents/machine-durable.ts +17 -0
  18. package/lib/agents/opencode.ts +1 -0
  19. package/lib/agents/proc.ts +6 -1
  20. package/lib/agents/session-state.ts +107 -0
  21. package/lib/agents/transport.ts +125 -58
  22. package/lib/agents/types.ts +3 -3
  23. package/lib/browser-testing.ts +6 -0
  24. package/lib/codex-auth.ts +2 -0
  25. package/lib/credential-store.ts +141 -6
  26. package/lib/engine-accounts.ts +1 -0
  27. package/lib/env-file.ts +7 -0
  28. package/lib/env-schema.ts +841 -51
  29. package/lib/git-diff.ts +1 -0
  30. package/lib/git-identity.ts +2 -0
  31. package/lib/github-tokens.ts +1 -0
  32. package/lib/host-env.ts +98 -18
  33. package/lib/lease.ts +11 -3
  34. package/lib/machine-gateway-tunnel.ts +195 -17
  35. package/lib/machine-project-env-loader.ts +152 -0
  36. package/lib/managed-runtime.ts +270 -51
  37. package/lib/mcp-gateway.ts +777 -26
  38. package/lib/orchestrator.ts +1681 -138
  39. package/lib/project-env-authority.ts +1319 -0
  40. package/lib/project-env-feature.ts +74 -0
  41. package/lib/project-env-loader.ts +237 -0
  42. package/lib/project-env-replica.ts +2094 -0
  43. package/lib/project-env-schema-capture.ts +342 -0
  44. package/lib/project-env-staging.ts +148 -0
  45. package/lib/project-env-task-policy.ts +176 -0
  46. package/lib/runtime-archive.ts +79 -16
  47. package/lib/runtime-authority.ts +44 -0
  48. package/lib/runtime-state.ts +175 -10
  49. package/lib/secrets-proxy/address.ts +72 -0
  50. package/lib/secrets-proxy/audit.ts +30 -0
  51. package/lib/secrets-proxy/ca-store.ts +200 -0
  52. package/lib/secrets-proxy/ca.ts +470 -0
  53. package/lib/secrets-proxy/container-steps.ts +138 -0
  54. package/lib/secrets-proxy/der.ts +273 -0
  55. package/lib/secrets-proxy/engine.ts +1633 -0
  56. package/lib/secrets-proxy/host.ts +93 -0
  57. package/lib/secrets-proxy/hostname.ts +60 -0
  58. package/lib/secrets-proxy/placeholders.ts +196 -0
  59. package/lib/secrets-proxy/policy.ts +141 -0
  60. package/lib/secrets-proxy/readiness.ts +148 -0
  61. package/lib/secrets-proxy/runtime.ts +582 -0
  62. package/lib/secrets-proxy/scrub.ts +559 -0
  63. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  64. package/lib/secrets-proxy/task-scripts.ts +262 -0
  65. package/lib/secrets-proxy/task-trust.ts +574 -0
  66. package/lib/secrets-proxy/trust-watch.ts +80 -0
  67. package/lib/shared-files-s3.ts +3 -0
  68. package/lib/sidecar-guard.ts +58 -0
  69. package/lib/sidecar-resume.ts +51 -3
  70. package/lib/standard-image.ts +50 -12
  71. package/lib/task-env-forwarder.ts +228 -0
  72. package/lib/task-env-source.ts +193 -0
  73. package/lib/task-environment/apple-container.ts +45 -11
  74. package/lib/task-environment/docker.ts +51 -12
  75. package/lib/task-environment/index.ts +6 -2
  76. package/lib/task-environment/machine-task-up.ts +258 -25
  77. package/lib/task-environment/machine.ts +80 -22
  78. package/lib/task-environment/registry.ts +3 -2
  79. package/lib/task-environment/self-identity.ts +20 -0
  80. package/lib/task-environment/self.ts +92 -16
  81. package/lib/task-environment/types.ts +63 -0
  82. package/lib/task-gateway-port.ts +22 -0
  83. package/lib/task-gateway-protocol.ts +190 -0
  84. package/lib/task-gateway-recovery.ts +44 -0
  85. package/lib/task-gateway-state.ts +497 -0
  86. package/lib/transcript.ts +1 -0
  87. package/lib/ulid.ts +10 -1
  88. package/package.json +13 -1
  89. package/scripts/agent/env-hook.sh +626 -0
  90. package/scripts/agent/task-up.sh +476 -27
  91. package/scripts/agent/uai-env-exec.sh +26 -0
  92. package/src/cli.ts +26 -3
  93. package/src/index.ts +273 -19
  94. package/src/main.ts +941 -48
  95. package/src/project-env-latest.ts +130 -0
  96. package/src/project-env-policy.ts +281 -0
  97. package/src/project-env-wording.ts +47 -0
  98. package/src/protocol.ts +2239 -15
@@ -23,9 +23,12 @@ import type {
23
23
  MachineProvider,
24
24
  } from "../machine-provider";
25
25
  import { machineEnvironmentSshArgs } from "../machine-exec";
26
+ import { applyProjectEnvProfile } from "../runtime-authority";
26
27
  import { capturedCliExec, streamingCliExec } from "./docker";
27
28
  import {
29
+ assertTaskEnvironmentExecRequest,
28
30
  assertTaskEnvironmentProcessRequest,
31
+ refusePrivilegedExec,
29
32
  assertTaskEnvironmentSessionRequest,
30
33
  assertWorkspaceScopedPath,
31
34
  type TaskEnvironmentCopyRequest,
@@ -37,6 +40,7 @@ import {
37
40
  type TaskEnvironmentLocator,
38
41
  type TaskEnvironmentPort,
39
42
  type TaskEnvironmentProcess,
43
+ type ProjectEnvMode,
40
44
  type TaskEnvironmentProvider,
41
45
  type TaskEnvironmentProvisionRequest,
42
46
  type TaskEnvironmentProvisioned,
@@ -185,6 +189,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
185
189
  constructor(
186
190
  locator: TaskEnvironmentLocator,
187
191
  deps: MachineTaskEnvironmentDeps,
192
+ envMode: ProjectEnvMode,
188
193
  ) {
189
194
  this.#value = parseMachineTaskEnvironmentLocator(locator);
190
195
  this.#deps = {
@@ -197,6 +202,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
197
202
  this.descriptor = {
198
203
  taskId: this.#value.taskId,
199
204
  locator,
205
+ envMode,
200
206
  workspacePath: this.#value.workspacePath,
201
207
  };
202
208
  }
@@ -242,6 +248,24 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
242
248
  return info;
243
249
  }
244
250
 
251
+ #maintenanceRequest<
252
+ T extends {
253
+ argv: readonly [string, ...string[]];
254
+ cwd?: string;
255
+ user?: string;
256
+ inheritEnv?: readonly string[];
257
+ env?: Readonly<Record<string, string>>;
258
+ },
259
+ >(
260
+ request: T,
261
+ ): T & { projectEnvProfile: "maintenance" } {
262
+ return applyProjectEnvProfile(
263
+ { ...request, projectEnvProfile: "maintenance" as const },
264
+ this.descriptor.envMode ?? "baked",
265
+ "/bin/bash",
266
+ );
267
+ }
268
+
245
269
  async readWorkspaceFile(path: string): Promise<Uint8Array | null> {
246
270
  assertWorkspaceScopedPath(this.descriptor, path);
247
271
  const target = await this.#target();
@@ -250,7 +274,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
250
274
  "ssh",
251
275
  machineEnvironmentSshArgs(
252
276
  target,
253
- {
277
+ this.#maintenanceRequest({
254
278
  argv: [
255
279
  "/bin/sh",
256
280
  "-c",
@@ -260,11 +284,12 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
260
284
  "read",
261
285
  path,
262
286
  ],
263
- },
287
+ }),
264
288
  "interactive",
265
289
  ),
266
290
  {
267
291
  argv: ["/bin/cat"],
292
+ projectEnvProfile: "maintenance",
268
293
  timeoutMs: 60_000,
269
294
  maxOutputBytes: 64 * 1024 * 1024,
270
295
  },
@@ -286,7 +311,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
286
311
  "ssh",
287
312
  machineEnvironmentSshArgs(
288
313
  target,
289
- {
314
+ this.#maintenanceRequest({
290
315
  argv: [
291
316
  "/bin/sh",
292
317
  "-c",
@@ -294,11 +319,12 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
294
319
  "write",
295
320
  path,
296
321
  ],
297
- },
322
+ }),
298
323
  "interactive",
299
324
  ),
300
325
  {
301
326
  argv: ["/bin/sh"],
327
+ projectEnvProfile: "maintenance",
302
328
  stdin: bytes,
303
329
  timeoutMs: 120_000,
304
330
  maxOutputBytes: 1024 * 1024,
@@ -314,13 +340,20 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
314
340
  async exec(
315
341
  request: TaskEnvironmentExecRequest,
316
342
  ): Promise<TaskEnvironmentExecResult> {
317
- assertTaskEnvironmentProcessRequest(request);
343
+ assertTaskEnvironmentExecRequest(request);
344
+ refusePrivilegedExec(request);
345
+ const profiled = applyProjectEnvProfile(
346
+ request,
347
+ this.descriptor.envMode ?? "baked",
348
+ "/bin/bash",
349
+ );
318
350
  const target = await this.#target();
351
+ request.admit?.();
319
352
  return capturedCliExec(
320
353
  this.#deps.spawn,
321
354
  "ssh",
322
- machineEnvironmentSshArgs(target, request, "interactive"),
323
- request,
355
+ machineEnvironmentSshArgs(target, profiled, "interactive"),
356
+ profiled,
324
357
  );
325
358
  }
326
359
 
@@ -328,12 +361,17 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
328
361
  request: TaskEnvironmentSpawnRequest,
329
362
  ): Promise<TaskEnvironmentProcess> {
330
363
  assertTaskEnvironmentProcessRequest(request);
364
+ const profiled = applyProjectEnvProfile(
365
+ request,
366
+ this.descriptor.envMode ?? "baked",
367
+ "/bin/bash",
368
+ );
331
369
  const target = await this.#target();
332
370
  return streamingCliExec(
333
371
  this.#deps.spawn,
334
372
  "ssh",
335
- machineEnvironmentSshArgs(target, request, "interactive"),
336
- request,
373
+ machineEnvironmentSshArgs(target, profiled, "interactive"),
374
+ profiled,
337
375
  );
338
376
  }
339
377
 
@@ -343,10 +381,10 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
343
381
  assertTaskEnvironmentSessionRequest(request);
344
382
  // Sessions default to the workspace the way container images default to
345
383
  // WORKDIR /workspace — an agent CLI must start in the task's world.
346
- const sessionRequest = {
384
+ const sessionRequest = applyProjectEnvProfile({
347
385
  ...request,
348
386
  cwd: request.cwd ?? this.#value.workspacePath,
349
- };
387
+ }, this.descriptor.envMode ?? "baked", "/bin/bash");
350
388
  const target = await this.#target();
351
389
  return streamingCliExec(
352
390
  this.#deps.spawn,
@@ -360,10 +398,10 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
360
398
  request: TaskEnvironmentDetachedSessionRequest,
361
399
  ): Promise<TaskEnvironmentExecResult> {
362
400
  assertTaskEnvironmentSessionRequest(request);
363
- const sessionRequest = {
401
+ const sessionRequest = applyProjectEnvProfile({
364
402
  ...request,
365
403
  cwd: request.cwd ?? this.#value.workspacePath,
366
- };
404
+ }, this.descriptor.envMode ?? "baked", "/bin/bash");
367
405
  const target = await this.#target();
368
406
  return capturedCliExec(
369
407
  this.#deps.spawn,
@@ -402,7 +440,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
402
440
  "ssh",
403
441
  machineEnvironmentSshArgs(
404
442
  target,
405
- {
443
+ this.#maintenanceRequest({
406
444
  argv: [
407
445
  "/bin/sh",
408
446
  "-c",
@@ -410,11 +448,12 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
410
448
  "copy",
411
449
  request.destination,
412
450
  ],
413
- },
451
+ }),
414
452
  "interactive",
415
453
  ),
416
454
  {
417
455
  argv: ["/bin/sh"],
456
+ projectEnvProfile: "maintenance",
418
457
  stdin: tarball.stdout,
419
458
  timeoutMs: 300_000,
420
459
  maxOutputBytes: 1024 * 1024,
@@ -433,7 +472,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
433
472
  "ssh",
434
473
  machineEnvironmentSshArgs(
435
474
  target,
436
- {
475
+ this.#maintenanceRequest({
437
476
  argv: [
438
477
  "/bin/sh",
439
478
  "-c",
@@ -441,11 +480,12 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
441
480
  "copy",
442
481
  request.destination,
443
482
  ],
444
- },
483
+ }),
445
484
  "interactive",
446
485
  ),
447
486
  {
448
487
  argv: ["/bin/sh"],
488
+ projectEnvProfile: "maintenance",
449
489
  stdin: bytes,
450
490
  timeoutMs: 120_000,
451
491
  maxOutputBytes: 1024 * 1024,
@@ -463,11 +503,12 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
463
503
  "ssh",
464
504
  machineEnvironmentSshArgs(
465
505
  target,
466
- { argv: ["/bin/cat", request.source] },
506
+ this.#maintenanceRequest({ argv: ["/bin/cat", request.source] }),
467
507
  "interactive",
468
508
  ),
469
509
  {
470
510
  argv: ["/bin/cat"],
511
+ projectEnvProfile: "maintenance",
471
512
  timeoutMs: 120_000,
472
513
  maxOutputBytes: 64 * 1024 * 1024,
473
514
  },
@@ -572,11 +613,14 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
572
613
  "ssh",
573
614
  machineEnvironmentSshArgs(
574
615
  target,
575
- { argv: ["/usr/bin/test", "-d", this.#value.workspacePath] },
616
+ this.#maintenanceRequest({
617
+ argv: ["/usr/bin/test", "-d", this.#value.workspacePath],
618
+ }),
576
619
  "interactive",
577
620
  ),
578
621
  {
579
622
  argv: ["/usr/bin/test"],
623
+ projectEnvProfile: "maintenance",
580
624
  timeoutMs: 15_000,
581
625
  maxOutputBytes: 4 * 1024,
582
626
  },
@@ -618,6 +662,7 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
618
662
  if (!context.isCurrent()) {
619
663
  return { outcome: "deferred", detail: "runtime generation superseded" };
620
664
  }
665
+ await context.prepareConsumers?.();
621
666
  context.allowConsumers?.();
622
667
  await context.ensureConsumersStarted?.();
623
668
  if (!context.isCurrent()) {
@@ -633,9 +678,14 @@ class MachineTaskEnvironmentHandle implements TaskEnvironmentHandle {
633
678
  const result = await capturedCliExec(
634
679
  this.#deps.spawn,
635
680
  "ssh",
636
- machineEnvironmentSshArgs(target, { argv: ["/bin/true"] }, "interactive"),
681
+ machineEnvironmentSshArgs(
682
+ target,
683
+ this.#maintenanceRequest({ argv: ["/bin/true"] }),
684
+ "interactive",
685
+ ),
637
686
  {
638
687
  argv: ["/bin/true"],
688
+ projectEnvProfile: "maintenance",
639
689
  timeoutMs: 15_000,
640
690
  maxOutputBytes: 4 * 1024,
641
691
  },
@@ -745,7 +795,11 @@ export function createMachineTaskEnvironmentProvider(
745
795
  }
746
796
  // running/pending: fall through to the ssh-readiness proof below.
747
797
 
748
- const handle = new MachineTaskEnvironmentHandle(locator, deps);
798
+ const handle = new MachineTaskEnvironmentHandle(
799
+ locator,
800
+ deps,
801
+ request.envMode ?? "loader",
802
+ );
749
803
  await waitForSsh(handle, deps.readyTimeoutMs ?? 120_000);
750
804
 
751
805
  // Workspace + optional in-env clone (ADR-121: the clone happens in the
@@ -762,6 +816,7 @@ export function createMachineTaskEnvironmentProvider(
762
816
  workspacePath,
763
817
  ],
764
818
  user: "root",
819
+ projectEnvProfile: "maintenance",
765
820
  timeoutMs: 30_000,
766
821
  maxOutputBytes: 64 * 1024,
767
822
  });
@@ -780,6 +835,7 @@ export function createMachineTaskEnvironmentProvider(
780
835
  ];
781
836
  const clone = await handle.exec({
782
837
  argv: cloneArgv,
838
+ projectEnvProfile: "maintenance",
783
839
  timeoutMs: 10 * 60_000,
784
840
  maxOutputBytes: 4 * 1024 * 1024,
785
841
  });
@@ -794,8 +850,9 @@ export function createMachineTaskEnvironmentProvider(
794
850
 
795
851
  async reconstruct(
796
852
  locator: TaskEnvironmentLocator,
853
+ envMode: ProjectEnvMode = "baked",
797
854
  ): Promise<TaskEnvironmentHandle> {
798
- return new MachineTaskEnvironmentHandle(locator, deps);
855
+ return new MachineTaskEnvironmentHandle(locator, deps, envMode);
799
856
  },
800
857
  };
801
858
  }
@@ -810,6 +867,7 @@ async function waitForSsh(
810
867
  try {
811
868
  const probe = await handle.exec({
812
869
  argv: ["/bin/true"],
870
+ projectEnvProfile: "maintenance",
813
871
  timeoutMs: 10_000,
814
872
  maxOutputBytes: 4 * 1024,
815
873
  });
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ ProjectEnvMode,
2
3
  TaskEnvironmentLocator,
3
4
  TaskEnvironmentProvider,
4
5
  } from "./types";
@@ -48,8 +49,8 @@ export class TaskEnvironmentRegistry<
48
49
  return provider;
49
50
  }
50
51
 
51
- reconstruct(locator: TaskEnvironmentLocator) {
52
- return this.provider(locator.provider).reconstruct(locator);
52
+ reconstruct(locator: TaskEnvironmentLocator, envMode: ProjectEnvMode = "baked") {
53
+ return this.provider(locator.provider).reconstruct(locator, envMode);
53
54
  }
54
55
 
55
56
  kinds(): string[] {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The `self` provider's fixed names: a Uai Cloud machine's own task, run by
3
+ * the sidecar on that machine (ADR-133). A module with no imports, so the
4
+ * secrets proxy can name them without loading the provider, which reaches it
5
+ * back through the agent.
6
+ */
7
+
8
+ export const SELF_TASK_ENVIRONMENT_PROVIDER = "self";
9
+
10
+ /** The task user every agent, editor, and shell runs as. */
11
+ export const SELF_TASK_USER = "node";
12
+
13
+ /**
14
+ * A task process's own view of the hosts on its machine (ADR-153): the
15
+ * machine's /etc/hosts plus the secrets proxy's pins, which the root
16
+ * sidecar's own processes must never resolve. Every process the sidecar
17
+ * starts as the task user gets it bind-mounted over /etc/hosts, in a mount
18
+ * namespace of its own that still follows the machine's mounts.
19
+ */
20
+ export const TASK_HOSTS_VIEW_PATH = "/run/uai/task-hosts";
@@ -19,10 +19,16 @@ import { existsSync, readFileSync } from "node:fs";
19
19
  import { readFile } from "node:fs/promises";
20
20
 
21
21
  import { buildRemoteCommand } from "../machine-exec";
22
+ import {
23
+ applyProjectEnvProfile,
24
+ PROJECT_ENV_PROFILE_ENV_NAMES,
25
+ } from "../runtime-authority";
22
26
  import { capturedCliExec, streamingCliExec } from "./docker";
23
27
  import type { MachineTaskInput } from "./machine";
24
28
  import {
29
+ assertTaskEnvironmentExecRequest,
25
30
  assertTaskEnvironmentProcessRequest,
31
+ refusePrivilegedExec,
26
32
  assertTaskEnvironmentSessionRequest,
27
33
  assertWorkspaceScopedPath,
28
34
  type TaskEnvironmentCopyRequest,
@@ -34,6 +40,7 @@ import {
34
40
  type TaskEnvironmentLocator,
35
41
  type TaskEnvironmentPort,
36
42
  type TaskEnvironmentProcess,
43
+ type ProjectEnvMode,
37
44
  type TaskEnvironmentProvider,
38
45
  type TaskEnvironmentProvisionRequest,
39
46
  type TaskEnvironmentProvisioned,
@@ -44,11 +51,29 @@ import {
44
51
  type TaskEnvironmentStatus,
45
52
  TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION,
46
53
  } from "./types";
54
+ import { SELF_TASK_ENVIRONMENT_PROVIDER, SELF_TASK_USER, TASK_HOSTS_VIEW_PATH } from "./self-identity";
47
55
 
48
- export const SELF_TASK_ENVIRONMENT_PROVIDER = "self";
56
+ export { SELF_TASK_ENVIRONMENT_PROVIDER, SELF_TASK_USER, TASK_HOSTS_VIEW_PATH };
49
57
 
50
- /** The task user every agent, editor, and shell runs as. */
51
- export const SELF_TASK_USER = "node";
58
+ /**
59
+ * Binds the view, then runs the rest of its argv. With no view yet (nothing
60
+ * pinned so far: a start whose CA failed, the proxy off), it first makes one,
61
+ * a copy of the machine's file, so every task process has the view bound
62
+ * from its start and a later pin, rewritten into it in place, reaches them
63
+ * all. Made whole under another name and linked in, so it never shows half
64
+ * written and never replaces one made meanwhile. A view that cannot be
65
+ * bound fails the start: the task never runs with the proxy half in place.
66
+ */
67
+ const TASK_VIEW_SCRIPT = [
68
+ "PATH=/usr/sbin:/usr/bin:/sbin:/bin; view=$1; shift",
69
+ 'if [ ! -f "$view" ]; then',
70
+ ' umask 022; dir=${view%/*}; mkdir -p "$dir"',
71
+ ' tmp=$(mktemp "$dir/.task-hosts.XXXXXX") || exit 126',
72
+ ' cat /etc/hosts > "$tmp" && chmod 0644 "$tmp" && ln "$tmp" "$view" 2>/dev/null; rm -f "$tmp"',
73
+ "fi",
74
+ 'mount --bind "$view" /etc/hosts || exit 126',
75
+ 'exec "$@"',
76
+ ].join("\n");
52
77
 
53
78
  const SAFE_TASK_ID = /^[a-z0-9]{1,32}$/i;
54
79
 
@@ -163,23 +188,43 @@ export function selfCommand(
163
188
  privileged: boolean,
164
189
  ): { command: string; args: string[] } {
165
190
  const line = buildRemoteCommand({ ...request, detached: mode === "detached" });
191
+ const outerProfileEnv = PROJECT_ENV_PROFILE_ENV_NAMES.flatMap((name) => {
192
+ const value = request.env?.[name];
193
+ return value === undefined ? [] : [`${name}=${value}`];
194
+ });
195
+ const shellCommand =
196
+ outerProfileEnv.length === 0
197
+ ? { command: "/bin/sh", args: ["-lc", line] }
198
+ : {
199
+ command: "/usr/bin/env",
200
+ args: [...outerProfileEnv, "/bin/sh", "-lc", line],
201
+ };
166
202
  const user = request.user ?? SELF_TASK_USER;
167
203
  if (privileged && user !== "root") {
168
204
  return {
169
- command: "/usr/bin/setpriv",
205
+ command: "/usr/bin/unshare",
170
206
  args: [
207
+ "--mount",
208
+ "--propagation",
209
+ "slave",
210
+ "--",
211
+ "/bin/sh",
212
+ "-c",
213
+ TASK_VIEW_SCRIPT,
214
+ "uai-task-view",
215
+ TASK_HOSTS_VIEW_PATH,
216
+ "/usr/bin/setpriv",
171
217
  `--reuid=${user}`,
172
218
  `--regid=${user}`,
173
219
  "--init-groups",
174
220
  "--reset-env",
175
221
  "--",
176
- "/bin/sh",
177
- "-lc",
178
- line,
222
+ shellCommand.command,
223
+ ...shellCommand.args,
179
224
  ],
180
225
  };
181
226
  }
182
- return { command: "/bin/sh", args: ["-lc", line] };
227
+ return shellCommand;
183
228
  }
184
229
 
185
230
  class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
@@ -190,7 +235,11 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
190
235
  readonly #privileged: () => boolean;
191
236
  readonly #etcEnvironment: () => Readonly<Record<string, string>>;
192
237
 
193
- constructor(locator: TaskEnvironmentLocator, deps: SelfTaskEnvironmentDeps) {
238
+ constructor(
239
+ locator: TaskEnvironmentLocator,
240
+ deps: SelfTaskEnvironmentDeps,
241
+ envMode: ProjectEnvMode,
242
+ ) {
194
243
  this.#value = parseSelfTaskEnvironmentLocator(locator);
195
244
  this.#spawn = deps.spawn ?? nodeSpawn;
196
245
  this.#privileged =
@@ -200,6 +249,7 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
200
249
  this.descriptor = {
201
250
  taskId: this.#value.taskId,
202
251
  locator,
252
+ envMode,
203
253
  workspacePath: this.#value.workspacePath,
204
254
  };
205
255
  }
@@ -220,6 +270,7 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
220
270
  // agents touch (a root-owned file inside the workspace is a trap).
221
271
  const result = await this.exec({
222
272
  argv: ["/bin/sh", "-c", 'mkdir -p "$(dirname "$1")" && cat > "$1"', "write", path],
273
+ projectEnvProfile: "maintenance",
223
274
  stdin: bytes,
224
275
  timeoutMs: 120_000,
225
276
  maxOutputBytes: 1024 * 1024,
@@ -233,16 +284,30 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
233
284
 
234
285
  /** The request with the machine-wide environment beneath its own: what an
235
286
  * ssh session on a machine task gets from pam_env, applied here by hand. */
236
- #withMachineEnv<T extends { env?: Readonly<Record<string, string>> }>(request: T): T {
287
+ #withMachineEnv<
288
+ T extends {
289
+ env?: Readonly<Record<string, string>>;
290
+ projectEnvProfile: "maintenance" | "task";
291
+ },
292
+ >(request: T): T {
237
293
  const base = this.#etcEnvironment();
238
- if (Object.keys(base).length === 0) return request;
239
- return { ...request, env: { ...base, ...request.env } };
294
+ const withMachineEnv =
295
+ Object.keys(base).length === 0
296
+ ? request
297
+ : { ...request, env: { ...base, ...request.env } };
298
+ return applyProjectEnvProfile(
299
+ withMachineEnv,
300
+ this.descriptor.envMode ?? "baked",
301
+ "/bin/bash",
302
+ );
240
303
  }
241
304
 
242
305
  async exec(request: TaskEnvironmentExecRequest): Promise<TaskEnvironmentExecResult> {
243
- assertTaskEnvironmentProcessRequest(request);
306
+ assertTaskEnvironmentExecRequest(request);
307
+ refusePrivilegedExec(request);
244
308
  const scoped = this.#withMachineEnv(request);
245
309
  const { command, args } = selfCommand(scoped, "interactive", this.#privileged());
310
+ request.admit?.();
246
311
  return capturedCliExec(this.#spawn, command, args, scoped);
247
312
  }
248
313
 
@@ -306,6 +371,7 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
306
371
  const result = await this.exec({
307
372
  argv: ["/bin/sh", "-c", script, "copy", request.source, request.destination],
308
373
  user: "root",
374
+ projectEnvProfile: "maintenance",
309
375
  timeoutMs: 5 * 60_000,
310
376
  maxOutputBytes: 64 * 1024,
311
377
  });
@@ -341,6 +407,7 @@ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
341
407
  if (!context.isCurrent()) {
342
408
  return { outcome: "deferred", detail: "runtime generation superseded" };
343
409
  }
410
+ await context.prepareConsumers?.();
344
411
  context.allowConsumers?.();
345
412
  await context.ensureConsumersStarted?.();
346
413
  if (!context.isCurrent()) {
@@ -378,10 +445,15 @@ export function createSelfTaskEnvironmentProvider(
378
445
  const workspacePath = input.workspacePath ?? "/workspace";
379
446
  const locator = selfTaskEnvironmentLocator({ taskId, workspacePath });
380
447
  await request.onPrepared?.(locator);
381
- const handle = new SelfTaskEnvironmentHandle(locator, deps);
448
+ const handle = new SelfTaskEnvironmentHandle(
449
+ locator,
450
+ deps,
451
+ request.envMode ?? "loader",
452
+ );
382
453
  const prepare = await handle.exec({
383
454
  argv: ["/bin/sh", "-c", 'mkdir -p "$1" && chown node:node "$1"', "prepare", workspacePath],
384
455
  user: "root",
456
+ projectEnvProfile: "maintenance",
385
457
  timeoutMs: 30_000,
386
458
  maxOutputBytes: 64 * 1024,
387
459
  });
@@ -399,6 +471,7 @@ export function createSelfTaskEnvironmentProvider(
399
471
  input.repo.url,
400
472
  workspacePath,
401
473
  ],
474
+ projectEnvProfile: "maintenance",
402
475
  timeoutMs: 10 * 60_000,
403
476
  maxOutputBytes: 4 * 1024 * 1024,
404
477
  });
@@ -411,8 +484,11 @@ export function createSelfTaskEnvironmentProvider(
411
484
  return { handle, result: handle.descriptor };
412
485
  },
413
486
 
414
- async reconstruct(locator: TaskEnvironmentLocator): Promise<TaskEnvironmentHandle> {
415
- return new SelfTaskEnvironmentHandle(locator, deps);
487
+ async reconstruct(
488
+ locator: TaskEnvironmentLocator,
489
+ envMode: ProjectEnvMode = "baked",
490
+ ): Promise<TaskEnvironmentHandle> {
491
+ return new SelfTaskEnvironmentHandle(locator, deps, envMode);
416
492
  },
417
493
  };
418
494
  }