@runuai/host 0.9.150 → 0.9.152

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 (107) hide show
  1. package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
  2. package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
  3. package/db/migrations/meta/_journal.json +14 -0
  4. package/db/schema.ts +188 -0
  5. package/lib/agent-cli.ts +11 -13
  6. package/lib/agent.ts +177 -7
  7. package/lib/agents/durable-proc.ts +56 -44
  8. package/lib/agents/registry.ts +19 -3
  9. package/lib/agents/session-state.ts +23 -10
  10. package/lib/agents/transport.ts +101 -69
  11. package/lib/apple-one-off.ts +277 -0
  12. package/lib/apple-runtime-recycle.ts +4 -3
  13. package/lib/attachments.ts +19 -17
  14. package/lib/capture-flow.ts +319 -0
  15. package/lib/cloud-credential-cleanup-adapter.ts +123 -0
  16. package/lib/cloud-credential-host.ts +512 -0
  17. package/lib/cloud-credential-leases.ts +2544 -0
  18. package/lib/cloud-credential-runtime.ts +1035 -0
  19. package/lib/codex-auth.ts +21 -1
  20. package/lib/container-runtime.ts +76 -0
  21. package/lib/credential-cleanup-inventory.ts +405 -0
  22. package/lib/credential-consumers/engine-anchor.ts +1880 -0
  23. package/lib/credential-consumers/engines.ts +773 -0
  24. package/lib/credential-consumers/github.ts +594 -0
  25. package/lib/credential-consumers/mcp.ts +384 -0
  26. package/lib/credential-consumers/task-files.ts +127 -0
  27. package/lib/credential-store-pickup.ts +15 -2
  28. package/lib/credential-store.ts +361 -33
  29. package/lib/credentials-activation-authority.ts +546 -0
  30. package/lib/credentials-purge.ts +1006 -0
  31. package/lib/db.ts +11 -0
  32. package/lib/docker-exec.ts +3 -1
  33. package/lib/engine-accounts.ts +42 -32
  34. package/lib/engine-connect.ts +47 -2
  35. package/lib/engine-credential-env.ts +50 -0
  36. package/lib/engine-login.ts +83 -1
  37. package/lib/env-file.ts +62 -8
  38. package/lib/env.ts +5 -0
  39. package/lib/fs-aliases.ts +49 -0
  40. package/lib/git-clone-credentials.ts +334 -0
  41. package/lib/git-diff.ts +219 -177
  42. package/lib/git-identity.ts +59 -14
  43. package/lib/github-origin.ts +28 -0
  44. package/lib/github-tokens.ts +56 -21
  45. package/lib/host-env.ts +17 -4
  46. package/lib/launch-mode.ts +25 -0
  47. package/lib/legacy-runtime-retirement.ts +388 -0
  48. package/lib/machine-exec.ts +14 -3
  49. package/lib/machine-provider-aws.ts +50 -8
  50. package/lib/machine-provider-local.ts +4 -0
  51. package/lib/machine-provider.ts +4 -0
  52. package/lib/managed-operation-drain.ts +8 -0
  53. package/lib/managed-runtime.ts +371 -7
  54. package/lib/mcp-capture.ts +860 -0
  55. package/lib/mcp-connections.ts +8 -1
  56. package/lib/mcp-gateway.ts +168 -10
  57. package/lib/obs.ts +2 -6
  58. package/lib/orchestrator.ts +1837 -224
  59. package/lib/person-capture.ts +327 -0
  60. package/lib/project-env-legacy-tags.ts +130 -0
  61. package/lib/project-env-task-replica.ts +1752 -0
  62. package/lib/runner-boot.ts +10 -0
  63. package/lib/runtime-activation-tail.ts +50 -3
  64. package/lib/runtime-archive.ts +68 -1
  65. package/lib/runtime-reconnect.ts +86 -0
  66. package/lib/runtime-state.ts +429 -13
  67. package/lib/safe-http.ts +364 -0
  68. package/lib/secrets-proxy/task-trust.ts +34 -3
  69. package/lib/sidecar-resume.ts +18 -5
  70. package/lib/skills.ts +47 -8
  71. package/lib/standard-image.ts +157 -103
  72. package/lib/stopped-task-status-queue.ts +68 -10
  73. package/lib/task-credential-cleanup.ts +525 -0
  74. package/lib/task-diff-sandbox.ts +464 -0
  75. package/lib/task-diff.ts +66 -163
  76. package/lib/task-env-source.ts +68 -14
  77. package/lib/task-environment/anchor.ts +31 -0
  78. package/lib/task-environment/apple-container.ts +69 -28
  79. package/lib/task-environment/docker.ts +101 -20
  80. package/lib/task-environment/machine-environment.ts +61 -0
  81. package/lib/task-environment/machine-task-up.ts +459 -96
  82. package/lib/task-environment/machine.ts +101 -11
  83. package/lib/task-environment/self.ts +108 -50
  84. package/lib/task-environment/types.ts +89 -5
  85. package/lib/task-environment/workspace-files.ts +650 -25
  86. package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
  87. package/lib/task-gateway-recovery.ts +4 -1
  88. package/lib/task-gateway-state.ts +15 -0
  89. package/lib/task-machine-swap-safety.ts +729 -0
  90. package/lib/token-broker-adopt.ts +344 -0
  91. package/lib/token-broker-lambda.ts +56 -2
  92. package/lib/token-broker-local.ts +28 -4
  93. package/lib/token-broker-revision.ts +310 -0
  94. package/lib/token-broker-ssm.ts +368 -28
  95. package/lib/token-lanes/openai-oauth.ts +7 -3
  96. package/lib/token-lanes/xai-oauth.ts +7 -3
  97. package/lib/transcript.ts +9 -5
  98. package/package.json +6 -2
  99. package/scripts/agent/task-up.sh +242 -100
  100. package/src/cloud-credentials.ts +2399 -0
  101. package/src/event-outbox.ts +8 -6
  102. package/src/index.ts +1386 -875
  103. package/src/main.ts +1061 -139
  104. package/src/outbound-frame.ts +31 -0
  105. package/src/project-env-policy.ts +125 -10
  106. package/src/protocol.ts +699 -23
  107. package/src/ui/server.ts +48 -10
