@cotal-ai/manager 0.72.1 → 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();
@@ -842,9 +885,10 @@ export class Manager {
842
885
  // dir) - two managers in ONE space are two workspace roots.
843
886
  //
844
887
  // First mint on a fresh root is exclusive create (#1263). Two processes that both observe no
845
- // file must not keep different in-memory ids: of N concurrent starts exactly one creates the
846
- // file (`link` / O_EXCL) and the others adopt that identity before taking a lease, or refuse
847
- // with identity-record-create-lost. Atomic rename is not enough, because the loser
888
+ // file must not keep different in-memory ids: of N concurrent starts exactly one publishes the
889
+ // file by hard link and the others adopt that identity before taking a lease, or refuse with
890
+ // identity-record-create-lost. A filesystem without hard links refuses the first start with
891
+ // atomic-publish-unsupported. Atomic rename is not enough, because the loser
848
892
  // of a replace would still serve under the id it minted.
849
893
  {
850
894
  if (this.remoteAuthority) {
@@ -921,7 +965,7 @@ export class Manager {
921
965
  this.ep.on("roster", () => this.logSeatMeshTransitions());
922
966
  this.ep.on("presence-view", () => this.logSeatMeshTransitions());
923
967
  await this.ep.start();
924
- await this.ep.setActivity(`supervisor (${this.runtime.kind})`);
968
+ await this.ep.setActivity(`supervisor (${this.runtimeKind})`);
925
969
  // Per-instance liveness lease (P2 item 3 — the old per-space singleton is DEMOTED per D9). Acquire
926
970
  // THIS logical instance's own key (atomic CAS create). A DIFFERENT instance (a second manager in a
927
971
  // second workspace root) has a distinct id ⇒ a distinct key ⇒ it coexists; the create THROWS only
@@ -929,7 +973,7 @@ export class Manager {
929
973
  // crashed predecessor's not-yet-expired key), and we REFUSE loud. A crashed holder's key auto-expires
930
974
  // (bucket TTL). Losing the key LATER ends this process only when another process holds it; otherwise
931
975
  // the renew loop puts it back or keeps retrying ({@link renewLease}).
932
- 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 };
933
977
  try {
934
978
  this.leaseRevision = await this.ep.acquireManagerLease(this.leaseInfo);
935
979
  }
@@ -1073,7 +1117,7 @@ export class Manager {
1073
1117
  // refuses them until its reconcile has returned. A remote user-auth manager uses its four
1074
1118
  // registered issuer callbacks instead of holding a signer; a local user mesh stands none up.
1075
1119
  const remoteRuns = this.remoteAuthority?.runHosting;
1076
- if (this.remoteAuthority && remoteRuns) {
1120
+ if (this.execution !== "none" && this.remoteAuthority && remoteRuns) {
1077
1121
  this.runHosting = new RunHosting({
1078
1122
  space: this.space,
1079
1123
  servers: this.servers,
@@ -1083,6 +1127,7 @@ export class Manager {
1083
1127
  auth: undefined,
1084
1128
  log: (line) => console.error(line),
1085
1129
  admitRun: remoteRuns.admitRun,
1130
+ revokeRun: remoteRuns.revokeRun,
1086
1131
  issueAttempt: remoteRuns.issueAttempt,
1087
1132
  issueOperator: remoteRuns.issueOperator,
1088
1133
  renewRun: remoteRuns.renewRun,
@@ -1525,6 +1570,8 @@ export class Manager {
1525
1570
  async renewRemoteStandingBundle(force = false) {
1526
1571
  if (this.remoteBundleRenewal)
1527
1572
  return this.remoteBundleRenewal;
1573
+ if (this.leaseStopping)
1574
+ return;
1528
1575
  const renewal = this.renewRemoteStandingBundleOnce(force);
1529
1576
  this.remoteBundleRenewal = renewal;
1530
1577
  try {
@@ -2040,12 +2087,17 @@ export class Manager {
2040
2087
  * touches NEITHER the lease NOR the endpoints — the caller owns those. */
2041
2088
  async teardownManagedAgents(managed = [...this.agents.values()]) {
2042
2089
  const failures = [];
2090
+ const stopped = [];
2043
2091
  for (const a of managed) {
2044
2092
  // Free the slot + hard-stop each; `stopHandle` is best-effort (never throws — see it), so one bad
2045
2093
  // stop can't strand the rest, and every snapshot entry is deprovisioned below regardless.
2046
2094
  this.recordContinuityAtStop(a, "shutdown");
2047
2095
  this.agents.delete(a.name);
2048
- 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}`);
2049
2101
  }
2050
2102
  // The slot delete above bypasses freeSlot, which is where the reap line lives — so say it here,
2051
2103
  // once per seat, with the one stop cause that names this path: the shutdown itself. Without
@@ -2056,7 +2108,7 @@ export class Manager {
2056
2108
  // while any seat may still hold this instance's broker rails: that creates the exact orphan
2057
2109
  // window where a successor sees a dead manager but live predecessor authority. Every shipped
2058
2110
  // runtime now supplies waitForExit; absence or timeout is a failed shutdown, never a clean one.
2059
- await Promise.all(managed.map(async (a) => {
2111
+ await Promise.all(stopped.map(async (a) => {
2060
2112
  try {
2061
2113
  await this.awaitHandleExit(a.handle);
2062
2114
  }
@@ -2151,6 +2203,9 @@ export class Manager {
2151
2203
  // still move the key's revision after we clear the timer, and releasing against a cached
2152
2204
  // revision it has since moved is a CAS the broker correctly refuses, silently, forever.
2153
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(() => { });
2154
2209
  // A spawn admitted before the fence closed may still be launching. Its seat must be in the
2155
2210
  // snapshot below, or it would outlive a stop that reported success.
2156
2211
  await this.awaitLifecycleDrain();
@@ -2267,13 +2322,7 @@ export class Manager {
2267
2322
  endpoint: MANAGER_ENDPOINT,
2268
2323
  instanceId: iid,
2269
2324
  registrationRevision,
2270
- observeGeneration: async () => {
2271
- const key = epgateKey(MANAGER_ENDPOINT, iid);
2272
- const entry = await authKv.get(key);
2273
- if (!entry || entry.operation !== "PUT")
2274
- throw new Error(`no issuance gate at ${key}`);
2275
- return parseEndpointGate(entry.value, key).generation;
2276
- },
2325
+ observeGeneration: () => readEndpointGateGeneration(authKv, { endpoint: MANAGER_ENDPOINT, instanceId: iid }),
2277
2326
  });
2278
2327
  try {
2279
2328
  const outcome = await ((this.auth || this.remoteAuthority)
@@ -2524,6 +2573,8 @@ export class Manager {
2524
2573
  {
2525
2574
  try {
2526
2575
  const args = parseResumeControlArgs(rawArgs);
2576
+ if (this.execution === "none" && args.inventory.agents.length !== 0)
2577
+ return { ok: false, error: this.executionRefusal() };
2527
2578
  const inventoryDigest = createHash("sha256").update(JSON.stringify(args.inventory)).digest("hex");
2528
2579
  if (!this.resumeAttemptId)
2529
2580
  return { ok: false, error: "resumePreserved requires a manager started with --resume-attempt" };
@@ -2674,6 +2725,8 @@ export class Manager {
2674
2725
  * surface comes up before the host): `unavailable`, retry. The three are told apart, since
2675
2726
  * the first sentence steers a caller to `--local` and the others must not. */
2676
2727
  runHost() {
2728
+ if (this.execution === "none")
2729
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
2677
2730
  if (this.runHosting)
2678
2731
  return this.runHosting;
2679
2732
  if (this.remoteAuthority && !this.remoteAuthority.runHosting)
@@ -2871,13 +2924,19 @@ export class Manager {
2871
2924
  return { goalId, ...(result === undefined ? {} : { result }) };
2872
2925
  }),
2873
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
+ }),
2874
2933
  // P2 item 2: `spawn` is an ACTION - accept a goal + reply the acceptance floor payload, drive
2875
2934
  // progress + terminal off-handler (no ~30s block). The blocking reply path is gone (pin 8).
2876
2935
  // #373: arming the event plane (events: true, or the omission that arms it by default)
2877
2936
  // follows owner-domain reach on a user mesh. Resolve the caller's admin tier once for
2878
2937
  // the residual cross-owner case; opStart decides before connector resolution or grant
2879
2938
  // mutation. Static meshes resolve true and nothing changes.
2880
- 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))),
2881
2940
  despawn: (ctx) => this.serveGated(ctx, async () => {
2882
2941
  const a = targetAgent(ctx);
2883
2942
  const denied = await this.authorizeNamed(a, callerOf(ctx), await this.epAnyModeAdmin(ctx), ctx.subject.caller);
@@ -3121,11 +3180,11 @@ export class Manager {
3121
3180
  * grace window. POSIX delivers SIGTERM→SIGKILL natively, so it keeps the signal path. A hard stop
3122
3181
  * (`graceful:false`, e.g. emergency reap) skips the cooperative step on every platform.
3123
3182
  *
3124
- * BEST-EFFORT / never throws (#159 B2): a runtime hard-stop CAN throw (tmux `closeWindow` / cmux
3125
- * `closeWorkspace` are direct calls), and every caller (despawn / self-stop / reap / shutdown) frees the
3126
- * slot + deprovisions RIGHT AFTER — so a throwing stop must not abort that cleanup and leak the agent's
3127
- * footprint, nor (in `reapChildrenOf`) abort the reap of later siblings. The failure is logged loudly,
3128
- * 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. */
3129
3188
  stopHandle(a, graceful) {
3130
3189
  // The F5 TERMINALIZING latch (Unit B): flipped SYNCHRONOUSLY, before any await anywhere on
3131
3190
  // this stop path — later renewals refuse, while an already-admitted renewal drains before the
@@ -3143,7 +3202,9 @@ export class Manager {
3143
3202
  a.handle.stop({ graceful });
3144
3203
  }
3145
3204
  catch (e) {
3146
- 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;
3147
3208
  }
3148
3209
  }
3149
3210
  /** Keep an accepted stop inside the lifecycle drain until the runtime proves the child is gone,
@@ -3487,7 +3548,7 @@ export class Manager {
3487
3548
  const info = a.handle.exitInfo?.();
3488
3549
  detail = info === undefined
3489
3550
  ? `exit detail unavailable from runtime "${a.handle.kind}"`
3490
- : `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}` : ""}`;
3491
3552
  }
3492
3553
  catch (e) {
3493
3554
  // A runtime that throws while being asked has told us something real; it must not take the
@@ -3561,6 +3622,10 @@ export class Manager {
3561
3622
  this.failSeatTurns(a.name, a.lifecycleUid);
3562
3623
  if (floor && Date.now() - a.startedAt < MIN_LIFETIME)
3563
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 } };
3564
3629
  // #29 piece 3: on a USER mesh the name is RESERVED PENDING RETIREMENT — despawn started this
3565
3630
  // lifecycle's FULL teardown (footprint + standing-authority revoke + the auth-side retirement),
3566
3631
  // and the alias frees only when all of it completes (not the retirement alone). The detached
@@ -3572,24 +3637,22 @@ export class Manager {
3572
3637
  if (this.userMode) {
3573
3638
  const p = parsePrincipalKey(a.id);
3574
3639
  if (p)
3575
- 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() });
3576
3641
  }
3577
3642
  else if (this.auth) {
3578
3643
  // Unit B: a STATIC lifecycle now also holds its name pending its own terminal (the F1
3579
3644
  // static retirement the detached deprovision below drives) — the alias frees only when the
3580
3645
  // gate+head terminal completes, exactly the user-mode discipline. The wire principal is the
3581
3646
  // incarnation-unique nkey (F5-bind); owner is the dev owner.
3582
- 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() });
3583
3648
  }
3584
3649
  // Auth mode: tear down the departed agent's minted broker footprint + creds file (#159 B2). The
3585
3650
  // process is already gone, so this must never block the slot free or throw into the caller — it runs
3586
3651
  // detached, and a failure is logged loudly (never swallowed), not retried. The `agents` guard above
3587
3652
  // makes this fire exactly once per agent across every free path (despawn / self-stop / reap / exit).
3588
- // The departed agent's own custody reference rides the teardown: the exit that freed this slot
3589
- // is usually the seat ending, but a stop that could not prove the process gone leaves one the
3590
- // 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.
3591
3654
  if (!a.suppressCleanup && (this.maintenanceState === "active" || acceptedBeforeFence))
3592
- this.trackDeprovision({ ...a, runtime: a.handle?.reference, ...(a.handedOff ? { delegatedHandle: a.handle } : {}) });
3655
+ this.trackDeprovision({ ...a, ...target });
3593
3656
  }
3594
3657
  /** Tear down a departed agent's minted footprint (#159 B2, auth mode): its local-principal durables
3595
3658
  * (`dm_local-<id>`, `dlv_local-<id>`), its read-ACL row, and its creds file — everything the spawn's
@@ -3627,7 +3690,7 @@ export class Manager {
3627
3690
  // The agent is its user's: only that user's retirement intent retires it, so the name stays
3628
3691
  // held until retireDelegatedAgent confirms the host retired this UID.
3629
3692
  const held = this.retiring.get(a.name);
3630
- if (held?.lifecycleUid === a.lifecycleUid)
3693
+ if (held?.target.lifecycleUid === a.lifecycleUid)
3631
3694
  held.lastError = `"${a.name}" was launched under a delegated user intent and retires only through its user's retirement intent`;
3632
3695
  return;
3633
3696
  }
@@ -3697,7 +3760,7 @@ export class Manager {
3697
3760
  // legible operator copy. A retry re-drives this whole teardown (the same-name-spawn nudge routes
3698
3761
  // through deprovision, not the rail alone), so the revoke is re-attempted, not stranded.
3699
3762
  const holdRevoke = this.retiring.get(a.name);
3700
- if (holdRevoke && holdRevoke.lifecycleUid === a.lifecycleUid)
3763
+ if (holdRevoke && holdRevoke.target.lifecycleUid === a.lifecycleUid)
3701
3764
  holdRevoke.standingAuthorityLive = true;
3702
3765
  try {
3703
3766
  await resolveAuthProvider().revokeAgent({
@@ -3706,12 +3769,12 @@ export class Manager {
3706
3769
  actor: a.name,
3707
3770
  });
3708
3771
  const done = this.retiring.get(a.name);
3709
- if (done && done.lifecycleUid === a.lifecycleUid)
3772
+ if (done && done.target.lifecycleUid === a.lifecycleUid)
3710
3773
  done.standingAuthorityLive = false;
3711
3774
  }
3712
3775
  catch (e) {
3713
3776
  const h = this.retiring.get(a.name);
3714
- if (h && h.lifecycleUid === a.lifecycleUid)
3777
+ if (h && h.target.lifecycleUid === a.lifecycleUid)
3715
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.`;
3716
3779
  console.error(`revoke agent grant ${a.name}: ${rejectionText(e)}`);
3717
3780
  }
@@ -3720,12 +3783,12 @@ export class Manager {
3720
3783
  try {
3721
3784
  const res = await this.deprovisionBroker(a);
3722
3785
  const h = this.retiring.get(a.name);
3723
- if (h && h.lifecycleUid === a.lifecycleUid)
3786
+ if (h && h.target.lifecycleUid === a.lifecycleUid)
3724
3787
  h.lastResources = res;
3725
3788
  }
3726
3789
  catch (e) {
3727
3790
  const h = this.retiring.get(a.name);
3728
- if (h && h.lifecycleUid === a.lifecycleUid) {
3791
+ if (h && h.target.lifecycleUid === a.lifecycleUid) {
3729
3792
  h.lastError = rejectionText(e);
3730
3793
  if (e instanceof DeprovisionError)
3731
3794
  h.lastResources = e.accounting;
@@ -3885,7 +3948,7 @@ export class Manager {
3885
3948
  // CAS the hold clear (audit #1 ABA): free the alias ONLY if the current hold is still THIS
3886
3949
  // lifecycle's. A late reply for a retired predecessor must never clear a successor's newer hold.
3887
3950
  const cur = this.retiring.get(a.name);
3888
- if (cur && cur.lifecycleUid === a.lifecycleUid) {
3951
+ if (cur && cur.target.lifecycleUid === a.lifecycleUid) {
3889
3952
  if (cur.standingAuthorityLive) {
3890
3953
  // INT-2: the auth-plane lifecycle retired, but the manager-side STANDING mint authority is
3891
3954
  // not yet revoked. A copied token could still mint, so neither free the alias nor classify
@@ -4158,6 +4221,8 @@ export class Manager {
4158
4221
  * comes from `restart.policy` when spawn carried `supervise`; otherwise the Pi session-recovery
4159
4222
  * constants. Spending the budget falls through to normal retirement. */
4160
4223
  recoverManagedSession(a) {
4224
+ if (this.execution === "none")
4225
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
4161
4226
  const restart = a.restart;
4162
4227
  if (!restart || !restart.armed || restart.recovering || a.terminalizing)
4163
4228
  return;
@@ -4370,18 +4435,7 @@ export class Manager {
4370
4435
  return false;
4371
4436
  }
4372
4437
  /** Parse an untyped control-plane `start` request into {@link StartAgentOpts}. */
4373
- opStart(args, caller, admin, hooks, route = "inst") {
4374
- // `resume`, when present, must be a non-empty session id. An empty/whitespace value is a
4375
- // malformed request, not an implicit "spawn fresh" (no fallbacks). The CLI surfaces reject it,
4376
- // but a raw control message could otherwise slip an empty value through and silently start fresh.
4377
- if (args.resume !== undefined && !String(args.resume).trim())
4378
- return Promise.resolve({ ok: false, error: "resume: session id must not be empty" });
4379
- if (args.model !== undefined && !String(args.model).trim())
4380
- return Promise.resolve({ ok: false, error: "model: must not be empty" });
4381
- if (args.variant !== undefined && !String(args.variant).trim())
4382
- return Promise.resolve({ ok: false, error: "variant: must not be empty" });
4383
- if (args.defaultAgent !== undefined && !String(args.defaultAgent).trim())
4384
- return Promise.resolve({ ok: false, error: "defaultAgent: must not be empty" });
4438
+ opStart(args, caller, admin, hooks, route = "inst", authenticatedCaller) {
4385
4439
  // Opaque launch options, when present, must be a mapping — a raw control message could send a
4386
4440
  // scalar/array (the CLI never does). Core doesn't interpret the keys; the connector validates them.
4387
4441
  if (args.launchOptions !== undefined && (typeof args.launchOptions !== "object" || args.launchOptions === null || Array.isArray(args.launchOptions)))
@@ -4454,28 +4508,28 @@ export class Manager {
4454
4508
  }
4455
4509
  return this.startAgent({
4456
4510
  name: String(args.name ?? "").trim(),
4457
- agent: args.agent ? String(args.agent) : undefined,
4458
- defaultAgent: args.defaultAgent ? String(args.defaultAgent) : undefined,
4459
- role: args.role ? String(args.role) : undefined,
4460
- config: args.config ? String(args.config) : undefined,
4461
- identity: args.identity ? String(args.identity) : undefined,
4462
- model: args.model ? String(args.model) : undefined,
4463
- variant: args.variant ? String(args.variant) : undefined,
4511
+ agent: args.agent,
4512
+ defaultAgent: args.defaultAgent,
4513
+ role: args.role,
4514
+ config: args.config,
4515
+ identity: args.identity,
4516
+ model: args.model,
4517
+ variant: args.variant,
4464
4518
  launchOptions: args.launchOptions,
4465
- resume: args.resume ? String(args.resume) : undefined,
4466
- resumeClaim: args.resumeClaim !== undefined ? String(args.resumeClaim) : undefined,
4467
- resumeAgent: args.resumeAgent !== undefined ? String(args.resumeAgent) : undefined,
4519
+ resume: args.resume,
4520
+ resumeClaim: args.resumeClaim,
4521
+ resumeAgent: args.resumeAgent,
4468
4522
  events,
4469
4523
  eventsNotice,
4470
- cwd: args.cwd ? String(args.cwd) : undefined,
4471
- prompt: args.prompt ? String(args.prompt) : undefined,
4524
+ cwd: args.cwd,
4525
+ prompt: args.prompt,
4472
4526
  subscribe,
4473
4527
  allowSubscribe,
4474
4528
  allowPublish,
4475
4529
  shareTools,
4476
4530
  ...(supervise !== undefined ? { supervise } : {}),
4477
4531
  route,
4478
- }, caller, hooks);
4532
+ }, caller, hooks, authenticatedCaller);
4479
4533
  }
4480
4534
  /** The realpath + directory checks a placed spawn directory must pass on this manager's host,
4481
4535
  * shared by the served `resolve-cwd` command and the `spawn` admission check (#385 item 2): one
@@ -4534,6 +4588,10 @@ export class Manager {
4534
4588
  * reports ready. A missing harness degrades only that connector, never unrelated lifecycle work:
4535
4589
  * boot continues, but prints a named refusal and records it on both manager status surfaces. */
4536
4590
  async inspectConnectorsAtBoot() {
4591
+ if (this.execution === "none") {
4592
+ this.connectorStatuses = [];
4593
+ return;
4594
+ }
4537
4595
  const rows = [];
4538
4596
  let declared;
4539
4597
  if (this.installedExtensions) {
@@ -4569,6 +4627,8 @@ export class Manager {
4569
4627
  * inventory load) skips the class `one` subscribe for spawn/launch so this member cannot
4570
4628
  * consume an unpinned request a sibling could serve. */
4571
4629
  classSpawnEnabled() {
4630
+ if (this.execution === "none")
4631
+ return false;
4572
4632
  return this.connectorStatuses.some((row) => row.state === "available");
4573
4633
  }
4574
4634
  omitClassCommands() {
@@ -4692,6 +4752,8 @@ export class Manager {
4692
4752
  * `runId`, and resolved `hash`. USER mesh: a privileged-tier launch is owner-equality-authorized
4693
4753
  * (spec owner === caller owner) before any side effect; the admin tier keeps operator behavior. */
4694
4754
  async opLaunch(args, caller, admin, hooks, route = "inst") {
4755
+ if (this.execution === "none")
4756
+ return { ok: false, error: this.executionRefusal() };
4695
4757
  const runId = String(args.runId ?? "").trim();
4696
4758
  const name = String(args.name ?? "").trim();
4697
4759
  if (!runId || !name)
@@ -4756,30 +4818,23 @@ export class Manager {
4756
4818
  * `spawner` is the authenticated id of the peer that requested the spawn (`req.from.id`),
4757
4819
  * defaulting to the manager's own id for roster/pre-spawn — recorded for the spawner
4758
4820
  * ledger (own-children despawn + reap-on-parent-exit). */
4759
- async startAgent(opts, spawner, hooks) {
4821
+ async startAgent(opts, spawner, hooks, authenticatedCaller) {
4822
+ if (this.execution === "none")
4823
+ return { ok: false, error: this.executionRefusal() };
4760
4824
  const release = this.beginLifecycle();
4761
4825
  if (!release)
4762
4826
  return { ok: false, error: this.maintenanceError() };
4763
4827
  try {
4764
- return await this.startAgentActive(opts, spawner, hooks);
4828
+ return await this.startAgentActive(opts, spawner, hooks, authenticatedCaller);
4765
4829
  }
4766
4830
  finally {
4767
4831
  release();
4768
4832
  }
4769
4833
  }
4770
- async startAgentActive(opts, spawner, hooks) {
4771
- // Before the first await, so the grant, the launch and the retained form all read the lists as admitted.
4772
- opts = ownLists(opts);
4773
- if (opts.resolved)
4774
- opts.resolved = ownLists(opts.resolved);
4775
- if (opts.delegatedIntent) {
4776
- if (!this.remoteAuthority?.executeDelegatedUserIntent)
4777
- return { ok: false, error: `"${opts.name}" names a delegated user intent, and this manager has no host execution for one` };
4778
- if (opts.supervise)
4779
- return { ok: false, error: `"${opts.name}" names a delegated user intent, and a delegated agent is never restarted (SPEC 13.16)` };
4780
- // A delegated launch's spawner is the user's principal, never the composition root's call.
4781
- spawner = opts.delegatedIntent.parent;
4782
- }
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) {
4783
4838
  // The spawn argument is a persona REF — a filename in `.cotal/agents` (the unique spawn KEY), or
4784
4839
  // a path via `--config`. It is NOT the mesh identity: the identity comes from inside the file
4785
4840
  // (`name:`), so a persona can be filed descriptively (review-critic.md) yet present under a
@@ -4787,48 +4842,79 @@ export class Manager {
4787
4842
  // filename (unique on disk); two files can't collide on the key.
4788
4843
  const ref = opts.name.trim();
4789
4844
  if (!ref)
4790
- return { ok: false, error: "name required" };
4845
+ return { error: "name required" };
4791
4846
  // A bare ref maps to `.cotal/agents/<ref>.md`, so it must be a safe token (no path traversal); a
4792
4847
  // `--config` path is validated by existsSync below instead.
4793
4848
  if (!opts.config) {
4794
4849
  const refErr = this.nameError(ref);
4795
4850
  if (refErr)
4796
- return { ok: false, error: refErr };
4851
+ return { error: refErr };
4797
4852
  }
4798
4853
  // Resolve the persona file (fail loud — NO silent default-ACL fallback). A missing persona used
4799
4854
  // to mint DEFAULT creds (read `general` only, default-deny publish, no capabilities), so a
4800
4855
  // typo'd / renamed / spawned-by-display-name agent became live with silently-wrong ACLs — a
4801
4856
  // behavioral/security bug. Fail loud instead, matching `cotal spawn` (loadAgentFile throws).
4802
- // This runs BEFORE the connector resolution below: the file's `agent:` pin participates in the
4803
- // harness choice (flag > file > COTAL_DEFAULT_AGENT > default, #869), which is impossible if the
4804
- // connector is materialized first. Still synchronous (existsSync/readFileSync), so the capacity/
4805
- // 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.
4806
4860
  let configPath;
4807
4861
  if (opts.config) {
4808
4862
  configPath = agentFilePath(this.workspaceRoot, opts.config);
4809
4863
  if (!existsSync(configPath))
4810
- return { ok: false, error: `agent file not found: ${configPath}` };
4864
+ return { error: `agent file not found: ${configPath}` };
4811
4865
  }
4812
4866
  else {
4813
4867
  configPath = agentFilePath(this.workspaceRoot, ref);
4814
4868
  if (!existsSync(configPath))
4815
- 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\`)` };
4816
4870
  }
4817
4871
  // Load the persona ONCE, here, ahead of the connector choice. The file's `agent:` pin feeds the
4818
- // harness resolution below; the identity/ACL profile block later consumes the SAME def (no second
4819
- // load). A manifest launch (`resolved`) materializes its own transient persona and the file is
4820
- // NOT its authority, so its def (and its agent pin) is deliberately not consulted here — the
4821
- // 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.
4822
4876
  let def;
4823
4877
  if (!opts.resolved) {
4824
4878
  try {
4825
4879
  def = loadAgentFile(configPath);
4826
4880
  }
4827
4881
  catch (e) {
4828
- 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}"` };
4829
4894
  }
4830
4895
  }
4831
- 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;
4832
4918
  // Materialize the requested connector up front — the ONE async step in the spawn path (a lazy
4833
4919
  // `cotal ext` manifest import on the published binary). It runs BEFORE the capacity/reserve span
4834
4920
  // below so that span stays fully SYNCHRONOUS and atomic. On the manifest binary this also honors a
@@ -4886,15 +4972,15 @@ export class Manager {
4886
4972
  // exit, so a name cannot be respawned in place. User-mode seats have no static slot that
4887
4973
  // keeps the incarnation owned across a process death, so the same refusal applies there.
4888
4974
  if (opts.supervise !== undefined) {
4889
- if (this.runtime.kind !== "pty")
4890
- 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` };
4891
4977
  if (this.userMode)
4892
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" };
4893
4979
  }
4894
4980
  // A delegated seat starts outside this host from values alone, so a choice only this host can
4895
4981
  // honour is refused here, before enrollment, and named rather than dropped.
4896
- if (this.runtime.spawnDelegated) {
4897
- 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`;
4898
4984
  if (!this.remoteAuthority?.enrollManagedAgent)
4899
4985
  return { ok: false, error: `${delegatedBy}, which needs a host that enrolls managed agents (remoteAuthority.enrollManagedAgent); a delegated seat has no local grant path` };
4900
4986
  try {
@@ -5055,7 +5141,7 @@ export class Manager {
5055
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.` };
5056
5142
  const held = this.retiring.get(identityName);
5057
5143
  if (held !== undefined) {
5058
- 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(() => { });
5059
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" });
5060
5146
  return { ok: false, error: renderLifecycleBlocked(err.message, err), details: err.details };
5061
5147
  }
@@ -5076,9 +5162,9 @@ export class Manager {
5076
5162
  }
5077
5163
  }
5078
5164
  // The child takes launch options as `--opt k=v`, so only string values can cross to it.
5079
- 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") : [];
5080
5166
  if (unportable.length)
5081
- 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(", ")})` };
5082
5168
  // The AG-UI event plane is on unless the launch explicitly opted out. Refused HERE, before
5083
5169
  // anything is minted: a connector that cannot
5084
5170
  // emit must fail before provisioning rather than after, exactly as an unsupported `resume` does.
@@ -5115,7 +5201,7 @@ export class Manager {
5115
5201
  // incarnation REFUSES loud at accept, BEFORE any reserve/mint/bind (pin 1), never a silent `_2`
5116
5202
  // suffix (so an address-by-triple caller's pinned name can't be re-pointed). A PERSONA-DERIVED
5117
5203
  // base name (no pin) keeps uniqueName's collision numbering, so multi-peer `spawn reviewer` twice
5118
- // 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.
5119
5205
  const hardPinned = opts.identity !== undefined || opts.resolved !== undefined;
5120
5206
  let name;
5121
5207
  if (hardPinned) {
@@ -5399,12 +5485,12 @@ export class Manager {
5399
5485
  // manager path is produced for it, only the handoff its runtime writes into the child.
5400
5486
  let spec;
5401
5487
  let handle;
5402
- if (this.runtime.spawnDelegated) {
5488
+ if (this.requiredRuntime.spawnDelegated) {
5403
5489
  const handoff = this.composeManagedHandoff(enrolled.material, enrolled.actorToken);
5404
5490
  if (this.delegatedLaunched.has(lifecycleUid))
5405
- 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`);
5406
5492
  this.delegatedLaunched.add(lifecycleUid);
5407
- 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);
5408
5494
  }
5409
5495
  else {
5410
5496
  if (carried) {
@@ -5619,7 +5705,7 @@ export class Manager {
5619
5705
  // retires it, and its name stays held for retireDelegatedAgent.
5620
5706
  const p = orphan.delegated ? parsePrincipalKey(orphan.id) : null;
5621
5707
  if (p)
5622
- 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() });
5623
5709
  this.trackDeprovision(orphan, "(orphaned spawn)");
5624
5710
  }
5625
5711
  }
@@ -5628,6 +5714,8 @@ export class Manager {
5628
5714
  * principal without provisioning. A later runtime launch failure is reported per-agent, but malformed
5629
5715
  * or missing inventory material can never produce a partially resumed set. */
5630
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() };
5631
5719
  const release = this.beginLifecycle(true);
5632
5720
  if (!release)
5633
5721
  return { ok: false, agents: [], error: this.maintenanceError() };
@@ -5660,19 +5748,11 @@ export class Manager {
5660
5748
  // already removed the hold, so fresh-lifecycle resume still reaches the fail-closed roster gate.
5661
5749
  const held = this.retiring.get(entry.name);
5662
5750
  if (held) {
5663
- void this.deprovision({
5664
- id: held.agentId,
5665
- name: entry.name,
5666
- lifecycleUid: held.lifecycleUid,
5667
- userOwner: held.userOwner,
5668
- delegated: held.delegated,
5669
- secretPaths: held.secretPaths,
5670
- delegatedHandle: held.delegatedHandle,
5671
- }).catch(() => { });
5751
+ void this.deprovision(held.target).catch(() => { });
5672
5752
  return {
5673
5753
  ok: false,
5674
5754
  agents: [],
5675
- 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`,
5676
5756
  };
5677
5757
  }
5678
5758
  let principal;
@@ -5938,7 +6018,7 @@ export class Manager {
5938
6018
  async orphanedResumeSeat(entry, principal, liveRoster) {
5939
6019
  if (!this.auth || this.userMode || entry.identity.mode !== "static")
5940
6020
  return undefined;
5941
- if (typeof this.runtime.reap !== "function")
6021
+ if (typeof this.requiredRuntime.reap !== "function")
5942
6022
  return undefined;
5943
6023
  if (liveRoster.some((p) => p.card.id === principal && p.lifecycleUid !== entry.identity.lifecycleUid))
5944
6024
  return undefined;
@@ -5947,14 +6027,14 @@ export class Manager {
5947
6027
  return undefined;
5948
6028
  if (ownedBySibling(slot, this.managerInstanceId))
5949
6029
  return undefined;
5950
- return slot.runtime?.kind === this.runtime.kind ? slot.runtime : undefined;
6030
+ return slot.runtime?.kind === this.requiredRuntime.kind ? slot.runtime : undefined;
5951
6031
  }
5952
6032
  /** Reap a seat {@link orphanedResumeSeat} found, then wait for its principal to leave presence so
5953
6033
  * the relaunch's readiness cannot match the dead seat's row. Returns the refusal when the reap
5954
6034
  * proves nothing or the principal stays live, which means another process holds it. */
5955
6035
  async reapOrphanedResumeSeat(principal, reference) {
5956
6036
  try {
5957
- const evidence = await requireRuntimeReap(this.runtime, reference);
6037
+ const evidence = await requireRuntimeReap(this.requiredRuntime, reference);
5958
6038
  console.error(`resume: reaped orphaned seat ${reference.kind}:${reference.id} of retained principal "${principal}" (${evidence.detail})`);
5959
6039
  }
5960
6040
  catch (e) {
@@ -5971,14 +6051,16 @@ export class Manager {
5971
6051
  /** Validate/relaunch one retained inventory entry. Called only through resumePreserved so all
5972
6052
  * records pass the same preflight before the first child is exposed. */
5973
6053
  async resumePreservedAgent(entry, preflightOnly = false, batchReserved = false, prepared) {
6054
+ if (this.execution === "none")
6055
+ return { ok: false, error: this.executionRefusal() };
5974
6056
  const release = this.beginLifecycle(batchReserved);
5975
6057
  if (!release)
5976
6058
  return { ok: false, error: this.maintenanceError() };
5977
6059
  try {
5978
6060
  if (entry.space !== this.space)
5979
6061
  return { ok: false, error: `retained agent ${entry.name} belongs to space "${entry.space}", not "${this.space}"` };
5980
- if (entry.launch.runtime !== this.runtime.kind)
5981
- 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}"` };
5982
6064
  const nameErr = this.nameError(entry.name);
5983
6065
  if (nameErr)
5984
6066
  return { ok: false, error: nameErr };
@@ -6259,13 +6341,13 @@ export class Manager {
6259
6341
  * would reap it: it is freed only once its exit is proved, and otherwise stays managed until it
6260
6342
  * exits or the manager stops it. */
6261
6343
  async stopFailedResume(managed, cause, detail) {
6262
- this.stopHandle(managed, false);
6263
- try {
6264
- await this.awaitHandleExit(managed.handle);
6265
- }
6266
- 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) {
6267
6349
  this.watchExit(managed);
6268
- 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}` };
6269
6351
  }
6270
6352
  this.freeSlot(managed, true, cause, true);
6271
6353
  return { ok: false, error: detail };
@@ -6366,28 +6448,9 @@ export class Manager {
6366
6448
  if (done || !s)
6367
6449
  return;
6368
6450
  clearTimeout(timer);
6369
- // ┌─ DO NOT MOVE THIS READ. Its POSITION is the fix; its value is not. ──────────────────┐
6370
- // MOVING IT BELOW `onAgentExit` MAKES READINESS STOP REPORTING GENUINE LAUNCH FAILURES.
6371
- // That is what breaks — not a style regression, a silent loss of every `failed` terminal
6372
- // for an agent that really did die on launch. `onAgentExit` reaches `freeSlot`, which sets
6373
- // this SAME latch on its way through (see its "also covers exit/reap paths" comment), so a
6374
- // read taken after that call is true for EVERY exit, deliberate or not.
6375
- //
6376
- // Read HERE — first statement, before the await and before `onAgentExit` — a set latch can
6377
- // only have been set by someone else, and the only other setter on this path is
6378
- // `stopHandle`, which latches SYNCHRONOUSLY and then kills with no suspension between. So a
6379
- // despawn-caused exit is guaranteed observed with the latch UP and a natural exit with it
6380
- // DOWN: the distinction holds by program order, never by winning a race.
6381
- //
6382
- // The same latch, read one function call apart, answers two different questions.
6383
- //
6384
- // TO RE-VERIFY (this is the mutation that proves it, and it is the exact regression a tidy
6385
- // refactor produces): move this capture below `onAgentExit` and run
6386
- // `pnpm smoke:manager-spawn-action`. M3 `process exit -> failed` must FAIL. Note that M4 —
6387
- // the case this fix exists for — still PASSES under that mutation, so the suite this fix
6388
- // was written against cannot catch its own regression. M3 catches it only because it
6389
- // happens to share a file.
6390
- // └──────────────────────────────────────────────────────────────────────────────────────┘
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.
6391
6454
  const deliberate = a.terminalizing === true;
6392
6455
  void (async () => {
6393
6456
  // waitForExit may close the attach stream before this snapshot
@@ -6412,7 +6475,23 @@ export class Manager {
6412
6475
  finish({ ok: false, deliberate: true, detail: `${a.name} was stopped before it reported ready` });
6413
6476
  return;
6414
6477
  }
6415
- 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}` });
6416
6495
  })();
6417
6496
  };
6418
6497
  timer = setTimeout(() => finish({
@@ -6459,25 +6538,15 @@ export class Manager {
6459
6538
  return chars.length > 160 ? `…${chars.slice(-160).join("")}` : text;
6460
6539
  }
6461
6540
  /** Subscribe to a managed agent's process-exit so a self-driven exit frees its slot and reaps
6462
- * its children (P4b/P4c). Only pty streams exit (via the attach session's `onExit`); external runtimes'
6463
- * attach() throws, so this is a no-op there — a self-EXITED agent under those runtimes is reaped
6464
- * by nothing until it's explicitly despawned (graceful-stop runs on despawn, not self-exit). The
6465
- * cap still holds (a lingering corpse counts toward it); runtime-agnostic exit-reaping (a real
6466
- * 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. */
6467
6543
  watchExit(a) {
6468
- try {
6469
- const session = a.handle.attach();
6470
- session.onExit(() => this.onAgentExit(a));
6471
- // Close the TOCTOU between the early-exit probe's unsubscribe and this subscribe: if the child
6472
- // exited in that gap, the `onExit` above never fires (a late subscriber can't hear a past event),
6473
- // so the agent would leak (never reaped, never deprovisioned). Re-check status right after
6474
- // subscribing and reap it now if it already went. onAgentExit is idempotent (freeSlot's guard).
6475
- if (a.handle.status() === "exited")
6476
- this.onAgentExit(a);
6477
- }
6478
- catch {
6479
- /* runtime doesn't stream an exit signal — nothing to wire */
6480
- }
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`);
6481
6550
  }
6482
6551
  /** Prune expired cooling stamps (drop those at/before now) and return the live count — the
6483
6552
  * recycle floor's contribution to the ceiling (P4c). Lazy: pruned only when the gate consults it. */
@@ -6571,8 +6640,8 @@ export class Manager {
6571
6640
  try {
6572
6641
  const live = this.agents.get(name);
6573
6642
  const held = live ? undefined : this.retiring.get(name);
6574
- const principal = live?.delegated ? parsePrincipalKey(live.id) : held?.delegated ? held : null;
6575
- 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;
6576
6645
  if (!principal || !lifecycleUid)
6577
6646
  return { ok: false, error: `no agent named "${name}" was launched on this manager under a delegated user intent` };
6578
6647
  const target = { owner: principal.owner, actor: principal.actor, lifecycleUid };
@@ -6587,7 +6656,7 @@ export class Manager {
6587
6656
  catch (e) {
6588
6657
  const error = `the delegated retirement under intent ${intentId} did not complete (${rejectionText(e)})`;
6589
6658
  const hold = this.retiring.get(name);
6590
- if (hold?.lifecycleUid === target.lifecycleUid)
6659
+ if (hold?.target.lifecycleUid === target.lifecycleUid)
6591
6660
  hold.lastError = error;
6592
6661
  return { ok: false, error: `${error}; "${name}" is unchanged` };
6593
6662
  }
@@ -6733,8 +6802,11 @@ export class Manager {
6733
6802
  managerStatusData() {
6734
6803
  return {
6735
6804
  instanceId: this.managerInstanceId,
6736
- runtime: this.runtime.kind,
6737
- 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",
6738
6810
  agentCount: this.agents.size,
6739
6811
  uptimeMs: Date.now() - this.startedAtMs,
6740
6812
  connectors: this.connectorStatuses.map((row) => ({ ...row, binaries: { ...row.binaries } })),
@@ -7742,6 +7814,8 @@ export class Manager {
7742
7814
  * Returns the acceptance floor payload {name, owner, actor, uid, goalId, fingerprint, executor}
7743
7815
  * (the ALLOCATED identity). goalId = the request id (env.id, Q3). */
7744
7816
  async serveSpawnGoal(ctx, run) {
7817
+ if (this.execution === "none")
7818
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
7745
7819
  const gw = this.goalWriter;
7746
7820
  if (!gw)
7747
7821
  throw new EpEnvelopeError("unavailable", "the manager goal-writer connection is not standing; spawn-as-action cannot accept (SPEC 13.6)");
@@ -8008,9 +8082,11 @@ export class Manager {
8008
8082
  }
8009
8083
  const raw = (ctx.request.args ?? {});
8010
8084
  const payload = String(raw.payload);
8011
- 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;
8012
8089
  const handoffFrom = raw.handoffFrom === undefined ? undefined : String(raw.handoffFrom);
8013
- const deadlineAt = acceptedAt + deadlineMs;
8014
8090
  const note = JSON.stringify({ payload, deadlineAt, holdEpoch: executor.epoch, owner: t.owner, ...(handoffFrom !== undefined ? { handoffFrom } : {}) });
8015
8091
  // A goal that already ENDED is never accepted again, whatever the retry carries: the bind is
8016
8092
  // create-only and outlives an unwind (the failed terminal, the cleared index), so a retry of an
@@ -8531,13 +8607,13 @@ export class Manager {
8531
8607
  * exist. Absent for a runtime with no durable custody (there is nothing a successor could reap
8532
8608
  * by reference), which is the only case that still spawns unreserved. */
8533
8609
  reserveCustody() {
8534
- return isCustodialRuntime(this.runtime) ? this.runtime.reserve() : undefined;
8610
+ return isCustodialRuntime(this.requiredRuntime) ? this.requiredRuntime.reserve() : undefined;
8535
8611
  }
8536
8612
  /** Spawn under a reserved reference and prove the runtime honoured it. A runtime that minted its
8537
8613
  * own id instead would leave every durable record addressing a seat that does not exist, so the
8538
8614
  * mismatch tears the new seat down and throws rather than returning an unaddressable handle. */
8539
8615
  async spawnCustodied(name, spec, cwd, reserved) {
8540
- const handle = this.runtime.spawn(name, spec, cwd, reserved);
8616
+ const handle = this.requiredRuntime.spawn(name, spec, cwd, reserved);
8541
8617
  if (reserved === undefined)
8542
8618
  return handle;
8543
8619
  const got = handle.reference;
@@ -8559,7 +8635,7 @@ export class Manager {
8559
8635
  }
8560
8636
  else {
8561
8637
  try {
8562
- const evidence = await requireRuntimeReap(this.runtime, got);
8638
+ const evidence = await requireRuntimeReap(this.requiredRuntime, got);
8563
8639
  disposal = `the spawned seat was reaped (${evidence.detail})`;
8564
8640
  }
8565
8641
  catch (e) {
@@ -8573,7 +8649,7 @@ export class Manager {
8573
8649
  : `the spawned seat could NOT be reaped: ${rejectionText(e)}`;
8574
8650
  }
8575
8651
  }
8576
- 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 ` +
8577
8653
  `${got ? `${got.kind}:${got.id}` : "a handle with no reference"}; the recorded reference would address no seat, so ${disposal}`);
8578
8654
  }
8579
8655
  /** The terminal's process step: an orphan (no live handle in THIS process) is reaped by the custody
@@ -8588,14 +8664,14 @@ export class Manager {
8588
8664
  // before the reservation existed, or a caller that dropped the reference on the way here.
8589
8665
  // Do not name the slot row: the row may well hold a reference this caller never read, and
8590
8666
  // a diagnostic that guesses which is which sends the reader the wrong way.
8591
- if (isCustodialRuntime(this.runtime))
8592
- 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`);
8593
8669
  return;
8594
8670
  }
8595
8671
  // An unproven reap throws from here rather than printing, so it propagates into
8596
8672
  // driveStaticRetirement's catch, which records the failure and HOLDS the name. That is the whole
8597
8673
  // point of refusing: the alias must not be freed while a seat nobody proved gone may still run.
8598
- const evidence = await requireRuntimeReap(this.runtime, a.runtime);
8674
+ const evidence = await requireRuntimeReap(this.requiredRuntime, a.runtime);
8599
8675
  console.error(`static retirement ${a.name}: orphan seat process ${evidence.detail}`);
8600
8676
  }
8601
8677
  /** The static F1 terminal for one departed incarnation (Unit B): delegates the gate/head CAS
@@ -8663,14 +8739,14 @@ export class Manager {
8663
8739
  const principal = principalKey(DEV_OWNER, a.id).key;
8664
8740
  this.retiredPrincipals.add(principal);
8665
8741
  const cur = this.retiring.get(a.name);
8666
- if (cur && cur.lifecycleUid === a.lifecycleUid) {
8742
+ if (cur && cur.target.lifecycleUid === a.lifecycleUid) {
8667
8743
  this.retiring.delete(a.name); // ABA-guarded hold clear
8668
8744
  this.confirmedRetiredPredecessors.set(a.name, { principal, lifecycleUid: a.lifecycleUid });
8669
8745
  }
8670
8746
  }
8671
8747
  catch (e) {
8672
8748
  const h = this.retiring.get(a.name);
8673
- if (h && h.lifecycleUid === a.lifecycleUid)
8749
+ if (h && h.target.lifecycleUid === a.lifecycleUid)
8674
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})`;
8675
8751
  console.error(`static retirement ${a.name} (${a.id}): ${rejectionText(e)}`);
8676
8752
  if (surfaceFailure)
@@ -9173,9 +9249,9 @@ export class Manager {
9173
9249
  : undefined;
9174
9250
  }
9175
9251
  for (const [name, hold] of this.retiring) {
9176
- 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;
9177
9253
  if (held === caller)
9178
- 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)`;
9179
9255
  }
9180
9256
  if (this.retiredPrincipals.has(caller))
9181
9257
  return "the caller's lifecycle is retired; a retired incarnation's credential holds no control authority (F5)";
@@ -9189,26 +9265,8 @@ export class Manager {
9189
9265
  async withProvisioner(fn) {
9190
9266
  if (!this.auth)
9191
9267
  throw new Error("withProvisioner: no space auth (an open mesh has no scoped creds)");
9192
- const identity = newIdentity();
9193
- const creds = await mintCreds(this.auth, identity, "provisioner");
9194
- const prov = new CotalEndpoint({
9195
- space: this.space,
9196
- servers: this.servers,
9197
- channels: [],
9198
- creds,
9199
- card: { id: identity.id, name: "provisioner", role: "provisioner", kind: "endpoint" },
9200
- registerPresence: false,
9201
- watchPresence: false,
9202
- watchChannels: false,
9203
- consume: false,
9204
- });
9205
- await prov.start();
9206
- try {
9207
- return await fn(prov);
9208
- }
9209
- finally {
9210
- await prov.stop();
9211
- }
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);
9212
9270
  }
9213
9271
  /** A connection holding this instance's transfer reader grants (docs/design/resume-transfer.md
9214
9272
  * section 6) for this one receive or sweep: minted from the space's signing seed, or issued by the