@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.
- package/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +312 -2
- package/images/standard/Dockerfile +44 -6
- package/images/standard/container/uai-init +85 -2
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/lib/agent-cli.ts +18 -1
- package/lib/agent.ts +99 -34
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/codex.ts +137 -13
- package/lib/agents/cursor.ts +15 -1
- package/lib/agents/durable-proc.ts +2 -0
- package/lib/agents/grok.ts +8 -0
- package/lib/agents/kimi.ts +1 -0
- package/lib/agents/machine-durable.ts +17 -0
- package/lib/agents/opencode.ts +1 -0
- package/lib/agents/proc.ts +6 -1
- package/lib/agents/session-state.ts +107 -0
- package/lib/agents/transport.ts +125 -58
- package/lib/agents/types.ts +3 -3
- package/lib/browser-testing.ts +6 -0
- package/lib/codex-auth.ts +2 -0
- package/lib/credential-store.ts +141 -6
- package/lib/engine-accounts.ts +1 -0
- package/lib/env-file.ts +7 -0
- package/lib/env-schema.ts +841 -51
- package/lib/git-diff.ts +1 -0
- package/lib/git-identity.ts +2 -0
- package/lib/github-tokens.ts +1 -0
- package/lib/host-env.ts +98 -18
- package/lib/lease.ts +11 -3
- package/lib/machine-gateway-tunnel.ts +195 -17
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/managed-runtime.ts +270 -51
- package/lib/mcp-gateway.ts +777 -26
- package/lib/orchestrator.ts +1681 -138
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-loader.ts +237 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/runtime-archive.ts +79 -16
- package/lib/runtime-authority.ts +44 -0
- package/lib/runtime-state.ts +175 -10
- package/lib/secrets-proxy/address.ts +72 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +60 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +141 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +574 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/shared-files-s3.ts +3 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +51 -3
- package/lib/standard-image.ts +50 -12
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +193 -0
- package/lib/task-environment/apple-container.ts +45 -11
- package/lib/task-environment/docker.ts +51 -12
- package/lib/task-environment/index.ts +6 -2
- package/lib/task-environment/machine-task-up.ts +258 -25
- package/lib/task-environment/machine.ts +80 -22
- package/lib/task-environment/registry.ts +3 -2
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +92 -16
- package/lib/task-environment/types.ts +63 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +44 -0
- package/lib/task-gateway-state.ts +497 -0
- package/lib/transcript.ts +1 -0
- package/lib/ulid.ts +10 -1
- package/package.json +13 -1
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/task-up.sh +476 -27
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/src/cli.ts +26 -3
- package/src/index.ts +273 -19
- package/src/main.ts +941 -48
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +281 -0
- package/src/project-env-wording.ts +47 -0
- package/src/protocol.ts +2239 -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,
|
|
@@ -212,6 +250,7 @@ import {
|
|
|
212
250
|
HOST_ENGINE_ACCOUNT_SCOPES_PROTOCOL_FEATURE,
|
|
213
251
|
HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
|
|
214
252
|
HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
|
|
253
|
+
LIVE_ROSTER_EDIT_PROTOCOL_FEATURE,
|
|
215
254
|
isEngineAccountScope,
|
|
216
255
|
HOST_LOGS_PROTOCOL_FEATURE,
|
|
217
256
|
HOST_TASK_INVENTORY_PROTOCOL_FEATURE,
|
|
@@ -223,6 +262,8 @@ import {
|
|
|
223
262
|
type EngineAccountSummary,
|
|
224
263
|
type EngineCatalogCapability,
|
|
225
264
|
MAX_HOST_TASK_INVENTORY_PAGE_SIZE,
|
|
265
|
+
MAX_PROJECT_ENV_ACK_PAGE_BYTES,
|
|
266
|
+
MAX_REMOTE_HOST_MESSAGE_BYTES,
|
|
226
267
|
MCP_GATEWAY_HEALTH_PROTOCOL_FEATURE,
|
|
227
268
|
SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE,
|
|
228
269
|
TRANSCRIPT_TARGETS_PROTOCOL_FEATURE,
|
|
@@ -234,6 +275,8 @@ import {
|
|
|
234
275
|
type HostCommandResult,
|
|
235
276
|
type HostCommands,
|
|
236
277
|
type HostToCloud,
|
|
278
|
+
type ProjectEnvListFrame,
|
|
279
|
+
type ProjectEnvReplicaAckFrame,
|
|
237
280
|
type PermissionDecision,
|
|
238
281
|
parseGitHubCredentialGeneration,
|
|
239
282
|
parseChannelMode,
|
|
@@ -246,6 +289,19 @@ import {
|
|
|
246
289
|
isHostConfigSetFrame,
|
|
247
290
|
isHostLogsStartFrame,
|
|
248
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,
|
|
249
305
|
type TaskAgent,
|
|
250
306
|
type TaskCommandProject,
|
|
251
307
|
type TaskCommandTask,
|
|
@@ -258,8 +314,14 @@ import {
|
|
|
258
314
|
isEngineAccountLabel,
|
|
259
315
|
isEngineConnectKind,
|
|
260
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";
|
|
261
323
|
import { isEnvironmentTransportProvider } from "../lib/task-environment/types";
|
|
262
|
-
import { LeaseManager } from "../lib/lease";
|
|
324
|
+
import { LeaseManager, leaseScope } from "../lib/lease";
|
|
263
325
|
import { markSidecarReady, sidecarMode, sidecarPoolMode, sidecarTaskId } from "../lib/sidecar";
|
|
264
326
|
import {
|
|
265
327
|
purgeDefaultEngineSlots,
|
|
@@ -274,9 +336,13 @@ import {
|
|
|
274
336
|
reconcileUserCredentialMetadataFromStore,
|
|
275
337
|
setProjectEnvStoreOnly,
|
|
276
338
|
} from "../lib/signin-box-credentials";
|
|
339
|
+
import {
|
|
340
|
+
ProjectEnvStagingInvalidError,
|
|
341
|
+
writeProjectEnvStaging,
|
|
342
|
+
} from "../lib/project-env-staging";
|
|
277
343
|
import { loadGithubTokenIntoCache } from "../lib/github-tokens";
|
|
278
344
|
import { ensureSshKeyStoreFirst } from "../lib/ssh";
|
|
279
|
-
import { resumeSidecarTask } from "../lib/sidecar-resume";
|
|
345
|
+
import { classifySidecarTaskView, resumeSidecarTask } from "../lib/sidecar-resume";
|
|
280
346
|
import { declareAgentClisReady } from "../lib/standard-image";
|
|
281
347
|
|
|
282
348
|
setContainerRuntimeOperationalCheck(() => containerRuntimeProblem() === null);
|
|
@@ -488,6 +554,46 @@ try {
|
|
|
488
554
|
);
|
|
489
555
|
}
|
|
490
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
|
+
}
|
|
491
597
|
// ADR-134: on a Uai-operated host the engine-account rows are a cache of the
|
|
492
598
|
// credential store. Import what this host lacks and upload what was never
|
|
493
599
|
// mirrored, off the startup path — task-up resolves locally either way.
|
|
@@ -672,6 +778,11 @@ const leaseManager: LeaseManager | null = sidecarTaskId()
|
|
|
672
778
|
console.warn(
|
|
673
779
|
`[sidecar] resume failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
674
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();
|
|
675
786
|
});
|
|
676
787
|
});
|
|
677
788
|
startCredentialStorePickup({ repush: repushBrokeredCopies });
|
|
@@ -688,6 +799,18 @@ const leaseManager: LeaseManager | null = sidecarTaskId()
|
|
|
688
799
|
})
|
|
689
800
|
: null;
|
|
690
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
|
+
}
|
|
691
814
|
// Reconcile a durable retention-off setting before recovery/connect can make
|
|
692
815
|
// crash-left owner-facing terminal history observable again. Correctness
|
|
693
816
|
// tombstones remain intact inside applyTerminalHistoryRetention.
|
|
@@ -803,10 +926,90 @@ hostEvents.subscribe((event) => {
|
|
|
803
926
|
eventOutbox.enqueue(event);
|
|
804
927
|
flushEvents();
|
|
805
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;
|
|
806
1003
|
// Start local health-bearing services before the bridge. The first
|
|
807
1004
|
// authenticated capability frame must describe the gateway even when its
|
|
808
1005
|
// initial bind is still pending, and both one-way readiness edges re-advertise
|
|
809
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
|
+
}
|
|
810
1013
|
const mcpGateway = startMcpGateway();
|
|
811
1014
|
const stopMcpGatewaySubscription = mcpGateway.subscribe(() =>
|
|
812
1015
|
sendCapabilities(),
|
|
@@ -895,6 +1098,7 @@ async function startLocalUi(): Promise<void> {
|
|
|
895
1098
|
/** Build the current host capability advertisement (ADR-021). */
|
|
896
1099
|
function buildCapabilities(): HostCapabilities {
|
|
897
1100
|
const runtime = containerRuntimeCapabilityState();
|
|
1101
|
+
const projectEnvBindings = projectEnvBindingCapability();
|
|
898
1102
|
return {
|
|
899
1103
|
version: packageVersion(),
|
|
900
1104
|
protocolFeatures: [
|
|
@@ -916,13 +1120,18 @@ function buildCapabilities(): HostCapabilities {
|
|
|
916
1120
|
HOST_ENGINE_ACCOUNT_SCOPES_PROTOCOL_FEATURE,
|
|
917
1121
|
HOST_ENGINE_ACCOUNT_MANAGE_PROTOCOL_FEATURE,
|
|
918
1122
|
HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
|
|
1123
|
+
LIVE_ROSTER_EDIT_PROTOCOL_FEATURE,
|
|
919
1124
|
// The echo adapter cannot execute the in-task CLI. Advertising typed
|
|
920
1125
|
// dispatch in mock mode would let the composer create a Secretary that
|
|
921
1126
|
// has no way to wake crew.
|
|
922
1127
|
...(canAdvertiseTypedSecretaryDispatch()
|
|
923
1128
|
? [SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE]
|
|
924
1129
|
: []),
|
|
1130
|
+
...(projectEnvBindings === null ? [] : [projectEnvBindings.feature]),
|
|
925
1131
|
],
|
|
1132
|
+
...(projectEnvBindings === null
|
|
1133
|
+
? {}
|
|
1134
|
+
: { projectEnvPolicyEpochs: projectEnvBindings.policyEpochs }),
|
|
926
1135
|
agentKinds: agentKindCapabilities(),
|
|
927
1136
|
// Language runtimes live in the standard container image. Do not
|
|
928
1137
|
// advertise them as usable while the machine cannot run a container.
|
|
@@ -1182,6 +1391,7 @@ async function activateContainerRuntime(
|
|
|
1182
1391
|
) {
|
|
1183
1392
|
watchCodexAuth(() => containerRuntimeProblem() === null);
|
|
1184
1393
|
resumeRuntimeDeferredAgentWork();
|
|
1394
|
+
startSecretsProxyUpkeep();
|
|
1185
1395
|
sendCapabilities();
|
|
1186
1396
|
}
|
|
1187
1397
|
},
|
|
@@ -1330,9 +1540,516 @@ async function remoteOperationsReadyForConnect(
|
|
|
1330
1540
|
);
|
|
1331
1541
|
}
|
|
1332
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
|
+
|
|
1333
2046
|
async function connect(): Promise<void> {
|
|
1334
2047
|
if (stopping || fatal) return;
|
|
1335
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();
|
|
1336
2053
|
|
|
1337
2054
|
// A connection owns every interactive remote operation it starts. Tear the
|
|
1338
2055
|
// previous generation down before constructing its replacement so a late
|
|
@@ -1390,6 +2107,34 @@ async function connect(): Promise<void> {
|
|
|
1390
2107
|
}
|
|
1391
2108
|
};
|
|
1392
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
|
+
|
|
1393
2138
|
const emitEngineLoginEvent = (
|
|
1394
2139
|
event: Extract<HostToCloud, { kind: "engine.login.event" }>,
|
|
1395
2140
|
): void => {
|
|
@@ -1403,6 +2148,15 @@ async function connect(): Promise<void> {
|
|
|
1403
2148
|
};
|
|
1404
2149
|
|
|
1405
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
|
+
}
|
|
1406
2160
|
void stopRemoteOperations();
|
|
1407
2161
|
if (pingTimer) clearInterval(pingTimer);
|
|
1408
2162
|
if (deadTimer) clearInterval(deadTimer);
|
|
@@ -2004,9 +2758,115 @@ async function connect(): Promise<void> {
|
|
|
2004
2758
|
);
|
|
2005
2759
|
break;
|
|
2006
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
|
+
}
|
|
2007
2847
|
case "env.var.list":
|
|
2008
2848
|
case "env.var.set":
|
|
2009
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
|
+
}
|
|
2010
2870
|
if (signInBoxMode()) {
|
|
2011
2871
|
// ADR-136 sign-in box: no local rows — every op is a store call,
|
|
2012
2872
|
// and the ack lists names from the store.
|
|
@@ -3161,55 +4021,33 @@ function parseCloudFrame(data: RawData): CloudToHost | null {
|
|
|
3161
4021
|
) {
|
|
3162
4022
|
return { kind: frame.kind, userId: frame.userId };
|
|
3163
4023
|
}
|
|
3164
|
-
//
|
|
3165
|
-
//
|
|
3166
|
-
//
|
|
3167
|
-
//
|
|
3168
|
-
|
|
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;
|
|
3169
4031
|
if (
|
|
3170
|
-
frame
|
|
3171
|
-
|
|
3172
|
-
|
|
4032
|
+
isProjectEnvRemoveFrame(frame) ||
|
|
4033
|
+
isProjectEnvBindingSetFrame(frame) ||
|
|
4034
|
+
isProjectEnvReconcileBeginFrame(frame) ||
|
|
4035
|
+
isProjectEnvReconciledFrame(frame) ||
|
|
4036
|
+
isProjectEnvDeliverySkipFrame(frame)
|
|
3173
4037
|
) {
|
|
3174
|
-
return
|
|
3175
|
-
kind: "env.var.list",
|
|
3176
|
-
opId: frame.opId,
|
|
3177
|
-
projectId: frame.projectId,
|
|
3178
|
-
...(typeof frame.orgId === "string" && frame.orgId.length > 0
|
|
3179
|
-
? { orgId: frame.orgId }
|
|
3180
|
-
: {}),
|
|
3181
|
-
};
|
|
4038
|
+
return frame;
|
|
3182
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.
|
|
3183
4044
|
if (
|
|
3184
|
-
frame
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
typeof frame.value === "string"
|
|
4045
|
+
isProjectEnvReplicaBeginFrame(frame) ||
|
|
4046
|
+
isProjectEnvReplicaPageFrame(frame) ||
|
|
4047
|
+
isProjectEnvReplicaApplyFrame(frame) ||
|
|
4048
|
+
isProjectEnvReplicaCommitFrame(frame)
|
|
3189
4049
|
) {
|
|
3190
|
-
return
|
|
3191
|
-
kind: "env.var.set",
|
|
3192
|
-
opId: frame.opId,
|
|
3193
|
-
projectId: frame.projectId,
|
|
3194
|
-
key: frame.key,
|
|
3195
|
-
value: frame.value,
|
|
3196
|
-
...(typeof frame.orgId === "string" && frame.orgId.length > 0
|
|
3197
|
-
? { orgId: frame.orgId }
|
|
3198
|
-
: {}),
|
|
3199
|
-
};
|
|
3200
|
-
}
|
|
3201
|
-
if (
|
|
3202
|
-
frame.kind === "env.var.delete" &&
|
|
3203
|
-
typeof frame.opId === "string" &&
|
|
3204
|
-
typeof frame.projectId === "string" &&
|
|
3205
|
-
typeof frame.key === "string"
|
|
3206
|
-
) {
|
|
3207
|
-
return {
|
|
3208
|
-
kind: "env.var.delete",
|
|
3209
|
-
opId: frame.opId,
|
|
3210
|
-
projectId: frame.projectId,
|
|
3211
|
-
key: frame.key,
|
|
3212
|
-
};
|
|
4050
|
+
return frame;
|
|
3213
4051
|
}
|
|
3214
4052
|
if (
|
|
3215
4053
|
frame.kind === "mcp.op" &&
|
|
@@ -3437,8 +4275,27 @@ function rejectFrameDuringShutdown(
|
|
|
3437
4275
|
});
|
|
3438
4276
|
return true;
|
|
3439
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;
|
|
3440
4294
|
case "env.var.set":
|
|
3441
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;
|
|
3442
4299
|
send(socket, {
|
|
3443
4300
|
kind: "env.var.ack",
|
|
3444
4301
|
opId: frame.opId,
|
|
@@ -3446,6 +4303,20 @@ function rejectFrameDuringShutdown(
|
|
|
3446
4303
|
error,
|
|
3447
4304
|
});
|
|
3448
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;
|
|
3449
4320
|
case "mcp.op":
|
|
3450
4321
|
send(socket, {
|
|
3451
4322
|
kind: "mcp.ack",
|
|
@@ -3971,6 +4842,27 @@ function requestShutdown(signal: NodeJS.Signals): void {
|
|
|
3971
4842
|
void stopRemoteOperationManagers();
|
|
3972
4843
|
stopMcpGatewaySubscription();
|
|
3973
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
|
+
}
|
|
3974
4866
|
void mcpGateway.stop();
|
|
3975
4867
|
managedMutationDrain.stopAccepting();
|
|
3976
4868
|
stopRuntimeAutoRecheck();
|
|
@@ -3984,7 +4876,8 @@ function maybeExit(): void {
|
|
|
3984
4876
|
!shutdownRequested ||
|
|
3985
4877
|
inFlight > 0 ||
|
|
3986
4878
|
managedMutationDrain.activeCount > 0 ||
|
|
3987
|
-
pendingRemoteOperationCleanups.size > 0
|
|
4879
|
+
pendingRemoteOperationCleanups.size > 0 ||
|
|
4880
|
+
!secretsProxyStopped
|
|
3988
4881
|
) return;
|
|
3989
4882
|
if (ws) {
|
|
3990
4883
|
if (ws.readyState === WebSocket.OPEN) {
|