@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
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+ # Refresh ADR-153 project env in this process, then replace it with the task
3
+ # command. The mounted production name is /usr/local/bin/uai-env-exec.
4
+
5
+ if [ "$#" -eq 0 ]; then
6
+ printf '%s\n' 'usage: uai-env-exec COMMAND [ARG ...]' >&2
7
+ exit 64
8
+ fi
9
+
10
+ if [ "${UAI_ENV_HOOK-0}" = "1" ]; then
11
+ __uai_hook=/usr/local/share/uai/env-hook.sh
12
+ if [ "${UAI_ENV_HOOK_TESTING-}" = "1" ] &&
13
+ [ -n "${UAI_ENV_TEST_HOOK_PATH-}" ]; then
14
+ __uai_hook=$UAI_ENV_TEST_HOOK_PATH
15
+ fi
16
+ if [ ! -r "$__uai_hook" ]; then
17
+ printf '%s\n' 'uai-env-exec: project environment hook is unavailable' >&2
18
+ exit 127
19
+ fi
20
+ # shellcheck source=/dev/null
21
+ . "$__uai_hook"
22
+ __uai_env_sync
23
+ unset __uai_hook
24
+ fi
25
+
26
+ exec -- "$@"
package/src/cli.ts CHANGED
@@ -53,13 +53,14 @@ import {
53
53
  import {
54
54
  abortManagedRuntimeInstall,
55
55
  acknowledgeManagedRuntimeActivation,
56
+ acknowledgeManagedRuntimeActivationWithCapabilityFloor,
56
57
  beginManagedRuntimeInstall,
57
58
  completeManagedRuntimeInstall,
58
59
  installManagedRuntimeArchive,
59
60
  managedRuntimeInstallStartupState,
60
61
  managedRuntimePaths,
61
62
  nativeReleasePlatform,
62
- reconcileManagedRuntimeActivation,
63
+ reconcileManagedRuntimeActivationWithCapabilityFloor,
63
64
  reconcileManagedRuntimeInstallLifecycle,
64
65
  reconcileManagedRuntimeUninstall,
65
66
  removeManagedRuntime,
@@ -76,6 +77,7 @@ import {
76
77
  type ManagedServiceManagerState,
77
78
  type ReconcileManagedRuntimeInstallLifecycleOptions,
78
79
  } from "../lib/managed-runtime";
80
+ import { PROJECT_ENV_BINDINGS_RUNTIME_CAPABILITY } from "../lib/project-env-feature";
79
81
  import { teardownAppleRuntimeStateForUninstall } from "../lib/apple-uninstall-teardown";
80
82
  import {
81
83
  readRuntimeProviderState,
@@ -269,7 +271,14 @@ async function cmdRun(rest: string[] = []): Promise<void> {
269
271
  ) {
270
272
  const activationWasPending = existsSync(managedPaths.activationPending);
271
273
  try {
272
- if (!(await acknowledgeManagedRuntimeActivation(runningVersion))) {
274
+ const acknowledged = activationWasPending
275
+ ? await acknowledgeManagedRuntimeActivationWithCapabilityFloor(
276
+ runningVersion,
277
+ PROJECT_ENV_BINDINGS_RUNTIME_CAPABILITY,
278
+ host.completeProjectEnvBindingsAfterManagedRuntimeFloor,
279
+ )
280
+ : await acknowledgeManagedRuntimeActivation(runningVersion);
281
+ if (!acknowledged) {
273
282
  // Selection changed while the service module was loading. Its task
274
283
  // admission remains closed by the marker; drain and let the supervisor
275
284
  // start the newly selected stable command.
@@ -277,6 +286,7 @@ async function cmdRun(rest: string[] = []): Promise<void> {
277
286
  return;
278
287
  }
279
288
  if (activationWasPending) {
289
+ host.advertiseProjectEnvBindingsAfterManagedRuntimeActivation();
280
290
  console.log(
281
291
  `[host-agent] managed runtime ${runningVersion} activated (pending activation acknowledged)`,
282
292
  );
@@ -295,7 +305,20 @@ async function cmdRun(rest: string[] = []): Promise<void> {
295
305
  new ManagedActivationWatcher({
296
306
  restartRequired: async () => {
297
307
  if (localManagedUpdateRunning) return false;
298
- return reconcileManagedRuntimeActivation(runningVersion);
308
+ let capabilityCompleted = false;
309
+ const restartRequired =
310
+ await reconcileManagedRuntimeActivationWithCapabilityFloor(
311
+ runningVersion,
312
+ PROJECT_ENV_BINDINGS_RUNTIME_CAPABILITY,
313
+ () => {
314
+ host.completeProjectEnvBindingsAfterManagedRuntimeFloor();
315
+ capabilityCompleted = true;
316
+ },
317
+ );
318
+ if (capabilityCompleted) {
319
+ host.advertiseProjectEnvBindingsAfterManagedRuntimeActivation();
320
+ }
321
+ return restartRequired;
299
322
  },
300
323
  isIdle: host.isIdleForManagedHostUpdate,
301
324
  onReady: host.requestManagedHostRestart,
package/src/index.ts CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  import { readAttachment, writeAttachment } from "../lib/attachments";
32
32
  import { readFileSync } from "node:fs";
33
33
  import { resolve } from "node:path";
34
- import { taskProjectWorktree } from "../lib/env";
34
+ import { env, taskProjectWorktree } from "../lib/env";
35
35
  import { getDecryptedForProject } from "../lib/host-env";
36
36
  import {
37
37
  envSchemaNote,
@@ -46,20 +46,39 @@ import { buildTaskDiff } from "../lib/task-diff";
46
46
  import {
47
47
  deleteHostTask,
48
48
  getHostTask,
49
+ hostTaskEnvMode,
49
50
  recordHostEvent,
50
51
  recordTaskDown,
51
52
  recordTaskEnvironmentPrepared,
52
53
  recordTaskError,
53
54
  recordTaskOwner,
55
+ recordTaskProjectEnvSpec,
54
56
  recordTaskStarting,
55
57
  recordTaskUpResult,
58
+ taskProjectEnvSpec,
56
59
  purgeHostTaskState,
57
60
  upsertHostTask,
58
61
  } from "../lib/runtime-state";
59
62
  import {
60
63
  clearTaskGatewayAcl,
61
64
  clearTaskGatewayAclStrict,
65
+ waitForMcpGatewayReady,
62
66
  } from "../lib/mcp-gateway";
67
+ import {
68
+ recoverTaskGatewayTask,
69
+ registerTaskGatewayTask,
70
+ reactivateTaskGatewayTask,
71
+ removeTaskGatewayTask,
72
+ suspendTaskGatewayTask,
73
+ taskGatewayForwarderSecretInput,
74
+ } from "../lib/task-gateway-state";
75
+ import { captureTaskEnvSchemas, forgetTaskEnvSchemaObservations } from "../lib/project-env-schema-capture";
76
+ import { forgetTaskEnvSnapshot, taskEnvResolver, taskEnvWithheldNoteNow } from "../lib/task-env-source";
77
+ import { activeSecretsProxy } from "../lib/secrets-proxy/host";
78
+ import { secretsProxyEnabled } from "../lib/secrets-proxy/runtime";
79
+ import { forgetTaskProxyReadiness } from "../lib/secrets-proxy/readiness";
80
+ import { admitFreshTaskView } from "../lib/secrets-proxy/sidecar-view";
81
+ import { invalidateTaskWrites, reconcileTaskProxyState } from "../lib/secrets-proxy/task-trust";
63
82
  import { parsePreviewPortRuntimes } from "../lib/preview-ports";
64
83
  import { dockerCli } from "../lib/docker-exec";
65
84
  import {
@@ -186,6 +205,9 @@ export type {
186
205
  } from "../lib/preview-ports";
187
206
  import { isEnvironmentTransportProvider } from "../lib/task-environment/types";
188
207
  import { awaitSidecarReady, sidecarMode } from "../lib/sidecar";
208
+ import { guardSidecarMachine } from "../lib/sidecar-guard";
209
+ import { projectEnvBindingCapability } from "../lib/project-env-feature";
210
+ import { awaitProjectEnvReplicaReady } from "../lib/project-env-replica";
189
211
 
190
212
  export const hostEvents: HostEventStream = {
191
213
  subscribe(handler) {
@@ -297,6 +319,62 @@ export const hostCommands: HostCommands = {
297
319
  if (readyAdmissionFailure) return readyAdmissionFailure;
298
320
  const readyRuntimeFailure = runtimeUnavailable();
299
321
  if (readyRuntimeFailure) return readyRuntimeFailure;
322
+ try {
323
+ await waitForMcpGatewayReady();
324
+ } catch (error) {
325
+ console.warn(
326
+ `[task-gateway] refusing task ${input.task.id} before provisioning: ${
327
+ error instanceof Error ? error.message : String(error)
328
+ }`,
329
+ );
330
+ return {
331
+ ok: false,
332
+ code: HostErrorCode.HostUnavailable,
333
+ message:
334
+ "The task environment gateway is not ready on this host. Retry the task; if it persists, check the host service log.",
335
+ retryable: true,
336
+ };
337
+ }
338
+ // A task machine's own guards first: its runner's data directory root's
339
+ // alone, and the metadata service closed to the task's user. Without
340
+ // them the task does not start: it could read the sidecar's host token.
341
+ if (sidecarMode()) {
342
+ const guard = guardSidecarMachine({ dataDir: env.dataDir });
343
+ if (!guard.ok) {
344
+ console.warn(`[sidecar] refusing task ${input.task.id}: ${guard.reason}`);
345
+ return {
346
+ ok: false,
347
+ code: HostErrorCode.Internal,
348
+ message: `This task machine is not safe to run the task on: ${guard.reason}. Close the task and start it again on a new machine.`,
349
+ retryable: false,
350
+ };
351
+ }
352
+ }
353
+ // A bindings-capable task sidecar must activate and commit its exact
354
+ // replica before any task state, /env generation or process can exist.
355
+ // Otherwise the source's intentional pre-activation legacy fallback
356
+ // could expose stored plaintext during the first reconciliation.
357
+ if (sidecarMode()) {
358
+ try {
359
+ await awaitSidecarReady();
360
+ if (projectEnvBindingCapability() !== null) {
361
+ await awaitProjectEnvReplicaReady();
362
+ }
363
+ } catch (error) {
364
+ console.warn(
365
+ `[task-env] refusing task ${input.task.id} before provisioning: ${
366
+ error instanceof Error ? error.message : String(error)
367
+ }`,
368
+ );
369
+ return {
370
+ ok: false,
371
+ code: HostErrorCode.HostUnavailable,
372
+ message:
373
+ "This task machine is still synchronizing its environment. Retry shortly.",
374
+ retryable: true,
375
+ };
376
+ }
377
+ }
300
378
  // ADR-118 follow-up: each apple-container task is its own VM, and each
301
379
  // VM pins host vnodes for its life. Refuse to start ANOTHER one when the
302
380
  // table is nearly full — a clear, retryable refusal now beats ENFILE
@@ -327,6 +405,19 @@ export const hostCommands: HostCommands = {
327
405
  // that lost-response retry as the successful operation it already was:
328
406
  // every cloud caller records a non-ok taskUp as an errored task.
329
407
  const existingTask = getHostTask(input.task.id);
408
+ const incomingEnvSpec = taskProjectEnvSpec(input);
409
+ let existingGatewayReady = true;
410
+ if (existingTask && hostTaskEnvMode(existingTask) === "loader") {
411
+ // A lost-response retry is also the repair path after a host process
412
+ // restart: reattach the live resolver before any task process can be
413
+ // treated as reusable. Missing/corrupt durable identity forces normal
414
+ // recreation, which mints a generation and restarts its forwarder.
415
+ recordTaskProjectEnvSpec(input.task.id, input);
416
+ existingGatewayReady =
417
+ recoverTaskGatewayTask(input.task.id, {
418
+ resolveEnv: taskEnvResolver(input.task.id, incomingEnvSpec),
419
+ }) !== null;
420
+ }
330
421
  let runtimeQuarantinedForRecreate = false;
331
422
  let channelClosedForRecreate = false;
332
423
  if (
@@ -370,6 +461,7 @@ export const hostCommands: HostCommands = {
370
461
  if (environment === null) {
371
462
  // The persisted locator no longer reconstructs (e.g. an engine
372
463
  // identity change) — recreate through the normal path.
464
+ suspendTaskGatewayTask(input.task.id);
373
465
  await orchestrator.closeChannel(input.task.id);
374
466
  // Session drain can outlive the generation too: a superseded pass
375
467
  // must not reach recordTaskStarting/provisioning below.
@@ -412,6 +504,7 @@ export const hostCommands: HostCommands = {
412
504
  ? input.projects
413
505
  : []) {
414
506
  const gitHealth = await environment.exec({
507
+ projectEnvProfile: "maintenance",
415
508
  argv: [
416
509
  "/usr/bin/env",
417
510
  "-i",
@@ -450,7 +543,11 @@ export const hostCommands: HostCommands = {
450
543
  break;
451
544
  }
452
545
  }
453
- if (verdict.kind === "holds" && projectsHealthy) {
546
+ if (
547
+ verdict.kind === "holds" &&
548
+ projectsHealthy &&
549
+ existingGatewayReady
550
+ ) {
454
551
  if (fastStale()) return staleRetry;
455
552
  orchestrator.allowChannel(input.task.id);
456
553
  return {
@@ -462,12 +559,14 @@ export const hostCommands: HostCommands = {
462
559
  previewPorts: parsePreviewPortRuntimes(
463
560
  existingTask.previewPorts,
464
561
  ),
562
+ envMode: hostTaskEnvMode(existingTask),
465
563
  },
466
564
  };
467
565
  }
468
566
  // Violated contract or broken project repo: drain, stop, and
469
567
  // prove before task-up recreates the same stable name.
470
568
  if (fastStale()) return staleRetry;
569
+ suspendTaskGatewayTask(input.task.id);
471
570
  await orchestrator.closeChannel(input.task.id);
472
571
  channelClosedForRecreate = true;
473
572
  if (fastStale()) return staleRetry;
@@ -494,6 +593,7 @@ export const hostCommands: HostCommands = {
494
593
  // recreate — and the rollback state must say stopped for the
495
594
  // same reason as above.
496
595
  if (fastStale()) return staleRetry;
596
+ suspendTaskGatewayTask(input.task.id);
497
597
  await orchestrator.closeChannel(input.task.id);
498
598
  // Fence AFTER the drain as well: provisioning must never start
499
599
  // under a generation this pass no longer owns.
@@ -517,7 +617,7 @@ export const hostCommands: HostCommands = {
517
617
  );
518
618
  if (environment) {
519
619
  const machineStatus = await environment.status();
520
- if (machineStatus.state === "running") {
620
+ if (machineStatus.state === "running" && existingGatewayReady) {
521
621
  orchestrator.allowChannel(input.task.id);
522
622
  return {
523
623
  ok: true,
@@ -525,6 +625,7 @@ export const hostCommands: HostCommands = {
525
625
  composeProject: existingTask.composeProject ?? "",
526
626
  worktreePath: existingTask.worktreePath ?? "",
527
627
  codeServerPort: existingTask.codeServerPort ?? undefined,
628
+ envMode: hostTaskEnvMode(existingTask),
528
629
  },
529
630
  };
530
631
  }
@@ -597,12 +698,14 @@ export const hostCommands: HostCommands = {
597
698
  !runtimeContract.restarting &&
598
699
  !runtimeContract.paused &&
599
700
  runtimeContract.sharedMount === "read-only" &&
600
- runtimeContract.taskEnvironment === "current";
701
+ runtimeContract.taskEnvironment === "current" &&
702
+ existingGatewayReady;
601
703
  if (!reusable) {
602
704
  // Drain/tombstone before touching the exact container. task-up's
603
705
  // pre-materialization cleanup then removes this stopped immutable
604
706
  // generation, so Compose must create the current RO/private-prefix
605
707
  // contract rather than `docker start`ing it again.
708
+ suspendTaskGatewayTask(input.task.id);
606
709
  await orchestrator.closeChannel(input.task.id);
607
710
  channelClosedForRecreate = true;
608
711
  const quarantined = await quarantineTaskRuntimeContainer({
@@ -740,6 +843,7 @@ export const hostCommands: HostCommands = {
740
843
  previewPorts: parsePreviewPortRuntimes(
741
844
  existingTask.previewPorts,
742
845
  ),
846
+ envMode: hostTaskEnvMode(existingTask),
743
847
  },
744
848
  };
745
849
  }
@@ -749,6 +853,7 @@ export const hostCommands: HostCommands = {
749
853
  // Drain and invalidate any stale in-memory channel before taskUp
750
854
  // recreates the same stable compose/container names and repairs Git.
751
855
  if (!channelClosedForRecreate) {
856
+ suspendTaskGatewayTask(input.task.id);
752
857
  await orchestrator.closeChannel(input.task.id);
753
858
  }
754
859
  }
@@ -763,6 +868,8 @@ export const hostCommands: HostCommands = {
763
868
  endedAt: existingTask.endedAt,
764
869
  environmentProvider: existingTask.environmentProvider,
765
870
  environmentLocator: existingTask.environmentLocator,
871
+ envMode: existingTask.envMode,
872
+ projectEnvSpec: existingTask.projectEnvSpec,
766
873
  }
767
874
  : {
768
875
  statusMirror: existingTask.statusMirror,
@@ -771,6 +878,8 @@ export const hostCommands: HostCommands = {
771
878
  endedAt: existingTask.endedAt,
772
879
  environmentProvider: existingTask.environmentProvider,
773
880
  environmentLocator: existingTask.environmentLocator,
881
+ envMode: existingTask.envMode,
882
+ projectEnvSpec: existingTask.projectEnvSpec,
774
883
  }
775
884
  : {
776
885
  statusMirror: input.task.status,
@@ -778,6 +887,11 @@ export const hostCommands: HostCommands = {
778
887
  startedAt: null,
779
888
  endedAt: null,
780
889
  };
890
+ // ADR-153 machines: a task with no row has never run on this machine,
891
+ // so everything it runs comes through its hosts view. Recorded before
892
+ // the row, and so before the task's first process; a task with a row
893
+ // (a start driven again, a start after a stop) stands as recorded.
894
+ if (sidecarMode() && !existingTask) admitFreshTaskView(input.task.id);
781
895
  recordTaskStarting(input.task.id);
782
896
  // Cross-process managed maintenance and task admission use a two-sided
783
897
  // handshake: update/uninstall writes its durable marker before checking
@@ -806,6 +920,7 @@ export const hostCommands: HostCommands = {
806
920
  .sort((a, b) => a.position - b.position)
807
921
  .map((p) => p.id),
808
922
  );
923
+ recordTaskProjectEnvSpec(input.task.id, input);
809
924
  // ADR-048: persist the cloud-issued per-task cli secret (host-private) so the
810
925
  // `uai` CLI's per-agent tokens can be minted now + after a host restart.
811
926
  if (input.task.cliSecret) {
@@ -813,6 +928,7 @@ export const hostCommands: HostCommands = {
813
928
  }
814
929
  recordHostEvent(input.task.id, "task.created");
815
930
  let environmentLocator: TaskEnvironmentLocator | undefined;
931
+ let registeredTaskGatewayGeneration: string | null = null;
816
932
  const result = await wrapAgent(ctx, "taskUp", async () => {
817
933
  // Self-heal the standard base image before task-up needs it. The
818
934
  // boot-time build is best-effort + one-shot: a host that started before
@@ -848,9 +964,6 @@ export const hostCommands: HostCommands = {
848
964
  "GitHub is connected on this host, but Uai could not prepare its credential for Git. Retry the task; if it persists, reconnect GitHub on this host.",
849
965
  );
850
966
  }
851
- // ADR-133 sidecar: nothing to clone or spawn with until the lease
852
- // has landed and the store is reconciled under it.
853
- await awaitSidecarReady();
854
967
  if (sidecarMode()) {
855
968
  // ADR-133: project env lives under /uai/projects/<id>, readable by
856
969
  // the lease only for parameters tagged with this org, and only the
@@ -870,6 +983,24 @@ export const hostCommands: HostCommands = {
870
983
  );
871
984
  }
872
985
  }
986
+ // Mint the authenticated task generation before its environment
987
+ // starts. The declarations are pinned; values remain live DB reads.
988
+ const gatewayRegistration = registerTaskGatewayTask(input.task.id, {
989
+ resolveEnv: taskEnvResolver(input.task.id, incomingEnvSpec),
990
+ });
991
+ registeredTaskGatewayGeneration = gatewayRegistration.generation;
992
+ const gatewaySecret = taskGatewayForwarderSecretInput(
993
+ input.task.id,
994
+ gatewayRegistration.generation,
995
+ );
996
+ if (gatewaySecret === null) {
997
+ removeTaskGatewayTask(
998
+ input.task.id,
999
+ gatewayRegistration.generation,
1000
+ );
1001
+ registeredTaskGatewayGeneration = null;
1002
+ throw new Error("could not export the task gateway generation secret");
1003
+ }
873
1004
  // Launch screen: the first stage boundary. A machine-backed task
874
1005
  // spends the next minute launching an EC2 instance; a container
875
1006
  // task goes straight to preparing its workspace.
@@ -878,22 +1009,68 @@ export const hostCommands: HostCommands = {
878
1009
  machineTasksEnabled() ? "machine" : "workspace",
879
1010
  );
880
1011
  try {
1012
+ const taskGateway = {
1013
+ generation: gatewayRegistration.generation,
1014
+ secret: gatewaySecret,
1015
+ };
881
1016
  const provisioned = gitCredential
882
1017
  ? await gitCredential.run(() =>
883
1018
  provisionTaskEnvironment(
884
1019
  input,
885
- { githubCredentialSocket: gitCredential.socketPath },
1020
+ {
1021
+ githubCredentialSocket: gitCredential.socketPath,
1022
+ taskGateway,
1023
+ },
886
1024
  async (locator) => {
887
- recordTaskEnvironmentPrepared(input.task.id, locator);
1025
+ recordTaskEnvironmentPrepared(
1026
+ input.task.id,
1027
+ locator,
1028
+ "loader",
1029
+ );
888
1030
  },
889
1031
  ),
890
1032
  )
891
- : await provisionTaskEnvironment(input, {}, async (locator) => {
892
- recordTaskEnvironmentPrepared(input.task.id, locator);
893
- });
1033
+ : await provisionTaskEnvironment(
1034
+ input,
1035
+ { taskGateway },
1036
+ async (locator) => {
1037
+ recordTaskEnvironmentPrepared(
1038
+ input.task.id,
1039
+ locator,
1040
+ "loader",
1041
+ );
1042
+ },
1043
+ );
894
1044
  environmentLocator = provisioned.handle.descriptor.locator;
895
- return provisioned.result;
1045
+ // ADR-153 §4: the task's own schema observations, read from the
1046
+ // bases task-up recorded before its container started (a machine
1047
+ // captures its own). Stored once; a resume reuses them.
1048
+ const envSchemas = isEnvironmentTransportProvider(getHostTask(input.task.id)?.environmentProvider)
1049
+ ? undefined
1050
+ : captureTaskEnvSchemas(
1051
+ input.task.id,
1052
+ incomingEnvSpec.projects.map((project) => project.projectId),
1053
+ );
1054
+ // ADR-153: before any consumer is let in, bring the task to now:
1055
+ // a CA successor minted while it started, and the domains its
1056
+ // policy has now rather than those task-up was handed. A failure
1057
+ // leaves those keys withheld, and the trust watcher retries.
1058
+ const secretsProxy = activeSecretsProxy();
1059
+ if (secretsProxy !== null) {
1060
+ await reconcileTaskProxyState(provisioned.handle, secretsProxy.runtime).catch((error: unknown) => {
1061
+ console.warn(
1062
+ `[secrets-proxy] task ${input.task.id}: could not bring the task's trust and pins up to date: ${
1063
+ error instanceof Error ? error.message : String(error)
1064
+ }`,
1065
+ );
1066
+ secretsProxy.runtime.requestTrustDelivery();
1067
+ });
1068
+ }
1069
+ return envSchemas === undefined || envSchemas.length === 0
1070
+ ? { ...provisioned.result, envMode: "loader" as const }
1071
+ : { ...provisioned.result, envMode: "loader" as const, envSchemas };
896
1072
  } finally {
1073
+ gatewaySecret.fill(0);
897
1074
  await gitCredential?.close().catch((error: unknown) => {
898
1075
  // Cleanup must not replace the actual task-up result/error. The
899
1076
  // credential is short-lived and close() also removes its private
@@ -904,13 +1081,58 @@ export const hostCommands: HostCommands = {
904
1081
  });
905
1082
  }
906
1083
  });
1084
+ if (!result.ok && registeredTaskGatewayGeneration !== null) {
1085
+ forgetTaskEnvSnapshot(input.task.id);
1086
+ try {
1087
+ removeTaskGatewayTask(
1088
+ input.task.id,
1089
+ registeredTaskGatewayGeneration,
1090
+ );
1091
+ } catch (error) {
1092
+ // The in-process authority is revoked before durable deletion is
1093
+ // attempted. Preserve the task-up error while retaining a cleanup
1094
+ // diagnostic; a leftover file loads inactive after restart.
1095
+ console.warn(
1096
+ `[task-gateway] task ${input.task.id}: failed to remove the rejected generation: ${
1097
+ error instanceof Error ? error.message : String(error)
1098
+ }`,
1099
+ );
1100
+ }
1101
+ }
907
1102
  if (result.ok) {
908
1103
  orchestrator.allowChannel(input.task.id);
909
1104
  if (environmentLocator === undefined) {
910
1105
  throw new Error("task environment provision returned no locator");
911
1106
  }
912
- recordTaskUpResult(input.task.id, result.value, environmentLocator);
1107
+ recordTaskUpResult(
1108
+ input.task.id,
1109
+ result.value,
1110
+ environmentLocator,
1111
+ "loader",
1112
+ );
913
1113
  recordHostEvent(input.task.id, "task.started");
1114
+ // ADR-153: with the proxy off, every start says so, since the task's
1115
+ // proxied keys are withheld rather than delivered.
1116
+ if (activeSecretsProxy() === null) {
1117
+ orchestrator.emitSystemNote(
1118
+ input.task.id,
1119
+ secretsProxyEnabled()
1120
+ ? "The secrets proxy could not start on this host, so this task's proxied env vars are withheld. See the host log."
1121
+ : "The secrets proxy is off on this host (UAI_SECRETS_PROXY=off), so this task's proxied env vars are withheld.",
1122
+ );
1123
+ }
1124
+ // ADR-153 §4: one note naming the keys this task does not get, and
1125
+ // why, so nobody hunts for a value Uai withholds on purpose.
1126
+ try {
1127
+ const withheld = taskEnvWithheldNoteNow(input.task.id, incomingEnvSpec);
1128
+ if (withheld !== null) orchestrator.emitSystemNote(input.task.id, withheld);
1129
+ } catch (error) {
1130
+ console.warn(
1131
+ `[project-env] task ${input.task.id}: withheld-keys note failed: ${
1132
+ error instanceof Error ? error.message : String(error)
1133
+ }`,
1134
+ );
1135
+ }
914
1136
  // Degraded start (uai-init failed twice): the task is up, but say so
915
1137
  // in the feed — a silent half-start reads as a broken product.
916
1138
  if (result.value.initWarning) {
@@ -1017,6 +1239,10 @@ export const hostCommands: HostCommands = {
1017
1239
  }
1018
1240
  // Tombstone + close before container destruction. A concurrent cloud
1019
1241
  // ensure cannot reopen the channel while teardown is awaiting Docker.
1242
+ const suspendedTaskGateway = suspendTaskGatewayTask(input.taskId);
1243
+ // ADR-153: the suspended generation is no longer served; end the
1244
+ // proxy connections it already had, too.
1245
+ activeSecretsProxy()?.runtime.stopTask(input.taskId);
1020
1246
  await orchestrator.closeChannel(input.taskId);
1021
1247
  if (orphanGc) {
1022
1248
  const inSlotRuntimeFailure = runtimeUnavailable();
@@ -1067,6 +1293,14 @@ export const hostCommands: HostCommands = {
1067
1293
  clearTaskGatewayAclStrict(input.taskId);
1068
1294
  removeTaskCliSecretStrict(input.taskId);
1069
1295
  removeTaskIdentityStrict(input.taskId);
1296
+ removeTaskGatewayTask(input.taskId);
1297
+ // No longer served: the proxy forgets its placeholders, and no
1298
+ // write still queued for the task may land or record.
1299
+ activeSecretsProxy()?.runtime.dropTask(input.taskId);
1300
+ invalidateTaskWrites(input.taskId);
1301
+ forgetTaskProxyReadiness(input.taskId);
1302
+ forgetTaskEnvSnapshot(input.taskId);
1303
+ forgetTaskEnvSchemaObservations(input.taskId);
1070
1304
  clearRefresh(input.taskId);
1071
1305
  // Last, atomically. Do not record taskDown/task.ended first: both
1072
1306
  // would recreate the very inventory evidence being pruned.
@@ -1081,10 +1315,25 @@ export const hostCommands: HostCommands = {
1081
1315
  return failFromUnknown(error);
1082
1316
  }
1083
1317
  }
1084
- // Revoke only after destruction succeeds. On failure the task is
1085
- // reopened below and its on-disk MCP definitions must retain the same
1086
- // gateway token; deleting it early would make those exact definitions
1087
- // look foreign and permanently strand the recovered channel.
1318
+ // The live generation was suspended before channel drain. Delete its
1319
+ // durable state only after destruction succeeds; on failure the task
1320
+ // is reopened below with the same on-disk MCP definitions.
1321
+ try {
1322
+ removeTaskGatewayTask(input.taskId);
1323
+ } catch (error) {
1324
+ // Destruction succeeded and the live authority was already revoked,
1325
+ // but keep the lifecycle retryable until the durable identity is
1326
+ // gone. Never reopen a channel onto the destroyed runtime.
1327
+ recordTaskError(input.taskId);
1328
+ return failFromUnknown(error);
1329
+ }
1330
+ // No longer served: the proxy forgets its placeholders, and no write
1331
+ // still queued for the task may land or record.
1332
+ activeSecretsProxy()?.runtime.dropTask(input.taskId);
1333
+ invalidateTaskWrites(input.taskId);
1334
+ forgetTaskProxyReadiness(input.taskId);
1335
+ forgetTaskEnvSnapshot(input.taskId);
1336
+ forgetTaskEnvSchemaObservations(input.taskId);
1088
1337
  clearTaskGatewayAcl(input.taskId);
1089
1338
  recordTaskDown(input.taskId, result.value.status);
1090
1339
  recordHostEvent(input.taskId, "task.ended");
@@ -1092,7 +1341,12 @@ export const hostCommands: HostCommands = {
1092
1341
  } else {
1093
1342
  // Destruction failed; let a subsequent ensure restore the still-running
1094
1343
  // task instead of leaving it permanently tombstoned.
1095
- if (!orphanGc) orchestrator.allowChannel(input.taskId);
1344
+ if (!orphanGc) {
1345
+ if (suspendedTaskGateway) {
1346
+ reactivateTaskGatewayTask(suspendedTaskGateway);
1347
+ }
1348
+ orchestrator.allowChannel(input.taskId);
1349
+ }
1096
1350
  }
1097
1351
  return result;
1098
1352
  });