@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.
Files changed (93) 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/codex.ts +9 -1
  12. package/lib/agents/cursor.ts +15 -1
  13. package/lib/agents/grok.ts +8 -0
  14. package/lib/agents/kimi.ts +1 -0
  15. package/lib/agents/machine-durable.ts +2 -0
  16. package/lib/agents/opencode.ts +1 -0
  17. package/lib/agents/proc.ts +6 -1
  18. package/lib/agents/transport.ts +9 -5
  19. package/lib/browser-testing.ts +6 -0
  20. package/lib/codex-auth.ts +2 -0
  21. package/lib/credential-store.ts +141 -6
  22. package/lib/engine-accounts.ts +1 -0
  23. package/lib/env-file.ts +7 -0
  24. package/lib/env-schema.ts +841 -51
  25. package/lib/git-diff.ts +1 -0
  26. package/lib/git-identity.ts +2 -0
  27. package/lib/github-tokens.ts +1 -0
  28. package/lib/host-env.ts +98 -18
  29. package/lib/lease.ts +11 -3
  30. package/lib/machine-gateway-tunnel.ts +195 -17
  31. package/lib/machine-project-env-loader.ts +152 -0
  32. package/lib/managed-runtime.ts +270 -51
  33. package/lib/mcp-gateway.ts +777 -26
  34. package/lib/orchestrator.ts +391 -69
  35. package/lib/project-env-authority.ts +1319 -0
  36. package/lib/project-env-feature.ts +74 -0
  37. package/lib/project-env-loader.ts +237 -0
  38. package/lib/project-env-replica.ts +2094 -0
  39. package/lib/project-env-schema-capture.ts +342 -0
  40. package/lib/project-env-staging.ts +148 -0
  41. package/lib/project-env-task-policy.ts +176 -0
  42. package/lib/runtime-archive.ts +79 -16
  43. package/lib/runtime-authority.ts +44 -0
  44. package/lib/runtime-state.ts +175 -10
  45. package/lib/secrets-proxy/address.ts +72 -0
  46. package/lib/secrets-proxy/audit.ts +30 -0
  47. package/lib/secrets-proxy/ca-store.ts +200 -0
  48. package/lib/secrets-proxy/ca.ts +470 -0
  49. package/lib/secrets-proxy/container-steps.ts +138 -0
  50. package/lib/secrets-proxy/der.ts +273 -0
  51. package/lib/secrets-proxy/engine.ts +1633 -0
  52. package/lib/secrets-proxy/host.ts +93 -0
  53. package/lib/secrets-proxy/hostname.ts +60 -0
  54. package/lib/secrets-proxy/placeholders.ts +196 -0
  55. package/lib/secrets-proxy/policy.ts +141 -0
  56. package/lib/secrets-proxy/readiness.ts +148 -0
  57. package/lib/secrets-proxy/runtime.ts +582 -0
  58. package/lib/secrets-proxy/scrub.ts +559 -0
  59. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  60. package/lib/secrets-proxy/task-scripts.ts +262 -0
  61. package/lib/secrets-proxy/task-trust.ts +574 -0
  62. package/lib/secrets-proxy/trust-watch.ts +80 -0
  63. package/lib/shared-files-s3.ts +3 -0
  64. package/lib/sidecar-guard.ts +58 -0
  65. package/lib/sidecar-resume.ts +51 -3
  66. package/lib/standard-image.ts +50 -12
  67. package/lib/task-env-forwarder.ts +228 -0
  68. package/lib/task-env-source.ts +193 -0
  69. package/lib/task-environment/apple-container.ts +45 -11
  70. package/lib/task-environment/docker.ts +51 -12
  71. package/lib/task-environment/index.ts +6 -2
  72. package/lib/task-environment/machine-task-up.ts +258 -25
  73. package/lib/task-environment/machine.ts +80 -22
  74. package/lib/task-environment/registry.ts +3 -2
  75. package/lib/task-environment/self-identity.ts +20 -0
  76. package/lib/task-environment/self.ts +92 -16
  77. package/lib/task-environment/types.ts +63 -0
  78. package/lib/task-gateway-port.ts +22 -0
  79. package/lib/task-gateway-protocol.ts +190 -0
  80. package/lib/task-gateway-recovery.ts +44 -0
  81. package/lib/task-gateway-state.ts +497 -0
  82. package/lib/transcript.ts +1 -0
  83. package/package.json +13 -1
  84. package/scripts/agent/env-hook.sh +626 -0
  85. package/scripts/agent/task-up.sh +476 -27
  86. package/scripts/agent/uai-env-exec.sh +26 -0
  87. package/src/cli.ts +26 -3
  88. package/src/index.ts +273 -19
  89. package/src/main.ts +939 -48
  90. package/src/project-env-latest.ts +130 -0
  91. package/src/project-env-policy.ts +281 -0
  92. package/src/project-env-wording.ts +47 -0
  93. package/src/protocol.ts +2219 -15
package/src/main.ts CHANGED
@@ -67,6 +67,7 @@ import {
67
67
  watchCodexAuth,
68
68
  } from "../lib/codex-auth";
69
69
  import { startCredentialStorePickup } from "../lib/credential-store-pickup";
70
+ import { ImmutableParameterConflictError } from "../lib/credential-store";
70
71
  import { configureTokenBroker, startTokenBrokerLoop } from "../lib/token-broker-local";