@@ -15,6 +15,7 @@ import {
15
15
  type TaskUpResult,
16
16
  } from "../agent";
17
17
  import { dockerCli, type DockerResult } from "../docker-exec";
18
+ import { isLaunchSecretEnvName } from "../engine-credential-env";
18
19
  import {
19
20
  hostWorkspaceRead,
20
21
  hostWorkspaceWrite,
@@ -23,10 +24,13 @@ import { taskDir } from "../env";
23
24
  import { PREVIEW_SIDECAR_TASK_LABEL } from "../preview-sidecar";
24
25
  import { assertSafeHostTaskId } from "../task-identity";
25
26
  import { applyProjectEnvProfile } from "../runtime-authority";
27
+ import { rootAnchoredRequest } from "./anchor";
26
28
  import {
27
29
  assertTaskEnvironmentExecRequest,
28
30
  assertTaskEnvironmentProcessRequest,
29
31
  assertTaskEnvironmentSessionRequest,
32
+ type TaskEnvironmentCallOptions,
33
+ type TaskEnvironmentReadOptions,
30
34
  type TaskEnvironmentCopyRequest,
31
35
  type TaskEnvironmentDescriptor,
32
36
  type TaskEnvironmentDetachedSessionRequest,
@@ -45,6 +49,7 @@ import {
45
49
  type TaskEnvironmentSpawnRequest,
46
50
  type TaskEnvironmentStatus,
47
51
  } from "./types";
52
+ import type { TaskWriteEnvironment } from "../credential-consumers/task-files";
48
53
 
49
54
  export const DOCKER_TASK_ENVIRONMENT_PROVIDER = "docker-compose";
50
55
  const DOCKER_LOCATOR_SCHEMA_VERSION = 2;
@@ -77,6 +82,7 @@ export interface DockerTaskEnvironmentDeps {
77
82
  input: TaskLaunchInput,
78
83
  credentials?: TaskUpCredentials,
79
84
  envMode?: ProjectEnvMode,
85
+ runtimeEnvironment?: () => TaskWriteEnvironment,
80
86
  ) => Promise<TaskUpResult>;
81
87
  taskEnvironmentDown?: (taskId: string) => Promise<TaskDownResult>;
82
88
  taskStatus?: (taskId: string) => Promise<TaskStatusResult>;
@@ -131,6 +137,9 @@ export function createDockerTaskEnvironmentProvider(
131
137
  request.input,
132
138
  request.credentials,
133
139
  request.envMode ?? "loader",
140
+ // ADR-154 §12: the container as task-up has created it, for the
141
+ // runtime-ready writes before uai-init.
142
+ () => new DockerTaskEnvironmentHandle(preparedLocator, resolved, request.envMode ?? "loader"),
134
143
  );
135
144
  const prepared = parseDockerTaskEnvironmentLocator(preparedLocator);
136
145
  // task-up composes its worktree path in shell from the operator's
@@ -254,6 +263,8 @@ class DockerTaskEnvironmentHandle
254
263
  {
255
264
  readonly descriptor: TaskEnvironmentDescriptor;
256
265
  readonly durableIdentity: string;
266
+ /** ADR-154 §6: anchored requests start under the engine supervisor. */
267
+ readonly anchors = true;
257
268
  readonly #value: DockerTaskEnvironmentLocatorValue;
258
269
 
259
270
  constructor(
@@ -289,16 +300,19 @@ class DockerTaskEnvironmentHandle
289
300
  };
290
301
  }
291
302
 
292
- async readWorkspaceFile(filePath: string): Promise<Uint8Array | null> {
303
+ async readWorkspaceFile(filePath: string, options?: TaskEnvironmentReadOptions): Promise<Uint8Array | null> {
304
+ options?.admit?.();
293
305
  return hostWorkspaceRead(
294
306
  this.descriptor,
295
307
  path.join(this.#value.hostWorktreePath, "workspace"),
296
308
  filePath,
309
+ options,
297
310
  );
298
311
  }
299
312
 
300
- async writeWorkspaceFile(filePath: string, bytes: Uint8Array): Promise<void> {
301
- hostWorkspaceWrite(
313
+ async writeWorkspaceFile(filePath: string, bytes: Uint8Array, options?: TaskEnvironmentCallOptions): Promise<void> {
314
+ options?.admit?.();
315
+ await hostWorkspaceWrite(
302
316
  this.descriptor,
303
317
  path.join(this.#value.hostWorktreePath, "workspace"),
304
318
  filePath,
@@ -310,16 +324,23 @@ class DockerTaskEnvironmentHandle
310
324
  assertTaskEnvironmentExecRequest(request);
311
325
  const { privileged, ...unprivileged } = request;
312
326
  const profiled = applyProjectEnvProfile(
313
- unprivileged,
327
+ rootAnchoredRequest(unprivileged),
314
328
  this.descriptor.envMode ?? "baked",
315
329
  "/usr/bin/zsh",
316
330
  );
317
331
  request.admit?.();
332
+ const invocation = dockerExecArgs(
333
+ this.#value.containerName,
334
+ profiled,
335
+ "interactive",
336
+ privileged === true,
337
+ );
318
338
  return capturedCliExec(
319
339
  this.deps.spawn,
320
340
  "docker",
321
- dockerExecArgs(this.#value.containerName, profiled, "interactive", privileged === true),
341
+ invocation.args,
322
342
  profiled,
343
+ invocation.env,
323
344
  );
324
345
  }
325
346
 
@@ -328,15 +349,18 @@ class DockerTaskEnvironmentHandle
328
349
  ): Promise<TaskEnvironmentProcess> {
329
350
  assertTaskEnvironmentProcessRequest(request);
330
351
  const profiled = applyProjectEnvProfile(
331
- request,
352
+ rootAnchoredRequest(request),
332
353
  this.descriptor.envMode ?? "baked",
333
354
  "/usr/bin/zsh",
334
355
  );
356
+ const invocation = dockerExecArgs(this.#value.containerName, profiled);
357
+ request.admit?.();
335
358
  return streamingCliExec(
336
359
  this.deps.spawn,
337
360
  "docker",
338
- dockerExecArgs(this.#value.containerName, profiled),
361
+ invocation.args,
339
362
  profiled,
363
+ invocation.env,
340
364
  );
341
365
  }
342
366
 
@@ -345,15 +369,18 @@ class DockerTaskEnvironmentHandle
345
369
  ): Promise<TaskEnvironmentProcess> {
346
370
  assertTaskEnvironmentSessionRequest(request);
347
371
  const profiled = applyProjectEnvProfile(
348
- request,
372
+ rootAnchoredRequest(request),
349
373
  this.descriptor.envMode ?? "baked",
350
374
  "/usr/bin/zsh",
351
375
  );
376
+ const invocation = dockerExecArgs(this.#value.containerName, profiled);
377
+ request.admit?.();
352
378
  return streamingCliExec(
353
379
  this.deps.spawn,
354
380
  "docker",
355
- dockerExecArgs(this.#value.containerName, profiled),
381
+ invocation.args,
356
382
  profiled,
383
+ invocation.env,
357
384
  );
358
385
  }
359
386
 
@@ -362,17 +389,21 @@ class DockerTaskEnvironmentHandle
362
389
  ): Promise<TaskEnvironmentExecResult> {
363
390
  assertTaskEnvironmentSessionRequest(request);
364
391
  const profiled = applyProjectEnvProfile(
365
- request,
392
+ rootAnchoredRequest(request),
366
393
  this.descriptor.envMode ?? "baked",
367
394
  "/usr/bin/zsh",
368
395
  );
369
- const result = await this.deps.docker(
370
- dockerExecArgs(this.#value.containerName, profiled, "detached"),
371
- {
372
- timeoutMs: request.launchTimeoutMs,
373
- maxOutputBytes: request.maxOutputBytes,
374
- },
396
+ const invocation = dockerExecArgs(
397
+ this.#value.containerName,
398
+ profiled,
399
+ "detached",
375
400
  );
401
+ request.admit?.();
402
+ const result = await this.deps.docker(invocation.args, {
403
+ timeoutMs: request.launchTimeoutMs,
404
+ maxOutputBytes: request.maxOutputBytes,
405
+ ...(invocation.env === undefined ? {} : { env: invocation.env }),
406
+ });
376
407
  return {
377
408
  exitCode: result.status,
378
409
  signal: null,
@@ -397,6 +428,7 @@ class DockerTaskEnvironmentHandle
397
428
  `${this.#value.containerName}:${request.source}`,
398
429
  request.destination,
399
430
  ];
431
+ request.admit?.();
400
432
  const result = await this.deps.docker(args, {
401
433
  timeoutMs: 120_000,
402
434
  maxOutputBytes: 1024 * 1024,
@@ -634,6 +666,8 @@ function requiredPort(value: unknown, name: string): number {
634
666
  return Number(value);
635
667
  }
636
668
 
669
+ /** The argv of one `docker exec`, and the environment the docker CLI must run
670
+ * with to resolve the credentials it names (cliExecEnv). */
637
671
  function dockerExecArgs(
638
672
  containerName: string,
639
673
  request:
@@ -644,21 +678,60 @@ function dockerExecArgs(
644
678
  mode: "interactive" | "detached" = "interactive",
645
679
  /** Passed only by exec(), after assertTaskEnvironmentExecRequest. */
646
680
  privileged = false,
647
- ): string[] {
681
+ ): { args: string[]; env: NodeJS.ProcessEnv | undefined } {
648
682
  const args = ["exec", mode === "detached" ? "-d" : "-i"];
649
683
  if (privileged) args.push("--privileged");
650
684
  if (request.cwd) args.push("--workdir", request.cwd);
651
685
  if (request.user) args.push("--user", request.user);
686
+ const execEnv = cliExecEnv(request);
687
+ args.push(...execEnv.args, containerName, ...request.argv);
688
+ return { args, env: execEnv.env };
689
+ }
690
+
691
+ /**
692
+ * The `--env` options of one exec, for a CLI that resolves a bare
693
+ * `--env NAME` from its own environment (docker, Apple `container`), and the
694
+ * environment the CLI child must then run with.
695
+ *
696
+ * An engine credential, or an agent's task token under any of its names, is
697
+ * named bare and its value set in that child's environment only, so it never
698
+ * lands on the runner's argv, which every local user can read with `ps`
699
+ * (ADR-154 §6). The request's value wins over the
700
+ * runner's own variable of the same name (an owned runner may still carry a
701
+ * legacy CLAUDE_CODE_OAUTH_TOKEN), and a credential the request does not
702
+ * carry is never named here; a name in `inheritEnv` still forwards the
703
+ * runner's own value, as that field promises. Every other entry keeps its
704
+ * `NAME=value` form: in the child's environment, a task's HOME or PATH would
705
+ * steer the CLI's own config and credential-helper lookup. With no
706
+ * credential, `env` is undefined and the child inherits the runner's
707
+ * environment as before.
708
+ */
709
+ export function cliExecEnv(request: {
710
+ inheritEnv?: readonly string[];
711
+ env?: Readonly<Record<string, string>>;
712
+ }): { args: string[]; env: NodeJS.ProcessEnv | undefined } {
713
+ const args: string[] = [];
652
714
  for (const name of request.inheritEnv ?? []) {
653
715
  if (process.env[name] !== undefined) args.push("--env", name);
654
716
  }
717
+ const credentials: Record<string, string> = {};
655
718
  for (const [name, value] of Object.entries(request.env ?? {}).sort(([a], [b]) =>
656
719
  a.localeCompare(b),
657
720
  )) {
658
- args.push("--env", `${name}=${value}`);
721
+ if (isLaunchSecretEnvName(name)) {
722
+ args.push("--env", name);
723
+ credentials[name] = value;
724
+ } else {
725
+ args.push("--env", `${name}=${value}`);
726
+ }
659
727
  }
660
- args.push(containerName, ...request.argv);
661
- return args;
728
+ return {
729
+ args,
730
+ env:
731
+ Object.keys(credentials).length === 0
732
+ ? undefined
733
+ : { ...process.env, ...credentials },
734
+ };
662
735
  }
663
736
 
664
737
  /** Bounded one-shot exec, generalized over the CLI so the Apple container
@@ -668,10 +741,14 @@ export function capturedCliExec(
668
741
  command: string,
669
742
  args: readonly string[],
670
743
  request: TaskEnvironmentExecRequest,
744
+ /** The child's own environment, when it must differ from the runner's
745
+ * (cliExecEnv). */
746
+ env?: NodeJS.ProcessEnv,
671
747
  ): Promise<TaskEnvironmentExecResult> {
672
748
  return new Promise((resolve) => {
673
749
  const child = spawn(command, [...args], {
674
750
  stdio: ["pipe", "pipe", "pipe"],
751
+ ...(env === undefined ? {} : { env }),
675
752
  }) as ChildProcessWithoutNullStreams;
676
753
  let stdout: Buffer[] = [];
677
754
  let stderr: Buffer[] = [];
@@ -739,9 +816,12 @@ export function streamingCliExec(
739
816
  command: string,
740
817
  args: readonly string[],
741
818
  request: TaskEnvironmentSpawnRequest | TaskEnvironmentSessionRequest,
819
+ /** As for capturedCliExec. */
820
+ env?: NodeJS.ProcessEnv,
742
821
  ): TaskEnvironmentProcess {
743
822
  const child = spawn(command, [...args], {
744
823
  stdio: ["pipe", "pipe", "pipe"],
824
+ ...(env === undefined ? {} : { env }),
745
825
  }) as ChildProcessWithoutNullStreams;
746
826
  // Every kill forces settlement within a bounded grace, like the captured
747
827
  // runner: a wedged client must not leave an agent session's completion
@@ -997,6 +1077,7 @@ async function inspectDockerTaskEnvironmentStatus(
997
1077
  state: state === "running" ? "running" : "stopped",
998
1078
  instances: [environment.containerName],
999
1079
  workspacePresent,
1080
+ ...(state === "paused" || state === "restarting" ? { frozen: true as const } : {}),
1000
1081
  };
1001
1082
  }
1002
1083
 
@@ -0,0 +1,61 @@
1
+ /**
2
+ * A task machine's machine-wide environment, /etc/environment, which sshd's
3
+ * pam_env applies to every session on it: what a self or machine task's
4
+ * processes run beneath their own `env`.
5
+ *
6
+ * ADR-154 §6: it never supplies an engine's credential. A machine-wide value
7
+ * of one (an owner's sign-in left in an older image) never reaches a task's
8
+ * process, leased or not: a leased engine's comes from its admission, and an
9
+ * unleased one's from its own request.
10
+ */
11
+ import { isEngineCredentialEnvName, isLaunchSecretEnvName } from "../engine-credential-env";
12
+
13
+ export const ETC_ENVIRONMENT_PATH = "/etc/environment";
14
+
15
+ /** What an anchored launch's environment starts from where nothing else
16
+ * gives a PATH: the usual system one. */
17
+ export const BASELINE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
18
+
19
+ /**
20
+ * /etc/environment, as pam_env reads it: `KEY=value` lines, optional
21
+ * `export`, optional single or double quotes, `#` comments.
22
+ */
23
+ export function parseEtcEnvironment(text: string): Record<string, string> {
24
+ const out: Record<string, string> = {};
25
+ for (const raw of text.split("\n")) {
26
+ const line = raw.trim();
27
+ if (!line || line.startsWith("#")) continue;
28
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
29
+ if (!match) continue;
30
+ let value = match[2]!.trim();
31
+ if (
32
+ value.length >= 2 &&
33
+ ((value.startsWith('"') && value.endsWith('"')) ||
34
+ (value.startsWith("'") && value.endsWith("'")))
35
+ ) {
36
+ value = value.slice(1, -1);
37
+ }
38
+ out[match[1]!] = value;
39
+ }
40
+ return out;
41
+ }
42
+
43
+ /** `environment` without any engine's credential. */
44
+ export function withoutEngineCredentials(
45
+ environment: Readonly<Record<string, string>>,
46
+ ): Record<string, string> {
47
+ return Object.fromEntries(Object.entries(environment).filter(([name]) => !isEngineCredentialEnvName(name)));
48
+ }
49
+
50
+ /**
51
+ * `environment` without any launch secret: an engine's credential, or an
52
+ * agent's task token under any of its names. What an anchored launch's
53
+ * baseline takes from the machine-wide environment, which rides its command
54
+ * line: its secrets come from its admission. An unanchored launch keeps the
55
+ * machine-wide values it always had, but for an engine's credential.
56
+ */
57
+ export function withoutLaunchSecrets(
58
+ environment: Readonly<Record<string, string>>,
59
+ ): Record<string, string> {
60
+ return Object.fromEntries(Object.entries(environment).filter(([name]) => !isLaunchSecretEnvName(name)));
61
+ }