@cotal-ai/manager 0.65.0 → 0.67.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/dist/commands.js +9 -0
- package/dist/commands.js.map +1 -1
- package/dist/manager-service-contract.d.ts +8 -2
- package/dist/manager-service-contract.d.ts.map +1 -1
- package/dist/manager-service-contract.js +46 -6
- package/dist/manager-service-contract.js.map +1 -1
- package/dist/manager.d.ts +32 -7
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +199 -67
- package/dist/manager.js.map +1 -1
- package/dist/remote-authority.d.ts +1 -1
- package/dist/remote-authority.d.ts.map +1 -1
- package/dist/remote-authority.js +7 -7
- package/dist/remote-authority.js.map +1 -1
- package/dist/remote-register.d.ts +2 -2
- package/dist/remote-register.d.ts.map +1 -1
- package/dist/remote-register.js +5 -39
- package/dist/remote-register.js.map +1 -1
- package/dist/run-hosting.d.ts +4 -1
- package/dist/run-hosting.d.ts.map +1 -1
- package/dist/run-hosting.js +16 -4
- package/dist/run-hosting.js.map +1 -1
- package/dist/static-lifecycle.js +11 -11
- package/dist/static-lifecycle.js.map +1 -1
- package/dist/transcript-receive.d.ts +63 -0
- package/dist/transcript-receive.d.ts.map +1 -0
- package/dist/transcript-receive.js +178 -0
- package/dist/transcript-receive.js.map +1 -0
- package/package.json +12 -12
package/dist/manager.js
CHANGED
|
@@ -2,10 +2,10 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { createHash, randomUUID, randomBytes } from "node:crypto";
|
|
3
3
|
import { hostname } from "node:os";
|
|
4
4
|
import { credsAuthenticator } from "@nats-io/transport-node";
|
|
5
|
-
import { existsSync, lstatSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
5
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
6
6
|
import { isAbsolute, join, dirname, resolve } from "node:path";
|
|
7
7
|
import { CotalEndpoint, MANAGED_HANDOFF_KIND, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, DeprovisionError, isConcreteChannel, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, modelPolicyRefusal, 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
|
-
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";
|
|
8
|
+
import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, claimManagerSiblingIdentities, 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";
|
|
11
11
|
import { DELIVERY_ADMIN_BOOT_WAIT_MS, isUnansweredDeliveryAdmin, makeManagerEndpointEvictionEvidence, makeManagerEndpointHolderEvictor, untilDeliveryAdminAnswers } from "./endpoint-evict.js";
|
|
@@ -17,11 +17,12 @@ import { controlShutdown } from "./control-shutdown.js";
|
|
|
17
17
|
import { RunHosting } from "./run-hosting.js";
|
|
18
18
|
import { controlSession } from "./control-session.js";
|
|
19
19
|
import { MAX_AGENTS, parseResumeCommitArgs, parseResumeControlArgs, parseResumeFinalizeArgs } from "./resume.js";
|
|
20
|
+
import { TranscriptReceiver } from "./transcript-receive.js";
|
|
20
21
|
// Unit B (the static §13.1 lifecycle executor): the shared grammar/stores from core plus the
|
|
21
22
|
// manager-side adapter (transport + slot orchestration + the F1 terminal) — see static-lifecycle.ts.
|
|
22
23
|
import { jetstream, jetstreamManager } from "@nats-io/jetstream";
|
|
23
24
|
import { Kvm } from "@nats-io/kv";
|
|
24
|
-
import { recordsBucket, epAuthBucket, ensureAuthorityStores, ensureContractStore, createEndpointStreams, contractStoreContext, publishContractArtifact, contractArtifactCanonicalBytes, standaloneConnectOpts, STATIC_SLOT_PREFIX, rawDigest, STANDING_RENEWABLE_TTL_SEC as MANAGED_STATIC_TTL_SEC, newArtifactSigner, RotatingSigner, generationAnchor, sessionsBucket, SESSION_GRANT_MAX_TTL_MS, } from "@cotal-ai/core";
|
|
25
|
+
import { recordsBucket, epAuthBucket, ensureAuthorityStores, ensureContractStore, createEndpointStreams, contractStoreContext, publishContractArtifact, contractArtifactCanonicalBytes, standaloneConnectOpts, STATIC_SLOT_PREFIX, rawDigest, STANDING_RENEWABLE_TTL_SEC as MANAGED_STATIC_TTL_SEC, newArtifactSigner, RotatingSigner, generationAnchor, sessionsBucket, SESSION_GRANT_MAX_TTL_MS, REMOTE_MANAGER_IDENTITY_NAMES, } from "@cotal-ai/core";
|
|
25
26
|
// P2 item 6: the manager's ONE §13.6 session plane — offer mint + one-use redeem + PTY-bridge
|
|
26
27
|
// standup for `attach`, over a dedicated standing session-LEDGER connection (the byte rails ride
|
|
27
28
|
// per-session credentials on their own short-lived connections).
|
|
@@ -29,7 +30,7 @@ import { ManagerSessionPlane, openSessionLedgerKv } from "./session/index.js";
|
|
|
29
30
|
// P2 item 1 (1a-serve): the manager as an ordinary v0.4 `service` endpoint — the §13.1
|
|
30
31
|
// endpoint-serve credential subsystem (gate provisioning, registration barrier, mint fence) plus
|
|
31
32
|
// the register/authorize/serve seams, all driven over a scoped one-shot executor connection.
|
|
32
|
-
import { provisionEndpointGateOpen, endpointRegistrationBarrier, serveIssuanceGateKv, commitSiblingIssuance, markLedgerRowRevoked, epcredRowKey, epgateKey, parseEndpointGate, readEndpointGateGeneration,
|
|
33
|
+
import { provisionEndpointGateOpen, endpointRegistrationBarrier, serveIssuanceGateKv, commitSiblingIssuance, markLedgerRowRevoked, epcredRowKey, epgateKey, parseEndpointGate, readEndpointGateGeneration, registerServingInstance, deregisterServiceInstance, serveEndpoint, bindGoal, createGoal, transitionGoal, commitGoalResult, settleGoalUncertain, readGoalResult, readGoalStatus, readGoalSpec, recordGoalIndex, readGoalIndex, clearGoalIndex, listGoalIndex, mintCheckpoint, resumeCheckpoint, readCheckpointSettle, readCheckpointSpec, expireCheckpoint, GOAL_TERMINAL_STATES, goalRefOf, goalProgressTopic, epeSubject, submissionFingerprint, EpEnvelopeError, lifecycleBlocked, lifecycleBlockedFrom, renderLifecycleBlocked, } from "@cotal-ai/core";
|
|
33
34
|
import { MANAGER_ENDPOINT, managerClusterArtifacts, managerCommandDefs, managerContractArtifactValues } from "./manager-service-contract.js";
|
|
34
35
|
import { staticLifecycleTransport, activateStaticLifecycle, runStaticTerminal, readStaticSlot, casStaticSlot, recordSlotCredential, appendStaticCredentialRow, planStaticSlotResume, observeStaticSlot, listStaticSlotObservations, renderStaticSlotObservation, StaticSlotReadError, STATIC_SLOT_READ_FAILED_DETAIL, } from "./static-lifecycle.js";
|
|
35
36
|
/** Minimum slot lifetime for rate-flooring (P4c). A slot freed (by despawn OR natural exit/reap)
|
|
@@ -172,6 +173,8 @@ const FREE_SLOT_CAUSE_TEXT = {
|
|
|
172
173
|
"supervise-recovery-failed": "this manager retired it after a supervised restart failed",
|
|
173
174
|
"session-bind-failed": "this manager stopped it: its host session could not be bound at launch",
|
|
174
175
|
"resume-session-rebind-failed": "this manager stopped it: its host session could not be rebound on resume",
|
|
176
|
+
"carried-fork-refused": "this manager stopped it: it recorded no fork of the carried transcript",
|
|
177
|
+
"resume-custody-unrecorded": "this manager stopped it: its custody reference could not be recorded on resume",
|
|
175
178
|
};
|
|
176
179
|
/** The stop family's sentences — the same job {@link FREE_SLOT_CAUSE_TEXT} does for the string
|
|
177
180
|
* causes, beside it so both stay the single place a cause becomes a sentence. A requested stop's
|
|
@@ -467,6 +470,8 @@ export class Manager {
|
|
|
467
470
|
/** The complete boot task. stop() joins it after fencing static reconciliation, so a registration
|
|
468
471
|
* already past an earlier shutdown check cannot finish after stop() returns. */
|
|
469
472
|
startTask;
|
|
473
|
+
/** This instance's side of a carried resume transcript (#1499); absent where it has no transfer reader. */
|
|
474
|
+
transcripts;
|
|
470
475
|
/** The one stop this manager runs, with the agent policy it was asked for. A stop takes seats out
|
|
471
476
|
* of `agents` before they have exited, so a second teardown would find none and report success
|
|
472
477
|
* while the first still waits on a live seat. */
|
|
@@ -877,11 +882,11 @@ export class Manager {
|
|
|
877
882
|
catch (e) {
|
|
878
883
|
// Our OWN instance id already holds a live key ⇒ refuse. Anything else (e.g. a KV/JS error) is a
|
|
879
884
|
// real failure to surface, not a silent "held" — keep the cause so it isn't misread as a conflict.
|
|
880
|
-
const held = await this.ep.
|
|
885
|
+
const held = (await this.ep.readOwnManagerLease(this.managerInstanceId).catch(() => undefined))?.info;
|
|
881
886
|
await this.ep.stop();
|
|
882
887
|
await this.attach.stop();
|
|
883
888
|
throw new Error(held
|
|
884
|
-
? `manager instance ${this.managerInstanceId} already serves space "${this.space}" from this workspace root (${held.runtime}, pid ${held.pid}, root ${held.root}) - stop it first before restarting the same instance`
|
|
889
|
+
? `manager instance ${this.managerInstanceId} already serves space "${this.space}" from ${held.root === this.leaseInfo.root ? "this" : "another"} workspace root (${held.runtime}, pid ${held.pid}, root ${held.root}) - stop it first before restarting the same instance`
|
|
885
890
|
: `could not acquire the manager lease for space "${this.space}": ${e.message}`);
|
|
886
891
|
}
|
|
887
892
|
this.leaseTimer = setInterval(() => { void this.renewLease(); }, MANAGER_LEASE_RENEW_MS);
|
|
@@ -1042,6 +1047,18 @@ export class Manager {
|
|
|
1042
1047
|
});
|
|
1043
1048
|
await this.runHosting.reconcile();
|
|
1044
1049
|
}
|
|
1050
|
+
// A carried resume (#1499) needs this instance's transfer reader: minted from the space's signing
|
|
1051
|
+
// seed, issued by the host to a remote manager, or none on an open mesh.
|
|
1052
|
+
if (!this.remoteAuthority || this.remoteAuthority.mintTransferReader) {
|
|
1053
|
+
this.transcripts = new TranscriptReceiver({
|
|
1054
|
+
space: this.space,
|
|
1055
|
+
instanceId: this.managerInstanceId,
|
|
1056
|
+
workspaceRoot: this.workspaceRoot,
|
|
1057
|
+
withReader: (fn) => this.withTransferReader(fn),
|
|
1058
|
+
log: (line) => console.error(line),
|
|
1059
|
+
});
|
|
1060
|
+
await this.transcripts.start();
|
|
1061
|
+
}
|
|
1045
1062
|
// Plane-3 (durable backstop) is NOT the manager's job — the manager only manages agent lifecycle.
|
|
1046
1063
|
// The server-side delivery daemon hosts the fan-out writer + trusted reader, owns the durable
|
|
1047
1064
|
// membership registry, and serves the runtime durable join/leave/list ops (on `ctl.delivery`). The
|
|
@@ -1483,10 +1500,9 @@ export class Manager {
|
|
|
1483
1500
|
let candidate;
|
|
1484
1501
|
try {
|
|
1485
1502
|
candidate = await remote.renewStandingBundle(processEpoch);
|
|
1486
|
-
|
|
1487
|
-
if (Object.keys(candidate).sort().join(",") !== [...names].sort().join(","))
|
|
1503
|
+
if (Object.keys(candidate).sort().join(",") !== [...REMOTE_MANAGER_IDENTITY_NAMES].sort().join(","))
|
|
1488
1504
|
throw new Error("host returned an incomplete or widened standing credential family");
|
|
1489
|
-
for (const name of
|
|
1505
|
+
for (const name of REMOTE_MANAGER_IDENTITY_NAMES) {
|
|
1490
1506
|
if (typeof candidate[name] !== "string" || inspectCredHealth(candidate[name]).state === "unreadable")
|
|
1491
1507
|
throw new Error(`host returned an unreadable ${name} credential`);
|
|
1492
1508
|
if (!remote.accountPublicKey || accountFromCreds(candidate[name]) !== remote.accountPublicKey)
|
|
@@ -1676,10 +1692,14 @@ export class Manager {
|
|
|
1676
1692
|
});
|
|
1677
1693
|
}
|
|
1678
1694
|
// A later manager does not resume a delegated seat, and its close runs only inside its
|
|
1679
|
-
// retirement, so a cut that holds one is refused before any child stops.
|
|
1680
|
-
|
|
1695
|
+
// retirement, so a cut that holds one is refused before any child stops. A carried resume seat's
|
|
1696
|
+
// private home is removed when its slot is freed, so a later manager would find nothing to fork.
|
|
1697
|
+
for (const a of this.agents.values()) {
|
|
1681
1698
|
if (a.handedOff)
|
|
1682
1699
|
failures.push({ name: a.name, id: a.id, error: "a delegated seat is not resumed by a later manager; stop it before preserving" });
|
|
1700
|
+
if (a.seatHome)
|
|
1701
|
+
failures.push({ name: a.name, id: a.id, error: "a carried resume seat's private home is not kept for a later manager; stop it before preserving" });
|
|
1702
|
+
}
|
|
1683
1703
|
const unverifiedStops = this.unverifiedStops.filter((stopped) => {
|
|
1684
1704
|
try {
|
|
1685
1705
|
if (!stopped.authoritative && stopped.handle.status() === "exited")
|
|
@@ -2049,6 +2069,7 @@ export class Manager {
|
|
|
2049
2069
|
}
|
|
2050
2070
|
async stopOnce(withAgents) {
|
|
2051
2071
|
this.leaseStopping = true;
|
|
2072
|
+
this.transcripts?.stop();
|
|
2052
2073
|
this.staticReconcileStopping = true;
|
|
2053
2074
|
const starting = this.startTask;
|
|
2054
2075
|
for (const item of this.staticReconcileItems.values()) {
|
|
@@ -2554,8 +2575,13 @@ export class Manager {
|
|
|
2554
2575
|
async epAdminReach(caller) {
|
|
2555
2576
|
if (!this.userMode)
|
|
2556
2577
|
return true;
|
|
2557
|
-
if (this.remoteAuthority)
|
|
2558
|
-
|
|
2578
|
+
if (this.remoteAuthority) {
|
|
2579
|
+
// A hosted run's own caller is derived from its id and holds no ledger row: it acts for the
|
|
2580
|
+
// caller its run was admitted for, so that caller's current grant decides, and a revoked
|
|
2581
|
+
// login demotes the run's next spawn.
|
|
2582
|
+
const asked = this.runHosting?.admittedCaller(caller) ?? caller;
|
|
2583
|
+
return this.remoteAuthority.authorizeAdmin({ owner: asked.owner, actor: asked.actor, lifecycleUid: asked.uid });
|
|
2584
|
+
}
|
|
2559
2585
|
try {
|
|
2560
2586
|
const scope = await resolveAuthProvider().actorScope({
|
|
2561
2587
|
dir: userAuthStateDir(this.workspaceRoot, this.space),
|
|
@@ -2878,6 +2904,11 @@ export class Manager {
|
|
|
2878
2904
|
preparePreservation: (ctx) => adminGated(ctx, async () => unwrap(await this.opPreservationCtl("preparePreservation", args(ctx)))),
|
|
2879
2905
|
commitPreservation: (ctx) => adminGated(ctx, async () => unwrap(await this.opPreservationCtl("commitPreservation", args(ctx)))),
|
|
2880
2906
|
abortPreservation: (ctx) => adminGated(ctx, async () => unwrap(await this.opPreservationCtl("abortPreservation", args(ctx)))),
|
|
2907
|
+
transcriptReceive: (ctx) => this.serveGated(ctx, () => adminGated(ctx, () => {
|
|
2908
|
+
if (!this.transcripts)
|
|
2909
|
+
throw new EpEnvelopeError("failed-precondition", "transcript-receive: this manager does not receive carried transcripts (its remote authority issues no transfer reader)");
|
|
2910
|
+
return this.transcripts.receive(args(ctx));
|
|
2911
|
+
})),
|
|
2881
2912
|
});
|
|
2882
2913
|
}
|
|
2883
2914
|
async commitResumeActivation(attemptId) {
|
|
@@ -3459,6 +3490,8 @@ export class Manager {
|
|
|
3459
3490
|
this.recordContinuityAtStop(a, "stop");
|
|
3460
3491
|
if (a.restart?.sessionStatePath)
|
|
3461
3492
|
rmSync(a.restart.sessionStatePath, { force: true });
|
|
3493
|
+
if (a.seatHome)
|
|
3494
|
+
rmSync(a.seatHome, { recursive: true, force: true });
|
|
3462
3495
|
// P2 item 6 (pin 4): end any live §13.6 attach session bound to THIS incarnation with the honest
|
|
3463
3496
|
// `target-despawn` reason. Fires once per agent on every free path (despawn / self-stop / reap /
|
|
3464
3497
|
// exit) via the `agents` guard above; a no-op when no plane or no live session for the target.
|
|
@@ -3929,8 +3962,15 @@ export class Manager {
|
|
|
3929
3962
|
* record cannot be read or does not name this seat's source, it is absent: `ps` then shows the
|
|
3930
3963
|
* source id alone rather than failing the listing for every other seat. */
|
|
3931
3964
|
forkProvenance(a) {
|
|
3932
|
-
|
|
3965
|
+
// A carried seat's provenance is set only by refuseCarriedFork, once its record names the claim.
|
|
3966
|
+
if (a.launch.resumed || a.launch.carried)
|
|
3933
3967
|
return a.launch.resumed;
|
|
3968
|
+
a.launch.resumed = this.readForkRecord(a);
|
|
3969
|
+
return a.launch.resumed;
|
|
3970
|
+
}
|
|
3971
|
+
readForkRecord(a) {
|
|
3972
|
+
if (!a.launch.resumeRecordPath || !a.launch.forkSource)
|
|
3973
|
+
return undefined;
|
|
3934
3974
|
let record;
|
|
3935
3975
|
try {
|
|
3936
3976
|
record = JSON.parse(readFileSync(a.launch.resumeRecordPath, "utf8"));
|
|
@@ -3942,8 +3982,43 @@ export class Manager {
|
|
|
3942
3982
|
if (r.source !== a.launch.forkSource || typeof r.transcriptSha256 !== "string" || !/^[a-f0-9]{64}$/.test(r.transcriptSha256) ||
|
|
3943
3983
|
!(r.title === undefined || r.title === null || (typeof r.title === "string" && r.title.length <= 1024)))
|
|
3944
3984
|
return undefined;
|
|
3945
|
-
|
|
3946
|
-
|
|
3985
|
+
return { source: r.source, ...(typeof r.title === "string" && r.title ? { title: r.title } : {}), transcriptSha256: r.transcriptSha256 };
|
|
3986
|
+
}
|
|
3987
|
+
/** Design section 9: a carried seat must fork the bytes the operator carried. Wait for its fork
|
|
3988
|
+
* record and hold it to the claim; on a match it becomes the seat's provenance, otherwise the
|
|
3989
|
+
* refusal is returned. A seat that writes no record cannot be shown to have forked the claim. */
|
|
3990
|
+
async refuseCarriedFork(a, timeoutMs) {
|
|
3991
|
+
const claim = a.launch.carried;
|
|
3992
|
+
const deadline = Date.now() + timeoutMs;
|
|
3993
|
+
let record;
|
|
3994
|
+
while (!(record = this.readForkRecord(a))) {
|
|
3995
|
+
if (a.terminalizing || a.handle.status() === "exited")
|
|
3996
|
+
return `${a.name} stopped before it wrote a fork record`;
|
|
3997
|
+
if (Date.now() >= deadline)
|
|
3998
|
+
return `${a.name} wrote no fork record within ${timeoutMs} ms, so it cannot be shown to have forked the carried transcript`;
|
|
3999
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4000
|
+
}
|
|
4001
|
+
if (record.transcriptSha256 !== claim.transcriptSha256)
|
|
4002
|
+
return `${a.name} forked a transcript with sha256:${record.transcriptSha256}, not the carried sha256:${claim.transcriptSha256}`;
|
|
4003
|
+
a.launch.resumed = { ...record, host: claim.host, transferredAt: claim.transferredAt };
|
|
4004
|
+
return undefined;
|
|
4005
|
+
}
|
|
4006
|
+
/** A carried seat whose launch was uncertain is held to the claim once it joins, as a started one
|
|
4007
|
+
* is; the wait ends when the seat stops or exits, which its exit watcher reaps. */
|
|
4008
|
+
async refuseLateCarriedFork(a, timeoutMs) {
|
|
4009
|
+
for (;;) {
|
|
4010
|
+
const late = await this.awaitReadiness(a, timeoutMs, { reapOnExit: false });
|
|
4011
|
+
if (late.ok)
|
|
4012
|
+
break;
|
|
4013
|
+
if (!late.uncertain || a.terminalizing)
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
const refused = await this.refuseCarriedFork(a, timeoutMs);
|
|
4017
|
+
if (refused === undefined || a.terminalizing || a.handle.status() === "exited")
|
|
4018
|
+
return;
|
|
4019
|
+
console.error(`! ${refused}; stopping it`);
|
|
4020
|
+
this.stopHandle(a, false);
|
|
4021
|
+
this.freeSlot(a, true, "carried-fork-refused");
|
|
3947
4022
|
}
|
|
3948
4023
|
readManagedSession(a) {
|
|
3949
4024
|
return this.readManagedSessionState(a).sessionId;
|
|
@@ -4096,10 +4171,10 @@ export class Manager {
|
|
|
4096
4171
|
}
|
|
4097
4172
|
a.handle = handle;
|
|
4098
4173
|
a.control = spec.control;
|
|
4174
|
+
await this.recordSlotRuntime(a);
|
|
4099
4175
|
replacement = undefined;
|
|
4100
4176
|
restart.opts = opts;
|
|
4101
4177
|
restart.recovering = false;
|
|
4102
|
-
await this.recordSlotRuntime(a);
|
|
4103
4178
|
if (continueSession !== undefined)
|
|
4104
4179
|
console.error(`! ${a.name}: recovered Pi session ${continueSession} after crash (${restart.crashes.length}/${limit})`);
|
|
4105
4180
|
else
|
|
@@ -4316,6 +4391,8 @@ export class Manager {
|
|
|
4316
4391
|
variant: args.variant ? String(args.variant) : undefined,
|
|
4317
4392
|
launchOptions: args.launchOptions,
|
|
4318
4393
|
resume: args.resume ? String(args.resume) : undefined,
|
|
4394
|
+
resumeClaim: args.resumeClaim !== undefined ? String(args.resumeClaim) : undefined,
|
|
4395
|
+
resumeAgent: args.resumeAgent !== undefined ? String(args.resumeAgent) : undefined,
|
|
4319
4396
|
events,
|
|
4320
4397
|
eventsNotice,
|
|
4321
4398
|
cwd: args.cwd ? String(args.cwd) : undefined,
|
|
@@ -4711,6 +4788,25 @@ export class Manager {
|
|
|
4711
4788
|
// reject-before-side-effects window as the harness preflight above; buildLaunch stays the backstop.
|
|
4712
4789
|
if (opts.resume && !connector.supportsResume)
|
|
4713
4790
|
return { ok: false, error: `${agent} connector does not support resuming an existing session (resume)` };
|
|
4791
|
+
// A carried resume (#1499) is checked here too, before anything is minted. The claim is only
|
|
4792
|
+
// looked at: the connector may still refuse the launch, and a refused launch leaves it unspent.
|
|
4793
|
+
let carried;
|
|
4794
|
+
if (opts.resumeClaim !== undefined) {
|
|
4795
|
+
if (opts.resume === undefined)
|
|
4796
|
+
return { ok: false, error: "resumeClaim: names carried bytes of a resume; the launch carries no resume" };
|
|
4797
|
+
if (!connector.resumeTranscript)
|
|
4798
|
+
return { ok: false, error: `${agent} connector does not take a carried transcript (resumeClaim)` };
|
|
4799
|
+
if (opts.resumeAgent !== agent)
|
|
4800
|
+
return { ok: false, error: `resumeClaim: the session was carried for agent "${opts.resumeAgent ?? "<none>"}", and this launch resolves agent "${agent}"` };
|
|
4801
|
+
if (!this.transcripts)
|
|
4802
|
+
return { ok: false, error: "resumeClaim: this manager does not receive carried transcripts" };
|
|
4803
|
+
try {
|
|
4804
|
+
carried = this.transcripts.peek(opts.resumeClaim, opts.resume);
|
|
4805
|
+
}
|
|
4806
|
+
catch (e) {
|
|
4807
|
+
return { ok: false, error: e.message };
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4714
4810
|
// A restart policy this host cannot honour is refused at accept, never accepted and ignored.
|
|
4715
4811
|
// External runtimes (tmux/cmux/orca/herdr) attach to a process they do not own and stream no
|
|
4716
4812
|
// exit, so a name cannot be respawned in place. User-mode seats have no static slot that
|
|
@@ -4880,7 +4976,7 @@ export class Manager {
|
|
|
4880
4976
|
const held = this.retiring.get(identityName);
|
|
4881
4977
|
if (held !== undefined) {
|
|
4882
4978
|
void this.deprovision({ id: held.agentId, name: identityName, lifecycleUid: held.lifecycleUid, userOwner: held.userOwner, delegated: held.delegated, secretPaths: held.secretPaths, delegatedHandle: held.delegatedHandle, launch: held.launch }).catch(() => { });
|
|
4883
|
-
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",
|
|
4979
|
+
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", opId: held.opId, remedy: "retry" });
|
|
4884
4980
|
return { ok: false, error: renderLifecycleBlocked(err.message, err), details: err.details };
|
|
4885
4981
|
}
|
|
4886
4982
|
if (variant && !connector.supportsModelVariant)
|
|
@@ -4981,6 +5077,8 @@ export class Manager {
|
|
|
4981
5077
|
// and retires over a live process. The slot row holds the same reference; this is the copy the
|
|
4982
5078
|
// in-process rollback can actually read.
|
|
4983
5079
|
let provisioned;
|
|
5080
|
+
// A carried resume's seat-private home, removed by the finally unless a live seat took it.
|
|
5081
|
+
let seatHome;
|
|
4984
5082
|
try {
|
|
4985
5083
|
// A stable nkey identity assigned at spawn: the public key is the agent's card.id (threaded via
|
|
4986
5084
|
// COTAL_ID); the seed is retained to mint matching creds later.
|
|
@@ -5223,7 +5321,14 @@ export class Manager {
|
|
|
5223
5321
|
handle = this.runtime.spawnDelegated({ agent, persona: readFileSync(configPath, "utf8"), role, model, variant, prompt, launchOptions: launchOptions, events }, handoff);
|
|
5224
5322
|
}
|
|
5225
5323
|
else {
|
|
5324
|
+
if (carried) {
|
|
5325
|
+
seatHome = join(this.workspaceRoot, ".cotal", "seat-homes", lifecycleUid);
|
|
5326
|
+
mkdirSync(seatHome, { recursive: true, mode: 0o700 });
|
|
5327
|
+
launchOpts.carried = { home: seatHome, transcript: carried.path, cwd };
|
|
5328
|
+
}
|
|
5226
5329
|
spec = connector.buildLaunch(launchOpts);
|
|
5330
|
+
if (carried)
|
|
5331
|
+
this.transcripts.consume(opts.resumeClaim, opts.resume);
|
|
5227
5332
|
spec.env = { ...spec.env, COTAL_MANAGER_INSTANCE: this.managerInstanceId };
|
|
5228
5333
|
handle = await this.spawnCustodied(name, spec, cwd, custody);
|
|
5229
5334
|
}
|
|
@@ -5232,6 +5337,7 @@ export class Manager {
|
|
|
5232
5337
|
name,
|
|
5233
5338
|
role,
|
|
5234
5339
|
agent,
|
|
5340
|
+
...(seatHome ? { seatHome } : {}),
|
|
5235
5341
|
id: userLaunch ? principalKey(userLaunch.owner, name).key : identity.id,
|
|
5236
5342
|
lifecycleUid,
|
|
5237
5343
|
// The lifecycle-keyed family this spawn just materialized (absent on an open mesh) — the
|
|
@@ -5272,6 +5378,7 @@ export class Manager {
|
|
|
5272
5378
|
shareTools: opts.shareTools,
|
|
5273
5379
|
forkSource: opts.resume,
|
|
5274
5380
|
...(opts.resume !== undefined && spec?.resumeRecordPath ? { resumeRecordPath: spec.resumeRecordPath } : {}),
|
|
5381
|
+
...(carried ? { carried: { transcriptSha256: carried.sha256, host: carried.sourceHost, transferredAt: carried.stagedAt } } : {}),
|
|
5275
5382
|
// Opaque values may contain secrets. Preserve only their keys and require the referenced
|
|
5276
5383
|
// persona/manifest to resolve the values again; imperative overrides have no safe payload.
|
|
5277
5384
|
unresolvedLaunchOptionKeys: opts.launchOptions && Object.keys(opts.launchOptions).length
|
|
@@ -5311,6 +5418,7 @@ export class Manager {
|
|
|
5311
5418
|
// The live slot now owns teardown — freeSlot deprovisions this identity on exit — so the
|
|
5312
5419
|
// orphan-rollback in `finally` no longer applies to it.
|
|
5313
5420
|
provisioned = undefined;
|
|
5421
|
+
seatHome = undefined;
|
|
5314
5422
|
// #159 B1: reply on a REAL outcome, not a timer. Wait for the agent to actually join the mesh
|
|
5315
5423
|
// (presence) → started, the child to exit → failed (with its last output; already reaped), or
|
|
5316
5424
|
// neither in time → uncertain. `✓ started` therefore means "it joined", never just "a process
|
|
@@ -5336,9 +5444,24 @@ export class Manager {
|
|
|
5336
5444
|
// death — including one that follows an `uncertain` verdict, which deliberately does NOT deprovision).
|
|
5337
5445
|
if (!readiness.ok) {
|
|
5338
5446
|
this.watchExit(managed);
|
|
5447
|
+
if (managed.launch.carried)
|
|
5448
|
+
void this.refuseLateCarriedFork(managed, readinessTimeoutMs);
|
|
5339
5449
|
await hooks?.onOutcome?.({ kind: "uncertain", data: { reason: readiness.detail } });
|
|
5340
5450
|
return { ok: false, error: readiness.detail };
|
|
5341
5451
|
}
|
|
5452
|
+
if (managed.launch.carried) {
|
|
5453
|
+
const refused = await this.refuseCarriedFork(managed, readinessTimeoutMs);
|
|
5454
|
+
if (refused !== undefined && managed.terminalizing) {
|
|
5455
|
+
hooks?.onTerminalDeferred?.();
|
|
5456
|
+
return { ok: false, error: refused };
|
|
5457
|
+
}
|
|
5458
|
+
if (refused !== undefined) {
|
|
5459
|
+
this.stopHandle(managed, false);
|
|
5460
|
+
this.freeSlot(managed, true, "carried-fork-refused");
|
|
5461
|
+
await hooks?.onOutcome?.({ kind: "failed", data: { error: refused } });
|
|
5462
|
+
return { ok: false, error: refused };
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
5342
5465
|
if (managed.restart) {
|
|
5343
5466
|
if (connector.supportsSessionContinuation) {
|
|
5344
5467
|
try {
|
|
@@ -5393,6 +5516,8 @@ export class Manager {
|
|
|
5393
5516
|
finally {
|
|
5394
5517
|
this.reserved.delete(name);
|
|
5395
5518
|
this.reservedLive.delete(name);
|
|
5519
|
+
if (seatHome)
|
|
5520
|
+
rmSync(seatHome, { recursive: true, force: true });
|
|
5396
5521
|
// Minted but never handed to a live slot (buildLaunch / runtime.spawn threw after mint) → tear the
|
|
5397
5522
|
// orphan down (detached, fail-loud) so a failed spawn leaves no creds/durables behind (#159 B).
|
|
5398
5523
|
if (provisioned) {
|
|
@@ -5981,7 +6106,24 @@ export class Manager {
|
|
|
5981
6106
|
this.reservedLive.add(entry.name);
|
|
5982
6107
|
if (this.resumeAttemptId)
|
|
5983
6108
|
this.resumedAgentNames.add(entry.name);
|
|
5984
|
-
|
|
6109
|
+
try {
|
|
6110
|
+
await this.recordSlotRuntime(managed);
|
|
6111
|
+
}
|
|
6112
|
+
catch (error) {
|
|
6113
|
+
const detail = `${managed.name} resumed, but ${error.message}`;
|
|
6114
|
+
this.stopHandle(managed, false);
|
|
6115
|
+
try {
|
|
6116
|
+
await this.awaitHandleExit(managed.handle);
|
|
6117
|
+
}
|
|
6118
|
+
catch (exit) {
|
|
6119
|
+
// A resumed seat keeps its retained credentials, so freeing it runs no deprovision and
|
|
6120
|
+
// nothing would reap it: it stays managed until it exits or the manager stops it.
|
|
6121
|
+
this.watchExit(managed);
|
|
6122
|
+
return { ok: false, error: `${detail}; it stays managed because its stop is unproven: ${exit.message}` };
|
|
6123
|
+
}
|
|
6124
|
+
this.freeSlot(managed, true, "resume-custody-unrecorded", true);
|
|
6125
|
+
return { ok: false, error: detail };
|
|
6126
|
+
}
|
|
5985
6127
|
const readiness = await this.awaitReadiness(managed, readinessTimeoutMs);
|
|
5986
6128
|
if (!readiness.ok && !readiness.uncertain)
|
|
5987
6129
|
return { ok: false, error: readiness.detail };
|
|
@@ -6752,13 +6894,15 @@ export class Manager {
|
|
|
6752
6894
|
// with a fresh bounded exp; a restart re-provisions the same (idempotent) gate + re-registers.
|
|
6753
6895
|
const serveIdentity = this.managerServeIdentity;
|
|
6754
6896
|
const servePrincipal = principalKey(DEV_OWNER, serveIdentity.id).key;
|
|
6897
|
+
// An open mesh mints no credentials, so it has no family for a fresh pair to grow.
|
|
6898
|
+
const siblings = auth ? claimManagerSiblingIdentities(this.workspaceRoot, this.space) : { goalWriter: newIdentity(), sessionLedger: newIdentity() };
|
|
6755
6899
|
// must-5 (b): the STABLE goal-writer identity — a SIBLING credential in the same §13.1 family
|
|
6756
|
-
// (not the gate's bound serving principal),
|
|
6757
|
-
this.goalWriterIdentity =
|
|
6900
|
+
// (not the gate's bound serving principal), so the run block can family-stage it.
|
|
6901
|
+
this.goalWriterIdentity = siblings.goalWriter;
|
|
6758
6902
|
// P2 item 6: the STABLE session-LEDGER identity — another SIBLING in the SAME §13.1 family, so
|
|
6759
6903
|
// the takeover barrier revokes a deposed manager's ledger cred alongside its goal-writer. The
|
|
6760
6904
|
// per-session serving creds join the same family, each with its own fresh identity.
|
|
6761
|
-
this.sessionLedgerIdentity =
|
|
6905
|
+
this.sessionLedgerIdentity = siblings.sessionLedger;
|
|
6762
6906
|
// One registration operation for this boot. Fresh executors may replace a dead connection, but
|
|
6763
6907
|
// they must resume THIS freeze rather than minting a new op that discards Phase-2 progress.
|
|
6764
6908
|
const registrationOpId = mintLifecycleUid();
|
|
@@ -6809,7 +6953,7 @@ export class Manager {
|
|
|
6809
6953
|
...(auth ? { evict: makeManagerEndpointHolderEvictor({ space: this.space, servers: this.servers ?? DEFAULT_SERVER, auth, log: (line) => console.error(line), unreachableWaitMs: DELIVERY_ADMIN_BOOT_WAIT_MS }) } : {}),
|
|
6810
6954
|
});
|
|
6811
6955
|
const spec = { endpoint: MANAGER_ENDPOINT, owner: DEV_OWNER, clusterDigests: [artifacts.closureDigest], protocol: { v: 1 } };
|
|
6812
|
-
const {
|
|
6956
|
+
const { grant } = await registerServingInstance(recordsKv, {
|
|
6813
6957
|
space: this.space, spec, instanceId: iid, registrant: { owner: DEV_OWNER }, authority, barrier, readClusterArtifact,
|
|
6814
6958
|
// #1393: when a FOREIGN instance holds the endpoint governance slot, let core tell an
|
|
6815
6959
|
// in-flight registration from one abandoned by a predecessor that died between its
|
|
@@ -6817,43 +6961,9 @@ export class Manager {
|
|
|
6817
6961
|
// grant (no new grant, no new writer); core refuses on anything but a gate provably
|
|
6818
6962
|
// reopened past the slot's stamp.
|
|
6819
6963
|
observeHolderGeneration: (holderInstanceId) => readEndpointGateGeneration(authKv, { endpoint: MANAGER_ENDPOINT, instanceId: holderInstanceId }),
|
|
6964
|
+
status: { connectors: this.connectorStatuses.map((row) => ({ ...row, binaries: { ...row.binaries } })) },
|
|
6820
6965
|
});
|
|
6821
|
-
// processEpoch is the one this registration's reopen committed (checklist 4: never derived
|
|
6822
|
-
// from the uid string). A gate read here could already show a successor's epoch; the fences
|
|
6823
|
-
// below refuse that successor instead of serving at it. The fence is also the mint's §13.1
|
|
6824
|
-
// release CAS.
|
|
6825
6966
|
const fence = serveIssuanceGateKv(authKv, this.space, { endpoint: MANAGER_ENDPOINT, instanceId: iid });
|
|
6826
|
-
const grant = await authorizeServeGrant(recordsKv, {
|
|
6827
|
-
space: this.space, endpoint: MANAGER_ENDPOINT, instanceId: iid, epoch: processEpoch,
|
|
6828
|
-
holder: { owner: DEV_OWNER }, authority, readClusterArtifact,
|
|
6829
|
-
readProcessEpoch: async () => {
|
|
6830
|
-
const g = await fence.observe();
|
|
6831
|
-
if (g === null)
|
|
6832
|
-
throw new Error(`no issuance gate for ${MANAGER_ENDPOINT}/${iid}`);
|
|
6833
|
-
return g.processEpoch;
|
|
6834
|
-
},
|
|
6835
|
-
});
|
|
6836
|
-
// P2 item 3 (class scatter): write this instance's CONVERGED svc status so it is a §13.5
|
|
6837
|
-
// scatter member — `freezeExpectedSet` skips any instance whose status is absent or lags the
|
|
6838
|
-
// current registration. Instance-side `ready` at the just-registered spec revision, epoch-fenced
|
|
6839
|
-
// to the gate's processEpoch (the same leader-served reader `authorizeServeGrant` used); on a
|
|
6840
|
-
// restart it CAS-updates the predecessor's status forward (the advanced epoch supersedes the old
|
|
6841
|
-
// one). Key-pinned to this instance's own status key on the SAME executor.
|
|
6842
|
-
await writeServiceStatus(recordsKv, {
|
|
6843
|
-
endpoint: MANAGER_ENDPOINT, instanceId: iid, epoch: processEpoch,
|
|
6844
|
-
status: {
|
|
6845
|
-
state: SERVICE_READY,
|
|
6846
|
-
epoch: processEpoch,
|
|
6847
|
-
observedSpecRevision: registrationRevision,
|
|
6848
|
-
connectors: this.connectorStatuses.map((row) => ({ ...row, binaries: { ...row.binaries } })),
|
|
6849
|
-
},
|
|
6850
|
-
readProcessEpoch: async () => {
|
|
6851
|
-
const g = await fence.observe();
|
|
6852
|
-
if (g === null)
|
|
6853
|
-
throw new Error(`no issuance gate for ${MANAGER_ENDPOINT}/${iid}`);
|
|
6854
|
-
return g.processEpoch;
|
|
6855
|
-
},
|
|
6856
|
-
});
|
|
6857
6967
|
// Open mesh: NO mint - the §13.1 fence is issuance-only and nothing is ever issued, so the
|
|
6858
6968
|
// gate keeps an empty `epcred` family; the serve connection below stays bare.
|
|
6859
6969
|
const creds = auth ? await mintCreds(auth, serveIdentity, "endpoint-serve", { serveIssuance: fence, endpointServe: grant }) : undefined;
|
|
@@ -7502,9 +7612,7 @@ export class Manager {
|
|
|
7502
7612
|
* 3. OPEN MESH. No credential family exists, so the revoke/evict loop is vacuous and this belt
|
|
7503
7613
|
* is COOPERATIVE only: a non-conformant process simply does not run it.
|
|
7504
7614
|
* The named follow-up that would close 2 and 3 is the gate-linearized commit (routing the
|
|
7505
|
-
* terminal through the issuance gate's own CAS), deliberately deferred as substrate territory.
|
|
7506
|
-
* An earlier revision of this comment claimed the residual was "closed by item-3 slice 3.0,
|
|
7507
|
-
* never a permanent residual". That asserted a closure that does not exist. */
|
|
7615
|
+
* terminal through the issuance gate's own CAS), deliberately deferred as substrate territory. */
|
|
7508
7616
|
async assertGoalWriterEpochCurrent(epoch) {
|
|
7509
7617
|
const gate = this.goalWriter?.gate;
|
|
7510
7618
|
if (!gate)
|
|
@@ -8227,8 +8335,7 @@ export class Manager {
|
|
|
8227
8335
|
/** Re-record the custody reference of a managed agent's CURRENT handle on its active slot: a
|
|
8228
8336
|
* same-lifecycle restart or a resume binds a new custody under the old uid, and the successor's
|
|
8229
8337
|
* reap must address the live one. Static auth only; a runtime without durable custody records
|
|
8230
|
-
* nothing (there is nothing to reap by reference).
|
|
8231
|
-
* handle is already live and a stale reference is refused by identity at reap time. */
|
|
8338
|
+
* nothing (there is nothing to reap by reference). */
|
|
8232
8339
|
async recordSlotRuntime(a) {
|
|
8233
8340
|
if (a.handle.reference === undefined)
|
|
8234
8341
|
return;
|
|
@@ -8236,7 +8343,9 @@ export class Manager {
|
|
|
8236
8343
|
}
|
|
8237
8344
|
/** Record one custody reference on an agent's ACTIVE slot. Split from {@link recordSlotRuntime}
|
|
8238
8345
|
* so a restart or a resume can record the reference it RESERVED before spawning, while no
|
|
8239
|
-
* handle exists yet. Static auth only; a runtime without durable custody reserves nothing.
|
|
8346
|
+
* handle exists yet. Static auth only; a runtime without durable custody reserves nothing.
|
|
8347
|
+
* Throws when the slot does not take the reference: a slot left on the previous one reaps that
|
|
8348
|
+
* seat, already gone, so a successor would retire the lifecycle over the new one. */
|
|
8240
8349
|
async recordSlotCustody(a, runtime) {
|
|
8241
8350
|
if (!this.auth || this.userMode)
|
|
8242
8351
|
return;
|
|
@@ -8244,14 +8353,14 @@ export class Manager {
|
|
|
8244
8353
|
await this.withLifecycleExecutor({ owner: DEV_OWNER, actor: a.id, lifecycleUid: a.lifecycleUid, alias: a.name }, async (t) => {
|
|
8245
8354
|
const slot = await readStaticSlot(t, DEV_OWNER, a.name);
|
|
8246
8355
|
if (slot === undefined || slot.row.lifecycleUid !== a.lifecycleUid || slot.row.phase !== "active")
|
|
8247
|
-
|
|
8356
|
+
throw new Error(`the slot is ${slot === undefined ? "absent" : `${slot.row.phase} at uid ${slot.row.lifecycleUid}`}`);
|
|
8248
8357
|
if (slot.row.runtime?.kind === runtime.kind && slot.row.runtime.id === runtime.id)
|
|
8249
8358
|
return;
|
|
8250
8359
|
await casStaticSlot(t, { ...slot.row, runtime }, slot.revision);
|
|
8251
8360
|
});
|
|
8252
8361
|
}
|
|
8253
8362
|
catch (e) {
|
|
8254
|
-
|
|
8363
|
+
throw new Error(`could not record the custody reference ${runtime.kind}:${runtime.id} on the static slot of "${a.name}": ${e.message}`);
|
|
8255
8364
|
}
|
|
8256
8365
|
}
|
|
8257
8366
|
/** Mint the custody reference for a seat this manager is about to spawn, BEFORE the processes
|
|
@@ -8939,6 +9048,28 @@ export class Manager {
|
|
|
8939
9048
|
await prov.stop();
|
|
8940
9049
|
}
|
|
8941
9050
|
}
|
|
9051
|
+
/** A connection holding this instance's transfer reader grants (docs/design/resume-transfer.md
|
|
9052
|
+
* section 6) for this one receive or sweep: minted from the space's signing seed, or issued by the
|
|
9053
|
+
* host to a remote manager. An open mesh enforces no grants, so a bare one-shot connection is that
|
|
9054
|
+
* reader there. */
|
|
9055
|
+
async withTransferReader(fn) {
|
|
9056
|
+
const issue = this.remoteAuthority?.mintTransferReader;
|
|
9057
|
+
if (this.remoteAuthority && !issue)
|
|
9058
|
+
throw new Error("withTransferReader: this manager's remote authority issues no transfer reader");
|
|
9059
|
+
const identity = newIdentity();
|
|
9060
|
+
const creds = issue
|
|
9061
|
+
? await issue(identity)
|
|
9062
|
+
: this.auth && await mintCreds(this.auth, identity, "transfer-reader", { transferReader: { instanceId: this.managerInstanceId } });
|
|
9063
|
+
const nc = await this.dial(creds
|
|
9064
|
+
? { ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 }
|
|
9065
|
+
: { maxReconnectAttempts: 0 });
|
|
9066
|
+
try {
|
|
9067
|
+
return await fn(nc);
|
|
9068
|
+
}
|
|
9069
|
+
finally {
|
|
9070
|
+
await nc.drain().catch(() => nc.close());
|
|
9071
|
+
}
|
|
9072
|
+
}
|
|
8942
9073
|
/** Purge the space's retained message backlog (chat, optionally DMs). Privileged — the manager mints a
|
|
8943
9074
|
* short-lived "purger" cred (same destructive grant as `cotal history clear`, isolated off the
|
|
8944
9075
|
* supervisor); regular agents are denied STREAM.PURGE under auth. Cleanup only: leaves live agents and
|
|
@@ -9234,7 +9365,8 @@ export class Manager {
|
|
|
9234
9365
|
}
|
|
9235
9366
|
// establishAttach releases the claim it was handed on every exit; nothing to unwind here.
|
|
9236
9367
|
const { grant } = await this.sessionPlane.establishAttach(caller, { name: a.name, lifecycleUid: a.lifecycleUid }, session, slot);
|
|
9237
|
-
|
|
9368
|
+
const carried = a.launch.resumed?.host;
|
|
9369
|
+
return { ok: true, data: { grant, ...(carried ? { resumedFrom: { host: carried, source: a.launch.resumed.source } } : {}) } };
|
|
9238
9370
|
}
|
|
9239
9371
|
/** P2 item 6: the console's mesh §13.6 session establisher (backing `POST /session/<name>` on the
|
|
9240
9372
|
* loopback face). Drives THE ONE plane — same establishAttach as the ep `attach` command — with
|