71
72
  import {
72
73
  ensureKeyForUser as ensureSshKeyForUser,
@@ -75,11 +76,24 @@ import {
75
76
  import {
76
77
  deleteProjectEnvVar,
77
78
  listProjectEnvKeys,
79
+ onProjectEnvChange,
78
80
  setProjectEnvVar,
79
81
  noteProjectOrg,
80
82
  } from "../lib/host-env";
81
83
  import { handleMcpOp } from "../lib/mcp-connections";
82
- import { startMcpGateway } from "../lib/mcp-gateway";
84
+ import { registerTaskGatewayTlsHandler, startMcpGateway } from "../lib/mcp-gateway";
85
+ import { setActiveSecretsProxy, startSecretsProxyHost, type SecretsProxyHost } from "../lib/secrets-proxy/host";
86
+ import {
87
+ createTaskProxyPolicySource,
88
+ gateTaskProxyPolicy,
89
+ setActiveTaskProxyPolicy,
90
+ } from "../lib/secrets-proxy/policy";
91
+ import { quiesceTaskProxyMutations } from "../lib/secrets-proxy/task-trust";
92
+ import { resolveWithSystem, resolverIgnoringHostsFile, secretsProxyEnabled } from "../lib/secrets-proxy/runtime";
93
+ import { reinstallTrustInRunningTasks, unpinRunningTasks } from "../lib/secrets-proxy/trust-watch";
94
+ import { currentTaskGatewayRegistration } from "../lib/task-gateway-state";
95
+ import { proxyTaskEnvSnapshot } from "../lib/task-env-source";
96
+ import { recoverActiveTaskGateways } from "../lib/task-gateway-recovery";
83
97
  import { addHostBreadcrumb, initHostObs, setHostObsTag } from "../lib/obs";
84
98
  import {
85
99
  hostIdFilePath,
@@ -148,9 +162,33 @@ import {
148
162
  import { canAdvertiseTypedSecretaryDispatch } from "../lib/agents/mode";
149
163
  import {
150
164
  MANAGED_UPDATE_RESTART_EXIT_CODE,
165
+ claimManagedRuntimeCapabilityFloor,
151
166
  managedRuntimeAdmissionBlocker,
152
167
  reclaimAbandonedMaintenanceCheck,
153
168
  } from "../lib/managed-runtime";
169
+ import {
170
+ applyProjectEnvDelivery,
171
+ beginProjectEnvReconciliation,
172
+ closeProjectEnvAuthorityGate,
173
+ completeProjectEnvReconciliation,
174
+ PROJECT_ENV_AUTHORITY_RUNTIME_CAPABILITY,
175
+ readProjectEnvAuthorityStatus,
176
+ readProjectEnvRecordInventory,
177
+ type ProjectEnvAuthorityStatus,
178
+ type ProjectEnvDeliveryApplyResult,
179
+ type ProjectEnvOwnedDeliveryFrame,
180
+ type ProjectEnvRecordInventory,
181
+ } from "../lib/project-env-authority";
182
+ import {
183
+ applyProjectEnvReplicaDelivery,
184
+ applyProjectEnvReplicaPage,
185
+ beginProjectEnvReplica,
186
+ closeProjectEnvReplicaGate,
187
+ completeProjectEnvReplica,
188
+ type ProjectEnvReplicaApplyResult,
189
+ type ProjectEnvReplicaCommitResult,
190
+ type ProjectEnvReplicaPageResult,
191
+ } from "../lib/project-env-replica";
154
192
  import { ManagedOperationDrain } from "../lib/managed-operation-drain";
155
193
  import {
156
194
  createHostLogManager,
@@ -224,6 +262,8 @@ import {
224
262
  type EngineAccountSummary,
225
263
  type EngineCatalogCapability,
226
264
  MAX_HOST_TASK_INVENTORY_PAGE_SIZE,
265
+ MAX_PROJECT_ENV_ACK_PAGE_BYTES,
266
+ MAX_REMOTE_HOST_MESSAGE_BYTES,
227
267
  MCP_GATEWAY_HEALTH_PROTOCOL_FEATURE,
228
268
  SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE,
229
269
  TRANSCRIPT_TARGETS_PROTOCOL_FEATURE,
@@ -235,6 +275,8 @@ import {
235
275
  type HostCommandResult,
236
276
  type HostCommands,
237
277
  type HostToCloud,
278
+ type ProjectEnvListFrame,
279
+ type ProjectEnvReplicaAckFrame,
238
280
  type PermissionDecision,
239
281
  parseGitHubCredentialGeneration,
240
282
  parseChannelMode,
@@ -247,6 +289,19 @@ import {
247
289
  isHostConfigSetFrame,
248
290
  isHostLogsStartFrame,
249
291
  isHostLogsStopFrame,
292
+ isProjectEnvBindingSetFrame,
293
+ isProjectEnvDeleteFrame,
294
+ isProjectEnvDeliverySkipFrame,
295
+ isProjectEnvListFrame,
296
+ isProjectEnvReconcileBeginFrame,
297
+ isProjectEnvReconciledFrame,
298
+ isProjectEnvRemoveFrame,
299
+ isProjectEnvReplicaApplyFrame,
300
+ isProjectEnvReplicaBeginFrame,
301
+ isProjectEnvReplicaCommitFrame,
302
+ isProjectEnvReplicaPageFrame,
303
+ isProjectEnvSetFrame,
304
+ isProjectEnvStagingPutFrame,
250
305
  type TaskAgent,
251
306
  type TaskCommandProject,
252
307
  type TaskCommandTask,
@@ -259,8 +314,14 @@ import {
259
314
  isEngineAccountLabel,
260
315
  isEngineConnectKind,
261
316
  } from "./protocol";
317
+ import { PROJECT_ENV_POLICY_EPOCHS } from "./project-env-policy";
318
+ import {
319
+ completeProjectEnvBindingCapabilityAfterRuntimeActivation,
320
+ initializeProjectEnvBindingCapability,
321
+ projectEnvBindingCapability,
322
+ } from "../lib/project-env-feature";
262
323
  import { isEnvironmentTransportProvider } from "../lib/task-environment/types";
263
- import { LeaseManager } from "../lib/lease";
324
+ import { LeaseManager, leaseScope } from "../lib/lease";
264
325
  import { markSidecarReady, sidecarMode, sidecarPoolMode, sidecarTaskId } from "../lib/sidecar";
265
326
  import {
266
327
  purgeDefaultEngineSlots,
@@ -275,9 +336,13 @@ import {
275
336
  reconcileUserCredentialMetadataFromStore,
276
337
  setProjectEnvStoreOnly,
277
338
  } from "../lib/signin-box-credentials";
339
+ import {
340
+ ProjectEnvStagingInvalidError,
341
+ writeProjectEnvStaging,
342
+ } from "../lib/project-env-staging";
278
343
  import { loadGithubTokenIntoCache } from "../lib/github-tokens";
279
344
  import { ensureSshKeyStoreFirst } from "../lib/ssh";
280
- import { resumeSidecarTask } from "../lib/sidecar-resume";
345
+ import { classifySidecarTaskView, resumeSidecarTask } from "../lib/sidecar-resume";
281
346
  import { declareAgentClisReady } from "../lib/standard-image";
282
347
 
283
348
  setContainerRuntimeOperationalCheck(() => containerRuntimeProblem() === null);
@@ -489,6 +554,46 @@ try {
489
554
  );
490
555
  }
491
556
  migrateHostDb();
557
+ // A task-machine process starts fail-closed. The prior process may have died
558
+ // after opening its replica, and no task loader or proxy may serve that view
559
+ // until this authenticated socket completes a fresh whole-task snapshot.
560
+ if (sidecarMode()) closeProjectEnvReplicaGate();
561
+ // The release switch is not enough to advertise bindings. First make rollback
562
+ // to a value-only runtime durably impossible. A failed claim leaves both the
563
+ // feature and its policy epochs absent from every capability frame for this
564
+ // process; an already-activated owned store is closed as well.
565
+ try {
566
+ await initializeProjectEnvBindingCapability(
567
+ claimManagedRuntimeCapabilityFloor,
568
+ );
569
+ } catch (error) {
570
+ if (!sidecarMode()) {
571
+ try {
572
+ closeProjectEnvAuthorityGate();
573
+ } catch (gateError) {
574
+ console.warn(
575
+ `[project-env] could not close the authority after its runtime floor failed: ${gateError instanceof Error ? gateError.message : String(gateError)}`,
576
+ );
577
+ }
578
+ }
579
+ console.warn(
580
+ `[project-env] bindings stay disabled because the runtime floor could not be persisted: ${error instanceof Error ? error.message : String(error)}`,
581
+ );
582
+ }
583
+
584
+ /**
585
+ * Synchronous half of the managed-runtime acknowledgement transaction. The
586
+ * caller holds the operation lock, floor 4 is already durable, and the
587
+ * activation marker still closes task admission.
588
+ */
589
+ export function completeProjectEnvBindingsAfterManagedRuntimeFloor(): void {
590
+ completeProjectEnvBindingCapabilityAfterRuntimeActivation();
591
+ }
592
+
593
+ /** Re-advertise only after the atomic acknowledgement has removed its gate. */
594
+ export function advertiseProjectEnvBindingsAfterManagedRuntimeActivation(): void {
595
+ sendCapabilities();
596
+ }
492
597
  // ADR-134: on a Uai-operated host the engine-account rows are a cache of the
493
598
  // credential store. Import what this host lacks and upload what was never
494
599
  // mirrored, off the startup path — task-up resolves locally either way.
@@ -673,6 +778,11 @@ const leaseManager: LeaseManager | null = sidecarTaskId()
673
778
  console.warn(
674
779
  `[sidecar] resume failed: ${error instanceof Error ? error.message : String(error)}`,
675
780
  );
781
+ })
782
+ // A sidecar runs no container-runtime activation: its task is
783
+ // reconciled by the resume (or by task-up), and kept there from now.
784
+ .finally(() => {
785
+ if (!stopping) startSecretsProxyUpkeep();
676
786
  });
677
787
  });
678
788
  startCredentialStorePickup({ repush: repushBrokeredCopies });
@@ -689,6 +799,18 @@ const leaseManager: LeaseManager | null = sidecarTaskId()
689
799
  })
690
800
  : null;
691
801
  if (sidecarMode()) declareAgentClisReady();
802
+ // ADR-153 machines: the task's row is held unless its hosts-view record
803
+ // proves it, before connect() can bring any command for it.
804
+ if (sidecarMode()) {
805
+ try {
806
+ classifySidecarTaskView();
807
+ } catch (error) {
808
+ // Each later look holds a task without a record anyway.
809
+ console.warn(
810
+ `[sidecar] could not classify the task's hosts view: ${error instanceof Error ? error.message : String(error)}`,
811
+ );
812
+ }
813
+ }
692
814
  // Reconcile a durable retention-off setting before recovery/connect can make
693
815
  // crash-left owner-facing terminal history observable again. Correctness
694
816
  // tombstones remain intact inside applyTerminalHistoryRetention.
@@ -804,10 +926,90 @@ hostEvents.subscribe((event) => {
804
926
  eventOutbox.enqueue(event);
805
927
  flushEvents();
806
928
  });
929
+ // ADR-153: the secrets proxy starts before the gateway generations are
930
+ // recovered, so their env resolvers already see it, and registers as the
931
+ // gateway's TLS consumer. Null under the kill switch (UAI_SECRETS_PROXY=off),
932
+ // and when it cannot start (an unreadable CA store must not take the runner
933
+ // down; proxied keys are then withheld). A machine's sidecar shares
934
+ // /etc/hosts with its task, whose pins must never answer for an upstream.
935
+ // The policy is registered whether or not the proxy runs: with it off, the
936
+ // task's proxy-mode keys must still be known, to be withheld.
937
+ const secretsProxyPolicy = gateTaskProxyPolicy(
938
+ createTaskProxyPolicySource((taskId) => currentTaskGatewayRegistration(taskId) !== null, proxyTaskEnvSnapshot),
939
+ );
940
+ setActiveTaskProxyPolicy(secretsProxyPolicy);
941
+ function startSecretsProxy(): SecretsProxyHost | null {
942
+ if (signInBoxMode()) return null;
943
+ try {
944
+ return startSecretsProxyHost({
945
+ dataDir: env.dataDir,
946
+ policy: secretsProxyPolicy,
947
+ resolveHost: sidecarMode() ? resolverIgnoringHostsFile() : resolveWithSystem,
948
+ registerTlsHandler: registerTaskGatewayTlsHandler,
949
+ });
950
+ } catch (error) {
951
+ console.error(
952
+ `[secrets-proxy] could not start; proxied keys are withheld: ${error instanceof Error ? error.message : String(error)}`,
953
+ );
954
+ return null;
955
+ }
956
+ }
957
+ const secretsProxy = startSecretsProxy();
958
+ setActiveSecretsProxy(secretsProxy);
959
+ if (secretsProxy === null && !signInBoxMode() && !secretsProxyEnabled()) {
960
+ console.warn(
961
+ "[secrets-proxy] UAI_SECRETS_PROXY=off: no proxy and no CA in tasks; proxied keys are withheld, never delivered",
962
+ );
963
+ }
964
+ let secretsProxyTrustWatch: (() => Promise<void>) | null = null;
965
+ /** With the proxy off: the hourly sweep that unpins running tasks. */
966
+ let secretsProxyUnpinSweep: NodeJS.Timeout | null = null;
967
+ /**
968
+ * ADR-153: once every running task has been reconciled (recovery on an owned
969
+ * runner, the resume on a machine's sidecar), keep them there. A changed CA
970
+ * bundle reaches them too, and this first delivery re-checks them. With the
971
+ * proxy off, no pin an earlier runner left may stay: recovery unpins, and an
972
+ * hourly sweep retries whatever it could not. Idempotent.
973
+ */
974
+ function startSecretsProxyUpkeep(): void {
975
+ if (secretsProxy !== null && secretsProxyTrustWatch === null) {
976
+ const runtime = secretsProxy.runtime;
977
+ secretsProxyTrustWatch = runtime.watchTrust((_bundle, signal) =>
978
+ reinstallTrustInRunningTasks(runtime, signal),
979
+ );
980
+ // A binding approved or changed mid-task moves a task's proxied domains:
981
+ // its pins follow, so the key is usable from the next command on rather
982
+ // than withheld until the next CA change.
983
+ onProjectEnvChange(() => runtime.requestTrustDelivery());
984
+ }
985
+ if (secretsProxy === null && !signInBoxMode() && secretsProxyUnpinSweep === null) {
986
+ const sweep = (): void => {
987
+ void unpinRunningTasks().catch((error: unknown) =>
988
+ console.warn(
989
+ `[secrets-proxy] proxy off: retrying the unpin hourly: ${error instanceof Error ? error.message : String(error)}`,
990
+ ),
991
+ );
992
+ };
993
+ sweep();
994
+ secretsProxyUnpinSweep = setInterval(sweep, 60 * 60 * 1_000);
995
+ secretsProxyUnpinSweep.unref();
996
+ }
997
+ }
998
+ /** Set once the proxy has stopped and every root mutation of a task (a
999
+ * recovery's CA install, a re-pin, an unpin) has ended, and no new one can
1000
+ * start. The process must not exit before: such a write would otherwise
1001
+ * outlive this runner and land over its successor's. */
1002
+ let secretsProxyStopped = false;
807
1003
  // Start local health-bearing services before the bridge. The first
808
1004
  // authenticated capability frame must describe the gateway even when its
809
1005
  // initial bind is still pending, and both one-way readiness edges re-advertise
810
1006
  // on the exact live socket generation.
1007
+ const taskGatewayRecovery = recoverActiveTaskGateways();
1008
+ if (taskGatewayRecovery.recovered > 0 || taskGatewayRecovery.failed > 0) {
1009
+ console.log(
1010
+ `[task-gateway] boot recovery: ${taskGatewayRecovery.recovered} active, ${taskGatewayRecovery.failed} closed`,
1011
+ );
1012
+ }
811
1013
  const mcpGateway = startMcpGateway();
812
1014
  const stopMcpGatewaySubscription = mcpGateway.subscribe(() =>
813
1015
  sendCapabilities(),
@@ -896,6 +1098,7 @@ async function startLocalUi(): Promise<void> {
896
1098
  /** Build the current host capability advertisement (ADR-021). */
897
1099
  function buildCapabilities(): HostCapabilities {
898
1100
  const runtime = containerRuntimeCapabilityState();
1101
+ const projectEnvBindings = projectEnvBindingCapability();
899
1102
  return {
900
1103
  version: packageVersion(),
901
1104
  protocolFeatures: [
@@ -924,7 +1127,11 @@ function buildCapabilities(): HostCapabilities {
924
1127
  ...(canAdvertiseTypedSecretaryDispatch()
925
1128
  ? [SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE]
926
1129
  : []),
1130
+ ...(projectEnvBindings === null ? [] : [projectEnvBindings.feature]),
927
1131
  ],
1132
+ ...(projectEnvBindings === null
1133
+ ? {}
1134
+ : { projectEnvPolicyEpochs: projectEnvBindings.policyEpochs }),
928
1135
  agentKinds: agentKindCapabilities(),
929
1136
  // Language runtimes live in the standard container image. Do not
930
1137
  // advertise them as usable while the machine cannot run a container.
@@ -1184,6 +1391,7 @@ async function activateContainerRuntime(
1184
1391
  ) {
1185
1392
  watchCodexAuth(() => containerRuntimeProblem() === null);
1186
1393
  resumeRuntimeDeferredAgentWork();
1394
+ startSecretsProxyUpkeep();
1187
1395
  sendCapabilities();
1188
1396
  }
1189
1397
  },
@@ -1332,9 +1540,516 @@ async function remoteOperationsReadyForConnect(
1332
1540
  );
1333
1541
  }
1334
1542
 
1543
+ type ModernProjectEnvListFrame = Extract<
1544
+ ProjectEnvListFrame,
1545
+ { reconciliationAttemptId: string }
1546
+ >;
1547
+
1548
+ function sameProjectEnvCursor(
1549
+ left: { streamId: string; sequence: number },
1550
+ right: { streamId: string; sequence: number },
1551
+ ): boolean {
1552
+ return left.streamId === right.streamId && left.sequence === right.sequence;
1553
+ }
1554
+
1555
+ function projectEnvRefusalMessage(code: string): string {
1556
+ return `project environment operation refused (${code})`;
1557
+ }
1558
+
1559
+ function boundedRemoteError(error: unknown, fallback: string): string {
1560
+ const message = error instanceof Error ? error.message : fallback;
1561
+ let bounded = "";
1562
+ for (const character of message) {
1563
+ if (
1564
+ Buffer.byteLength(bounded + character, "utf8") >
1565
+ MAX_REMOTE_HOST_MESSAGE_BYTES
1566
+ ) {
1567
+ break;
1568
+ }
1569
+ bounded += character;
1570
+ }
1571
+ return bounded || fallback;
1572
+ }
1573
+
1574
+ function projectEnvStagingErrorCode(
1575
+ error: unknown,
1576
+ ): Extract<
1577
+ HostToCloud,
1578
+ { kind: "env.staging.ack"; ok: false }
1579
+ >["code"] {
1580
+ if (error instanceof ImmutableParameterConflictError) {
1581
+ return "staging_conflict";
1582
+ }
1583
+ if (error instanceof ProjectEnvStagingInvalidError) {
1584
+ return "staging_invalid";
1585
+ }
1586
+ return "staging_unavailable";
1587
+ }
1588
+
1589
+ async function handleProjectEnvStagingPut(
1590
+ socket: WebSocket,
1591
+ frame: Extract<CloudToHost, { kind: "env.staging.put" }>,
1592
+ ): Promise<void> {
1593
+ try {
1594
+ await claimManagedRuntimeCapabilityFloor(
1595
+ PROJECT_ENV_AUTHORITY_RUNTIME_CAPABILITY,
1596
+ );
1597
+ await writeProjectEnvStaging(frame);
1598
+ send(socket, {
1599
+ kind: "env.staging.ack",
1600
+ opId: frame.opId,
1601
+ ok: true,
1602
+ stagingId: frame.stagingId,
1603
+ });
1604
+ } catch (error) {
1605
+ send(socket, {
1606
+ kind: "env.staging.ack",
1607
+ opId: frame.opId,
1608
+ ok: false,
1609
+ stagingId: frame.stagingId,
1610
+ code: projectEnvStagingErrorCode(error),
1611
+ error: boundedRemoteError(error, "environment staging failed"),
1612
+ });
1613
+ }
1614
+ }
1615
+
1616
+ function projectEnvReplicaErrorMessage(code: string): string {
1617
+ return boundedRemoteError(
1618
+ new Error(projectEnvRefusalMessage(code)),
1619
+ "environment replica operation refused",
1620
+ );
1621
+ }
1622
+
1623
+ async function handleProjectEnvReplicaBegin(
1624
+ socket: WebSocket,
1625
+ frame: Extract<CloudToHost, { kind: "env.replica.begin" }>,
1626
+ isCurrent: () => boolean,
1627
+ ): Promise<void> {
1628
+ // The authenticated bridge generation supplies the host identity. A real
1629
+ // task id (not a warm-pool sidecar) and its granted lease supply the task's
1630
+ // tenant; the cloud cannot choose either value in this frame.
1631
+ if (!sidecarMode() || sidecarTaskId() === null) {
1632
+ throw new Error("replica begin requires a task sidecar");
1633
+ }
1634
+ const scope = leaseScope();
1635
+ if (scope?.orgId === null || scope?.orgId === undefined) {
1636
+ throw new Error("replica begin requires an authenticated organization lease");
1637
+ }
1638
+ await claimManagedRuntimeCapabilityFloor(
1639
+ PROJECT_ENV_AUTHORITY_RUNTIME_CAPABILITY,
1640
+ );
1641
+ if (!isCurrent()) return;
1642
+ const currentOrgId = leaseScope()?.orgId;
1643
+ if (currentOrgId === null || currentOrgId === undefined) {
1644
+ throw new Error("replica begin requires an authenticated organization lease");
1645
+ }
1646
+ if (currentOrgId !== scope.orgId) {
1647
+ throw new Error("replica organization lease changed during activation");
1648
+ }
1649
+ const result = beginProjectEnvReplica(
1650
+ frame,
1651
+ PROJECT_ENV_POLICY_EPOCHS,
1652
+ currentOrgId,
1653
+ );
1654
+ if (!isCurrent()) return;
1655
+ const ack: ProjectEnvReplicaAckFrame = {
1656
+ kind: "env.replica.ack",
1657
+ opId: frame.opId,
1658
+ ok: true,
1659
+ phase: "begun",
1660
+ attemptId: frame.attempt.attemptId,
1661
+ policyEpochs: result.policyEpochs,
1662
+ appliedThrough: result.appliedThrough,
1663
+ };
1664
+ send(socket, ack);
1665
+ }
1666
+
1667
+ function projectEnvReplicaPageAck(
1668
+ frame: Extract<CloudToHost, { kind: "env.replica.page" }>,
1669
+ result: ProjectEnvReplicaPageResult,
1670
+ ): ProjectEnvReplicaAckFrame {
1671
+ return result.ok
1672
+ ? {
1673
+ kind: "env.replica.ack",
1674
+ opId: frame.opId,
1675
+ ok: true,
1676
+ phase: "page",
1677
+ attemptId: result.attemptId,
1678
+ projectId: result.projectId,
1679
+ after: result.after,
1680
+ recordVersion: result.recordVersion,
1681
+ results: result.results,
1682
+ complete: result.complete,
1683
+ policyEpochs: result.policyEpochs,
1684
+ appliedThrough: result.appliedThrough,
1685
+ }
1686
+ : {
1687
+ kind: "env.replica.ack",
1688
+ opId: frame.opId,
1689
+ ok: false,
1690
+ phase: "page",
1691
+ attemptId: result.attemptId,
1692
+ code: result.code,
1693
+ error: projectEnvReplicaErrorMessage(result.code),
1694
+ policyEpochs: result.policyEpochs,
1695
+ appliedThrough: result.appliedThrough,
1696
+ };
1697
+ }
1698
+
1699
+ async function handleProjectEnvReplicaPage(
1700
+ socket: WebSocket,
1701
+ frame: Extract<CloudToHost, { kind: "env.replica.page" }>,
1702
+ isCurrent: () => boolean,
1703
+ ): Promise<void> {
1704
+ const result = await applyProjectEnvReplicaPage(frame);
1705
+ // The await can include an exact SSM-version read. Never let its result
1706
+ // escape on a replacement socket generation.
1707
+ if (!isCurrent()) return;
1708
+ send(socket, projectEnvReplicaPageAck(frame, result));
1709
+ }
1710
+
1711
+ function projectEnvReplicaApplyAck(
1712
+ frame: Extract<CloudToHost, { kind: "env.replica.apply" }>,
1713
+ result: ProjectEnvReplicaApplyResult,
1714
+ ): ProjectEnvReplicaAckFrame {
1715
+ return result.ok
1716
+ ? {
1717
+ kind: "env.replica.ack",
1718
+ opId: frame.opId,
1719
+ ok: true,
1720
+ phase: "applied",
1721
+ attemptId: result.attemptId,
1722
+ projectId: result.projectId,
1723
+ recordVersion: result.recordVersion,
1724
+ result: result.result,
1725
+ policyEpochs: result.policyEpochs,
1726
+ appliedThrough: result.appliedThrough,
1727
+ }
1728
+ : {
1729
+ kind: "env.replica.ack",
1730
+ opId: frame.opId,
1731
+ ok: false,
1732
+ phase: "applied",
1733
+ attemptId: result.attemptId,
1734
+ code: result.code,
1735
+ error: projectEnvReplicaErrorMessage(result.code),
1736
+ policyEpochs: result.policyEpochs,
1737
+ appliedThrough: result.appliedThrough,
1738
+ };
1739
+ }
1740
+
1741
+ async function handleProjectEnvReplicaApply(
1742
+ socket: WebSocket,
1743
+ frame: Extract<CloudToHost, { kind: "env.replica.apply" }>,
1744
+ isCurrent: () => boolean,
1745
+ ): Promise<void> {
1746
+ const result = await applyProjectEnvReplicaDelivery(frame);
1747
+ if (!isCurrent()) return;
1748
+ send(socket, projectEnvReplicaApplyAck(frame, result));
1749
+ }
1750
+
1751
+ function projectEnvReplicaCommitAck(
1752
+ frame: Extract<CloudToHost, { kind: "env.replica.commit" }>,
1753
+ result: ProjectEnvReplicaCommitResult,
1754
+ ): ProjectEnvReplicaAckFrame {
1755
+ return result.ok
1756
+ ? {
1757
+ kind: "env.replica.ack",
1758
+ opId: frame.opId,
1759
+ ok: true,
1760
+ phase: "complete",
1761
+ attemptId: result.attemptId,
1762
+ highWater: frame.highWater,
1763
+ policyEpochs: result.policyEpochs,
1764
+ appliedThrough: result.appliedThrough,
1765
+ }
1766
+ : {
1767
+ kind: "env.replica.ack",
1768
+ opId: frame.opId,
1769
+ ok: false,
1770
+ phase: "complete",
1771
+ attemptId: result.attemptId,
1772
+ code: result.code,
1773
+ error: projectEnvReplicaErrorMessage(result.code),
1774
+ policyEpochs: result.policyEpochs,
1775
+ appliedThrough: result.appliedThrough,
1776
+ };
1777
+ }
1778
+
1779
+ function handleProjectEnvReplicaCommit(
1780
+ socket: WebSocket,
1781
+ frame: Extract<CloudToHost, { kind: "env.replica.commit" }>,
1782
+ isCurrent: () => boolean,
1783
+ ): void {
1784
+ const result = completeProjectEnvReplica(frame);
1785
+ if (!isCurrent()) return;
1786
+ send(socket, projectEnvReplicaCommitAck(frame, result));
1787
+ }
1788
+
1789
+ /** Build the largest stable inventory prefix whose complete wire frame stays
1790
+ * within the protocol's 8 MiB acknowledgement bound. */
1791
+ function buildProjectEnvListAck(
1792
+ frame: ModernProjectEnvListFrame,
1793
+ inventory: ProjectEnvRecordInventory,
1794
+ authority: ProjectEnvAuthorityStatus & {
1795
+ appliedThrough: NonNullable<ProjectEnvAuthorityStatus["appliedThrough"]>;
1796
+ },
1797
+ ): Extract<HostToCloud, { kind: "env.var.ack"; ok: true }> {
1798
+ const remaining = inventory.states.filter(
1799
+ (state) => frame.after === undefined || state.key > frame.after,
1800
+ );
1801
+ const ackFor = (
1802
+ count: number,
1803
+ ): Extract<HostToCloud, { kind: "env.var.ack"; ok: true }> => {
1804
+ const bindings = remaining.slice(0, count);
1805
+ return {
1806
+ kind: "env.var.ack",
1807
+ opId: frame.opId,
1808
+ ok: true,
1809
+ keys: bindings
1810
+ .filter((state) => !state.head.tombstone)
1811
+ .map((state) => state.key),
1812
+ bindings,
1813
+ policyEpochs: [...authority.policyEpochs],
1814
+ recordVersion: inventory.recordVersion,
1815
+ appliedThrough: authority.appliedThrough,
1816
+ complete: count === remaining.length,
1817
+ reconciliationAttemptId: frame.reconciliationAttemptId,
1818
+ };
1819
+ };
1820
+
1821
+ if (remaining.length === 0) return ackFor(0);
1822
+
1823
+ let low = 1;
1824
+ let high = remaining.length;
1825
+ let page: ReturnType<typeof ackFor> | null = null;
1826
+ while (low <= high) {
1827
+ const count = Math.floor((low + high) / 2);
1828
+ const candidate = ackFor(count);
1829
+ if (
1830
+ Buffer.byteLength(JSON.stringify(candidate), "utf8") <=
1831
+ MAX_PROJECT_ENV_ACK_PAGE_BYTES
1832
+ ) {
1833
+ page = candidate;
1834
+ low = count + 1;
1835
+ } else {
1836
+ high = count - 1;
1837
+ }
1838
+ }
1839
+ if (page === null) {
1840
+ throw new Error(
1841
+ "one project environment record exceeds the acknowledgement bound",
1842
+ );
1843
+ }
1844
+ return page;
1845
+ }
1846
+
1847
+ async function handleProjectEnvReconcileBegin(
1848
+ socket: WebSocket,
1849
+ frame: Extract<CloudToHost, { kind: "env.reconcile.begin" }>,
1850
+ isCurrent: () => boolean,
1851
+ ): Promise<void> {
1852
+ const before = readProjectEnvAuthorityStatus();
1853
+ if (!before.activated) {
1854
+ await claimManagedRuntimeCapabilityFloor(
1855
+ PROJECT_ENV_AUTHORITY_RUNTIME_CAPABILITY,
1856
+ );
1857
+ // Capability-floor persistence can wait on the managed-operation lock.
1858
+ // A socket replaced while it waited owns no later authority mutation.
1859
+ if (!isCurrent()) return;
1860
+ }
1861
+ const result = beginProjectEnvReconciliation(
1862
+ frame,
1863
+ PROJECT_ENV_POLICY_EPOCHS,
1864
+ );
1865
+ if (!isCurrent()) return;
1866
+ send(socket, {
1867
+ kind: "env.reconcile.ack",
1868
+ opId: frame.opId,
1869
+ ok: true,
1870
+ phase: "begun",
1871
+ attemptId: frame.attempt.attemptId,
1872
+ appliedThrough: result.appliedThrough,
1873
+ });
1874
+ }
1875
+
1876
+ function handleProjectEnvList(
1877
+ socket: WebSocket,
1878
+ frame: ModernProjectEnvListFrame,
1879
+ ): void {
1880
+ const authority = readProjectEnvAuthorityStatus();
1881
+ if (authority.appliedThrough === null) {
1882
+ // There is no authentic cursor to put in a modern error acknowledgement.
1883
+ throw new Error("project environment authority is not activated");
1884
+ }
1885
+ if (authority.attemptId !== frame.reconciliationAttemptId) {
1886
+ send(socket, {
1887
+ kind: "env.var.ack",
1888
+ opId: frame.opId,
1889
+ ok: false,
1890
+ code: "reconciliation_attempt_mismatch",
1891
+ error: projectEnvRefusalMessage("reconciliation_attempt_mismatch"),
1892
+ appliedThrough: authority.appliedThrough,
1893
+ reconciliationAttemptId: frame.reconciliationAttemptId,
1894
+ });
1895
+ return;
1896
+ }
1897
+ const inventory = readProjectEnvRecordInventory(frame.projectId);
1898
+ send(
1899
+ socket,
1900
+ buildProjectEnvListAck(frame, inventory, {
1901
+ ...authority,
1902
+ appliedThrough: authority.appliedThrough,
1903
+ }),
1904
+ );
1905
+ }
1906
+
1907
+ function sendProjectEnvDeliveryResult(
1908
+ socket: WebSocket,
1909
+ frame: ProjectEnvOwnedDeliveryFrame,
1910
+ result: ProjectEnvDeliveryApplyResult,
1911
+ ): void {
1912
+ if (frame.kind === "env.delivery.skip") {
1913
+ send(
1914
+ socket,
1915
+ result.ok
1916
+ ? {
1917
+ kind: "env.delivery.ack",
1918
+ opId: frame.opId,
1919
+ ok: true,
1920
+ appliedThrough: result.appliedThrough,
1921
+ }
1922
+ : {
1923
+ kind: "env.delivery.ack",
1924
+ opId: frame.opId,
1925
+ ok: false,
1926
+ code: result.code,
1927
+ error: projectEnvRefusalMessage(result.code),
1928
+ appliedThrough: result.appliedThrough,
1929
+ },
1930
+ );
1931
+ return;
1932
+ }
1933
+ if (!result.ok) {
1934
+ send(socket, {
1935
+ kind: "env.var.ack",
1936
+ opId: frame.opId,
1937
+ ok: false,
1938
+ code: result.code,
1939
+ error: projectEnvRefusalMessage(result.code),
1940
+ appliedThrough: result.appliedThrough,
1941
+ ...(frame.reconciliationAttemptId === undefined
1942
+ ? {}
1943
+ : { reconciliationAttemptId: frame.reconciliationAttemptId }),
1944
+ });
1945
+ return;
1946
+ }
1947
+ if (result.state === null || result.recordVersion === null) {
1948
+ throw new Error("project environment mutation produced no record outcome");
1949
+ }
1950
+ const authority = readProjectEnvAuthorityStatus();
1951
+ if (
1952
+ authority.appliedThrough === null ||
1953
+ !sameProjectEnvCursor(authority.appliedThrough, result.appliedThrough)
1954
+ ) {
1955
+ throw new Error("project environment outcome cursor is not durable");
1956
+ }
1957
+ send(socket, {
1958
+ kind: "env.var.ack",
1959
+ opId: frame.opId,
1960
+ ok: true,
1961
+ keys: result.state.head.tombstone ? [] : [result.state.key],
1962
+ bindings: [result.state],
1963
+ policyEpochs: [...authority.policyEpochs],
1964
+ recordVersion: result.recordVersion,
1965
+ appliedThrough: result.appliedThrough,
1966
+ ...(frame.reconciliationAttemptId === undefined
1967
+ ? {}
1968
+ : { reconciliationAttemptId: frame.reconciliationAttemptId }),
1969
+ });
1970
+ }
1971
+
1972
+ function handleProjectEnvDelivery(
1973
+ socket: WebSocket,
1974
+ frame: ProjectEnvOwnedDeliveryFrame,
1975
+ ): void {
1976
+ sendProjectEnvDeliveryResult(
1977
+ socket,
1978
+ frame,
1979
+ applyProjectEnvDelivery(frame),
1980
+ );
1981
+ }
1982
+
1983
+ function handleProjectEnvReconciled(
1984
+ socket: WebSocket,
1985
+ frame: Extract<CloudToHost, { kind: "env.reconciled" }>,
1986
+ ): void {
1987
+ const before = readProjectEnvAuthorityStatus();
1988
+ if (before.appliedThrough === null) {
1989
+ throw new Error("project environment authority is not activated");
1990
+ }
1991
+ if (before.attemptId !== frame.attemptId) {
1992
+ send(socket, {
1993
+ kind: "env.reconcile.ack",
1994
+ opId: frame.opId,
1995
+ ok: false,
1996
+ phase: "complete",
1997
+ attemptId: frame.attemptId,
1998
+ code: "reconciliation_attempt_mismatch",
1999
+ error: projectEnvRefusalMessage("reconciliation_attempt_mismatch"),
2000
+ appliedThrough: before.appliedThrough,
2001
+ });
2002
+ return;
2003
+ }
2004
+ // A success acknowledgement has one cursor field and the shared guard
2005
+ // requires it to equal the echoed high-water. Refuse a defensive marker
2006
+ // that would otherwise open the gate with an unrepresentable reply.
2007
+ if (!sameProjectEnvCursor(before.appliedThrough, frame.highWater)) {
2008
+ send(socket, {
2009
+ kind: "env.reconcile.ack",
2010
+ opId: frame.opId,
2011
+ ok: false,
2012
+ phase: "complete",
2013
+ attemptId: frame.attemptId,
2014
+ code: "delivery_incomplete",
2015
+ error: projectEnvRefusalMessage("delivery_incomplete"),
2016
+ appliedThrough: before.appliedThrough,
2017
+ });
2018
+ return;
2019
+ }
2020
+ const result = completeProjectEnvReconciliation(frame);
2021
+ send(
2022
+ socket,
2023
+ result.ok
2024
+ ? {
2025
+ kind: "env.reconcile.ack",
2026
+ opId: frame.opId,
2027
+ ok: true,
2028
+ phase: "complete",
2029
+ attemptId: frame.attemptId,
2030
+ highWater: frame.highWater,
2031
+ appliedThrough: result.appliedThrough,
2032
+ }
2033
+ : {
2034
+ kind: "env.reconcile.ack",
2035
+ opId: frame.opId,
2036
+ ok: false,
2037
+ phase: "complete",
2038
+ attemptId: frame.attemptId,
2039
+ code: result.code,
2040
+ error: projectEnvRefusalMessage(result.code),
2041
+ appliedThrough: result.appliedThrough,
2042
+ },
2043
+ );
2044
+ }
2045
+
1335
2046
  async function connect(): Promise<void> {
1336
2047
  if (stopping || fatal) return;
1337
2048
  const requestGeneration = ++connectionRequestGeneration;
2049
+ // Incrementing the generation retires the prior socket immediately. Close
2050
+ // its replica gate synchronously, before awaiting any deferred cleanup, so
2051
+ // a task can never keep serving while a replacement connection is pending.
2052
+ if (sidecarMode()) closeProjectEnvReplicaGate();
1338
2053
 
1339
2054
  // A connection owns every interactive remote operation it starts. Tear the
1340
2055
  // previous generation down before constructing its replacement so a late
@@ -1392,6 +2107,34 @@ async function connect(): Promise<void> {
1392
2107
  }
1393
2108
  };
1394
2109
 
2110
+ let projectEnvOperationTail = Promise.resolve();
2111
+ const projectEnvGenerationIsCurrent = (): boolean =>
2112
+ remoteOperationsActive &&
2113
+ ws === socket &&
2114
+ requestGeneration === connectionRequestGeneration &&
2115
+ socket.readyState === WebSocket.OPEN;
2116
+ const enqueueProjectEnvOperation = (
2117
+ kind: string,
2118
+ operation: () => void | Promise<void>,
2119
+ ): void => {
2120
+ const queued = projectEnvOperationTail.then(async () => {
2121
+ if (!projectEnvGenerationIsCurrent()) return;
2122
+ try {
2123
+ await operation();
2124
+ } catch (error) {
2125
+ // A storage failure has no trustworthy cursor to acknowledge. Leave
2126
+ // it unanswered so the cloud retries on this or the next generation.
2127
+ console.warn(
2128
+ `[project-env] ${kind} failed: ${error instanceof Error ? error.message : String(error)}`,
2129
+ );
2130
+ }
2131
+ });
2132
+ projectEnvOperationTail = queued;
2133
+ // Replacement sockets and shutdown both join every admitted operation.
2134
+ // Queued callbacks re-check the exact generation before touching state.
2135
+ void trackRemoteOperationCleanup(queued);
2136
+ };
2137
+
1395
2138
  const emitEngineLoginEvent = (
1396
2139
  event: Extract<HostToCloud, { kind: "engine.login.event" }>,
1397
2140
  ): void => {
@@ -1405,6 +2148,15 @@ async function connect(): Promise<void> {
1405
2148
  };
1406
2149
 
1407
2150
  const cleanup = (): void => {
2151
+ // Connection loss revokes the serving gate before deferred transport
2152
+ // cleanup yields. A late close from a superseded socket must not revoke a
2153
+ // replacement generation that has already committed its fresh snapshot.
2154
+ if (
2155
+ sidecarMode() &&
2156
+ requestGeneration === connectionRequestGeneration
2157
+ ) {
2158
+ closeProjectEnvReplicaGate();
2159
+ }
1408
2160
  void stopRemoteOperations();
1409
2161
  if (pingTimer) clearInterval(pingTimer);
1410
2162
  if (deadTimer) clearInterval(deadTimer);
@@ -2006,9 +2758,115 @@ async function connect(): Promise<void> {
2006
2758
  );
2007
2759
  break;
2008
2760
  }
2761
+ case "env.staging.put": {
2762
+ // Only the write-only sign-in box owns immutable staging paths. An
2763
+ // owned runner or task sidecar must never accept plaintext here.
2764
+ if (!signInBoxMode()) break;
2765
+ void managedMutationDrain.track(() =>
2766
+ handleProjectEnvStagingPut(socket, frame),
2767
+ );
2768
+ break;
2769
+ }
2770
+ case "env.replica.begin": {
2771
+ // Replica authority exists only on a task sidecar. Pool machines,
2772
+ // owned runners and the sign-in box deliberately leave these frames
2773
+ // unanswered so a mismatched cloud cannot activate local authority.
2774
+ if (!sidecarMode() || sidecarTaskId() === null) break;
2775
+ enqueueProjectEnvOperation(frame.kind, () =>
2776
+ handleProjectEnvReplicaBegin(
2777
+ socket,
2778
+ frame,
2779
+ projectEnvGenerationIsCurrent,
2780
+ ),
2781
+ );
2782
+ break;
2783
+ }
2784
+ case "env.replica.page": {
2785
+ if (!sidecarMode() || sidecarTaskId() === null) break;
2786
+ enqueueProjectEnvOperation(frame.kind, () =>
2787
+ handleProjectEnvReplicaPage(
2788
+ socket,
2789
+ frame,
2790
+ projectEnvGenerationIsCurrent,
2791
+ ),
2792
+ );
2793
+ break;
2794
+ }
2795
+ case "env.replica.apply": {
2796
+ if (!sidecarMode() || sidecarTaskId() === null) break;
2797
+ enqueueProjectEnvOperation(frame.kind, () =>
2798
+ handleProjectEnvReplicaApply(
2799
+ socket,
2800
+ frame,
2801
+ projectEnvGenerationIsCurrent,
2802
+ ),
2803
+ );
2804
+ break;
2805
+ }
2806
+ case "env.replica.commit": {
2807
+ if (!sidecarMode() || sidecarTaskId() === null) break;
2808
+ enqueueProjectEnvOperation(frame.kind, () =>
2809
+ handleProjectEnvReplicaCommit(
2810
+ socket,
2811
+ frame,
2812
+ projectEnvGenerationIsCurrent,
2813
+ ),
2814
+ );
2815
+ break;
2816
+ }
2817
+ case "env.reconcile.begin": {
2818
+ // Sign-in boxes own the cloud staging authority, and task-machine
2819
+ // sidecars consume replica frames. Neither may enter the owned-runner
2820
+ // SQLite authority even if a mismatched cloud sends this frame.
2821
+ if (signInBoxMode() || sidecarMode()) break;
2822
+ enqueueProjectEnvOperation(frame.kind, () =>
2823
+ handleProjectEnvReconcileBegin(
2824
+ socket,
2825
+ frame,
2826
+ projectEnvGenerationIsCurrent,
2827
+ ),
2828
+ );
2829
+ break;
2830
+ }
2831
+ case "env.reconciled": {
2832
+ if (signInBoxMode() || sidecarMode()) break;
2833
+ enqueueProjectEnvOperation(frame.kind, () =>
2834
+ handleProjectEnvReconciled(socket, frame),
2835
+ );
2836
+ break;
2837
+ }
2838
+ case "env.var.remove":
2839
+ case "env.binding.set":
2840
+ case "env.delivery.skip": {
2841
+ if (signInBoxMode() || sidecarMode()) break;
2842
+ enqueueProjectEnvOperation(frame.kind, () =>
2843
+ handleProjectEnvDelivery(socket, frame),
2844
+ );
2845
+ break;
2846
+ }
2009
2847
  case "env.var.list":
2010
2848
  case "env.var.set":
2011
2849
  case "env.var.delete": {
2850
+ if (
2851
+ frame.kind === "env.var.list" &&
2852
+ frame.reconciliationAttemptId !== undefined
2853
+ ) {
2854
+ if (signInBoxMode() || sidecarMode()) break;
2855
+ enqueueProjectEnvOperation(frame.kind, () =>
2856
+ handleProjectEnvList(socket, frame),
2857
+ );
2858
+ break;
2859
+ }
2860
+ if (
2861
+ (frame.kind === "env.var.set" || frame.kind === "env.var.delete") &&
2862
+ frame.delivery !== undefined
2863
+ ) {
2864
+ if (signInBoxMode() || sidecarMode()) break;
2865
+ enqueueProjectEnvOperation(frame.kind, () =>
2866
+ handleProjectEnvDelivery(socket, frame),
2867
+ );
2868
+ break;
2869
+ }
2012
2870
  if (signInBoxMode()) {
2013
2871
  // ADR-136 sign-in box: no local rows — every op is a store call,
2014
2872
  // and the ack lists names from the store.
@@ -3163,55 +4021,33 @@ function parseCloudFrame(data: RawData): CloudToHost | null {
3163
4021
  ) {
3164
4022
  return { kind: frame.kind, userId: frame.userId };
3165
4023
  }
3166
- // `orgId` (ADR-133 tag scope): the project's org, when the cloud sends it.
3167
- // This normaliser rebuilds every frame from a fixed field list, so a field
3168
- // added to the type alone never reaches the handler — the org was dropped
3169
- // here for a whole release (0.9.133, live 2026-09-10) while the cloud sent
3170
- // it and the handler waited for it.
4024
+ // These shared guards are exact-shape parsers. Returning their original
4025
+ // frame preserves every CAS, attempt and delivery field for the serialized
4026
+ // authority lane below; a partial modern frame cannot degrade into a legacy
4027
+ // mutation because the guards reject mixed shapes.
4028
+ if (isProjectEnvListFrame(frame)) return frame;
4029
+ if (isProjectEnvSetFrame(frame)) return frame;
4030
+ if (isProjectEnvDeleteFrame(frame)) return frame;
3171
4031
  if (
3172
- frame.kind === "env.var.list" &&
3173
- typeof frame.opId === "string" &&
3174
- typeof frame.projectId === "string"
4032
+ isProjectEnvRemoveFrame(frame) ||
4033
+ isProjectEnvBindingSetFrame(frame) ||
4034
+ isProjectEnvReconcileBeginFrame(frame) ||
4035
+ isProjectEnvReconciledFrame(frame) ||
4036
+ isProjectEnvDeliverySkipFrame(frame)
3175
4037
  ) {
3176
- return {
3177
- kind: "env.var.list",
3178
- opId: frame.opId,
3179
- projectId: frame.projectId,
3180
- ...(typeof frame.orgId === "string" && frame.orgId.length > 0
3181
- ? { orgId: frame.orgId }
3182
- : {}),
3183
- };
4038
+ return frame;
3184
4039
  }
4040
+ if (isProjectEnvStagingPutFrame(frame)) return frame;
4041
+ // Sidecar replica authority has a separate durable store and apply lane.
4042
+ // Exact guards keep every attempt, cursor and immutable value reference;
4043
+ // mode admission happens in the dispatcher below.
3185
4044
  if (
3186
- frame.kind === "env.var.set" &&
3187
- typeof frame.opId === "string" &&
3188
- typeof frame.projectId === "string" &&
3189
- typeof frame.key === "string" &&
3190
- typeof frame.value === "string"
4045
+ isProjectEnvReplicaBeginFrame(frame) ||
4046
+ isProjectEnvReplicaPageFrame(frame) ||
4047
+ isProjectEnvReplicaApplyFrame(frame) ||
4048
+ isProjectEnvReplicaCommitFrame(frame)
3191
4049
  ) {
3192
- return {
3193
- kind: "env.var.set",
3194
- opId: frame.opId,
3195
- projectId: frame.projectId,
3196
- key: frame.key,
3197
- value: frame.value,
3198
- ...(typeof frame.orgId === "string" && frame.orgId.length > 0
3199
- ? { orgId: frame.orgId }
3200
- : {}),
3201
- };
3202
- }
3203
- if (
3204
- frame.kind === "env.var.delete" &&
3205
- typeof frame.opId === "string" &&
3206
- typeof frame.projectId === "string" &&
3207
- typeof frame.key === "string"
3208
- ) {
3209
- return {
3210
- kind: "env.var.delete",
3211
- opId: frame.opId,
3212
- projectId: frame.projectId,
3213
- key: frame.key,
3214
- };
4050
+ return frame;
3215
4051
  }
3216
4052
  if (
3217
4053
  frame.kind === "mcp.op" &&
@@ -3439,8 +4275,27 @@ function rejectFrameDuringShutdown(
3439
4275
  });
3440
4276
  return true;
3441
4277
  case "env.var.list":
4278
+ // A modern page has to report the durable stream cursor. The store is
4279
+ // already closed here, so let the replacement socket retry it instead
4280
+ // of fabricating a legacy-shaped acknowledgement.
4281
+ if (
4282
+ "after" in frame ||
4283
+ "reconciliationAttemptId" in frame
4284
+ ) {
4285
+ return true;
4286
+ }
4287
+ send(socket, {
4288
+ kind: "env.var.ack",
4289
+ opId: frame.opId,
4290
+ ok: false,
4291
+ error,
4292
+ });
4293
+ return true;
3442
4294
  case "env.var.set":
3443
4295
  case "env.var.delete":
4296
+ // Ordered mutations likewise need an authentic appliedThrough cursor.
4297
+ // Only the pre-activation legacy shape can receive the legacy error.
4298
+ if ("delivery" in frame) return true;
3444
4299
  send(socket, {
3445
4300
  kind: "env.var.ack",
3446
4301
  opId: frame.opId,
@@ -3448,6 +4303,20 @@ function rejectFrameDuringShutdown(
3448
4303
  error,
3449
4304
  });
3450
4305
  return true;
4306
+ case "env.var.remove":
4307
+ case "env.binding.set":
4308
+ case "env.staging.put":
4309
+ case "env.reconcile.begin":
4310
+ case "env.reconciled":
4311
+ case "env.delivery.skip":
4312
+ case "env.replica.begin":
4313
+ case "env.replica.page":
4314
+ case "env.replica.apply":
4315
+ case "env.replica.commit":
4316
+ // These ordered operations need the durable current cursor in an error
4317
+ // ack. Shutdown has already closed the store admission gate, so leave
4318
+ // them unanswered for the cloud to retry on the replacement socket.
4319
+ return true;
3451
4320
  case "mcp.op":
3452
4321
  send(socket, {
3453
4322
  kind: "mcp.ack",
@@ -3973,6 +4842,27 @@ function requestShutdown(signal: NodeJS.Signals): void {
3973
4842
  void stopRemoteOperationManagers();
3974
4843
  stopMcpGatewaySubscription();
3975
4844
  stopAgentClisReadySubscription();
4845
+ // Before the gateway: unregister its TLS consumer, end proxy connections,
4846
+ // refuse new root mutations of tasks and wait for those under way. Exit
4847
+ // waits for all of it (see secretsProxyStopped).
4848
+ setActiveSecretsProxy(null);
4849
+ if (secretsProxyUnpinSweep !== null) clearInterval(secretsProxyUnpinSweep);
4850
+ if (!secretsProxyStopped) {
4851
+ void Promise.allSettled([secretsProxy?.stop(), quiesceTaskProxyMutations()])
4852
+ .then((outcomes) => {
4853
+ for (const outcome of outcomes) {
4854
+ if (outcome.status === "rejected") {
4855
+ console.warn(
4856
+ `[secrets-proxy] stop failed: ${outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason)}`,
4857
+ );
4858
+ }
4859
+ }
4860
+ })
4861
+ .finally(() => {
4862
+ secretsProxyStopped = true;
4863
+ maybeExit();
4864
+ });
4865
+ }
3976
4866
  void mcpGateway.stop();
3977
4867
  managedMutationDrain.stopAccepting();
3978
4868
  stopRuntimeAutoRecheck();
@@ -3986,7 +4876,8 @@ function maybeExit(): void {
3986
4876
  !shutdownRequested ||
3987
4877
  inFlight > 0 ||
3988
4878
  managedMutationDrain.activeCount > 0 ||
3989
- pendingRemoteOperationCleanups.size > 0
4879
+ pendingRemoteOperationCleanups.size > 0 ||
4880
+ !secretsProxyStopped
3990
4881
  ) return;
3991
4882
  if (ws) {
3992
4883
  if (ws.readyState === WebSocket.OPEN) {