@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.
- 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/codex.ts +9 -1
- package/lib/agents/cursor.ts +15 -1
- package/lib/agents/grok.ts +8 -0
- package/lib/agents/kimi.ts +1 -0
- package/lib/agents/machine-durable.ts +2 -0
- package/lib/agents/opencode.ts +1 -0
- package/lib/agents/proc.ts +6 -1
- package/lib/agents/transport.ts +9 -5
- 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 +391 -69
- 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/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 +939 -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 +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
|
-
//
|
|
3167
|
-
//
|
|
3168
|
-
//
|
|
3169
|
-
//
|
|
3170
|
-
|
|
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
|
|
3173
|
-
|
|
3174
|
-
|
|
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
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
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) {
|