@cotal-ai/manager 0.57.0 → 0.58.0
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/README.md +13 -0
- package/dist/commands.js +58 -5
- package/dist/commands.js.map +1 -1
- package/dist/console/session-bundle.js +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/manager.d.ts +50 -18
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +352 -129
- package/dist/manager.js.map +1 -1
- package/dist/remote-authority.d.ts +43 -3
- package/dist/remote-authority.d.ts.map +1 -1
- package/dist/remote-authority.js +140 -1
- package/dist/remote-authority.js.map +1 -1
- package/dist/run-hosting.d.ts +55 -1
- package/dist/run-hosting.d.ts.map +1 -1
- package/dist/run-hosting.js +98 -14
- package/dist/run-hosting.js.map +1 -1
- package/dist/runtime/custodial-pty.d.ts +4 -4
- package/dist/runtime/custodial-pty.js +4 -4
- package/dist/runtime/pty.d.ts.map +1 -1
- package/dist/runtime/pty.js +5 -1
- package/dist/runtime/pty.js.map +1 -1
- package/package.json +15 -13
package/dist/manager.js
CHANGED
|
@@ -4,7 +4,7 @@ import { hostname } from "node:os";
|
|
|
4
4
|
import { credsAuthenticator } from "@nats-io/transport-node";
|
|
5
5
|
import { existsSync, lstatSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
6
6
|
import { isAbsolute, join, dirname, resolve } from "node:path";
|
|
7
|
-
import { CotalEndpoint, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, divergentSecretStoreNotice, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, mintCreds, mintGeneration, mintAcceptedToken, openIssuedStore, acceptedBucket, issuedBucket, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, parseShareSelection, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE,
|
|
7
|
+
import { CotalEndpoint, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, divergentSecretStoreNotice, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, DeprovisionError, isConcreteChannel, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, mintCreds, mintGeneration, mintAcceptedToken, openIssuedStore, acceptedBucket, issuedBucket, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, parseShareSelection, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannelPrincipal, resolveService, invokeCommand, } from "@cotal-ai/core";
|
|
8
8
|
import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, connectorInstallHint, DEFAULT_CONNECTOR, defaultAgentType, DELIVERY_CREDS_KIND, extensionConnectors, findCotalRoot, getSpaceAuth, hasUserAuthState, loadExtensionsManifest, loadManagerInstanceIdentity, loadMeshes, localTrustOfSpace, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KIND, mergeLaunchOptions, remintDaemonCreds, resolveOnPath, createManagerInstanceIdentity, spaceMaterialKey, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
|
|
9
9
|
import { createRuntime, isCustodialRuntime, requireRuntimeAdopt, requireRuntimeReap, RuntimeReapUnproven, } from "./runtime/index.js";
|
|
10
10
|
import { AttachEndpoint } from "./attach-endpoint.js";
|
|
@@ -197,55 +197,6 @@ function parseTurnNote(raw) {
|
|
|
197
197
|
function turnHoldToken(goalId) {
|
|
198
198
|
return createHash("sha256").update(`${goalId}:turn-deadline`, "utf8").digest("base64url").slice(0, 43);
|
|
199
199
|
}
|
|
200
|
-
/** One ep request/reply round-trip on the caller's OWN reply-plane filter (§13.2). The responder
|
|
201
|
-
* derives the reply subject from the authenticated request, so there is no caller-selected reply
|
|
202
|
-
* target to honour; the caller binds the answer off the reply SUBJECT — endpoint and nonce, both
|
|
203
|
-
* broker-pinned by the responder's serve publish grant. A reply on another nonce belongs to a
|
|
204
|
-
* different request (the rail is shared) and is ignored rather than allowed to fail this one. */
|
|
205
|
-
export async function epAwaitReply(nc, space, caller, nonce, requestId, requestSubject, body, timeoutMs) {
|
|
206
|
-
let sub;
|
|
207
|
-
let timer;
|
|
208
|
-
try {
|
|
209
|
-
const got = new Promise((resolve, reject) => {
|
|
210
|
-
sub = nc.subscribe(epCallerReplyFilter(space, caller), {
|
|
211
|
-
callback: (err, msg) => {
|
|
212
|
-
if (err) {
|
|
213
|
-
reject(new Error(`the retirement reply subscription failed: ${err.message}`));
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const parsed = parseEpSubject(msg.subject);
|
|
217
|
-
if (!parsed || parsed.plane !== "reply" || parsed.endpoint !== AUTH_ENDPOINT || parsed.nonce !== nonce)
|
|
218
|
-
return;
|
|
219
|
-
let body;
|
|
220
|
-
try {
|
|
221
|
-
body = JSON.parse(new TextDecoder().decode(msg.data));
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
return;
|
|
225
|
-
} // a malformed body on our nonce is not an answer; keep waiting
|
|
226
|
-
// REQUIRE THE ID ECHO. Binding on (endpoint, nonce) alone would accept a malformed or
|
|
227
|
-
// WRONG-ID `{ok:true}` and clear a retirement hold on it. Ignore rather than fail: the
|
|
228
|
-
// rail is shared across concurrent requests and a responder may publish at any nonce, so
|
|
229
|
-
// an unmatched reply is someone else's answer, never grounds to fail the honest one.
|
|
230
|
-
if (body.id !== requestId)
|
|
231
|
-
return;
|
|
232
|
-
resolve(body);
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
|
|
236
|
-
nc.publish(requestSubject, new TextEncoder().encode(body));
|
|
237
|
-
});
|
|
238
|
-
return await got;
|
|
239
|
-
}
|
|
240
|
-
finally {
|
|
241
|
-
if (timer)
|
|
242
|
-
clearTimeout(timer);
|
|
243
|
-
try {
|
|
244
|
-
sub?.unsubscribe();
|
|
245
|
-
}
|
|
246
|
-
catch { /* connection already down */ }
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
200
|
/**
|
|
250
201
|
* The agent supervisor: a long-lived mesh node that owns agent process lifecycle.
|
|
251
202
|
* It serves control requests on the "manager" service and spawns/kills agents
|
|
@@ -467,6 +418,8 @@ export class Manager {
|
|
|
467
418
|
* lifecycle authority: every re-drive re-reads broker KV and re-runs planStaticSlotResume. */
|
|
468
419
|
staticReconcileItems = new Map();
|
|
469
420
|
staticReconcileLastSweep;
|
|
421
|
+
staticReconcileLastSweepResources;
|
|
422
|
+
lastDeprovisionResources;
|
|
470
423
|
staticReconcileSweepsInFlight = 0;
|
|
471
424
|
staticReconcileDrainWaiters = [];
|
|
472
425
|
staticReconcileStopping = false;
|
|
@@ -549,6 +502,11 @@ export class Manager {
|
|
|
549
502
|
resumedAgentNames = new Set();
|
|
550
503
|
remoteAuthority;
|
|
551
504
|
remoteExecutorCreds;
|
|
505
|
+
remoteSupervisorCreds;
|
|
506
|
+
remoteBundleRenewal;
|
|
507
|
+
/** A refused or failed standing candidate is cleanup debt, never a silent loss: the last-good
|
|
508
|
+
* family stays live and this records what the host may have signed but the manager never adopted. */
|
|
509
|
+
remoteRenewalDebt;
|
|
552
510
|
/** Every broker dial this manager makes, through ONE transport decision.
|
|
553
511
|
*
|
|
554
512
|
* `this.servers` is whatever the mesh record held (`cotal supervise` reads it from there), so it
|
|
@@ -561,6 +519,10 @@ export class Manager {
|
|
|
561
519
|
return dialerFor(servers)({ ...opts, servers });
|
|
562
520
|
}
|
|
563
521
|
constructor(opts) {
|
|
522
|
+
if (opts.pooled && (!opts.remoteAuthority || opts.runtime === undefined || opts.runtime === "auto" || opts.runtime === "pty"))
|
|
523
|
+
throw new Error("pooled control requires signerless remote authority and an explicit non-PTY runtime; local custodial execution is forbidden");
|
|
524
|
+
if (opts.pooled && !opts.remoteAuthority?.renewStandingBundle)
|
|
525
|
+
throw new Error("pooled control requires a closed host-issued all-duty renewal callback before construction");
|
|
564
526
|
this.space = opts.space;
|
|
565
527
|
this.servers = opts.servers;
|
|
566
528
|
this.name = opts.name ?? "manager";
|
|
@@ -569,6 +531,10 @@ export class Manager {
|
|
|
569
531
|
this.maxSessions = opts.maxSessions;
|
|
570
532
|
this.remoteAuthority = opts.remoteAuthority;
|
|
571
533
|
this.remoteExecutorCreds = opts.remoteAuthority?.executorCreds;
|
|
534
|
+
this.remoteSupervisorCreds = opts.remoteAuthority?.supervisorCreds;
|
|
535
|
+
if (opts.pooled && (!opts.remoteAuthority?.accountPublicKey ||
|
|
536
|
+
accountFromCreds(opts.remoteAuthority.supervisorCreds) !== opts.remoteAuthority.accountPublicKey))
|
|
537
|
+
throw new Error("pooled control requires a proved assigned account for its initial supervisor credential");
|
|
572
538
|
if (opts.remoteAuthority)
|
|
573
539
|
this.managerLifecycleUid = opts.remoteAuthority.lifecycleUid;
|
|
574
540
|
this.secrets = opts.secretStore ?? workspaceSecretStore(this.workspaceRoot);
|
|
@@ -577,6 +543,8 @@ export class Manager {
|
|
|
577
543
|
: { kind: "fs", root: resolve(this.workspaceRoot) };
|
|
578
544
|
this.installedExtensions = opts.installedExtensions ?? false;
|
|
579
545
|
this.runtime = createRuntime(opts.runtime ?? "auto", `cotal-${this.space}`);
|
|
546
|
+
if (opts.pooled && isCustodialRuntime(this.runtime))
|
|
547
|
+
throw new Error(`pooled control refuses custodial runtime "${this.runtime.kind}" before starting agents`);
|
|
580
548
|
this.preserveStopTimeoutMs = opts.preserveStopTimeoutMs ?? PRESERVE_STOP_TIMEOUT_MS;
|
|
581
549
|
this.endpointServeExecutorExpiresInSeconds = opts.endpointServeExecutorExpiresInSeconds;
|
|
582
550
|
if (opts.resumeAttemptId && !/^[A-Za-z0-9_-]{1,128}$/.test(opts.resumeAttemptId))
|
|
@@ -792,7 +760,7 @@ export class Manager {
|
|
|
792
760
|
if (this.remoteAuthority) {
|
|
793
761
|
const remote = this.remoteAuthority;
|
|
794
762
|
id = remote.identities.supervisor.id;
|
|
795
|
-
creds = async () =>
|
|
763
|
+
creds = async () => this.remoteSupervisorCreds;
|
|
796
764
|
}
|
|
797
765
|
else if (this.auth) {
|
|
798
766
|
const identity = newIdentity();
|
|
@@ -895,8 +863,38 @@ export class Manager {
|
|
|
895
863
|
this.credRenewTimer.unref?.();
|
|
896
864
|
}
|
|
897
865
|
else if (this.remoteAuthority) {
|
|
898
|
-
|
|
899
|
-
|
|
866
|
+
if (this.remoteAuthority.renewStandingBundle)
|
|
867
|
+
await this.renewRemoteStandingBundle();
|
|
868
|
+
else
|
|
869
|
+
await this.renewRemoteExecutor();
|
|
870
|
+
let standingTtl = STANDING_RENEWABLE_TTL_SEC;
|
|
871
|
+
if (this.remoteSupervisorCreds) {
|
|
872
|
+
try {
|
|
873
|
+
const claims = credsClaims(this.remoteSupervisorCreds);
|
|
874
|
+
if (typeof claims.exp === "number" && typeof claims.iat === "number" && claims.exp > claims.iat) {
|
|
875
|
+
standingTtl = claims.exp - claims.iat;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch { }
|
|
879
|
+
}
|
|
880
|
+
const intervalMs = Math.min(credRenewIntervalMs(5 * 60), credRenewIntervalMs(standingTtl));
|
|
881
|
+
this.credRenewTimer = setInterval(async () => {
|
|
882
|
+
try {
|
|
883
|
+
if (this.remoteAuthority?.renewStandingBundle)
|
|
884
|
+
await this.renewRemoteStandingBundle();
|
|
885
|
+
else
|
|
886
|
+
await this.renewRemoteExecutor();
|
|
887
|
+
}
|
|
888
|
+
catch (e) {
|
|
889
|
+
console.error(`! remote manager renewal pass failed: ${e.message}`);
|
|
890
|
+
}
|
|
891
|
+
try {
|
|
892
|
+
await this.runHosting?.renew();
|
|
893
|
+
}
|
|
894
|
+
catch (e) {
|
|
895
|
+
console.error(`! remote run-hosting renewal pass failed: ${e.message}`);
|
|
896
|
+
}
|
|
897
|
+
}, intervalMs);
|
|
900
898
|
this.credRenewTimer.unref?.();
|
|
901
899
|
}
|
|
902
900
|
// stop() fences before it waits for an accepted startup reconciliation terminal. Once that
|
|
@@ -929,9 +927,26 @@ export class Manager {
|
|
|
929
927
|
// incarnation takes back every run a dead predecessor was driving before it accepts new ones.
|
|
930
928
|
// The serve surface is already live by here, so the family itself holds the gate: `runHost()`
|
|
931
929
|
// refuses `run-start`/`run-resume` as `unavailable` until the host exists and `RunHosting`
|
|
932
|
-
// refuses them until its reconcile has returned. A user-auth
|
|
933
|
-
//
|
|
934
|
-
|
|
930
|
+
// refuses them until its reconcile has returned. A remote user-auth manager uses its four
|
|
931
|
+
// registered issuer callbacks instead of holding a signer; a local user mesh stands none up.
|
|
932
|
+
const remoteRuns = this.remoteAuthority?.runHosting;
|
|
933
|
+
if (this.remoteAuthority && remoteRuns) {
|
|
934
|
+
this.runHosting = new RunHosting({
|
|
935
|
+
space: this.space,
|
|
936
|
+
servers: this.servers,
|
|
937
|
+
endpoint: MANAGER_ENDPOINT,
|
|
938
|
+
instanceId: this.managerInstanceId,
|
|
939
|
+
holder: { id: this.remoteAuthority.identities.supervisor.id, lifecycleUid: this.managerLifecycleUid },
|
|
940
|
+
auth: undefined,
|
|
941
|
+
log: (line) => console.error(line),
|
|
942
|
+
admitRun: remoteRuns.admitRun,
|
|
943
|
+
issueAttempt: remoteRuns.issueAttempt,
|
|
944
|
+
issueOperator: remoteRuns.issueOperator,
|
|
945
|
+
renewRun: remoteRuns.renewRun,
|
|
946
|
+
});
|
|
947
|
+
await this.runHosting.reconcile();
|
|
948
|
+
}
|
|
949
|
+
else if (!this.remoteAuthority && !this.userMode) {
|
|
935
950
|
this.runHosting = new RunHosting({
|
|
936
951
|
space: this.space,
|
|
937
952
|
servers: this.servers,
|
|
@@ -1339,6 +1354,94 @@ export class Manager {
|
|
|
1339
1354
|
throw e;
|
|
1340
1355
|
}
|
|
1341
1356
|
}
|
|
1357
|
+
/** An active signerless manager renews its standing duties together. The issuer stages the closed
|
|
1358
|
+
* credential family first; broker preflight proves every JWT and existing caller-held nkey
|
|
1359
|
+
* before any mutable holder changes. A failed candidate leaves all current creds intact. */
|
|
1360
|
+
async renewRemoteStandingBundle(force = false) {
|
|
1361
|
+
if (this.remoteBundleRenewal)
|
|
1362
|
+
return this.remoteBundleRenewal;
|
|
1363
|
+
const renewal = this.renewRemoteStandingBundleOnce(force);
|
|
1364
|
+
this.remoteBundleRenewal = renewal;
|
|
1365
|
+
try {
|
|
1366
|
+
await renewal;
|
|
1367
|
+
}
|
|
1368
|
+
finally {
|
|
1369
|
+
if (this.remoteBundleRenewal === renewal)
|
|
1370
|
+
this.remoteBundleRenewal = undefined;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
/** A signerless manager's closed standing connection re-dials through the all-duty family only.
|
|
1374
|
+
* A healthy holder re-dials as is. An unhealthy one waits for the closed renewal and adoption,
|
|
1375
|
+
* and a refusal fails this attempt with last-good and debt kept. Nothing is minted locally. */
|
|
1376
|
+
async renewRemoteBeforeRedial(label, read) {
|
|
1377
|
+
if (inspectCredHealth(read() ?? "").state === "healthy")
|
|
1378
|
+
return;
|
|
1379
|
+
await this.renewRemoteStandingBundle(true);
|
|
1380
|
+
if (inspectCredHealth(read() ?? "").state !== "healthy")
|
|
1381
|
+
throw new Error(`the ${label} credential is not healthy after all-duty renewal; last-good is kept and nothing is minted locally`);
|
|
1382
|
+
}
|
|
1383
|
+
async renewRemoteStandingBundleOnce(force) {
|
|
1384
|
+
const remote = this.remoteAuthority;
|
|
1385
|
+
if (!remote?.renewStandingBundle || !this.serviceServe)
|
|
1386
|
+
return;
|
|
1387
|
+
const current = [this.remoteSupervisorCreds, this.remoteExecutorCreds, this.serviceServe.creds,
|
|
1388
|
+
this.goalWriterCreds, this.sessionLedgerCreds];
|
|
1389
|
+
if (!force && current.every((cred) => cred && inspectCredHealth(cred).state === "healthy"))
|
|
1390
|
+
return;
|
|
1391
|
+
const processEpoch = this.serviceServe.grant.epoch;
|
|
1392
|
+
let candidate;
|
|
1393
|
+
try {
|
|
1394
|
+
candidate = await remote.renewStandingBundle(processEpoch);
|
|
1395
|
+
const names = ["supervisor", "executor", "serve", "goalWriter", "sessionLedger"];
|
|
1396
|
+
if (Object.keys(candidate).sort().join(",") !== [...names].sort().join(","))
|
|
1397
|
+
throw new Error("host returned an incomplete or widened standing credential family");
|
|
1398
|
+
for (const name of names) {
|
|
1399
|
+
if (typeof candidate[name] !== "string" || inspectCredHealth(candidate[name]).state === "unreadable")
|
|
1400
|
+
throw new Error(`host returned an unreadable ${name} credential`);
|
|
1401
|
+
if (!remote.accountPublicKey || accountFromCreds(candidate[name]) !== remote.accountPublicKey)
|
|
1402
|
+
throw new Error(`host returned a ${name} credential for another account`);
|
|
1403
|
+
if (credsClaims(candidate[name]).sub !== remote.identities[name].id)
|
|
1404
|
+
throw new Error(`host returned a ${name} credential for a different nkey than the held identity`);
|
|
1405
|
+
const staged = candidate[name];
|
|
1406
|
+
const nc = await this.dial({ authenticator: (nonce) => credsAuthenticator(new TextEncoder().encode(staged))(nonce), maxReconnectAttempts: 0 });
|
|
1407
|
+
await nc.close();
|
|
1408
|
+
}
|
|
1409
|
+
if (this.leaseStopping || !this.serviceServe || this.serviceServe.grant.epoch !== processEpoch)
|
|
1410
|
+
throw new Error("manager changed its active serve epoch while standing renewal was in flight");
|
|
1411
|
+
this.remoteSupervisorCreds = candidate.supervisor;
|
|
1412
|
+
this.remoteExecutorCreds = candidate.executor;
|
|
1413
|
+
this.serviceServe.creds = candidate.serve;
|
|
1414
|
+
this.goalWriterCreds = candidate.goalWriter;
|
|
1415
|
+
this.sessionLedgerCreds = candidate.sessionLedger;
|
|
1416
|
+
if (this.goalWriter)
|
|
1417
|
+
this.goalWriter.creds = candidate.goalWriter;
|
|
1418
|
+
if (this.sessionLedgerConn)
|
|
1419
|
+
this.sessionLedgerConn.creds = candidate.sessionLedger;
|
|
1420
|
+
// Adoption is committed above; a connection that cannot come back yet is a push failure the
|
|
1421
|
+
// endpoint's own reconnect loop retries with the adopted credential, not unadopted debt.
|
|
1422
|
+
this.remoteRenewalDebt = undefined;
|
|
1423
|
+
await Promise.all([
|
|
1424
|
+
this.ep.reconnect().catch((e) => console.error(`! remote manager supervisor reconnect after renewal: ${e.message}`)),
|
|
1425
|
+
this.pushRenewedCreds(this.serviceServe.nc),
|
|
1426
|
+
...(this.goalWriter ? [this.pushRenewedCreds(this.goalWriter.nc)] : []),
|
|
1427
|
+
...(this.sessionLedgerConn ? [this.pushRenewedCreds(this.sessionLedgerConn.nc)] : []),
|
|
1428
|
+
]);
|
|
1429
|
+
}
|
|
1430
|
+
catch (e) {
|
|
1431
|
+
const unadoptedSubjects = candidate && typeof candidate === "object"
|
|
1432
|
+
? Object.values(candidate).flatMap((cred) => { try {
|
|
1433
|
+
return typeof cred === "string" && credsClaims(cred).sub ? [credsClaims(cred).sub] : [];
|
|
1434
|
+
}
|
|
1435
|
+
catch {
|
|
1436
|
+
return [];
|
|
1437
|
+
} })
|
|
1438
|
+
: [];
|
|
1439
|
+
this.remoteRenewalDebt = { at: Date.now(), processEpoch, reason: e.message, unadoptedSubjects };
|
|
1440
|
+
console.error(`! remote manager all-duty renewal: ${e.message} - no unsigned or local replacement is available`);
|
|
1441
|
+
if (force)
|
|
1442
|
+
throw e;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1342
1445
|
/** Warn, on every renewal pass, when a $SYS credential is at or past its renewal point.
|
|
1343
1446
|
*
|
|
1344
1447
|
* The manager is the renewal owner for every credential it CAN re-sign, and these two are the ones
|
|
@@ -2357,9 +2460,8 @@ export class Manager {
|
|
|
2357
2460
|
* admin tier allowed operator cross-owner redefine; (2) launch is owner-equality-only, above.
|
|
2358
2461
|
* Both are least-privilege reductions, never widenings. */
|
|
2359
2462
|
/** The run host, or one of three refusals. A remote-authority manager holds no space signer, so
|
|
2360
|
-
* it cannot mint the per-run driver credential SPEC 14.6 requires
|
|
2361
|
-
*
|
|
2362
|
-
* user-auth mesh is `unimplemented` too, for a different reason it names: a hosted run's seats
|
|
2463
|
+
* it cannot mint the per-run driver credential SPEC 14.6 requires without all four closed
|
|
2464
|
+
* host callbacks. A local user-auth mesh is `unimplemented`: a hosted run's seats
|
|
2363
2465
|
* are spawned, turned and despawned by a caller derived from the run id under the static
|
|
2364
2466
|
* owner, which the user-mode spawn door refuses (no `u_` owner), so a program would fail at
|
|
2365
2467
|
* its first seat; no path to `--local` is offered there since a user bearer holds no run rows
|
|
@@ -2369,9 +2471,9 @@ export class Manager {
|
|
|
2369
2471
|
runHost() {
|
|
2370
2472
|
if (this.runHosting)
|
|
2371
2473
|
return this.runHosting;
|
|
2372
|
-
if (this.remoteAuthority)
|
|
2373
|
-
throw new EpEnvelopeError("unimplemented", "this manager does not host workflow runs: a remote-authority manager mints no run-driver credentials (SPEC 14.6); drive the run from a terminal with `cotal run start --local --file <program>`");
|
|
2374
|
-
if (this.userMode)
|
|
2474
|
+
if (this.remoteAuthority && !this.remoteAuthority.runHosting)
|
|
2475
|
+
throw new EpEnvelopeError("unimplemented", "this manager does not host workflow runs: its issuing host supplied no closed run callbacks, and a remote-authority manager mints no run-driver credentials (SPEC 14.6); drive the run from a terminal with `cotal run start --local --file <program>`");
|
|
2476
|
+
if (this.userMode && !this.remoteAuthority?.runHosting)
|
|
2375
2477
|
throw new EpEnvelopeError("unimplemented", `user-auth space "${this.space}" hosts no workflow runs yet: a hosted run's seats would be spawned under the static owner, which a user mesh refuses; run programs on a static-auth mesh`);
|
|
2376
2478
|
throw new EpEnvelopeError("unavailable", "the manager is still booting its workflow-run host; retry shortly (SPEC 14.3)");
|
|
2377
2479
|
}
|
|
@@ -3101,14 +3203,14 @@ export class Manager {
|
|
|
3101
3203
|
if (this.userMode) {
|
|
3102
3204
|
const p = parsePrincipalKey(a.id);
|
|
3103
3205
|
if (p)
|
|
3104
|
-
this.retiring.set(a.name, { opId: retireOpId(a.lifecycleUid), lifecycleUid: a.lifecycleUid, owner: p.owner, actor: p.actor, agentId: a.id, userOwner: a.userOwner, secretPaths: a.secretPaths, startedAt: Date.now() });
|
|
3206
|
+
this.retiring.set(a.name, { opId: retireOpId(a.lifecycleUid), lifecycleUid: a.lifecycleUid, owner: p.owner, actor: p.actor, agentId: a.id, userOwner: a.userOwner, secretPaths: a.secretPaths, launch: { allowSubscribe: a.launch.allowSubscribe }, startedAt: Date.now() });
|
|
3105
3207
|
}
|
|
3106
3208
|
else if (this.auth) {
|
|
3107
3209
|
// Unit B: a STATIC lifecycle now also holds its name pending its own terminal (the F1
|
|
3108
3210
|
// static retirement the detached deprovision below drives) — the alias frees only when the
|
|
3109
3211
|
// gate+head terminal completes, exactly the user-mode discipline. The wire principal is the
|
|
3110
3212
|
// incarnation-unique nkey (F5-bind); owner is the dev owner.
|
|
3111
|
-
this.retiring.set(a.name, { opId: retireOpId(a.lifecycleUid), lifecycleUid: a.lifecycleUid, owner: DEV_OWNER, actor: a.id, agentId: a.id, secretPaths: a.secretPaths, runtime: a.handle?.reference, startedAt: Date.now() });
|
|
3213
|
+
this.retiring.set(a.name, { opId: retireOpId(a.lifecycleUid), lifecycleUid: a.lifecycleUid, owner: DEV_OWNER, actor: a.id, agentId: a.id, secretPaths: a.secretPaths, launch: { allowSubscribe: a.launch.allowSubscribe }, runtime: a.handle?.reference, startedAt: Date.now() });
|
|
3112
3214
|
}
|
|
3113
3215
|
// Auth mode: tear down the departed agent's minted broker footprint + creds file (#159 B2). The
|
|
3114
3216
|
// process is already gone, so this must never block the slot free or throw into the caller — it runs
|
|
@@ -3232,7 +3334,21 @@ export class Manager {
|
|
|
3232
3334
|
console.error(`revoke agent grant ${a.name}: ${e.message}`);
|
|
3233
3335
|
}
|
|
3234
3336
|
}
|
|
3235
|
-
|
|
3337
|
+
try {
|
|
3338
|
+
const res = await this.deprovisionBroker(a);
|
|
3339
|
+
const h = this.retiring.get(a.name);
|
|
3340
|
+
if (h && h.lifecycleUid === a.lifecycleUid)
|
|
3341
|
+
h.lastResources = res;
|
|
3342
|
+
}
|
|
3343
|
+
catch (e) {
|
|
3344
|
+
const h = this.retiring.get(a.name);
|
|
3345
|
+
if (h && h.lifecycleUid === a.lifecycleUid) {
|
|
3346
|
+
h.lastError = e.message;
|
|
3347
|
+
if (e instanceof DeprovisionError)
|
|
3348
|
+
h.lastResources = e.accounting;
|
|
3349
|
+
}
|
|
3350
|
+
throw e;
|
|
3351
|
+
}
|
|
3236
3352
|
// #29 piece 3: after the footprint teardown, ask the AUTH plane to RETIRE the lifecycle over
|
|
3237
3353
|
// the auth endpoint rail. The rail re-checks the SERVE-ISSUANCE GATE at serve time (not the
|
|
3238
3354
|
// space-manager lease - that check was replaced in 02794b2f) and refuses unless the registration
|
|
@@ -3324,46 +3440,44 @@ export class Manager {
|
|
|
3324
3440
|
: await mintCreds(this.auth, requestIdentity, "retirement-requester", {
|
|
3325
3441
|
retirementRequester: { ...caller, target: retirementTarget },
|
|
3326
3442
|
});
|
|
3327
|
-
|
|
3443
|
+
// The requester grant subscribes `_INBOX_<id>.>` only (provision.ts): the endpoint path's
|
|
3444
|
+
// JetStream API requests (the contract-store read inside resolveService) must reply under it, not under the client default `_INBOX.`.
|
|
3445
|
+
const nc = await this.dial({ authenticator: credsAuthenticator(new TextEncoder().encode(creds)), inboxPrefix: `_INBOX_${requestIdentity.id}`, maxReconnectAttempts: 0 });
|
|
3328
3446
|
try {
|
|
3329
|
-
//
|
|
3330
|
-
|
|
3331
|
-
//
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
caller,
|
|
3342
|
-
nonce,
|
|
3447
|
+
// The service resolves FRESH on every attempt (never cached on `this`): a plane restart
|
|
3448
|
+
// mid-despawn then lands on the new epoch through an ordinary resolve, rather than a
|
|
3449
|
+
// caller pinned to a now-superseded incarnation.
|
|
3450
|
+
const service = await resolveService(nc, this.space, AUTH_ENDPOINT, caller, { deadlineMs: 10_000 });
|
|
3451
|
+
// The TARGET rides the invoke's own target (authz mode `exact`, arity 3) — broker-enforced,
|
|
3452
|
+
// and pinned by the grant minted above, so it is not a body claim the caller can vary.
|
|
3453
|
+
const attributed = await invokeCommand(nc, this.space, service, EP_CMD_RETIRE_LIFECYCLE, {
|
|
3454
|
+
opId,
|
|
3455
|
+
serveEndpoint: MANAGER_ENDPOINT, serveInstanceId: this.managerInstanceId, serveEpoch,
|
|
3456
|
+
}, {
|
|
3457
|
+
target: { mode: "exact", owner: target.owner, actor: target.actor, lifecycleUid: a.lifecycleUid },
|
|
3458
|
+
deadlineMs: 20_000,
|
|
3343
3459
|
});
|
|
3344
|
-
|
|
3345
|
-
// prefixed with the RESPONDER's instance identity), so a caller-supplied `reply` header
|
|
3346
|
-
// would be ignored — that is the confused-deputy boundary being structural. The caller
|
|
3347
|
-
// therefore reads its own reply-plane filter and binds the answer off the reply SUBJECT.
|
|
3348
|
-
const m = await epAwaitReply(nc, this.space, caller, nonce, requestId, subject,
|
|
3349
|
-
// Declare THIS manager instance's serve identity. Since #350 these SELECT the gate row;
|
|
3350
|
-
// they no longer authorize — the rail refuses unless the row they name belongs to this
|
|
3351
|
-
// caller's own subject-derived principal. A superseded predecessor (same instanceId, OLD
|
|
3352
|
-
// epoch after a restart) is still refused by the epoch comparison.
|
|
3353
|
-
JSON.stringify({ id: requestId, op: "retireLifecycle", args: {
|
|
3354
|
-
opId,
|
|
3355
|
-
serveEndpoint: MANAGER_ENDPOINT, serveInstanceId: this.managerInstanceId, serveEpoch,
|
|
3356
|
-
} }), 20_000);
|
|
3357
|
-
const r = m;
|
|
3460
|
+
const r = attributed.reply;
|
|
3358
3461
|
if (r.ok) {
|
|
3359
3462
|
this.confirmRetirement(a);
|
|
3360
3463
|
}
|
|
3464
|
+
else if (r.error?.code === "expired") {
|
|
3465
|
+
// A stale-epoch refusal: the responder answered, but at another epoch than this resolve
|
|
3466
|
+
// bound (a plane restart mid-despawn landed on the new epoch) - the retry copy, never a
|
|
3467
|
+
// hard despawn failure line, since a same-name spawn (or the auth service's own boot
|
|
3468
|
+
// resume) re-drives the same teardown.
|
|
3469
|
+
const copy = uncertain(r.error.message);
|
|
3470
|
+
if (held)
|
|
3471
|
+
held.lastError = copy;
|
|
3472
|
+
console.error(`despawn ${a.name}: ${copy}`);
|
|
3473
|
+
}
|
|
3361
3474
|
else {
|
|
3362
3475
|
// The rail's refusal is already the operator copy (lease-loss/stale/foreign-op faces,
|
|
3363
3476
|
// full-no-op statements included) - surface it INTACT, never flattened.
|
|
3477
|
+
const message = r.error?.message ?? "the auth service refused the retirement without a reason";
|
|
3364
3478
|
if (held)
|
|
3365
|
-
held.lastError =
|
|
3366
|
-
console.error(`despawn ${a.name}: ${
|
|
3479
|
+
held.lastError = message;
|
|
3480
|
+
console.error(`despawn ${a.name}: ${message}`);
|
|
3367
3481
|
}
|
|
3368
3482
|
}
|
|
3369
3483
|
finally {
|
|
@@ -3414,13 +3528,59 @@ export class Manager {
|
|
|
3414
3528
|
// LIFECYCLE-PINNED (SPEC 13.1): both the credential's exact-name grants and the delete names
|
|
3415
3529
|
// carry a.lifecycleUid, so a stale/replayed teardown for this retired incarnation is broker-denied
|
|
3416
3530
|
// against a same-name successor's footprint (its names embed a different uid).
|
|
3531
|
+
// Durable membership rows are lifecycle-keyed per concrete channel. Start with the launch's
|
|
3532
|
+
// concrete read channels, then include wildcard-covered and unnamed channels from the inventory.
|
|
3533
|
+
const memberChannels = new Set((a.launch?.allowSubscribe ?? []).filter(isConcreteChannel));
|
|
3534
|
+
// Complete the set from the delivery daemon's lifecycle-exact inventory, which covers rows on
|
|
3535
|
+
// wildcard-granted channels and teardowns that carry no launch spec. An inventory that cannot be
|
|
3536
|
+
// read leaves those rows RETAINED and says so; it never widens the delete beyond exact keys.
|
|
3537
|
+
const inventory = await this.lifecycleMembershipInventory(a);
|
|
3538
|
+
for (const ch of inventory.channels)
|
|
3539
|
+
memberChannels.add(ch);
|
|
3540
|
+
if (!inventory.complete)
|
|
3541
|
+
console.error(`deprovision ${a.name} (${a.id}): membership inventory incomplete (${inventory.reason}); rows on channels outside its launch policy stay RETAINED for uid ${a.lifecycleUid}`);
|
|
3417
3542
|
const creds = await mintCreds(this.auth, newIdentity(), "deprovisioner", {
|
|
3418
|
-
deprovisionTarget: { principal: a.id, lifecycleUid: a.lifecycleUid },
|
|
3543
|
+
deprovisionTarget: { principal: a.id, lifecycleUid: a.lifecycleUid, memberChannels: [...memberChannels] },
|
|
3419
3544
|
});
|
|
3420
3545
|
// Bound the detached broker teardown so a wedged broker can't leave the deprovision promise pending
|
|
3421
3546
|
// forever with no log — the timeout rejects into freeSlot's fail-loud `.catch` (paired with the
|
|
3422
3547
|
// helper's own fail-fast connect). The durables/ACL row still fall to space teardown as a backstop.
|
|
3423
|
-
|
|
3548
|
+
let accounting;
|
|
3549
|
+
try {
|
|
3550
|
+
accounting = await withTimeout(deprovisionAgent({
|
|
3551
|
+
servers: this.servers ?? DEFAULT_SERVER,
|
|
3552
|
+
space: this.space,
|
|
3553
|
+
targetId: a.id,
|
|
3554
|
+
lifecycleUid: a.lifecycleUid,
|
|
3555
|
+
memberChannels: [...memberChannels],
|
|
3556
|
+
creds,
|
|
3557
|
+
}), DEPROVISION_TIMEOUT_MS, `deprovision ${a.name} (${a.id}): broker teardown timed out`);
|
|
3558
|
+
this.lastDeprovisionResources = accounting;
|
|
3559
|
+
}
|
|
3560
|
+
catch (e) {
|
|
3561
|
+
if (e instanceof DeprovisionError) {
|
|
3562
|
+
this.lastDeprovisionResources = e.accounting;
|
|
3563
|
+
}
|
|
3564
|
+
throw e;
|
|
3565
|
+
}
|
|
3566
|
+
if (!inventory.complete)
|
|
3567
|
+
throw new Error(`membership inventory incomplete (${inventory.reason}); rows on channels outside its launch policy stay RETAINED for uid ${a.lifecycleUid}`);
|
|
3568
|
+
return accounting;
|
|
3569
|
+
}
|
|
3570
|
+
/** The retiring lifecycle's durable membership channels, read from the delivery daemon's
|
|
3571
|
+
* `lifecycleMemberships` admin verb (read-only, lifecycle-exact). Incomplete on any refusal. */
|
|
3572
|
+
async lifecycleMembershipInventory(a) {
|
|
3573
|
+
const t = deprovisionTargetPrincipal({ principal: a.id, lifecycleUid: a.lifecycleUid });
|
|
3574
|
+
try {
|
|
3575
|
+
const r = await this.ep.requestDeliveryAdmin("lifecycleMemberships", { principal: principalKey(t.owner, t.actor).key, lifecycleUid: t.lifecycleUid }, 5_000);
|
|
3576
|
+
const data = r.data;
|
|
3577
|
+
if (!r.ok || data?.complete !== true || !Array.isArray(data.channels) || !data.channels.every((c) => typeof c === "string" && isConcreteChannel(c)))
|
|
3578
|
+
return { complete: false, channels: [], reason: r.ok ? "malformed inventory reply" : (r.error ?? "refused") };
|
|
3579
|
+
return { complete: true, channels: data.channels };
|
|
3580
|
+
}
|
|
3581
|
+
catch (e) {
|
|
3582
|
+
return { complete: false, channels: [], reason: e.message };
|
|
3583
|
+
}
|
|
3424
3584
|
}
|
|
3425
3585
|
/** Reap a parent's children on its exit (P4b). Every descendant remains managed until the runtime's
|
|
3426
3586
|
* authoritative wait proves exit; the wait participates in the lifecycle drain, so preservation can
|
|
@@ -4328,7 +4488,7 @@ export class Manager {
|
|
|
4328
4488
|
return { ok: false, error: `the name "${identityName}" is still reconciling at manager startup; its prior lifecycle terminal owns this alias until it completes. Retry shortly.` };
|
|
4329
4489
|
const held = this.retiring.get(identityName);
|
|
4330
4490
|
if (held !== undefined) {
|
|
4331
|
-
void this.deprovision({ id: held.agentId, name: identityName, lifecycleUid: held.lifecycleUid, userOwner: held.userOwner, secretPaths: held.secretPaths }).catch(() => { });
|
|
4491
|
+
void this.deprovision({ id: held.agentId, name: identityName, lifecycleUid: held.lifecycleUid, userOwner: held.userOwner, secretPaths: held.secretPaths, launch: held.launch }).catch(() => { });
|
|
4332
4492
|
const err = lifecycleBlocked("failed-precondition", `the name "${identityName}" is reserved pending retirement: its previous agent's despawn started that lifecycle's teardown (footprint + standing-authority revoke + auth-side retirement), and the name frees only when all of it completes${held.lastError !== undefined ? ` (last attempt: ${held.lastError})` : ""}. NEXT: wait a moment and retry this spawn (retrying re-drives the whole teardown), or pick another name.`, { blockedOp: "retirement", headState: "retiring", opId: held.opId, remedy: "retry" });
|
|
4333
4493
|
return { ok: false, error: renderLifecycleBlocked(err.message, err), details: err.details };
|
|
4334
4494
|
}
|
|
@@ -5623,8 +5783,14 @@ export class Manager {
|
|
|
5623
5783
|
* This is NEVER the manager's standing seed/supervisor connection. */
|
|
5624
5784
|
async withEndpointServeExecutor(fn) {
|
|
5625
5785
|
const identity = this.remoteAuthority?.identities.executor ?? newIdentity();
|
|
5626
|
-
if (this.remoteAuthority && (!this.remoteExecutorCreds || inspectCredHealth(this.remoteExecutorCreds).state !== "healthy"))
|
|
5627
|
-
|
|
5786
|
+
if (this.remoteAuthority && (!this.remoteExecutorCreds || inspectCredHealth(this.remoteExecutorCreds).state !== "healthy")) {
|
|
5787
|
+
// Shutdown has drained and discarded the standing serve context. Renew only its scoped
|
|
5788
|
+
// maintenance executor through the existing host operation; never resurrect standing duties.
|
|
5789
|
+
if (this.leaseStopping)
|
|
5790
|
+
await this.renewRemoteExecutor(true);
|
|
5791
|
+
else
|
|
5792
|
+
await (this.remoteAuthority.renewStandingBundle ? this.renewRemoteStandingBundle(true) : this.renewRemoteExecutor(true));
|
|
5793
|
+
}
|
|
5628
5794
|
const creds = this.remoteExecutorCreds ?? (this.auth
|
|
5629
5795
|
? await mintCreds(this.auth, identity, "endpoint-serve-executor", {
|
|
5630
5796
|
endpointServeExecutor: { endpoint: MANAGER_ENDPOINT, instanceId: this.managerInstanceId },
|
|
@@ -5755,7 +5921,6 @@ export class Manager {
|
|
|
5755
5921
|
inboxPrefix: `_INBOX_${state.identity.id}`,
|
|
5756
5922
|
maxReconnectAttempts: -1,
|
|
5757
5923
|
});
|
|
5758
|
-
nc.closed().then((err) => { void this.onServeConnectionClosed(err ?? undefined); });
|
|
5759
5924
|
let handle;
|
|
5760
5925
|
try {
|
|
5761
5926
|
// The 1b typed surface + the derived `describe`. The descriptor stays PUBLIC in static
|
|
@@ -5770,6 +5935,13 @@ export class Manager {
|
|
|
5770
5935
|
// durable slot rows mirror it). Static mode carries no mapping-revision dimension, so
|
|
5771
5936
|
// the revision is the constant 0 — a caller that pins a revision pins 0.
|
|
5772
5937
|
resolveTarget: (t) => {
|
|
5938
|
+
if (this.remoteAuthority) {
|
|
5939
|
+
const key = principalKey(t.owner, t.actor).key;
|
|
5940
|
+
for (const a of this.agents.values())
|
|
5941
|
+
if (a.id === key)
|
|
5942
|
+
return { lifecycleUid: a.lifecycleUid, mappingRevision: 0 };
|
|
5943
|
+
return undefined;
|
|
5944
|
+
}
|
|
5773
5945
|
if (t.owner === DEV_OWNER) {
|
|
5774
5946
|
for (const a of this.agents.values())
|
|
5775
5947
|
if (!a.userOwner && a.id === t.actor)
|
|
@@ -5789,6 +5961,9 @@ export class Manager {
|
|
|
5789
5961
|
await nc.drain().catch(() => nc.close());
|
|
5790
5962
|
throw e;
|
|
5791
5963
|
}
|
|
5964
|
+
// Wired only once the connection serves: a connection this method drains itself after a
|
|
5965
|
+
// refused serve is not a fault to recover, and the caller already sees the refusal.
|
|
5966
|
+
nc.closed().then((err) => { void this.onServeConnectionClosed(err ?? undefined); });
|
|
5792
5967
|
return { nc, handle };
|
|
5793
5968
|
}
|
|
5794
5969
|
/** #2073: re-mint the SAME `endpoint-serve` identity through the SAME scoped one-shot executor and
|
|
@@ -5823,7 +5998,9 @@ export class Manager {
|
|
|
5823
5998
|
const recovered = await this.boundedReconnect("service endpoint", async () => {
|
|
5824
5999
|
if (!s)
|
|
5825
6000
|
return false;
|
|
5826
|
-
if (
|
|
6001
|
+
if (this.remoteAuthority)
|
|
6002
|
+
await this.renewRemoteBeforeRedial("serve", () => s.creds);
|
|
6003
|
+
else if (inspectCredHealth(s.creds ?? "").state !== "healthy" && s.creds !== undefined) {
|
|
5827
6004
|
s.creds = await this.remintServeCreds(s);
|
|
5828
6005
|
this.scheduleServeRenewal(s.creds);
|
|
5829
6006
|
}
|
|
@@ -5867,28 +6044,9 @@ export class Manager {
|
|
|
5867
6044
|
grant: remote.serveGrant,
|
|
5868
6045
|
creds: remote.serveCreds,
|
|
5869
6046
|
};
|
|
5870
|
-
|
|
5871
|
-
const nc = await this.
|
|
5872
|
-
|
|
5873
|
-
inboxPrefix: `_INBOX_${state.identity.id}`,
|
|
5874
|
-
maxReconnectAttempts: -1,
|
|
5875
|
-
});
|
|
5876
|
-
try {
|
|
5877
|
-
state.handle = serveEndpoint(nc, this.space, state.grant, this.managerServiceDefs(), { public: true }, {
|
|
5878
|
-
resolveTarget: (t) => {
|
|
5879
|
-
const key = principalKey(t.owner, t.actor).key;
|
|
5880
|
-
for (const a of this.agents.values())
|
|
5881
|
-
if (a.id === key)
|
|
5882
|
-
return { lifecycleUid: a.lifecycleUid, mappingRevision: 0 };
|
|
5883
|
-
return undefined;
|
|
5884
|
-
},
|
|
5885
|
-
...(this.omitClassCommands() ? { omitClassCommands: this.omitClassCommands() } : {}),
|
|
5886
|
-
});
|
|
5887
|
-
}
|
|
5888
|
-
catch (e) {
|
|
5889
|
-
await nc.drain().catch(() => nc.close());
|
|
5890
|
-
throw e;
|
|
5891
|
-
}
|
|
6047
|
+
// The shared dialer wires the closed-connection handler, which re-dials through the all-duty family.
|
|
6048
|
+
const { nc, handle } = await this.dialServeConnection(state);
|
|
6049
|
+
state.handle = handle;
|
|
5892
6050
|
state.nc = nc;
|
|
5893
6051
|
this.serviceServe = state;
|
|
5894
6052
|
console.error(`remote manager service endpoint activated: ${MANAGER_ENDPOINT}/${this.managerInstanceId} (epoch ${state.grant.epoch})`);
|
|
@@ -6198,7 +6356,9 @@ export class Manager {
|
|
|
6198
6356
|
const recovered = await this.boundedReconnect("goal-writer", async () => {
|
|
6199
6357
|
if (!gw)
|
|
6200
6358
|
return false;
|
|
6201
|
-
if (
|
|
6359
|
+
if (this.remoteAuthority)
|
|
6360
|
+
await this.renewRemoteBeforeRedial("goal-writer", () => gw.creds);
|
|
6361
|
+
else if (gw.creds !== undefined && inspectCredHealth(gw.creds).state !== "healthy") {
|
|
6202
6362
|
gw.creds = await this.withEndpointServeExecutor(({ authKv }) => this.mintAndStageGoalWriter(authKv));
|
|
6203
6363
|
this.goalWriterCreds = gw.creds;
|
|
6204
6364
|
}
|
|
@@ -6313,7 +6473,9 @@ export class Manager {
|
|
|
6313
6473
|
const recovered = await this.boundedReconnect("session-ledger", async () => {
|
|
6314
6474
|
if (!sw || !identity)
|
|
6315
6475
|
return false;
|
|
6316
|
-
if (
|
|
6476
|
+
if (this.remoteAuthority)
|
|
6477
|
+
await this.renewRemoteBeforeRedial("session-ledger", () => sw.creds);
|
|
6478
|
+
else if (sw.creds !== undefined && inspectCredHealth(sw.creds).state !== "healthy") {
|
|
6317
6479
|
sw.creds = await this.withEndpointServeExecutor(({ authKv }) => this.mintAndStageSessionLedger(authKv));
|
|
6318
6480
|
this.sessionLedgerCreds = sw.creds;
|
|
6319
6481
|
}
|
|
@@ -7921,6 +8083,10 @@ export class Manager {
|
|
|
7921
8083
|
finally {
|
|
7922
8084
|
await nc.drain().catch(() => nc.close());
|
|
7923
8085
|
}
|
|
8086
|
+
let retiredSlots = 0;
|
|
8087
|
+
let preservedForeign = 0;
|
|
8088
|
+
let preservedAdopted = 0;
|
|
8089
|
+
let deferredSlots = 0;
|
|
7924
8090
|
const terminalRows = [];
|
|
7925
8091
|
for (const row of slotRows) {
|
|
7926
8092
|
if (row.phase === "retired") {
|
|
@@ -7928,6 +8094,7 @@ export class Manager {
|
|
|
7928
8094
|
// regardless of which instance owned it, so a sibling-retired incarnation's copied credential
|
|
7929
8095
|
// is refused at this control surface too. Ownership gates only the DESTRUCTIVE sweep below.
|
|
7930
8096
|
this.retiredPrincipals.add(principalKey(row.owner, row.actor).key);
|
|
8097
|
+
retiredSlots++;
|
|
7931
8098
|
continue;
|
|
7932
8099
|
}
|
|
7933
8100
|
// 3b-2 RECONCILE OWNERSHIP (multi-manager-per-space): a manager adjudicates ONLY the non-retired
|
|
@@ -7936,19 +8103,26 @@ export class Manager {
|
|
|
7936
8103
|
// hazard, now cross-instance). A legacy row (pre-3b-2, no owner recorded) predates multi-manager,
|
|
7937
8104
|
// so this manager is its legitimate single-manager-past successor and reconciles it. An orphaned
|
|
7938
8105
|
// sibling row is reclaimed only by an explicit operator CAS takeover (ruling 1), never here.
|
|
7939
|
-
if (row.ownerInstanceId !== undefined && row.ownerInstanceId !== this.managerInstanceId)
|
|
8106
|
+
if (row.ownerInstanceId !== undefined && row.ownerInstanceId !== this.managerInstanceId) {
|
|
8107
|
+
preservedForeign++;
|
|
7940
8108
|
continue;
|
|
8109
|
+
}
|
|
7941
8110
|
// ADOPTION is genuine membership: a slot backed by a live managed agent THIS process owns
|
|
7942
8111
|
// at the SAME uid is never an orphan (empty at boot; exactly the adopted set at the
|
|
7943
8112
|
// post-adoption sweep — the fix for the F3 resume hole).
|
|
7944
8113
|
const live = this.agents.get(row.alias);
|
|
7945
8114
|
const adopted = live !== undefined && live.lifecycleUid === row.lifecycleUid;
|
|
8115
|
+
if (adopted) {
|
|
8116
|
+
preservedAdopted++;
|
|
8117
|
+
}
|
|
7946
8118
|
// Boot sweep with a resume PENDING: an active slot may yet be adopted (the resume path runs
|
|
7947
8119
|
// AFTER this boot sweep), so DEFER it — the post-adoption sweep terminalizes any the resume
|
|
7948
8120
|
// did not claim. provisioning/terminalizing NEVER defer (they are crashed operations, never
|
|
7949
8121
|
// an agent to adopt). At `postAdoption` (or a non-resume boot) nothing defers.
|
|
7950
|
-
if (!postAdoption && row.phase === "active" && !adopted && this.resumeRequired)
|
|
8122
|
+
if (!postAdoption && row.phase === "active" && !adopted && this.resumeRequired) {
|
|
8123
|
+
deferredSlots++;
|
|
7951
8124
|
continue;
|
|
8125
|
+
}
|
|
7952
8126
|
if (planStaticSlotResume(row, adopted) !== "none")
|
|
7953
8127
|
terminalRows.push(row);
|
|
7954
8128
|
}
|
|
@@ -7957,6 +8131,17 @@ export class Manager {
|
|
|
7957
8131
|
// alias, but the initial serial sweep must close every discovery-to-await gap up front.
|
|
7958
8132
|
for (const row of terminalRows)
|
|
7959
8133
|
this.reconcilingAliases.add(row.alias);
|
|
8134
|
+
const sweepDeprovision = {
|
|
8135
|
+
examined: 0,
|
|
8136
|
+
deleted: 0,
|
|
8137
|
+
absent: 0,
|
|
8138
|
+
refused: 0,
|
|
8139
|
+
acknowledged: 0,
|
|
8140
|
+
disappeared: 0,
|
|
8141
|
+
consumers: { examined: 0, deleted: 0, absent: 0, refused: 0, acknowledged: 0, disappeared: 0 },
|
|
8142
|
+
acls: { examined: 0, deleted: 0, absent: 0, refused: 0, disappeared: 0 },
|
|
8143
|
+
members: { examined: 0, deleted: 0, absent: 0, refused: 0, disappeared: 0 },
|
|
8144
|
+
};
|
|
7960
8145
|
for (const row of terminalRows) {
|
|
7961
8146
|
if (this.staticReconcileStopping)
|
|
7962
8147
|
break;
|
|
@@ -7976,7 +8161,33 @@ export class Manager {
|
|
|
7976
8161
|
};
|
|
7977
8162
|
this.staticReconcileItems.set(key, item);
|
|
7978
8163
|
}
|
|
8164
|
+
this.lastDeprovisionResources = undefined;
|
|
7979
8165
|
const succeeded = await this.attemptStaticReconcile(key, item, row);
|
|
8166
|
+
const acc = this.lastDeprovisionResources;
|
|
8167
|
+
if (acc) {
|
|
8168
|
+
sweepDeprovision.examined += acc.examined;
|
|
8169
|
+
sweepDeprovision.deleted = sweepDeprovision.deleted === null || acc.deleted === null ? null : sweepDeprovision.deleted + acc.deleted;
|
|
8170
|
+
sweepDeprovision.absent += acc.absent;
|
|
8171
|
+
sweepDeprovision.refused += acc.refused;
|
|
8172
|
+
sweepDeprovision.acknowledged += acc.acknowledged;
|
|
8173
|
+
sweepDeprovision.disappeared += acc.disappeared;
|
|
8174
|
+
sweepDeprovision.consumers.examined += acc.consumers.examined;
|
|
8175
|
+
sweepDeprovision.consumers.deleted = sweepDeprovision.consumers.deleted === null || acc.consumers.deleted === null ? null : sweepDeprovision.consumers.deleted + acc.consumers.deleted;
|
|
8176
|
+
sweepDeprovision.consumers.absent += acc.consumers.absent;
|
|
8177
|
+
sweepDeprovision.consumers.refused += acc.consumers.refused;
|
|
8178
|
+
sweepDeprovision.consumers.acknowledged += acc.consumers.acknowledged;
|
|
8179
|
+
sweepDeprovision.consumers.disappeared += acc.consumers.disappeared;
|
|
8180
|
+
sweepDeprovision.acls.examined += acc.acls.examined;
|
|
8181
|
+
sweepDeprovision.acls.deleted += acc.acls.deleted;
|
|
8182
|
+
sweepDeprovision.acls.absent += acc.acls.absent;
|
|
8183
|
+
sweepDeprovision.acls.refused += acc.acls.refused;
|
|
8184
|
+
sweepDeprovision.acls.disappeared += acc.acls.disappeared;
|
|
8185
|
+
sweepDeprovision.members.examined += acc.members.examined;
|
|
8186
|
+
sweepDeprovision.members.deleted += acc.members.deleted;
|
|
8187
|
+
sweepDeprovision.members.absent += acc.members.absent;
|
|
8188
|
+
sweepDeprovision.members.refused += acc.members.refused;
|
|
8189
|
+
sweepDeprovision.members.disappeared += acc.members.disappeared;
|
|
8190
|
+
}
|
|
7980
8191
|
if (succeeded)
|
|
7981
8192
|
sweep.succeeded++;
|
|
7982
8193
|
else
|
|
@@ -7984,6 +8195,18 @@ export class Manager {
|
|
|
7984
8195
|
this.reconcilingAliases.delete(row.alias);
|
|
7985
8196
|
}
|
|
7986
8197
|
sweep.completedAt = new Date().toISOString();
|
|
8198
|
+
this.staticReconcileLastSweepResources = {
|
|
8199
|
+
slots: {
|
|
8200
|
+
examined: slotRows.length,
|
|
8201
|
+
retired: retiredSlots,
|
|
8202
|
+
preservedForeign,
|
|
8203
|
+
preservedAdopted,
|
|
8204
|
+
deferred: deferredSlots,
|
|
8205
|
+
terminalized: sweep.succeeded,
|
|
8206
|
+
failed: sweep.failed,
|
|
8207
|
+
},
|
|
8208
|
+
deprovision: sweepDeprovision,
|
|
8209
|
+
};
|
|
7987
8210
|
for (const row of terminalRows) {
|
|
7988
8211
|
const key = this.staticReconcileKey(row);
|
|
7989
8212
|
const item = this.staticReconcileItems.get(key);
|