@cotal-ai/manager 0.73.0 → 0.76.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/manager.js CHANGED
@@ -4,11 +4,11 @@ import { credsAuthenticator } from "@nats-io/transport-node";
4
4
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
5
5
  import { isAbsolute, join, dirname, resolve } from "node:path";
6
6
  import Headless from "@xterm/headless";
7
- import { CotalEndpoint, canonicalJson, 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, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, resolveReadAcl, runAgentBearer, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannelPrincipal, resolveService, invokeCommand, } from "@cotal-ai/core";
8
- import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, claimManagerSiblingIdentities, connectorInstallHint, DELIVERY_CREDS_KIND, extensionConnectors, findCotalRoot, getSpaceAuth, hasUserAuthState, loadExtensionsManifest, loadManagerInstanceIdentity, loadMeshes, localTrustOfSpace, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KIND, mergeLaunchOptions, remintDaemonCreds, resolveAgentType, resolveOnPath, createManagerInstanceIdentity, spaceMaterialKey, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
9
- import { createRuntime, isCustodialRuntime, requireRuntimeAdopt, requireRuntimeReap, RuntimeReapUnproven, } from "./runtime/index.js";
7
+ import { CotalEndpoint, canonicalJson, MANAGED_HANDOFF_KIND, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, injectedSecretStoreIdentity, 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, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, resolveReadAcl, runAgentBearer, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannelPrincipal, resolveService, invokeCommand, } from "@cotal-ai/core";
8
+ import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, claimIdentityRecord, claimManagerSiblingIdentities, connectorInstallHint, DELIVERY_CREDS_KIND, extensionConnectors, findCotalRoot, getSpaceAuth, hasUserAuthState, loadExtensionsManifest, loadManagerInstanceIdentity, loadMeshes, localTrustOfSpace, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KIND, mergeLaunchOptions, remintDaemonCreds, resolveAgentType, resolveOnPath, createManagerInstanceIdentity, spaceKey, spaceMaterialKey, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
9
+ import { createRuntime, isCustodialRuntime, requireRuntimeAdopt, requireRuntimeReap, RuntimeReapUnproven, watchProvedExit, } from "./runtime/index.js";
10
10
  import { AttachEndpoint } from "./attach-endpoint.js";
11
- import { DELIVERY_ADMIN_BOOT_WAIT_MS, isUnansweredDeliveryAdmin, makeManagerEndpointEvictionEvidence, makeManagerEndpointHolderEvictor, untilDeliveryAdminAnswers } from "./endpoint-evict.js";
11
+ import { DELIVERY_ADMIN_BOOT_WAIT_MS, isUnansweredDeliveryAdmin, makeManagerEndpointEvictionEvidence, makeManagerEndpointHolderEvictor, untilDeliveryAdminAnswers, withScopedEndpoint } from "./endpoint-evict.js";
12
12
  import { makeManagerHolderLivenessProbe } from "./holder-liveness.js";
13
13
  import { GateReconcileRefused, reconcileEndpointGate } from "./reconcile-gate.js";
14
14
  import { launchSpecForRun, materializePersona, launchAgentToStartOpts, parseLaunchSpec, persistLaunchSpec, readContinuityAssignment, writeContinuityAssignment } from "./launch.js";
@@ -294,14 +294,6 @@ function resolveHarness(name, requires) {
294
294
  }
295
295
  return missing.length ? { binaries, reason: `${name} harness needs ${missing.join(", ")} on PATH - not found` } : { binaries };
296
296
  }
297
- function injectedManagerStoreIdentity(store) {
298
- if (store.identity !== undefined)
299
- return parseSecretStoreIdentity(store.identity);
300
- const coordinate = process.env.COTAL_SECRET_STORE;
301
- if (!coordinate)
302
- throw new Error("ManagerOptions.secretStore must declare its identity or set COTAL_SECRET_STORE so the manager and delivery daemon can name the same authority (never a silent local-root fallback)");
303
- return { kind: "injected", coordinate };
304
- }
305
297
  /** True only when the delivery-admin rail has no bound responder. A timeout is a hung rail, not absence. */
306
298
  function isAbsentDeliveryAdmin(msg) {
307
299
  if (/timeout/i.test(msg))
@@ -341,7 +333,7 @@ export class Manager {
341
333
  secretStoreIdentity;
342
334
  /** See {@link ManagerOptions.installedExtensions}. */
343
335
  installedExtensions;
344
- runtime;
336
+ executionState;
345
337
  /** Internal test seam. Production leaves this undefined and uses the scoped delivery-admin evictor. */
346
338
  staticLifecycleEvict;
347
339
  preserveStopTimeoutMs;
@@ -629,7 +621,13 @@ export class Manager {
629
621
  return dialerFor(servers)({ ...opts, servers });
630
622
  }
631
623
  constructor(opts) {
632
- if (opts.pooled && (!opts.remoteAuthority || opts.runtime === undefined || opts.runtime === "auto" || opts.runtime === "pty"))
624
+ if (opts.execution !== undefined && opts.execution !== "runtime" && opts.execution !== "none")
625
+ throw new Error(`unknown manager execution capability ${JSON.stringify(opts.execution)}`);
626
+ if (opts.execution === "none" && (!opts.pooled || !opts.remoteAuthority || opts.runtime !== undefined))
627
+ throw new Error("execution: none requires pooled signerless remote authority and no runtime option");
628
+ if (opts.execution === "none" && ["renewExecutor", "renewStandingBundle", "mintSessionServing", "mintRetirementRequester", "prepareAgentRetirement", "validateRetainedAgent", "scanGoalIndex", "authorizeAdmin"].some((key) => typeof opts.remoteAuthority?.[key] !== "function"))
629
+ throw new Error("execution: none requires complete closed remote authority callbacks before construction");
630
+ if (opts.pooled && (!opts.remoteAuthority || (opts.execution !== "none" && (opts.runtime === undefined || opts.runtime === "auto" || opts.runtime === "pty"))))
633
631
  throw new Error("pooled control requires signerless remote authority and an explicit non-PTY runtime; local custodial execution is forbidden");
634
632
  if (opts.pooled && !opts.remoteAuthority?.renewStandingBundle)
635
633
  throw new Error("pooled control requires a closed host-issued all-duty renewal callback before construction");
@@ -649,11 +647,13 @@ export class Manager {
649
647
  this.managerLifecycleUid = opts.remoteAuthority.lifecycleUid;
650
648
  this.secrets = opts.secretStore ?? workspaceSecretStore(this.workspaceRoot);
651
649
  if (opts.secretStore)
652
- this.secretStoreIdentity = injectedManagerStoreIdentity(opts.secretStore);
650
+ this.secretStoreIdentity = injectedSecretStoreIdentity(opts.secretStore);
653
651
  this.installedExtensions = opts.installedExtensions ?? false;
654
- this.runtime = createRuntime(opts.runtime ?? "auto", `cotal-${this.space}`);
655
- if (opts.pooled && isCustodialRuntime(this.runtime))
656
- throw new Error(`pooled control refuses custodial runtime "${this.runtime.kind}" before starting agents`);
652
+ this.executionState = opts.execution === "none" ? { kind: "none" } : { kind: "runtime" };
653
+ if (this.executionState.kind === "runtime")
654
+ this.runtime = createRuntime(opts.runtime ?? "auto", `cotal-${this.space}`);
655
+ if (opts.pooled && this.executionState.kind === "runtime" && isCustodialRuntime(this.requiredRuntime))
656
+ throw new Error(`pooled control refuses custodial runtime "${this.requiredRuntime.kind}" before starting agents`);
657
657
  this.preserveStopTimeoutMs = opts.preserveStopTimeoutMs ?? PRESERVE_STOP_TIMEOUT_MS;
658
658
  this.endpointServeExecutorExpiresInSeconds = opts.endpointServeExecutorExpiresInSeconds;
659
659
  if (opts.resumeAttemptId && !/^[A-Za-z0-9_-]{1,128}$/.test(opts.resumeAttemptId))
@@ -681,18 +681,34 @@ export class Manager {
681
681
  opts.attachHost ?? "127.0.0.1");
682
682
  }
683
683
  get runtimeKind() {
684
- return this.runtime.kind;
684
+ return this.executionState.kind === "none" ? "none" : this.requiredRuntime.kind;
685
+ }
686
+ get execution() {
687
+ return this.executionState.kind;
688
+ }
689
+ executionRefusal() {
690
+ return "execution: none forbids seat launch and workflow hosting on this control instance; select the participant Manager with an instance-pinned manager-caller view";
691
+ }
692
+ /** A defensive last barrier, not a substitute Runtime. Entry admission runs before effects. */
693
+ get requiredRuntime() {
694
+ if (this.executionState.kind === "none")
695
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
696
+ return this.runtime;
685
697
  }
686
698
  /** What a default process stop does with this manager's seats. `release` drops manager-local
687
699
  * custody and leaves every seat running. `stop` means this runtime keeps its PTYs inside the
688
700
  * manager process, so those seats close with it: the stop stops and deprovisions them, and still
689
701
  * releases every seat that can outlive it. */
690
702
  get spareSeats() {
691
- return this.runtime.kind !== "pty" || this.runtime.supportsRelease === true ? "release" : "stop";
703
+ if (this.execution === "none")
704
+ return "release";
705
+ return this.requiredRuntime.kind !== "pty" || this.requiredRuntime.supportsRelease === true ? "release" : "stop";
692
706
  }
693
707
  /** Reattach this manager's runtime to a durable handle. Refuses by name when adopt is absent. */
694
708
  adoptRuntimeHandle(reference) {
695
- return requireRuntimeAdopt(this.runtime, reference);
709
+ if (this.execution === "none")
710
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
711
+ return requireRuntimeAdopt(this.requiredRuntime, reference);
696
712
  }
697
713
  /** The console page URL (manager-hosted, loopback). */
698
714
  get consoleUrl() {
@@ -795,7 +811,34 @@ export class Manager {
795
811
  this.startTask = wrapped;
796
812
  return wrapped;
797
813
  }
814
+ async admitExecutionState() {
815
+ if (this.remoteAuthority) {
816
+ // Execution is fixed for a registered instance. An older instance with no capability
817
+ // record might own retained seats/runs, so it cannot be converted to none by omission.
818
+ const remote = this.remoteAuthority;
819
+ const path = join(this.workspaceRoot, ".cotal", `manager-execution.${spaceKey(this.space)}.${remote.instanceId}.json`);
820
+ if (this.execution === "none" && !existsSync(path) && remote.serveGrant.epoch !== 0)
821
+ throw new Error("execution: none refuses an existing unclassified manager instance; keep its runtime and retained state, or register a fresh control instance");
822
+ if (this.execution === "none") {
823
+ const entries = await remote.scanGoalIndex();
824
+ if (entries.some((entry) => entry.iid === remote.instanceId && (entry.allocated !== undefined || entry.note !== undefined)))
825
+ throw new Error("execution: none refuses retained seat or turn inventory before recovery; keep the execution-owning Manager");
826
+ }
827
+ if (this.execution === "none" || existsSync(path)) {
828
+ const record = claimIdentityRecord(path, "manager execution capability", (raw) => {
829
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
830
+ return undefined;
831
+ const r = raw;
832
+ return Object.keys(r).sort().join(",") === "execution,instanceId,space,v" && r.v === 1 && r.space === this.space &&
833
+ r.instanceId === remote.instanceId && (r.execution === "none" || r.execution === "runtime") ? r : undefined;
834
+ }, () => ({ v: 1, space: this.space, instanceId: remote.instanceId, execution: this.execution }));
835
+ if (record.execution !== this.execution)
836
+ throw new Error("manager execution capability differs from its retained instance record; execution cannot change across restart");
837
+ }
838
+ }
839
+ }
798
840
  async runStart() {
841
+ await this.admitExecutionState();
799
842
  this.staticReconcileStopping = false;
800
843
  await this.inspectConnectorsAtBoot();
801
844
  await this.attach.start();
@@ -922,7 +965,7 @@ export class Manager {
922
965
  this.ep.on("roster", () => this.logSeatMeshTransitions());
923
966
  this.ep.on("presence-view", () => this.logSeatMeshTransitions());
924
967
  await this.ep.start();
925
- await this.ep.setActivity(`supervisor (${this.runtime.kind})`);
968
+ await this.ep.setActivity(`supervisor (${this.runtimeKind})`);
926
969
  // Per-instance liveness lease (P2 item 3 — the old per-space singleton is DEMOTED per D9). Acquire
927
970
  // THIS logical instance's own key (atomic CAS create). A DIFFERENT instance (a second manager in a
928
971
  // second workspace root) has a distinct id ⇒ a distinct key ⇒ it coexists; the create THROWS only
@@ -930,7 +973,7 @@ export class Manager {
930
973
  // crashed predecessor's not-yet-expired key), and we REFUSE loud. A crashed holder's key auto-expires
931
974
  // (bucket TTL). Losing the key LATER ends this process only when another process holds it; otherwise
932
975
  // the renew loop puts it back or keeps retrying ({@link renewLease}).
933
- this.leaseInfo = { holder: this.ep.ref().id, instanceId: this.managerInstanceId, runtime: this.runtime.kind, root: resolve(this.workspaceRoot), pid: process.pid };
976
+ this.leaseInfo = { holder: this.ep.ref().id, instanceId: this.managerInstanceId, runtime: this.runtimeKind, root: resolve(this.workspaceRoot), pid: process.pid };
934
977
  try {
935
978
  this.leaseRevision = await this.ep.acquireManagerLease(this.leaseInfo);
936
979
  }
@@ -1074,7 +1117,7 @@ export class Manager {
1074
1117
  // refuses them until its reconcile has returned. A remote user-auth manager uses its four
1075
1118
  // registered issuer callbacks instead of holding a signer; a local user mesh stands none up.
1076
1119
  const remoteRuns = this.remoteAuthority?.runHosting;
1077
- if (this.remoteAuthority && remoteRuns) {
1120
+ if (this.execution !== "none" && this.remoteAuthority && remoteRuns) {
1078
1121
  this.runHosting = new RunHosting({
1079
1122
  space: this.space,
1080
1123
  servers: this.servers,
@@ -1084,6 +1127,7 @@ export class Manager {
1084
1127
  auth: undefined,
1085
1128
  log: (line) => console.error(line),
1086
1129
  admitRun: remoteRuns.admitRun,
1130
+ revokeRun: remoteRuns.revokeRun,
1087
1131
  issueAttempt: remoteRuns.issueAttempt,
1088
1132
  issueOperator: remoteRuns.issueOperator,
1089
1133
  renewRun: remoteRuns.renewRun,
@@ -1526,6 +1570,8 @@ export class Manager {
1526
1570
  async renewRemoteStandingBundle(force = false) {
1527
1571
  if (this.remoteBundleRenewal)
1528
1572
  return this.remoteBundleRenewal;
1573
+ if (this.leaseStopping)
1574
+ return;
1529
1575
  const renewal = this.renewRemoteStandingBundleOnce(force);
1530
1576
  this.remoteBundleRenewal = renewal;
1531
1577
  try {
@@ -2041,12 +2087,17 @@ export class Manager {
2041
2087
  * touches NEITHER the lease NOR the endpoints — the caller owns those. */
2042
2088
  async teardownManagedAgents(managed = [...this.agents.values()]) {
2043
2089
  const failures = [];
2090
+ const stopped = [];
2044
2091
  for (const a of managed) {
2045
2092
  // Free the slot + hard-stop each; `stopHandle` is best-effort (never throws — see it), so one bad
2046
2093
  // stop can't strand the rest, and every snapshot entry is deprovisioned below regardless.
2047
2094
  this.recordContinuityAtStop(a, "shutdown");
2048
2095
  this.agents.delete(a.name);
2049
- this.stopHandle(a, false);
2096
+ const refused = this.stopHandle(a, false);
2097
+ if (refused === undefined)
2098
+ stopped.push(a);
2099
+ else
2100
+ failures.push(`${a.name}: stop failed: ${refused}`);
2050
2101
  }
2051
2102
  // The slot delete above bypasses freeSlot, which is where the reap line lives — so say it here,
2052
2103
  // once per seat, with the one stop cause that names this path: the shutdown itself. Without
@@ -2057,7 +2108,7 @@ export class Manager {
2057
2108
  // while any seat may still hold this instance's broker rails: that creates the exact orphan
2058
2109
  // window where a successor sees a dead manager but live predecessor authority. Every shipped
2059
2110
  // runtime now supplies waitForExit; absence or timeout is a failed shutdown, never a clean one.
2060
- await Promise.all(managed.map(async (a) => {
2111
+ await Promise.all(stopped.map(async (a) => {
2061
2112
  try {
2062
2113
  await this.awaitHandleExit(a.handle);
2063
2114
  }
@@ -2152,6 +2203,9 @@ export class Manager {
2152
2203
  // still move the key's revision after we clear the timer, and releasing against a cached
2153
2204
  // revision it has since moved is a CAS the broker correctly refuses, silently, forever.
2154
2205
  await this.leaseRenewTask?.catch(() => { });
2206
+ // An already accepted host issuance must finish before standing transports close. The
2207
+ // epoch/stop fence refuses adoption, but does not cancel the issuer's in-flight request.
2208
+ await this.remoteBundleRenewal?.catch(() => { });
2155
2209
  // A spawn admitted before the fence closed may still be launching. Its seat must be in the
2156
2210
  // snapshot below, or it would outlive a stop that reported success.
2157
2211
  await this.awaitLifecycleDrain();
@@ -2268,13 +2322,7 @@ export class Manager {
2268
2322
  endpoint: MANAGER_ENDPOINT,
2269
2323
  instanceId: iid,
2270
2324
  registrationRevision,
2271
- observeGeneration: async () => {
2272
- const key = epgateKey(MANAGER_ENDPOINT, iid);
2273
- const entry = await authKv.get(key);
2274
- if (!entry || entry.operation !== "PUT")
2275
- throw new Error(`no issuance gate at ${key}`);
2276
- return parseEndpointGate(entry.value, key).generation;
2277
- },
2325
+ observeGeneration: () => readEndpointGateGeneration(authKv, { endpoint: MANAGER_ENDPOINT, instanceId: iid }),
2278
2326
  });
2279
2327
  try {
2280
2328
  const outcome = await ((this.auth || this.remoteAuthority)
@@ -2525,6 +2573,8 @@ export class Manager {
2525
2573
  {
2526
2574
  try {
2527
2575
  const args = parseResumeControlArgs(rawArgs);
2576
+ if (this.execution === "none" && args.inventory.agents.length !== 0)
2577
+ return { ok: false, error: this.executionRefusal() };
2528
2578
  const inventoryDigest = createHash("sha256").update(JSON.stringify(args.inventory)).digest("hex");
2529
2579
  if (!this.resumeAttemptId)
2530
2580
  return { ok: false, error: "resumePreserved requires a manager started with --resume-attempt" };
@@ -2675,6 +2725,8 @@ export class Manager {
2675
2725
  * surface comes up before the host): `unavailable`, retry. The three are told apart, since
2676
2726
  * the first sentence steers a caller to `--local` and the others must not. */
2677
2727
  runHost() {
2728
+ if (this.execution === "none")
2729
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
2678
2730
  if (this.runHosting)
2679
2731
  return this.runHosting;
2680
2732
  if (this.remoteAuthority && !this.remoteAuthority.runHosting)
@@ -2872,13 +2924,19 @@ export class Manager {
2872
2924
  return { goalId, ...(result === undefined ? {} : { result }) };
2873
2925
  }),
2874
2926
  resolveCwd: (ctx) => this.serveGated(ctx, () => this.resolveSpawnCwd(args(ctx).cwd)),
2927
+ resolveAgent: (ctx) => this.serveGated(ctx, async () => {
2928
+ const persona = await this.spawnPersona(args(ctx), callerOf(ctx), ctx.subject.caller);
2929
+ if ("error" in persona)
2930
+ throw new EpEnvelopeError(persona.error.startsWith("no persona") ? "not-found" : "failed-precondition", persona.error);
2931
+ return { agent: persona.agent };
2932
+ }),
2875
2933
  // P2 item 2: `spawn` is an ACTION - accept a goal + reply the acceptance floor payload, drive
2876
2934
  // progress + terminal off-handler (no ~30s block). The blocking reply path is gone (pin 8).
2877
2935
  // #373: arming the event plane (events: true, or the omission that arms it by default)
2878
2936
  // follows owner-domain reach on a user mesh. Resolve the caller's admin tier once for
2879
2937
  // the residual cross-owner case; opStart decides before connector resolution or grant
2880
2938
  // mutation. Static meshes resolve true and nothing changes.
2881
- spawn: (ctx) => this.serveGated(ctx, async () => this.serveSpawnGoal(ctx, async (h) => this.opStart(args(ctx), callerOf(ctx), await this.epAdminReach(ctx.subject.caller), h, ctx.subject.route))),
2939
+ spawn: (ctx) => this.serveGated(ctx, async () => this.serveSpawnGoal(ctx, async (h) => this.opStart(args(ctx), callerOf(ctx), await this.epAdminReach(ctx.subject.caller), h, ctx.subject.route, ctx.subject.caller))),
2882
2940
  despawn: (ctx) => this.serveGated(ctx, async () => {
2883
2941
  const a = targetAgent(ctx);
2884
2942
  const denied = await this.authorizeNamed(a, callerOf(ctx), await this.epAnyModeAdmin(ctx), ctx.subject.caller);
@@ -3122,11 +3180,11 @@ export class Manager {
3122
3180
  * grace window. POSIX delivers SIGTERM→SIGKILL natively, so it keeps the signal path. A hard stop
3123
3181
  * (`graceful:false`, e.g. emergency reap) skips the cooperative step on every platform.
3124
3182
  *
3125
- * BEST-EFFORT / never throws (#159 B2): a runtime hard-stop CAN throw (tmux `closeWindow` / cmux
3126
- * `closeWorkspace` are direct calls), and every caller (despawn / self-stop / reap / shutdown) frees the
3127
- * slot + deprovisions RIGHT AFTER — so a throwing stop must not abort that cleanup and leak the agent's
3128
- * footprint, nor (in `reapChildrenOf`) abort the reap of later siblings. The failure is logged loudly,
3129
- * never swallowed silently. Being the single stop chokepoint, guarding here covers all callers at once. */
3183
+ * Never throws (#159 B2): a runtime hard-stop CAN throw (tmux `closeWindow` / cmux `closeWorkspace`
3184
+ * are direct calls), and a throwing stop must not abort a caller's cleanup and leak the agent's
3185
+ * footprint, nor (in `reapChildrenOf`) abort the reap of later siblings. The failure is logged and
3186
+ * returned: a caller that must prove exit (shutdown, a failed resume) reports it as the cause at
3187
+ * once instead of waiting out a stop the runtime already refused. */
3130
3188
  stopHandle(a, graceful) {
3131
3189
  // The F5 TERMINALIZING latch (Unit B): flipped SYNCHRONOUSLY, before any await anywhere on
3132
3190
  // this stop path — later renewals refuse, while an already-admitted renewal drains before the
@@ -3144,7 +3202,9 @@ export class Manager {
3144
3202
  a.handle.stop({ graceful });
3145
3203
  }
3146
3204
  catch (e) {
3147
- console.error(`stop ${a.name} (${a.id}): ${rejectionText(e)}`);
3205
+ const failure = rejectionText(e);
3206
+ console.error(`stop ${a.name} (${a.id}): ${failure}`);
3207
+ return failure;
3148
3208
  }
3149
3209
  }
3150
3210
  /** Keep an accepted stop inside the lifecycle drain until the runtime proves the child is gone,
@@ -3488,7 +3548,7 @@ export class Manager {
3488
3548
  const info = a.handle.exitInfo?.();
3489
3549
  detail = info === undefined
3490
3550
  ? `exit detail unavailable from runtime "${a.handle.kind}"`
3491
- : `exit code ${info.code ?? "unknown"}${info.signal === undefined ? "" : `, signal ${info.signal}`}${info.diagnostic ? `; last connector diagnostic: ${info.diagnostic}` : ""}`;
3551
+ : `exit code ${info.code ?? "unknown"}${info.signal === undefined ? "" : `, signal ${info.signal}`}${info.diagnostic ? `; diagnostic: ${info.diagnostic}` : ""}`;
3492
3552
  }
3493
3553
  catch (e) {
3494
3554
  // A runtime that throws while being asked has told us something real; it must not take the
@@ -3562,6 +3622,10 @@ export class Manager {
3562
3622
  this.failSeatTurns(a.name, a.lifecycleUid);
3563
3623
  if (floor && Date.now() - a.startedAt < MIN_LIFETIME)
3564
3624
  this.cooling.push(a.startedAt + MIN_LIFETIME);
3625
+ // The departed agent's own custody reference rides the teardown: the exit that freed this slot
3626
+ // is usually the seat ending, but a stop that could not prove the process gone leaves one the
3627
+ // terminal must still address before it frees the alias.
3628
+ const target = { id: a.id, name: a.name, lifecycleUid: a.lifecycleUid, userOwner: a.userOwner, delegated: a.delegated, secretPaths: a.secretPaths, runtime: a.handle?.reference, ...(a.handedOff ? { delegatedHandle: a.handle } : {}), launch: { allowSubscribe: a.launch.allowSubscribe } };
3565
3629
  // #29 piece 3: on a USER mesh the name is RESERVED PENDING RETIREMENT — despawn started this
3566
3630
  // lifecycle's FULL teardown (footprint + standing-authority revoke + the auth-side retirement),
3567
3631
  // and the alias frees only when all of it completes (not the retirement alone). The detached
@@ -3573,24 +3637,22 @@ export class Manager {
3573
3637
  if (this.userMode) {
3574
3638
  const p = parsePrincipalKey(a.id);
3575
3639
  if (p)
3576
- this.retiring.set(a.name, { opId: retireOpId(a.lifecycleUid), lifecycleUid: a.lifecycleUid, owner: p.owner, actor: p.actor, agentId: a.id, userOwner: a.userOwner, delegated: a.delegated, secretPaths: a.secretPaths, launch: { allowSubscribe: a.launch.allowSubscribe }, ...(a.handedOff ? { delegatedHandle: a.handle } : {}), startedAt: Date.now() });
3640
+ this.retiring.set(a.name, { opId: retireOpId(a.lifecycleUid), owner: p.owner, actor: p.actor, target, startedAt: Date.now() });
3577
3641
  }
3578
3642
  else if (this.auth) {
3579
3643
  // Unit B: a STATIC lifecycle now also holds its name pending its own terminal (the F1
3580
3644
  // static retirement the detached deprovision below drives) — the alias frees only when the
3581
3645
  // gate+head terminal completes, exactly the user-mode discipline. The wire principal is the
3582
3646
  // incarnation-unique nkey (F5-bind); owner is the dev owner.
3583
- 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() });
3647
+ this.retiring.set(a.name, { opId: retireOpId(a.lifecycleUid), owner: DEV_OWNER, actor: a.id, target, startedAt: Date.now() });
3584
3648
  }
3585
3649
  // Auth mode: tear down the departed agent's minted broker footprint + creds file (#159 B2). The
3586
3650
  // process is already gone, so this must never block the slot free or throw into the caller — it runs
3587
3651
  // detached, and a failure is logged loudly (never swallowed), not retried. The `agents` guard above
3588
3652
  // makes this fire exactly once per agent across every free path (despawn / self-stop / reap / exit).
3589
- // The departed agent's own custody reference rides the teardown: the exit that freed this slot
3590
- // is usually the seat ending, but a stop that could not prove the process gone leaves one the
3591
- // terminal must still address before it frees the alias.
3653
+ // The agent's own fields ride along for its accepted renewal, which the static terminal drains first.
3592
3654
  if (!a.suppressCleanup && (this.maintenanceState === "active" || acceptedBeforeFence))
3593
- this.trackDeprovision({ ...a, runtime: a.handle?.reference, ...(a.handedOff ? { delegatedHandle: a.handle } : {}) });
3655
+ this.trackDeprovision({ ...a, ...target });
3594
3656
  }
3595
3657
  /** Tear down a departed agent's minted footprint (#159 B2, auth mode): its local-principal durables
3596
3658
  * (`dm_local-<id>`, `dlv_local-<id>`), its read-ACL row, and its creds file — everything the spawn's
@@ -3628,7 +3690,7 @@ export class Manager {
3628
3690
  // The agent is its user's: only that user's retirement intent retires it, so the name stays
3629
3691
  // held until retireDelegatedAgent confirms the host retired this UID.
3630
3692
  const held = this.retiring.get(a.name);
3631
- if (held?.lifecycleUid === a.lifecycleUid)
3693
+ if (held?.target.lifecycleUid === a.lifecycleUid)
3632
3694
  held.lastError = `"${a.name}" was launched under a delegated user intent and retires only through its user's retirement intent`;
3633
3695
  return;
3634
3696
  }
@@ -3698,7 +3760,7 @@ export class Manager {
3698
3760
  // legible operator copy. A retry re-drives this whole teardown (the same-name-spawn nudge routes
3699
3761
  // through deprovision, not the rail alone), so the revoke is re-attempted, not stranded.
3700
3762
  const holdRevoke = this.retiring.get(a.name);
3701
- if (holdRevoke && holdRevoke.lifecycleUid === a.lifecycleUid)
3763
+ if (holdRevoke && holdRevoke.target.lifecycleUid === a.lifecycleUid)
3702
3764
  holdRevoke.standingAuthorityLive = true;
3703
3765
  try {
3704
3766
  await resolveAuthProvider().revokeAgent({
@@ -3707,12 +3769,12 @@ export class Manager {
3707
3769
  actor: a.name,
3708
3770
  });
3709
3771
  const done = this.retiring.get(a.name);
3710
- if (done && done.lifecycleUid === a.lifecycleUid)
3772
+ if (done && done.target.lifecycleUid === a.lifecycleUid)
3711
3773
  done.standingAuthorityLive = false;
3712
3774
  }
3713
3775
  catch (e) {
3714
3776
  const h = this.retiring.get(a.name);
3715
- if (h && h.lifecycleUid === a.lifecycleUid)
3777
+ if (h && h.target.lifecycleUid === a.lifecycleUid)
3716
3778
  h.lastError = `the agent's standing mint authority could not be revoked (${rejectionText(e)}); the name stays held so a copied actor token cannot mint fresh credentials. NEXT: a same-name spawn re-drives the full teardown (including the revoke), or recover the auth state.`;
3717
3779
  console.error(`revoke agent grant ${a.name}: ${rejectionText(e)}`);
3718
3780
  }
@@ -3721,12 +3783,12 @@ export class Manager {
3721
3783
  try {
3722
3784
  const res = await this.deprovisionBroker(a);
3723
3785
  const h = this.retiring.get(a.name);
3724
- if (h && h.lifecycleUid === a.lifecycleUid)
3786
+ if (h && h.target.lifecycleUid === a.lifecycleUid)
3725
3787
  h.lastResources = res;
3726
3788
  }
3727
3789
  catch (e) {
3728
3790
  const h = this.retiring.get(a.name);
3729
- if (h && h.lifecycleUid === a.lifecycleUid) {
3791
+ if (h && h.target.lifecycleUid === a.lifecycleUid) {
3730
3792
  h.lastError = rejectionText(e);
3731
3793
  if (e instanceof DeprovisionError)
3732
3794
  h.lastResources = e.accounting;
@@ -3886,7 +3948,7 @@ export class Manager {
3886
3948
  // CAS the hold clear (audit #1 ABA): free the alias ONLY if the current hold is still THIS
3887
3949
  // lifecycle's. A late reply for a retired predecessor must never clear a successor's newer hold.
3888
3950
  const cur = this.retiring.get(a.name);
3889
- if (cur && cur.lifecycleUid === a.lifecycleUid) {
3951
+ if (cur && cur.target.lifecycleUid === a.lifecycleUid) {
3890
3952
  if (cur.standingAuthorityLive) {
3891
3953
  // INT-2: the auth-plane lifecycle retired, but the manager-side STANDING mint authority is
3892
3954
  // not yet revoked. A copied token could still mint, so neither free the alias nor classify
@@ -4159,6 +4221,8 @@ export class Manager {
4159
4221
  * comes from `restart.policy` when spawn carried `supervise`; otherwise the Pi session-recovery
4160
4222
  * constants. Spending the budget falls through to normal retirement. */
4161
4223
  recoverManagedSession(a) {
4224
+ if (this.execution === "none")
4225
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
4162
4226
  const restart = a.restart;
4163
4227
  if (!restart || !restart.armed || restart.recovering || a.terminalizing)
4164
4228
  return;
@@ -4371,7 +4435,7 @@ export class Manager {
4371
4435
  return false;
4372
4436
  }
4373
4437
  /** Parse an untyped control-plane `start` request into {@link StartAgentOpts}. */
4374
- opStart(args, caller, admin, hooks, route = "inst") {
4438
+ opStart(args, caller, admin, hooks, route = "inst", authenticatedCaller) {
4375
4439
  // Opaque launch options, when present, must be a mapping — a raw control message could send a
4376
4440
  // scalar/array (the CLI never does). Core doesn't interpret the keys; the connector validates them.
4377
4441
  if (args.launchOptions !== undefined && (typeof args.launchOptions !== "object" || args.launchOptions === null || Array.isArray(args.launchOptions)))
@@ -4465,7 +4529,7 @@ export class Manager {
4465
4529
  shareTools,
4466
4530
  ...(supervise !== undefined ? { supervise } : {}),
4467
4531
  route,
4468
- }, caller, hooks);
4532
+ }, caller, hooks, authenticatedCaller);
4469
4533
  }
4470
4534
  /** The realpath + directory checks a placed spawn directory must pass on this manager's host,
4471
4535
  * shared by the served `resolve-cwd` command and the `spawn` admission check (#385 item 2): one
@@ -4524,6 +4588,10 @@ export class Manager {
4524
4588
  * reports ready. A missing harness degrades only that connector, never unrelated lifecycle work:
4525
4589
  * boot continues, but prints a named refusal and records it on both manager status surfaces. */
4526
4590
  async inspectConnectorsAtBoot() {
4591
+ if (this.execution === "none") {
4592
+ this.connectorStatuses = [];
4593
+ return;
4594
+ }
4527
4595
  const rows = [];
4528
4596
  let declared;
4529
4597
  if (this.installedExtensions) {
@@ -4559,6 +4627,8 @@ export class Manager {
4559
4627
  * inventory load) skips the class `one` subscribe for spawn/launch so this member cannot
4560
4628
  * consume an unpinned request a sibling could serve. */
4561
4629
  classSpawnEnabled() {
4630
+ if (this.execution === "none")
4631
+ return false;
4562
4632
  return this.connectorStatuses.some((row) => row.state === "available");
4563
4633
  }
4564
4634
  omitClassCommands() {
@@ -4682,6 +4752,8 @@ export class Manager {
4682
4752
  * `runId`, and resolved `hash`. USER mesh: a privileged-tier launch is owner-equality-authorized
4683
4753
  * (spec owner === caller owner) before any side effect; the admin tier keeps operator behavior. */
4684
4754
  async opLaunch(args, caller, admin, hooks, route = "inst") {
4755
+ if (this.execution === "none")
4756
+ return { ok: false, error: this.executionRefusal() };
4685
4757
  const runId = String(args.runId ?? "").trim();
4686
4758
  const name = String(args.name ?? "").trim();
4687
4759
  if (!runId || !name)
@@ -4746,33 +4818,23 @@ export class Manager {
4746
4818
  * `spawner` is the authenticated id of the peer that requested the spawn (`req.from.id`),
4747
4819
  * defaulting to the manager's own id for roster/pre-spawn — recorded for the spawner
4748
4820
  * ledger (own-children despawn + reap-on-parent-exit). */
4749
- async startAgent(opts, spawner, hooks) {
4821
+ async startAgent(opts, spawner, hooks, authenticatedCaller) {
4822
+ if (this.execution === "none")
4823
+ return { ok: false, error: this.executionRefusal() };
4750
4824
  const release = this.beginLifecycle();
4751
4825
  if (!release)
4752
4826
  return { ok: false, error: this.maintenanceError() };
4753
4827
  try {
4754
- return await this.startAgentActive(opts, spawner, hooks);
4828
+ return await this.startAgentActive(opts, spawner, hooks, authenticatedCaller);
4755
4829
  }
4756
4830
  finally {
4757
4831
  release();
4758
4832
  }
4759
4833
  }
4760
- async startAgentActive(opts, spawner, hooks) {
4761
- // Before the first await, so the grant, the launch, the retained form and the restart slot all read
4762
- // the lists and the restart policy as admitted.
4763
- opts = ownLists(opts);
4764
- if (opts.resolved)
4765
- opts.resolved = ownLists(opts.resolved);
4766
- if (opts.supervise)
4767
- opts.supervise = { restarts: opts.supervise.restarts, windowMs: opts.supervise.windowMs };
4768
- if (opts.delegatedIntent) {
4769
- if (!this.remoteAuthority?.executeDelegatedUserIntent)
4770
- return { ok: false, error: `"${opts.name}" names a delegated user intent, and this manager has no host execution for one` };
4771
- if (opts.supervise)
4772
- return { ok: false, error: `"${opts.name}" names a delegated user intent, and a delegated agent is never restarted (SPEC 13.16)` };
4773
- // A delegated launch's spawner is the user's principal, never the composition root's call.
4774
- spawner = opts.delegatedIntent.parent;
4775
- }
4834
+ /** The persona file a spawn loads and the harness it resolves, shared by `spawn` and the served
4835
+ * `resolve-agent` read: a detached resume locates the session it carries with the connector this
4836
+ * names, so the two doors must not drift (#2899). */
4837
+ async spawnPersona(opts, spawner, authenticatedCaller) {
4776
4838
  // The spawn argument is a persona REF — a filename in `.cotal/agents` (the unique spawn KEY), or
4777
4839
  // a path via `--config`. It is NOT the mesh identity: the identity comes from inside the file
4778
4840
  // (`name:`), so a persona can be filed descriptively (review-critic.md) yet present under a
@@ -4780,48 +4842,79 @@ export class Manager {
4780
4842
  // filename (unique on disk); two files can't collide on the key.
4781
4843
  const ref = opts.name.trim();
4782
4844
  if (!ref)
4783
- return { ok: false, error: "name required" };
4845
+ return { error: "name required" };
4784
4846
  // A bare ref maps to `.cotal/agents/<ref>.md`, so it must be a safe token (no path traversal); a
4785
4847
  // `--config` path is validated by existsSync below instead.
4786
4848
  if (!opts.config) {
4787
4849
  const refErr = this.nameError(ref);
4788
4850
  if (refErr)
4789
- return { ok: false, error: refErr };
4851
+ return { error: refErr };
4790
4852
  }
4791
4853
  // Resolve the persona file (fail loud — NO silent default-ACL fallback). A missing persona used
4792
4854
  // to mint DEFAULT creds (read `general` only, default-deny publish, no capabilities), so a
4793
4855
  // typo'd / renamed / spawned-by-display-name agent became live with silently-wrong ACLs — a
4794
4856
  // behavioral/security bug. Fail loud instead, matching `cotal spawn` (loadAgentFile throws).
4795
- // This runs BEFORE the connector resolution below: the file's `agent:` pin participates in the
4796
- // harness choice (flag > file > COTAL_DEFAULT_AGENT > default, #869), which is impossible if the
4797
- // connector is materialized first. Still synchronous (existsSync/readFileSync), so the capacity/
4798
- // reserve span below stays atomic.
4857
+ // This runs BEFORE the spawn path resolves its connector: the file's `agent:` pin participates in
4858
+ // the harness choice (flag > file > COTAL_DEFAULT_AGENT > default, #869), which is impossible if
4859
+ // the connector is materialized first.
4799
4860
  let configPath;
4800
4861
  if (opts.config) {
4801
4862
  configPath = agentFilePath(this.workspaceRoot, opts.config);
4802
4863
  if (!existsSync(configPath))
4803
- return { ok: false, error: `agent file not found: ${configPath}` };
4864
+ return { error: `agent file not found: ${configPath}` };
4804
4865
  }
4805
4866
  else {
4806
4867
  configPath = agentFilePath(this.workspaceRoot, ref);
4807
4868
  if (!existsSync(configPath))
4808
- return { ok: false, error: `no persona "${ref}" - ${configPath} not found; create it or pass --config (see \`cotal personas list\`)` };
4869
+ return { error: `no persona "${ref}" - ${configPath} not found; create it or pass --config (see \`cotal personas list\`)` };
4809
4870
  }
4810
4871
  // Load the persona ONCE, here, ahead of the connector choice. The file's `agent:` pin feeds the
4811
- // harness resolution below; the identity/ACL profile block later consumes the SAME def (no second
4812
- // load). A manifest launch (`resolved`) materializes its own transient persona and the file is
4813
- // NOT its authority, so its def (and its agent pin) is deliberately not consulted here — the
4814
- // manifest's own `agent:` field drove the launch object already.
4872
+ // harness resolution below; the spawn path's identity/ACL profile block consumes the SAME def (no
4873
+ // second load). A manifest launch (`resolved`) materializes its own transient persona and the
4874
+ // file is NOT its authority, so its def (and its agent pin) is deliberately not consulted here —
4875
+ // the manifest's own `agent:` field drove the launch object already.
4815
4876
  let def;
4816
4877
  if (!opts.resolved) {
4817
4878
  try {
4818
4879
  def = loadAgentFile(configPath);
4819
4880
  }
4820
4881
  catch (e) {
4821
- return { ok: false, error: rejectionText(e) };
4882
+ return { error: rejectionText(e) };
4883
+ }
4884
+ // Shared operator files remain launchable. Owned content follows the catalog's visibility
4885
+ // decision before connector loading or any allocation. A hosted run acts for its admission;
4886
+ // only a complete authenticated caller can request the current ledger's admin decision.
4887
+ if (this.userMode && def.owner !== undefined) {
4888
+ const acting = authenticatedCaller === undefined ? undefined
4889
+ : this.runHosting?.admittedCaller(authenticatedCaller) ?? authenticatedCaller;
4890
+ const personaCaller = acting === undefined ? spawner ?? "" : principalKey(acting.owner, acting.actor).key;
4891
+ const admin = authenticatedCaller !== undefined && await this.epAdminReach(authenticatedCaller);
4892
+ if (!this.canReadPersona(def.owner, personaCaller, admin))
4893
+ return { error: `no persona "${ref}"` };
4822
4894
  }
4823
4895
  }
4824
- const agent = resolveAgentType({ flag: opts.agent, pin: def?.agent, callerDefault: opts.defaultAgent });
4896
+ return { ref, configPath, def, agent: resolveAgentType({ flag: opts.agent, pin: def?.agent, callerDefault: opts.defaultAgent }) };
4897
+ }
4898
+ async startAgentActive(opts, spawner, hooks, authenticatedCaller) {
4899
+ // Before the first await, so the grant, the launch, the retained form and the restart slot all read
4900
+ // the lists and the restart policy as admitted.
4901
+ opts = ownLists(opts);
4902
+ if (opts.resolved)
4903
+ opts.resolved = ownLists(opts.resolved);
4904
+ if (opts.supervise)
4905
+ opts.supervise = { restarts: opts.supervise.restarts, windowMs: opts.supervise.windowMs };
4906
+ if (opts.delegatedIntent) {
4907
+ if (!this.remoteAuthority?.executeDelegatedUserIntent)
4908
+ return { ok: false, error: `"${opts.name}" names a delegated user intent, and this manager has no host execution for one` };
4909
+ if (opts.supervise)
4910
+ return { ok: false, error: `"${opts.name}" names a delegated user intent, and a delegated agent is never restarted (SPEC 13.16)` };
4911
+ // A delegated launch's spawner is the user's principal, never the composition root's call.
4912
+ spawner = opts.delegatedIntent.parent;
4913
+ }
4914
+ const persona = await this.spawnPersona(opts, spawner, authenticatedCaller);
4915
+ if ("error" in persona)
4916
+ return { ok: false, error: persona.error };
4917
+ const { ref, configPath, def, agent } = persona;
4825
4918
  // Materialize the requested connector up front — the ONE async step in the spawn path (a lazy
4826
4919
  // `cotal ext` manifest import on the published binary). It runs BEFORE the capacity/reserve span
4827
4920
  // below so that span stays fully SYNCHRONOUS and atomic. On the manifest binary this also honors a
@@ -4879,15 +4972,15 @@ export class Manager {
4879
4972
  // exit, so a name cannot be respawned in place. User-mode seats have no static slot that
4880
4973
  // keeps the incarnation owned across a process death, so the same refusal applies there.
4881
4974
  if (opts.supervise !== undefined) {
4882
- if (this.runtime.kind !== "pty")
4883
- return { ok: false, error: `supervise is a restart policy this host cannot enforce: runtime "${this.runtime.kind}" cannot respawn a name in place` };
4975
+ if (this.requiredRuntime.kind !== "pty")
4976
+ return { ok: false, error: `supervise is a restart policy this host cannot enforce: runtime "${this.requiredRuntime.kind}" cannot respawn a name in place` };
4884
4977
  if (this.userMode)
4885
4978
  return { ok: false, error: "supervise is a restart policy this host cannot enforce: a user-mode seat has no static slot to keep the incarnation owned across a process death" };
4886
4979
  }
4887
4980
  // A delegated seat starts outside this host from values alone, so a choice only this host can
4888
4981
  // honour is refused here, before enrollment, and named rather than dropped.
4889
- if (this.runtime.spawnDelegated) {
4890
- const delegatedBy = `runtime "${this.runtime.kind}" starts seats outside this host`;
4982
+ if (this.requiredRuntime.spawnDelegated) {
4983
+ const delegatedBy = `runtime "${this.requiredRuntime.kind}" starts seats outside this host`;
4891
4984
  if (!this.remoteAuthority?.enrollManagedAgent)
4892
4985
  return { ok: false, error: `${delegatedBy}, which needs a host that enrolls managed agents (remoteAuthority.enrollManagedAgent); a delegated seat has no local grant path` };
4893
4986
  try {
@@ -5048,7 +5141,7 @@ export class Manager {
5048
5141
  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.` };
5049
5142
  const held = this.retiring.get(identityName);
5050
5143
  if (held !== undefined) {
5051
- 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(() => { });
5144
+ void this.deprovision(held.target).catch(() => { });
5052
5145
  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" });
5053
5146
  return { ok: false, error: renderLifecycleBlocked(err.message, err), details: err.details };
5054
5147
  }
@@ -5069,9 +5162,9 @@ export class Manager {
5069
5162
  }
5070
5163
  }
5071
5164
  // The child takes launch options as `--opt k=v`, so only string values can cross to it.
5072
- const unportable = this.runtime.spawnDelegated ? Object.keys(launchOptions ?? {}).filter((k) => typeof launchOptions[k] !== "string") : [];
5165
+ const unportable = this.requiredRuntime.spawnDelegated ? Object.keys(launchOptions ?? {}).filter((k) => typeof launchOptions[k] !== "string") : [];
5073
5166
  if (unportable.length)
5074
- return { ok: false, error: `runtime "${this.runtime.kind}" starts seats outside this host, so it cannot pass non-string launch options (${unportable.join(", ")})` };
5167
+ return { ok: false, error: `runtime "${this.requiredRuntime.kind}" starts seats outside this host, so it cannot pass non-string launch options (${unportable.join(", ")})` };
5075
5168
  // The AG-UI event plane is on unless the launch explicitly opted out. Refused HERE, before
5076
5169
  // anything is minted: a connector that cannot
5077
5170
  // emit must fail before provisioning rather than after, exactly as an unsupported `resume` does.
@@ -5108,7 +5201,7 @@ export class Manager {
5108
5201
  // incarnation REFUSES loud at accept, BEFORE any reserve/mint/bind (pin 1), never a silent `_2`
5109
5202
  // suffix (so an address-by-triple caller's pinned name can't be re-pointed). A PERSONA-DERIVED
5110
5203
  // base name (no pin) keeps uniqueName's collision numbering, so multi-peer `spawn reviewer` twice
5111
- // still yields reviewer + reviewer_2. The retiring-hold refuse (~2472) is orthogonal and already fired.
5204
+ // still yields reviewer + reviewer_2. The alias-reuse gate above is orthogonal and already fired.
5112
5205
  const hardPinned = opts.identity !== undefined || opts.resolved !== undefined;
5113
5206
  let name;
5114
5207
  if (hardPinned) {
@@ -5392,12 +5485,12 @@ export class Manager {
5392
5485
  // manager path is produced for it, only the handoff its runtime writes into the child.
5393
5486
  let spec;
5394
5487
  let handle;
5395
- if (this.runtime.spawnDelegated) {
5488
+ if (this.requiredRuntime.spawnDelegated) {
5396
5489
  const handoff = this.composeManagedHandoff(enrolled.material, enrolled.actorToken);
5397
5490
  if (this.delegatedLaunched.has(lifecycleUid))
5398
- throw new Error(`lifecycle ${lifecycleUid} was already handed to runtime "${this.runtime.kind}"; a lifecycle is handed off at most once`);
5491
+ throw new Error(`lifecycle ${lifecycleUid} was already handed to runtime "${this.requiredRuntime.kind}"; a lifecycle is handed off at most once`);
5399
5492
  this.delegatedLaunched.add(lifecycleUid);
5400
- handle = this.runtime.spawnDelegated({ agent, persona: readFileSync(configPath, "utf8"), role, model, variant, prompt, launchOptions: launchOptions, events }, handoff);
5493
+ handle = this.requiredRuntime.spawnDelegated({ agent, persona: readFileSync(configPath, "utf8"), role, model, variant, prompt, launchOptions: launchOptions, events }, handoff);
5401
5494
  }
5402
5495
  else {
5403
5496
  if (carried) {
@@ -5612,7 +5705,7 @@ export class Manager {
5612
5705
  // retires it, and its name stays held for retireDelegatedAgent.
5613
5706
  const p = orphan.delegated ? parsePrincipalKey(orphan.id) : null;
5614
5707
  if (p)
5615
- this.retiring.set(orphan.name, { opId: retireOpId(orphan.lifecycleUid), lifecycleUid: orphan.lifecycleUid, owner: p.owner, actor: p.actor, agentId: orphan.id, userOwner: orphan.userOwner, delegated: true, secretPaths: orphan.secretPaths, startedAt: Date.now() });
5708
+ this.retiring.set(orphan.name, { opId: retireOpId(orphan.lifecycleUid), owner: p.owner, actor: p.actor, target: orphan, startedAt: Date.now() });
5616
5709
  this.trackDeprovision(orphan, "(orphaned spawn)");
5617
5710
  }
5618
5711
  }
@@ -5621,6 +5714,8 @@ export class Manager {
5621
5714
  * principal without provisioning. A later runtime launch failure is reported per-agent, but malformed
5622
5715
  * or missing inventory material can never produce a partially resumed set. */
5623
5716
  async resumePreserved(inventory) {
5717
+ if (this.execution === "none" && (!Array.isArray(inventory.agents) || inventory.agents.length !== 0))
5718
+ return { ok: false, agents: [], error: this.executionRefusal() };
5624
5719
  const release = this.beginLifecycle(true);
5625
5720
  if (!release)
5626
5721
  return { ok: false, agents: [], error: this.maintenanceError() };
@@ -5653,19 +5748,11 @@ export class Manager {
5653
5748
  // already removed the hold, so fresh-lifecycle resume still reaches the fail-closed roster gate.
5654
5749
  const held = this.retiring.get(entry.name);
5655
5750
  if (held) {
5656
- void this.deprovision({
5657
- id: held.agentId,
5658
- name: entry.name,
5659
- lifecycleUid: held.lifecycleUid,
5660
- userOwner: held.userOwner,
5661
- delegated: held.delegated,
5662
- secretPaths: held.secretPaths,
5663
- delegatedHandle: held.delegatedHandle,
5664
- }).catch(() => { });
5751
+ void this.deprovision(held.target).catch(() => { });
5665
5752
  return {
5666
5753
  ok: false,
5667
5754
  agents: [],
5668
- error: `retained agent "${entry.name}" is reserved pending retirement: its previous lifecycle ${held.lifecycleUid} still owns the alias${held.lastError ? ` (last attempt: ${held.lastError})` : ""}; retrying re-drives that exact teardown`,
5755
+ error: `retained agent "${entry.name}" is reserved pending retirement: its previous lifecycle ${held.target.lifecycleUid} still owns the alias${held.lastError ? ` (last attempt: ${held.lastError})` : ""}; retrying re-drives that exact teardown`,
5669
5756
  };
5670
5757
  }
5671
5758
  let principal;
@@ -5931,7 +6018,7 @@ export class Manager {
5931
6018
  async orphanedResumeSeat(entry, principal, liveRoster) {
5932
6019
  if (!this.auth || this.userMode || entry.identity.mode !== "static")
5933
6020
  return undefined;
5934
- if (typeof this.runtime.reap !== "function")
6021
+ if (typeof this.requiredRuntime.reap !== "function")
5935
6022
  return undefined;
5936
6023
  if (liveRoster.some((p) => p.card.id === principal && p.lifecycleUid !== entry.identity.lifecycleUid))
5937
6024
  return undefined;
@@ -5940,14 +6027,14 @@ export class Manager {
5940
6027
  return undefined;
5941
6028
  if (ownedBySibling(slot, this.managerInstanceId))
5942
6029
  return undefined;
5943
- return slot.runtime?.kind === this.runtime.kind ? slot.runtime : undefined;
6030
+ return slot.runtime?.kind === this.requiredRuntime.kind ? slot.runtime : undefined;
5944
6031
  }
5945
6032
  /** Reap a seat {@link orphanedResumeSeat} found, then wait for its principal to leave presence so
5946
6033
  * the relaunch's readiness cannot match the dead seat's row. Returns the refusal when the reap
5947
6034
  * proves nothing or the principal stays live, which means another process holds it. */
5948
6035
  async reapOrphanedResumeSeat(principal, reference) {
5949
6036
  try {
5950
- const evidence = await requireRuntimeReap(this.runtime, reference);
6037
+ const evidence = await requireRuntimeReap(this.requiredRuntime, reference);
5951
6038
  console.error(`resume: reaped orphaned seat ${reference.kind}:${reference.id} of retained principal "${principal}" (${evidence.detail})`);
5952
6039
  }
5953
6040
  catch (e) {
@@ -5964,14 +6051,16 @@ export class Manager {
5964
6051
  /** Validate/relaunch one retained inventory entry. Called only through resumePreserved so all
5965
6052
  * records pass the same preflight before the first child is exposed. */
5966
6053
  async resumePreservedAgent(entry, preflightOnly = false, batchReserved = false, prepared) {
6054
+ if (this.execution === "none")
6055
+ return { ok: false, error: this.executionRefusal() };
5967
6056
  const release = this.beginLifecycle(batchReserved);
5968
6057
  if (!release)
5969
6058
  return { ok: false, error: this.maintenanceError() };
5970
6059
  try {
5971
6060
  if (entry.space !== this.space)
5972
6061
  return { ok: false, error: `retained agent ${entry.name} belongs to space "${entry.space}", not "${this.space}"` };
5973
- if (entry.launch.runtime !== this.runtime.kind)
5974
- return { ok: false, error: `retained agent ${entry.name} requires runtime "${entry.launch.runtime}", current manager uses "${this.runtime.kind}"` };
6062
+ if (entry.launch.runtime !== this.requiredRuntime.kind)
6063
+ return { ok: false, error: `retained agent ${entry.name} requires runtime "${entry.launch.runtime}", current manager uses "${this.requiredRuntime.kind}"` };
5975
6064
  const nameErr = this.nameError(entry.name);
5976
6065
  if (nameErr)
5977
6066
  return { ok: false, error: nameErr };
@@ -6252,13 +6341,13 @@ export class Manager {
6252
6341
  * would reap it: it is freed only once its exit is proved, and otherwise stays managed until it
6253
6342
  * exits or the manager stops it. */
6254
6343
  async stopFailedResume(managed, cause, detail) {
6255
- this.stopHandle(managed, false);
6256
- try {
6257
- await this.awaitHandleExit(managed.handle);
6258
- }
6259
- catch (exit) {
6344
+ const refused = this.stopHandle(managed, false);
6345
+ const unproven = refused === undefined
6346
+ ? await this.awaitHandleExit(managed.handle).then(() => undefined, rejectionText)
6347
+ : `stop failed: ${refused}`;
6348
+ if (unproven !== undefined) {
6260
6349
  this.watchExit(managed);
6261
- return { ok: false, error: `${detail}; it stays managed because its stop is unproven: ${rejectionText(exit)}` };
6350
+ return { ok: false, error: `${detail}; it stays managed because its stop is unproven: ${unproven}` };
6262
6351
  }
6263
6352
  this.freeSlot(managed, true, cause, true);
6264
6353
  return { ok: false, error: detail };
@@ -6359,28 +6448,9 @@ export class Manager {
6359
6448
  if (done || !s)
6360
6449
  return;
6361
6450
  clearTimeout(timer);
6362
- // ┌─ DO NOT MOVE THIS READ. Its POSITION is the fix; its value is not. ──────────────────┐
6363
- // MOVING IT BELOW `onAgentExit` MAKES READINESS STOP REPORTING GENUINE LAUNCH FAILURES.
6364
- // That is what breaks — not a style regression, a silent loss of every `failed` terminal
6365
- // for an agent that really did die on launch. `onAgentExit` reaches `freeSlot`, which sets
6366
- // this SAME latch on its way through (see its "also covers exit/reap paths" comment), so a
6367
- // read taken after that call is true for EVERY exit, deliberate or not.
6368
- //
6369
- // Read HERE — first statement, before the await and before `onAgentExit` — a set latch can
6370
- // only have been set by someone else, and the only other setter on this path is
6371
- // `stopHandle`, which latches SYNCHRONOUSLY and then kills with no suspension between. So a
6372
- // despawn-caused exit is guaranteed observed with the latch UP and a natural exit with it
6373
- // DOWN: the distinction holds by program order, never by winning a race.
6374
- //
6375
- // The same latch, read one function call apart, answers two different questions.
6376
- //
6377
- // TO RE-VERIFY (this is the mutation that proves it, and it is the exact regression a tidy
6378
- // refactor produces): move this capture below `onAgentExit` and run
6379
- // `pnpm smoke:manager-spawn-action`. M3 `process exit -> failed` must FAIL. Note that M4 —
6380
- // the case this fix exists for — still PASSES under that mutation, so the suite this fix
6381
- // was written against cannot catch its own regression. M3 catches it only because it
6382
- // happens to share a file.
6383
- // └──────────────────────────────────────────────────────────────────────────────────────┘
6451
+ // `stopHandle` sets `terminalizing` synchronously before it kills, and `onAgentExit` sets it
6452
+ // through `freeSlot` when it retires the seat, so only a read before the await and the reap
6453
+ // tells a deliberate stop from a launch failure.
6384
6454
  const deliberate = a.terminalizing === true;
6385
6455
  void (async () => {
6386
6456
  // waitForExit may close the attach stream before this snapshot
@@ -6405,7 +6475,23 @@ export class Manager {
6405
6475
  finish({ ok: false, deliberate: true, detail: `${a.name} was stopped before it reported ready` });
6406
6476
  return;
6407
6477
  }
6408
- finish({ ok: false, detail: `${a.name} exited on launch${tail ? ` - last output: ${tail}` : ""}` });
6478
+ // The screen's last row can be a dialog footer, so the exit's diagnostic is named beside it:
6479
+ // it carries the runtime's own reason when it stopped the seat. A connector that ends on its
6480
+ // diagnostic already shows it as the last row, and it is not repeated.
6481
+ let exitDetail = "";
6482
+ try {
6483
+ const diagnostic = a.handle.exitInfo?.()?.diagnostic;
6484
+ if (diagnostic && diagnostic !== tail)
6485
+ exitDetail = `; diagnostic: ${diagnostic}`;
6486
+ }
6487
+ catch (e) {
6488
+ // The backstop is already cleared, so a reader that throws must still let this settle.
6489
+ exitDetail = `; exit detail unreadable from runtime "${a.handle.kind}": ${rejectionText(e)}`;
6490
+ }
6491
+ // A runtime's text can carry a lone surrogate, which is not I-JSON, and the failed terminal
6492
+ // this detail becomes is refused with it.
6493
+ exitDetail = exitDetail.replace(/\p{Cs}/gu, "\uFFFD");
6494
+ finish({ ok: false, detail: `${a.name} exited on launch${tail ? ` - last output: ${tail}` : ""}${exitDetail}` });
6409
6495
  })();
6410
6496
  };
6411
6497
  timer = setTimeout(() => finish({
@@ -6452,25 +6538,15 @@ export class Manager {
6452
6538
  return chars.length > 160 ? `…${chars.slice(-160).join("")}` : text;
6453
6539
  }
6454
6540
  /** Subscribe to a managed agent's process-exit so a self-driven exit frees its slot and reaps
6455
- * its children (P4b/P4c). Only pty streams exit (via the attach session's `onExit`); external runtimes'
6456
- * attach() throws, so this is a no-op there — a self-EXITED agent under those runtimes is reaped
6457
- * by nothing until it's explicitly despawned (graceful-stop runs on despawn, not self-exit). The
6458
- * cap still holds (a lingering corpse counts toward it); runtime-agnostic exit-reaping (a real
6459
- * per-runtime `status()` → exited-sweep at the availability gate) is a tracked follow-up. */
6541
+ * its children (P4b/P4c). The watch is bound to the current handle: a restart swaps in a new one
6542
+ * and watches it, so a late exit proved for the old handle must not retire the row. */
6460
6543
  watchExit(a) {
6461
- try {
6462
- const session = a.handle.attach();
6463
- session.onExit(() => this.onAgentExit(a));
6464
- // Close the TOCTOU between the early-exit probe's unsubscribe and this subscribe: if the child
6465
- // exited in that gap, the `onExit` above never fires (a late subscriber can't hear a past event),
6466
- // so the agent would leak (never reaped, never deprovisioned). Re-check status right after
6467
- // subscribing and reap it now if it already went. onAgentExit is idempotent (freeSlot's guard).
6468
- if (a.handle.status() === "exited")
6469
- this.onAgentExit(a);
6470
- }
6471
- catch {
6472
- /* runtime doesn't stream an exit signal — nothing to wire */
6473
- }
6544
+ const handle = a.handle;
6545
+ const current = () => this.agents.get(a.name) === a && a.handle === handle;
6546
+ const watched = watchProvedExit(handle, () => { if (current())
6547
+ this.onAgentExit(a); }, current);
6548
+ if (!watched)
6549
+ console.error(`! ${a.name}: runtime "${handle.kind}" cannot prove a child exit (AgentHandle.waitForExit is not implemented) - it stays managed after it exits, until it is stopped`);
6474
6550
  }
6475
6551
  /** Prune expired cooling stamps (drop those at/before now) and return the live count — the
6476
6552
  * recycle floor's contribution to the ceiling (P4c). Lazy: pruned only when the gate consults it. */
@@ -6564,8 +6640,8 @@ export class Manager {
6564
6640
  try {
6565
6641
  const live = this.agents.get(name);
6566
6642
  const held = live ? undefined : this.retiring.get(name);
6567
- const principal = live?.delegated ? parsePrincipalKey(live.id) : held?.delegated ? held : null;
6568
- const lifecycleUid = live?.lifecycleUid ?? held?.lifecycleUid;
6643
+ const principal = live?.delegated ? parsePrincipalKey(live.id) : held?.target.delegated ? held : null;
6644
+ const lifecycleUid = live?.lifecycleUid ?? held?.target.lifecycleUid;
6569
6645
  if (!principal || !lifecycleUid)
6570
6646
  return { ok: false, error: `no agent named "${name}" was launched on this manager under a delegated user intent` };
6571
6647
  const target = { owner: principal.owner, actor: principal.actor, lifecycleUid };
@@ -6580,7 +6656,7 @@ export class Manager {
6580
6656
  catch (e) {
6581
6657
  const error = `the delegated retirement under intent ${intentId} did not complete (${rejectionText(e)})`;
6582
6658
  const hold = this.retiring.get(name);
6583
- if (hold?.lifecycleUid === target.lifecycleUid)
6659
+ if (hold?.target.lifecycleUid === target.lifecycleUid)
6584
6660
  hold.lastError = error;
6585
6661
  return { ok: false, error: `${error}; "${name}" is unchanged` };
6586
6662
  }
@@ -6726,8 +6802,11 @@ export class Manager {
6726
6802
  managerStatusData() {
6727
6803
  return {
6728
6804
  instanceId: this.managerInstanceId,
6729
- runtime: this.runtime.kind,
6730
- custody: this.runtime.kind === "pty" && this.runtime.supportsRelease === true ? "custodied" : "legacy",
6805
+ execution: this.execution,
6806
+ runtime: this.runtimeKind,
6807
+ custody: this.execution === "none" ? "none" : this.requiredRuntime.kind === "pty" && this.requiredRuntime.supportsRelease === true ? "custodied" : "legacy",
6808
+ runHosting: this.execution !== "none" && (this.remoteAuthority ? this.remoteAuthority.runHosting !== undefined : !this.userMode && this.auth !== undefined),
6809
+ terminalSessions: this.execution !== "none",
6731
6810
  agentCount: this.agents.size,
6732
6811
  uptimeMs: Date.now() - this.startedAtMs,
6733
6812
  connectors: this.connectorStatuses.map((row) => ({ ...row, binaries: { ...row.binaries } })),
@@ -7735,6 +7814,8 @@ export class Manager {
7735
7814
  * Returns the acceptance floor payload {name, owner, actor, uid, goalId, fingerprint, executor}
7736
7815
  * (the ALLOCATED identity). goalId = the request id (env.id, Q3). */
7737
7816
  async serveSpawnGoal(ctx, run) {
7817
+ if (this.execution === "none")
7818
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
7738
7819
  const gw = this.goalWriter;
7739
7820
  if (!gw)
7740
7821
  throw new EpEnvelopeError("unavailable", "the manager goal-writer connection is not standing; spawn-as-action cannot accept (SPEC 13.6)");
@@ -8001,9 +8082,11 @@ export class Manager {
8001
8082
  }
8002
8083
  const raw = (ctx.request.args ?? {});
8003
8084
  const payload = String(raw.payload);
8004
- const deadlineMs = Number(raw.deadlineMs);
8085
+ const deadlineAt = raw.deadlineAt === undefined ? acceptedAt + Number(raw.deadlineMs) : Number(raw.deadlineAt);
8086
+ if (deadlineAt <= acceptedAt)
8087
+ throw new EpEnvelopeError("deadline-exceeded", `goal "${goalId}" names a deadline already past (${new Date(deadlineAt).toISOString()}); a turn with no time left to serve it is never accepted (SPEC 13.6)`);
8088
+ const deadlineMs = deadlineAt - acceptedAt;
8005
8089
  const handoffFrom = raw.handoffFrom === undefined ? undefined : String(raw.handoffFrom);
8006
- const deadlineAt = acceptedAt + deadlineMs;
8007
8090
  const note = JSON.stringify({ payload, deadlineAt, holdEpoch: executor.epoch, owner: t.owner, ...(handoffFrom !== undefined ? { handoffFrom } : {}) });
8008
8091
  // A goal that already ENDED is never accepted again, whatever the retry carries: the bind is
8009
8092
  // create-only and outlives an unwind (the failed terminal, the cleared index), so a retry of an
@@ -8524,13 +8607,13 @@ export class Manager {
8524
8607
  * exist. Absent for a runtime with no durable custody (there is nothing a successor could reap
8525
8608
  * by reference), which is the only case that still spawns unreserved. */
8526
8609
  reserveCustody() {
8527
- return isCustodialRuntime(this.runtime) ? this.runtime.reserve() : undefined;
8610
+ return isCustodialRuntime(this.requiredRuntime) ? this.requiredRuntime.reserve() : undefined;
8528
8611
  }
8529
8612
  /** Spawn under a reserved reference and prove the runtime honoured it. A runtime that minted its
8530
8613
  * own id instead would leave every durable record addressing a seat that does not exist, so the
8531
8614
  * mismatch tears the new seat down and throws rather than returning an unaddressable handle. */
8532
8615
  async spawnCustodied(name, spec, cwd, reserved) {
8533
- const handle = this.runtime.spawn(name, spec, cwd, reserved);
8616
+ const handle = this.requiredRuntime.spawn(name, spec, cwd, reserved);
8534
8617
  if (reserved === undefined)
8535
8618
  return handle;
8536
8619
  const got = handle.reference;
@@ -8552,7 +8635,7 @@ export class Manager {
8552
8635
  }
8553
8636
  else {
8554
8637
  try {
8555
- const evidence = await requireRuntimeReap(this.runtime, got);
8638
+ const evidence = await requireRuntimeReap(this.requiredRuntime, got);
8556
8639
  disposal = `the spawned seat was reaped (${evidence.detail})`;
8557
8640
  }
8558
8641
  catch (e) {
@@ -8566,7 +8649,7 @@ export class Manager {
8566
8649
  : `the spawned seat could NOT be reaped: ${rejectionText(e)}`;
8567
8650
  }
8568
8651
  }
8569
- throw new Error(`runtime "${this.runtime.kind}" reserved custody ${reserved.kind}:${reserved.id} for "${name}" but spawned ` +
8652
+ throw new Error(`runtime "${this.requiredRuntime.kind}" reserved custody ${reserved.kind}:${reserved.id} for "${name}" but spawned ` +
8570
8653
  `${got ? `${got.kind}:${got.id}` : "a handle with no reference"}; the recorded reference would address no seat, so ${disposal}`);
8571
8654
  }
8572
8655
  /** The terminal's process step: an orphan (no live handle in THIS process) is reaped by the custody
@@ -8581,14 +8664,14 @@ export class Manager {
8581
8664
  // before the reservation existed, or a caller that dropped the reference on the way here.
8582
8665
  // Do not name the slot row: the row may well hold a reference this caller never read, and
8583
8666
  // a diagnostic that guesses which is which sends the reader the wrong way.
8584
- if (isCustodialRuntime(this.runtime))
8585
- console.error(`static retirement ${a.name}: no custody reference reached this terminal, though runtime "${this.runtime.kind}" custodies its seats; any seat it launched is not addressable from here`);
8667
+ if (isCustodialRuntime(this.requiredRuntime))
8668
+ console.error(`static retirement ${a.name}: no custody reference reached this terminal, though runtime "${this.requiredRuntime.kind}" custodies its seats; any seat it launched is not addressable from here`);
8586
8669
  return;
8587
8670
  }
8588
8671
  // An unproven reap throws from here rather than printing, so it propagates into
8589
8672
  // driveStaticRetirement's catch, which records the failure and HOLDS the name. That is the whole
8590
8673
  // point of refusing: the alias must not be freed while a seat nobody proved gone may still run.
8591
- const evidence = await requireRuntimeReap(this.runtime, a.runtime);
8674
+ const evidence = await requireRuntimeReap(this.requiredRuntime, a.runtime);
8592
8675
  console.error(`static retirement ${a.name}: orphan seat process ${evidence.detail}`);
8593
8676
  }
8594
8677
  /** The static F1 terminal for one departed incarnation (Unit B): delegates the gate/head CAS
@@ -8656,14 +8739,14 @@ export class Manager {
8656
8739
  const principal = principalKey(DEV_OWNER, a.id).key;
8657
8740
  this.retiredPrincipals.add(principal);
8658
8741
  const cur = this.retiring.get(a.name);
8659
- if (cur && cur.lifecycleUid === a.lifecycleUid) {
8742
+ if (cur && cur.target.lifecycleUid === a.lifecycleUid) {
8660
8743
  this.retiring.delete(a.name); // ABA-guarded hold clear
8661
8744
  this.confirmedRetiredPredecessors.set(a.name, { principal, lifecycleUid: a.lifecycleUid });
8662
8745
  }
8663
8746
  }
8664
8747
  catch (e) {
8665
8748
  const h = this.retiring.get(a.name);
8666
- if (h && h.lifecycleUid === a.lifecycleUid)
8749
+ if (h && h.target.lifecycleUid === a.lifecycleUid)
8667
8750
  h.lastError = `the static retirement did not complete (${rejectionText(e)}); the name stays held - a same-name spawn retries the same terminal (op ${opId})`;
8668
8751
  console.error(`static retirement ${a.name} (${a.id}): ${rejectionText(e)}`);
8669
8752
  if (surfaceFailure)
@@ -9166,9 +9249,9 @@ export class Manager {
9166
9249
  : undefined;
9167
9250
  }
9168
9251
  for (const [name, hold] of this.retiring) {
9169
- const held = hold.userOwner ? hold.agentId : principalKey(DEV_OWNER, hold.agentId).key;
9252
+ const held = hold.target.userOwner ? hold.target.id : principalKey(DEV_OWNER, hold.target.id).key;
9170
9253
  if (held === caller)
9171
- return `the caller's lifecycle ${hold.lifecycleUid} (name "${name}") is retiring; a retiring incarnation's credential holds no control authority (F5)`;
9254
+ return `the caller's lifecycle ${hold.target.lifecycleUid} (name "${name}") is retiring; a retiring incarnation's credential holds no control authority (F5)`;
9172
9255
  }
9173
9256
  if (this.retiredPrincipals.has(caller))
9174
9257
  return "the caller's lifecycle is retired; a retired incarnation's credential holds no control authority (F5)";
@@ -9182,26 +9265,8 @@ export class Manager {
9182
9265
  async withProvisioner(fn) {
9183
9266
  if (!this.auth)
9184
9267
  throw new Error("withProvisioner: no space auth (an open mesh has no scoped creds)");
9185
- const identity = newIdentity();
9186
- const creds = await mintCreds(this.auth, identity, "provisioner");
9187
- const prov = new CotalEndpoint({
9188
- space: this.space,
9189
- servers: this.servers,
9190
- channels: [],
9191
- creds,
9192
- card: { id: identity.id, name: "provisioner", role: "provisioner", kind: "endpoint" },
9193
- registerPresence: false,
9194
- watchPresence: false,
9195
- watchChannels: false,
9196
- consume: false,
9197
- });
9198
- await prov.start();
9199
- try {
9200
- return await fn(prov);
9201
- }
9202
- finally {
9203
- await prov.stop();
9204
- }
9268
+ // Onboarding runs several provisioning ops, so it keeps the provisioner profile's five-minute window.
9269
+ return withScopedEndpoint({ space: this.space, servers: this.servers ?? DEFAULT_SERVER, auth: this.auth }, "provisioner", "provisioner", fn, 5 * 60);
9205
9270
  }
9206
9271
  /** A connection holding this instance's transfer reader grants (docs/design/resume-transfer.md
9207
9272
  * section 6) for this one receive or sweep: minted from the space's signing seed, or issued by the