@cotal-ai/manager 0.68.0 → 0.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands.js +11 -41
- package/dist/commands.js.map +1 -1
- package/dist/endpoint-evict.d.ts +10 -1
- package/dist/endpoint-evict.d.ts.map +1 -1
- package/dist/endpoint-evict.js +31 -51
- package/dist/endpoint-evict.js.map +1 -1
- package/dist/holder-liveness.d.ts.map +1 -1
- package/dist/holder-liveness.js +11 -48
- package/dist/holder-liveness.js.map +1 -1
- package/dist/manager-service-contract.d.ts +5 -1
- package/dist/manager-service-contract.d.ts.map +1 -1
- package/dist/manager-service-contract.js +7 -3
- package/dist/manager-service-contract.js.map +1 -1
- package/dist/manager.d.ts +12 -11
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +89 -86
- package/dist/manager.js.map +1 -1
- package/dist/remote-authority.d.ts +14 -1
- package/dist/remote-authority.d.ts.map +1 -1
- package/dist/remote-authority.js +50 -12
- package/dist/remote-authority.js.map +1 -1
- package/dist/remote-register.d.ts +1 -1
- package/dist/remote-register.d.ts.map +1 -1
- package/dist/remote-register.js +1 -9
- package/dist/remote-register.js.map +1 -1
- package/dist/resume.d.ts.map +1 -1
- package/dist/resume.js +24 -7
- package/dist/resume.js.map +1 -1
- package/dist/roster.d.ts.map +1 -1
- package/dist/roster.js +3 -9
- package/dist/roster.js.map +1 -1
- package/dist/run-hosting.js +1 -1
- package/dist/run-hosting.js.map +1 -1
- package/dist/static-lifecycle.d.ts +9 -5
- package/dist/static-lifecycle.d.ts.map +1 -1
- package/dist/static-lifecycle.js +14 -9
- package/dist/static-lifecycle.js.map +1 -1
- package/package.json +12 -12
package/dist/manager.js
CHANGED
|
@@ -4,8 +4,8 @@ import { hostname } from "node:os";
|
|
|
4
4
|
import { credsAuthenticator } from "@nats-io/transport-node";
|
|
5
5
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
6
6
|
import { isAbsolute, join, dirname, resolve } from "node:path";
|
|
7
|
-
import { CotalEndpoint, MANAGED_HANDOFF_KIND, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, DeprovisionError, isConcreteChannel, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, modelPolicyRefusal, mintCreds, mintGeneration, mintAcceptedToken, openIssuedStore, acceptedBucket, issuedBucket, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, actionContext, parsePrincipalKey,
|
|
8
|
-
import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, claimManagerSiblingIdentities, connectorInstallHint,
|
|
7
|
+
import { CotalEndpoint, MANAGED_HANDOFF_KIND, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, DeprovisionError, isConcreteChannel, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, modelPolicyRefusal, mintCreds, mintGeneration, mintAcceptedToken, openIssuedStore, acceptedBucket, issuedBucket, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, resolveReadAcl, bearerCommandFailure, 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
9
|
import { createRuntime, isCustodialRuntime, requireRuntimeAdopt, requireRuntimeReap, RuntimeReapUnproven, } from "./runtime/index.js";
|
|
10
10
|
import { AttachEndpoint } from "./attach-endpoint.js";
|
|
11
11
|
import { DELIVERY_ADMIN_BOOT_WAIT_MS, isUnansweredDeliveryAdmin, makeManagerEndpointEvictionEvidence, makeManagerEndpointHolderEvictor, untilDeliveryAdminAnswers } from "./endpoint-evict.js";
|
|
@@ -32,7 +32,7 @@ import { ManagerSessionPlane, openSessionLedgerKv } from "./session/index.js";
|
|
|
32
32
|
// the register/authorize/serve seams, all driven over a scoped one-shot executor connection.
|
|
33
33
|
import { provisionEndpointGateOpen, endpointRegistrationBarrier, serveIssuanceGateKv, commitSiblingIssuance, markLedgerRowRevoked, epcredRowKey, epgateKey, parseEndpointGate, readEndpointGateGeneration, registerServingInstance, deregisterServiceInstance, serveEndpoint, bindGoal, createGoal, transitionGoal, commitGoalResult, settleGoalUncertain, readGoalResult, readGoalStatus, readGoalSpec, recordGoalIndex, readGoalIndex, clearGoalIndex, listGoalIndex, mintCheckpoint, resumeCheckpoint, readCheckpointSettle, readCheckpointSpec, expireCheckpoint, GOAL_TERMINAL_STATES, goalRefOf, goalProgressTopic, epeSubject, submissionFingerprint, EpEnvelopeError, lifecycleBlocked, lifecycleBlockedFrom, renderLifecycleBlocked, } from "@cotal-ai/core";
|
|
34
34
|
import { MANAGER_ENDPOINT, managerClusterArtifacts, managerCommandDefs, managerContractArtifactValues } from "./manager-service-contract.js";
|
|
35
|
-
import { staticLifecycleTransport, activateStaticLifecycle, runStaticTerminal, readStaticSlot, casStaticSlot, recordSlotCredential, appendStaticCredentialRow, planStaticSlotResume, observeStaticSlot, listStaticSlotObservations, renderStaticSlotObservation, StaticSlotReadError, STATIC_SLOT_READ_FAILED_DETAIL, } from "./static-lifecycle.js";
|
|
35
|
+
import { staticLifecycleTransport, activateStaticLifecycle, runStaticTerminal, readStaticSlot, casStaticSlot, recordSlotCredential, appendStaticCredentialRow, planStaticSlotResume, ownedBySibling, observeStaticSlot, listStaticSlotObservations, renderStaticSlotObservation, StaticSlotReadError, STATIC_SLOT_READ_FAILED_DETAIL, } from "./static-lifecycle.js";
|
|
36
36
|
/** Minimum slot lifetime for rate-flooring (P4c). A slot freed (by despawn OR natural exit/reap)
|
|
37
37
|
* before living this long leaves a cooling stamp that still counts toward the ceiling until it
|
|
38
38
|
* expires — so churn (spawn↔despawn or spawn↔fast-exit) can't outrun the concurrency bound. */
|
|
@@ -293,7 +293,8 @@ export class Manager {
|
|
|
293
293
|
/** The ONE secret store for every kind this manager touches (daemon-cred remint + agent kinds).
|
|
294
294
|
* See {@link ManagerOptions.secretStore}. */
|
|
295
295
|
secrets;
|
|
296
|
-
/** Named identity of {@link secrets}: the same coordinate the delivery daemon must reload from.
|
|
296
|
+
/** Named identity of {@link secrets}: the same coordinate the delivery daemon must reload from.
|
|
297
|
+
* Set at construction for an injected store; the workspace store is named on first challenge. */
|
|
297
298
|
secretStoreIdentity;
|
|
298
299
|
/** See {@link ManagerOptions.installedExtensions}. */
|
|
299
300
|
installedExtensions;
|
|
@@ -604,9 +605,8 @@ export class Manager {
|
|
|
604
605
|
if (opts.remoteAuthority)
|
|
605
606
|
this.managerLifecycleUid = opts.remoteAuthority.lifecycleUid;
|
|
606
607
|
this.secrets = opts.secretStore ?? workspaceSecretStore(this.workspaceRoot);
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
: { kind: "fs", root: resolve(this.workspaceRoot) };
|
|
608
|
+
if (opts.secretStore)
|
|
609
|
+
this.secretStoreIdentity = injectedManagerStoreIdentity(opts.secretStore);
|
|
610
610
|
this.installedExtensions = opts.installedExtensions ?? false;
|
|
611
611
|
this.runtime = createRuntime(opts.runtime ?? "auto", `cotal-${this.space}`);
|
|
612
612
|
if (opts.pooled && isCustodialRuntime(this.runtime))
|
|
@@ -1140,6 +1140,9 @@ export class Manager {
|
|
|
1140
1140
|
`this space's delivery lease (${holder}), so its SecretStore is not the store the daemon ` +
|
|
1141
1141
|
`reloads from - nothing reminted`);
|
|
1142
1142
|
const daemon = answer.identity;
|
|
1143
|
+
// The workspace store records its id the first time it is named, so it is named here, where
|
|
1144
|
+
// only an auth manager reaches, rather than in a constructor every open-mode manager runs.
|
|
1145
|
+
this.secretStoreIdentity ??= parseSecretStoreIdentity(this.secrets.identity);
|
|
1143
1146
|
if (!sameSecretStoreIdentity(this.secretStoreIdentity, daemon)) {
|
|
1144
1147
|
console.error(`! ${divergentSecretStoreNotice(this.secretStoreIdentity, daemon)}`);
|
|
1145
1148
|
return "divergent";
|
|
@@ -1687,7 +1690,7 @@ export class Manager {
|
|
|
1687
1690
|
// actually saw.
|
|
1688
1691
|
const backfillFloor = await this.chatFrontierForPreservation();
|
|
1689
1692
|
const inventory = this.preservationInventory ?? {
|
|
1690
|
-
version: "cotal-manager-resume/
|
|
1693
|
+
version: "cotal-manager-resume/v2",
|
|
1691
1694
|
space: this.space,
|
|
1692
1695
|
createdAt: new Date().toISOString(),
|
|
1693
1696
|
agents: [...this.agents.values()].map((a) => this.resumeEntry(a, backfillFloor)),
|
|
@@ -2150,17 +2153,17 @@ export class Manager {
|
|
|
2150
2153
|
await step("release renewal lease", () => this.ep.releaseDaemonRenewalLease());
|
|
2151
2154
|
}
|
|
2152
2155
|
// Capture BEFORE the serve loop is torn down: `stopServiceServe` clears the state, and what is
|
|
2153
|
-
// being asked here is "did this process
|
|
2156
|
+
// being asked here is "which registration did this process write", which only the state
|
|
2154
2157
|
// before teardown can answer. Deregistration runs AFTER the serve loop has drained, so no
|
|
2155
2158
|
// in-flight command can write a status back onto the record it just removed.
|
|
2156
|
-
const
|
|
2159
|
+
const registrationRevision = this.serviceServe?.grant.registrationRevision;
|
|
2157
2160
|
await step("stop service serve", () => this.stopServiceServe());
|
|
2158
2161
|
// The drives AFTER the serve loop (no new `run-start` can land) and BEFORE deregistration: a
|
|
2159
2162
|
// released run's status write is the last thing this incarnation says about it.
|
|
2160
2163
|
await step("stop run hosting", () => this.runHosting?.stop());
|
|
2161
2164
|
this.runHosting = undefined;
|
|
2162
|
-
if (
|
|
2163
|
-
await step("deregister service", () => this.deregisterServiceOnStop());
|
|
2165
|
+
if (registrationRevision !== undefined && !seatFailure)
|
|
2166
|
+
await step("deregister service", () => this.deregisterServiceOnStop(registrationRevision));
|
|
2164
2167
|
await step("stop goal writer", () => this.stopGoalWriter());
|
|
2165
2168
|
await step("stop session plane", () => this.stopSessionPlane());
|
|
2166
2169
|
await step("stop endpoint", () => this.ep.stop());
|
|
@@ -2206,8 +2209,8 @@ export class Manager {
|
|
|
2206
2209
|
* deadline — on every `cotal ps`, `stop` and `attach` in that space, for good. A manager that is
|
|
2207
2210
|
* shutting down is the one participant that KNOWS it is going away, so it says so.
|
|
2208
2211
|
*
|
|
2209
|
-
* The delete is
|
|
2210
|
-
* persists the SAME instanceId and has already re-registered loses nothing to a slow stop here.
|
|
2212
|
+
* The delete is pinned to the `registrationRevision` this process registered at, so a successor
|
|
2213
|
+
* that persists the SAME instanceId and has already re-registered loses nothing to a slow stop here.
|
|
2211
2214
|
*
|
|
2212
2215
|
* Best-effort and LOUD, matching every other teardown step: a broker that is already gone must not
|
|
2213
2216
|
* turn a stop into a failure, but a registration that survives a stop is the exact defect this
|
|
@@ -2215,11 +2218,12 @@ export class Manager {
|
|
|
2215
2218
|
* rather than swallowed. It is not fatal because a crash leaves the same state and the operator
|
|
2216
2219
|
* verb handles both.
|
|
2217
2220
|
*/
|
|
2218
|
-
async deregisterServiceOnStop() {
|
|
2221
|
+
async deregisterServiceOnStop(registrationRevision) {
|
|
2219
2222
|
const iid = this.managerInstanceId;
|
|
2220
2223
|
const dereg = ({ recordsKv, authKv }) => deregisterServiceInstance(recordsKv, {
|
|
2221
2224
|
endpoint: MANAGER_ENDPOINT,
|
|
2222
2225
|
instanceId: iid,
|
|
2226
|
+
registrationRevision,
|
|
2223
2227
|
observeGeneration: async () => {
|
|
2224
2228
|
const key = epgateKey(MANAGER_ENDPOINT, iid);
|
|
2225
2229
|
const entry = await authKv.get(key);
|
|
@@ -2235,7 +2239,7 @@ export class Manager {
|
|
|
2235
2239
|
if (outcome.removed)
|
|
2236
2240
|
console.error(`✓ deregistered manager instance ${iid} from the ${MANAGER_ENDPOINT} service registry (spec revision ${outcome.specRevision})`);
|
|
2237
2241
|
else if (outcome.reason === "superseded")
|
|
2238
|
-
console.error(`! manager instance ${iid} was not deregistered:
|
|
2242
|
+
console.error(`! manager instance ${iid} was not deregistered: another incarnation has registered it since this process did - leaving it alone`);
|
|
2239
2243
|
else if (outcome.reason === "registration-in-flight")
|
|
2240
2244
|
console.error(`! manager instance ${iid} was not deregistered: a registration is still in flight (governance slot held at the live gate generation) - leaving it alone`);
|
|
2241
2245
|
// `absent` is silent: there was nothing registered to remove, which is not news at shutdown.
|
|
@@ -4275,7 +4279,7 @@ export class Manager {
|
|
|
4275
4279
|
nameInUse(name, live = this.liveRosterNames()) {
|
|
4276
4280
|
return this.agents.has(name) || this.reserved.has(name) || this.retiring.has(name) || live.has(name);
|
|
4277
4281
|
}
|
|
4278
|
-
/** First free name in the series `base`, `
|
|
4282
|
+
/** First free name in the series `base`, `base_2`, `base_3`, … — checked against live slots,
|
|
4279
4283
|
* in-flight (reserved) slots, names held pending retirement, AND the live mesh roster. The
|
|
4280
4284
|
* roster check covers occupants this manager does not manage (a foreground `cotal spawn`, a
|
|
4281
4285
|
* connector session, another manager's agent): allocating their name would mint a sibling the
|
|
@@ -4356,11 +4360,12 @@ export class Manager {
|
|
|
4356
4360
|
throw new Error(`${flag}: expected an array of strings`);
|
|
4357
4361
|
return v;
|
|
4358
4362
|
};
|
|
4359
|
-
let subscribe, allowSubscribe, allowPublish;
|
|
4363
|
+
let subscribe, allowSubscribe, allowPublish, shareTools;
|
|
4360
4364
|
try {
|
|
4361
4365
|
subscribe = strList(args.subscribe, "subscribe");
|
|
4362
4366
|
allowSubscribe = strList(args.allowSubscribe, "allowSubscribe");
|
|
4363
4367
|
allowPublish = strList(args.allowPublish, "allowPublish");
|
|
4368
|
+
shareTools = strList(args.shareTools, "shareTools");
|
|
4364
4369
|
}
|
|
4365
4370
|
catch (e) {
|
|
4366
4371
|
return Promise.resolve({ ok: false, error: e.message });
|
|
@@ -4410,7 +4415,7 @@ export class Manager {
|
|
|
4410
4415
|
subscribe,
|
|
4411
4416
|
allowSubscribe,
|
|
4412
4417
|
allowPublish,
|
|
4413
|
-
shareTools
|
|
4418
|
+
shareTools,
|
|
4414
4419
|
...(supervise !== undefined ? { supervise } : {}),
|
|
4415
4420
|
route,
|
|
4416
4421
|
}, caller, hooks);
|
|
@@ -4722,6 +4727,9 @@ export class Manager {
|
|
|
4722
4727
|
}
|
|
4723
4728
|
}
|
|
4724
4729
|
async startAgentActive(opts, spawner, hooks) {
|
|
4730
|
+
// The caller keeps its array, so the selection is copied before the first await: a later write
|
|
4731
|
+
// to it cannot change what this launch shares or what preservation retains.
|
|
4732
|
+
const shareTools = opts.shareTools && [...opts.shareTools];
|
|
4725
4733
|
if (opts.delegatedIntent) {
|
|
4726
4734
|
if (!this.remoteAuthority?.executeDelegatedUserIntent)
|
|
4727
4735
|
return { ok: false, error: `"${opts.name}" names a delegated user intent, and this manager has no host execution for one` };
|
|
@@ -4778,10 +4786,7 @@ export class Manager {
|
|
|
4778
4786
|
return { ok: false, error: e.message };
|
|
4779
4787
|
}
|
|
4780
4788
|
}
|
|
4781
|
-
|
|
4782
|
-
// this manager's COTAL_DEFAULT_AGENT > DEFAULT_CONNECTOR. Both environment values are defaults,
|
|
4783
|
-
// never overrides: neither may beat a deliberate per-persona pin.
|
|
4784
|
-
const agent = opts.agent ?? def?.agent ?? opts.defaultAgent ?? defaultAgentType(DEFAULT_CONNECTOR);
|
|
4789
|
+
const agent = resolveAgentType({ flag: opts.agent, pin: def?.agent, callerDefault: opts.defaultAgent });
|
|
4785
4790
|
// Materialize the requested connector up front — the ONE async step in the spawn path (a lazy
|
|
4786
4791
|
// `cotal ext` manifest import on the published binary). It runs BEFORE the capacity/reserve span
|
|
4787
4792
|
// below so that span stays fully SYNCHRONOUS and atomic. On the manifest binary this also honors a
|
|
@@ -4862,7 +4867,7 @@ export class Manager {
|
|
|
4862
4867
|
return { ok: false, error: `${delegatedBy}, so it cannot reopen a session held on this host (continuity: exact)` };
|
|
4863
4868
|
if (typeof opts.cwd === "string" && opts.cwd !== "")
|
|
4864
4869
|
return { ok: false, error: `${delegatedBy}, so it cannot run in a directory on this host's filesystem (cwd); the seat runs in its runtime resource's own directory` };
|
|
4865
|
-
const shared = Object.keys(connectorServers(loadCotalConfig(this.workspaceRoot), agent,
|
|
4870
|
+
const shared = Object.keys(connectorServers(loadCotalConfig(this.workspaceRoot), agent, shareTools));
|
|
4866
4871
|
if (shared.length)
|
|
4867
4872
|
return { ok: false, error: `${delegatedBy}, so it cannot share MCP servers that run on this host (${shared.join(", ")}); pass --share-tools none` };
|
|
4868
4873
|
}
|
|
@@ -4883,7 +4888,7 @@ export class Manager {
|
|
|
4883
4888
|
// Resolve the launch profile: IDENTITY (free-form `name:`) + role + read/post ACL + capabilities
|
|
4884
4889
|
// + model/variant. Either from a fully-resolved manifest launch object (`opts.resolved`, whose `config`
|
|
4885
4890
|
// is a materialized transient persona — the file is NOT the access authority), or from the
|
|
4886
|
-
// persona file. The number rides the IDENTITY (socrates →
|
|
4891
|
+
// persona file. The number rides the IDENTITY (socrates → socrates_2), not the file ref — a
|
|
4887
4892
|
// redelivered identical spawn yields a fresh numbered agent (MAX_AGENTS bounds the blast radius).
|
|
4888
4893
|
let identityName;
|
|
4889
4894
|
let role;
|
|
@@ -4898,13 +4903,13 @@ export class Manager {
|
|
|
4898
4903
|
if (opts.resolved) {
|
|
4899
4904
|
// A manifest launch is the access + identity authority: imperative overrides arriving
|
|
4900
4905
|
// alongside `resolved` are a caller contract error, not something to merge (no fallbacks).
|
|
4901
|
-
if (opts.subscribe || opts.allowSubscribe || opts.allowPublish || opts.prompt ||
|
|
4906
|
+
if (opts.subscribe || opts.allowSubscribe || opts.allowPublish || opts.prompt || shareTools || opts.identity)
|
|
4902
4907
|
return { ok: false, error: "a manifest launch (resolved) rejects imperative overrides (identity/subscribe/allow*/prompt/shareTools)" };
|
|
4903
4908
|
const r = opts.resolved;
|
|
4904
4909
|
identityName = r.name;
|
|
4905
4910
|
role = opts.role ?? r.role;
|
|
4906
4911
|
subscribe = r.subscribe;
|
|
4907
|
-
allowSubscribe = r.allowSubscribe
|
|
4912
|
+
allowSubscribe = r.allowSubscribe;
|
|
4908
4913
|
allowPublish = r.allowPublish;
|
|
4909
4914
|
capabilities = r.capabilities;
|
|
4910
4915
|
model = opts.model ?? r.model;
|
|
@@ -4926,9 +4931,7 @@ export class Manager {
|
|
|
4926
4931
|
// paths of the merged grammar can't diverge. One source feeds BOTH the minted creds and the
|
|
4927
4932
|
// connector env below.
|
|
4928
4933
|
subscribe = opts.subscribe ?? def.subscribe;
|
|
4929
|
-
|
|
4930
|
-
// boundary); runtime durable joins are re-authorized against the committed ACL by the daemon.
|
|
4931
|
-
allowSubscribe = opts.allowSubscribe ?? def.allowSubscribe ?? subscribe ?? [];
|
|
4934
|
+
allowSubscribe = opts.allowSubscribe ?? def.allowSubscribe ?? [];
|
|
4932
4935
|
allowPublish = opts.allowPublish ?? def.allowPublish;
|
|
4933
4936
|
capabilities = def.capabilities;
|
|
4934
4937
|
// #651: fold the persona's model into the launch record, mirroring the variant line below
|
|
@@ -4939,6 +4942,14 @@ export class Manager {
|
|
|
4939
4942
|
variant = opts.variant ?? def.variant;
|
|
4940
4943
|
launchOptions = mergeLaunchOptions(def.launchOptions, opts.launchOptions);
|
|
4941
4944
|
}
|
|
4945
|
+
// Resolved once for both branches, because this one list is granted, minted, recorded on the
|
|
4946
|
+
// launch and forwarded to the session, and each of those must read the list the provisioner does.
|
|
4947
|
+
try {
|
|
4948
|
+
allowSubscribe = resolveReadAcl(subscribe ?? [], allowSubscribe);
|
|
4949
|
+
}
|
|
4950
|
+
catch (e) {
|
|
4951
|
+
return { ok: false, error: opts.resolved ? `launch agent: ${e.message}` : `persona ${configPath}: ${e.message}` };
|
|
4952
|
+
}
|
|
4942
4953
|
// #651: an empty or whitespace-only model string is not a pin. Coerce it to undefined here, at
|
|
4943
4954
|
// the single point every path (persona, manifest, imperative) has resolved `model`, so it
|
|
4944
4955
|
// serializes ABSENT rather than present-but-empty (`"model": ""`), which a key-presence consumer
|
|
@@ -5026,6 +5037,30 @@ export class Manager {
|
|
|
5026
5037
|
const unportable = this.runtime.spawnDelegated ? Object.keys(launchOptions ?? {}).filter((k) => typeof launchOptions[k] !== "string") : [];
|
|
5027
5038
|
if (unportable.length)
|
|
5028
5039
|
return { ok: false, error: `runtime "${this.runtime.kind}" starts seats outside this host, so it cannot pass non-string launch options (${unportable.join(", ")})` };
|
|
5040
|
+
// The AG-UI event plane is on unless the launch explicitly opted out. Refused HERE, before
|
|
5041
|
+
// anything is minted: a connector that cannot
|
|
5042
|
+
// emit must fail before provisioning rather than after, exactly as an unsupported `resume` does.
|
|
5043
|
+
// The GRANT itself cannot be derived yet. It is keyed on the agent's PRINCIPAL, and in user mode
|
|
5044
|
+
// the principal's owner is resolved further down, so deriving it from anything in scope here
|
|
5045
|
+
// would mean guessing at the identity the child will actually connect as. It is added at the
|
|
5046
|
+
// accept seam below, where the allocated triple exists.
|
|
5047
|
+
if (this.eventsRequired && opts.events === false)
|
|
5048
|
+
return { ok: false, error: `space "${this.space}" requires the event plane by registration policy; --no-events (events: false on the start op) is not allowed` };
|
|
5049
|
+
const events = this.eventsRequired || opts.events !== false;
|
|
5050
|
+
if (events && !connector.eventChannel)
|
|
5051
|
+
return { ok: false, error: this.eventsRequired
|
|
5052
|
+
? `space "${this.space}" requires the event plane by registration policy, but connector "${connector.name}" does not publish one`
|
|
5053
|
+
: `connector "${connector.name}" does not publish an AG-UI event plane; pass --no-events (events: false on the start op) to launch it without one` };
|
|
5054
|
+
// F2 (Unit B): a STATIC managed spawn REFUSES endpoint capabilities, fail-closed IN CODE (not
|
|
5055
|
+
// a doc note): the static terminal has no obligation-drain/frontier steps yet, so an accepted-
|
|
5056
|
+
// but-uncompleted endpoint obligation could execute AFTER its uid is declared retired. The
|
|
5057
|
+
// refusal sits at spawn-accept, before any provisioning, over the same records a persona or
|
|
5058
|
+
// manifest self-claim would ride in on — capabilities cannot slip past it into the grant path.
|
|
5059
|
+
if (this.auth && !this.userMode) {
|
|
5060
|
+
const claims = [opts, (opts.resolved ?? {})];
|
|
5061
|
+
if (claims.some((c) => c.endpointCapabilities !== undefined))
|
|
5062
|
+
return { ok: false, error: "a static managed spawn refuses endpointCapabilities (Unit B F2): the static lifecycle terminal carries no obligation-drain/frontier steps, so endpoint-rail grants are not containable in static mode" };
|
|
5063
|
+
}
|
|
5029
5064
|
// #4 A4 (panel): the roster the allocation consults must reflect the initial presence snapshot,
|
|
5030
5065
|
// or a spawn immediately after manager boot races an already-live unmanaged peer and re-opens the
|
|
5031
5066
|
// very collision black-hole this closes. Await the snapshot (bounded internally, fail-safe on an
|
|
@@ -5035,10 +5070,10 @@ export class Manager {
|
|
|
5035
5070
|
await this.ep.waitForPresenceSnapshot();
|
|
5036
5071
|
// M6 (P2 item 2 spawn-as-action): a HARD-PINNED name — an imperative `--name`/identity override
|
|
5037
5072
|
// or a manifest-declared name (opts.resolved) — that collides with a LIVE/provisioning/reserved
|
|
5038
|
-
// incarnation REFUSES loud at accept, BEFORE any reserve/mint/bind (pin 1), never a silent
|
|
5073
|
+
// incarnation REFUSES loud at accept, BEFORE any reserve/mint/bind (pin 1), never a silent `_2`
|
|
5039
5074
|
// suffix (so an address-by-triple caller's pinned name can't be re-pointed). A PERSONA-DERIVED
|
|
5040
5075
|
// base name (no pin) keeps uniqueName's collision numbering, so multi-peer `spawn reviewer` twice
|
|
5041
|
-
// still yields reviewer +
|
|
5076
|
+
// still yields reviewer + reviewer_2. The retiring-hold refuse (~2472) is orthogonal and already fired.
|
|
5042
5077
|
const hardPinned = opts.identity !== undefined || opts.resolved !== undefined;
|
|
5043
5078
|
let name;
|
|
5044
5079
|
if (hardPinned) {
|
|
@@ -5054,42 +5089,6 @@ export class Manager {
|
|
|
5054
5089
|
else {
|
|
5055
5090
|
name = this.uniqueName(identityName);
|
|
5056
5091
|
}
|
|
5057
|
-
this.reserved.add(name);
|
|
5058
|
-
// The AG-UI event plane is on unless the launch explicitly opted out. Refused HERE, before
|
|
5059
|
-
// anything is minted: a connector that cannot
|
|
5060
|
-
// emit must fail before provisioning rather than after, exactly as an unsupported `resume` does.
|
|
5061
|
-
// The GRANT itself cannot be derived yet. It is keyed on the agent's PRINCIPAL, and in user mode
|
|
5062
|
-
// the principal's owner is resolved further down, so deriving it from anything in scope here
|
|
5063
|
-
// would mean guessing at the identity the child will actually connect as. It is added at the
|
|
5064
|
-
// accept seam below, where the allocated triple exists.
|
|
5065
|
-
if (this.eventsRequired && opts.events === false) {
|
|
5066
|
-
this.reserved.delete(name);
|
|
5067
|
-
return { ok: false, error: `space "${this.space}" requires the event plane by registration policy; --no-events (events: false on the start op) is not allowed` };
|
|
5068
|
-
}
|
|
5069
|
-
const events = this.eventsRequired || opts.events !== false;
|
|
5070
|
-
if (events && !connector.eventChannel) {
|
|
5071
|
-
// Release the just-reserved name on this fail-fast path. A leaked reserve is silent: it costs
|
|
5072
|
-
// the next spawn of this persona its un-suffixed name and nothing reports why.
|
|
5073
|
-
this.reserved.delete(name);
|
|
5074
|
-
return { ok: false, error: this.eventsRequired
|
|
5075
|
-
? `space "${this.space}" requires the event plane by registration policy, but connector "${connector.name}" does not publish one`
|
|
5076
|
-
: `connector "${connector.name}" does not publish an AG-UI event plane; pass --no-events (events: false on the start op) to launch it without one` };
|
|
5077
|
-
}
|
|
5078
|
-
// F2 (Unit B): a STATIC managed spawn REFUSES endpoint capabilities, fail-closed IN CODE (not
|
|
5079
|
-
// a doc note): the static terminal has no obligation-drain/frontier steps yet, so an accepted-
|
|
5080
|
-
// but-uncompleted endpoint obligation could execute AFTER its uid is declared retired. The
|
|
5081
|
-
// refusal sits at spawn-accept, before any provisioning, over the same records a persona or
|
|
5082
|
-
// manifest self-claim would ride in on — capabilities cannot slip past it into the grant path.
|
|
5083
|
-
if (this.auth && !this.userMode) {
|
|
5084
|
-
const claims = [opts, (opts.resolved ?? {})];
|
|
5085
|
-
if (claims.some((c) => c.endpointCapabilities !== undefined)) {
|
|
5086
|
-
this.reserved.delete(name);
|
|
5087
|
-
return { ok: false, error: "a static managed spawn refuses endpointCapabilities (Unit B F2): the static lifecycle terminal carries no obligation-drain/frontier steps, so endpoint-rail grants are not containable in static mode" };
|
|
5088
|
-
}
|
|
5089
|
-
}
|
|
5090
|
-
// Set once the agent's creds + durables are minted; cleared the moment a live slot takes ownership
|
|
5091
|
-
// (`agents.set`, after which freeSlot deprovisions on exit). If it survives to `finally`, the spawn
|
|
5092
|
-
// threw AFTER minting (buildLaunch / runtime.spawn) — tear the orphan down so no footprint leaks (#159 B).
|
|
5093
5092
|
// Set once the agent's footprint (durables + creds, or the user-mode grant + secret files)
|
|
5094
5093
|
// exists; cleared when a live slot takes ownership. If it survives to `finally`, the spawn threw
|
|
5095
5094
|
// AFTER provisioning (buildLaunch / runtime.spawn) — the orphan-rollback tears it down. Carries
|
|
@@ -5103,6 +5102,9 @@ export class Manager {
|
|
|
5103
5102
|
let provisioned;
|
|
5104
5103
|
// A carried resume's seat-private home, removed by the finally unless a live seat took it.
|
|
5105
5104
|
let seatHome;
|
|
5105
|
+
// Taken right before the try so its finally is the only release; a refusal belongs above the
|
|
5106
|
+
// allocation, where there is no name to give back.
|
|
5107
|
+
this.reserved.add(name);
|
|
5106
5108
|
try {
|
|
5107
5109
|
// A stable nkey identity assigned at spawn: the public key is the agent's card.id (threaded via
|
|
5108
5110
|
// COTAL_ID); the seed is retained to mint matching creds later.
|
|
@@ -5208,10 +5210,8 @@ export class Manager {
|
|
|
5208
5210
|
}
|
|
5209
5211
|
: undefined,
|
|
5210
5212
|
});
|
|
5211
|
-
if ("error" in prep)
|
|
5212
|
-
this.reserved.delete(name);
|
|
5213
|
+
if ("error" in prep)
|
|
5213
5214
|
return { ok: false, error: prep.error };
|
|
5214
|
-
}
|
|
5215
5215
|
// THE HOST'S UID WINS (#1972). On the local arm this is the value passed in, unchanged. On
|
|
5216
5216
|
// the hosted enrollment arm the host selected it, and from here every lifecycle-keyed thing
|
|
5217
5217
|
// this spawn records — the slot, the launch, the managed row, the teardown credential — must
|
|
@@ -5273,7 +5273,7 @@ export class Manager {
|
|
|
5273
5273
|
// (cotal config; default none → isolated, the memory-safe default this guards), narrowed by
|
|
5274
5274
|
// an optional --share-tools selection (absent → all declared, the pre-merge behavior).
|
|
5275
5275
|
const cotalConfig = loadCotalConfig(this.workspaceRoot);
|
|
5276
|
-
const mcpServers = connectorServers(cotalConfig, agent,
|
|
5276
|
+
const mcpServers = connectorServers(cotalConfig, agent, shareTools);
|
|
5277
5277
|
// The operator's spawn-env policy travels the same route: absent means no extras (the OS
|
|
5278
5278
|
// allow-list + operator knobs + connector-declared inputs), present means those names too.
|
|
5279
5279
|
// A connector never reads the config itself.
|
|
@@ -5400,7 +5400,7 @@ export class Manager {
|
|
|
5400
5400
|
allowPublish,
|
|
5401
5401
|
capabilities,
|
|
5402
5402
|
events,
|
|
5403
|
-
shareTools
|
|
5403
|
+
shareTools,
|
|
5404
5404
|
forkSource: opts.resume,
|
|
5405
5405
|
...(opts.resume !== undefined && spec?.resumeRecordPath ? { resumeRecordPath: spec.resumeRecordPath } : {}),
|
|
5406
5406
|
...(carried ? { carried: { transcriptSha256: carried.sha256, host: carried.sourceHost, transferredAt: carried.stagedAt } } : {}),
|
|
@@ -5573,19 +5573,22 @@ export class Manager {
|
|
|
5573
5573
|
const batchReservations = [];
|
|
5574
5574
|
const prepared = new Map();
|
|
5575
5575
|
try {
|
|
5576
|
-
if (inventory.version !== "cotal-manager-resume/
|
|
5576
|
+
if (inventory.version !== "cotal-manager-resume/v2")
|
|
5577
5577
|
return { ok: false, agents: [], error: `unsupported manager resume inventory version ${String(inventory.version)}` };
|
|
5578
5578
|
if (inventory.space !== this.space)
|
|
5579
5579
|
return { ok: false, agents: [], error: `resume inventory belongs to space "${inventory.space}", not "${this.space}"` };
|
|
5580
|
+
// The caller keeps its inventory, so each selection is copied before the first await: a later
|
|
5581
|
+
// write to it cannot change what a resumed seat shares or what the next preservation retains.
|
|
5582
|
+
const entries = inventory.agents.map((entry) => ({ ...entry, launch: { ...entry.launch, shareTools: entry.launch.shareTools && [...entry.launch.shareTools] } }));
|
|
5580
5583
|
const seen = new Set();
|
|
5581
5584
|
const principals = new Set();
|
|
5582
5585
|
const orphans = [];
|
|
5583
5586
|
await this.ep.waitForPresenceSnapshot();
|
|
5584
5587
|
const liveRoster = this.ep.getRoster().filter((presence) => presence.status !== "offline");
|
|
5585
5588
|
const occupancy = this.occupancy();
|
|
5586
|
-
if (occupancy.used +
|
|
5587
|
-
return { ok: false, agents: [], error: `resume inventory of ${
|
|
5588
|
-
for (const entry of
|
|
5589
|
+
if (occupancy.used + entries.length > MAX_AGENTS)
|
|
5590
|
+
return { ok: false, agents: [], error: `resume inventory of ${entries.length} would exceed manager capacity (${this.occupancyText(occupancy)})` };
|
|
5591
|
+
for (const entry of entries) {
|
|
5589
5592
|
if (seen.has(entry.name))
|
|
5590
5593
|
return { ok: false, agents: [], error: `resume inventory contains duplicate agent name "${entry.name}"` };
|
|
5591
5594
|
seen.add(entry.name);
|
|
@@ -5642,12 +5645,12 @@ export class Manager {
|
|
|
5642
5645
|
if (this.agents.has(entry.name) || this.reserved.has(entry.name))
|
|
5643
5646
|
return { ok: false, agents: [], error: `retained agent "${entry.name}" is already managed or reserved` };
|
|
5644
5647
|
}
|
|
5645
|
-
for (const entry of
|
|
5648
|
+
for (const entry of entries) {
|
|
5646
5649
|
this.reserved.add(entry.name);
|
|
5647
5650
|
batchReservations.push(entry.name);
|
|
5648
5651
|
}
|
|
5649
5652
|
const preflight = [];
|
|
5650
|
-
for (const entry of
|
|
5653
|
+
for (const entry of entries) {
|
|
5651
5654
|
const reply = await this.resumePreservedAgent(entry, true, true, prepared);
|
|
5652
5655
|
preflight.push({ name: entry.name, reply });
|
|
5653
5656
|
}
|
|
@@ -5666,12 +5669,12 @@ export class Manager {
|
|
|
5666
5669
|
return { ok: false, agents: [], error: refusal };
|
|
5667
5670
|
}
|
|
5668
5671
|
const agents = [];
|
|
5669
|
-
for (let i = 0; i <
|
|
5670
|
-
const entry =
|
|
5672
|
+
for (let i = 0; i < entries.length; i++) {
|
|
5673
|
+
const entry = entries[i];
|
|
5671
5674
|
const reply = await this.resumePreservedAgent(entry, false, true, prepared);
|
|
5672
5675
|
agents.push({ name: entry.name, reply });
|
|
5673
5676
|
if (!reply.ok) {
|
|
5674
|
-
for (const skipped of
|
|
5677
|
+
for (const skipped of entries.slice(i + 1))
|
|
5675
5678
|
agents.push({ name: skipped.name, reply: { ok: false, error: `not launched because ${entry.name} failed` } });
|
|
5676
5679
|
return { ok: false, agents, error: reply.error };
|
|
5677
5680
|
}
|
|
@@ -5881,7 +5884,7 @@ export class Manager {
|
|
|
5881
5884
|
const slot = await this.readStaticReconcileSlot(entry.name);
|
|
5882
5885
|
if (slot?.phase !== "active" || slot.actor !== entry.identity.id || slot.lifecycleUid !== entry.identity.lifecycleUid)
|
|
5883
5886
|
return undefined;
|
|
5884
|
-
if (slot
|
|
5887
|
+
if (ownedBySibling(slot, this.managerInstanceId))
|
|
5885
5888
|
return undefined;
|
|
5886
5889
|
return slot.runtime?.kind === this.runtime.kind ? slot.runtime : undefined;
|
|
5887
5890
|
}
|
|
@@ -6013,7 +6016,7 @@ export class Manager {
|
|
|
6013
6016
|
}
|
|
6014
6017
|
try {
|
|
6015
6018
|
const resumeConfig = loadCotalConfig(this.workspaceRoot);
|
|
6016
|
-
const mcpServers = connectorServers(resumeConfig, entry.launch.connector,
|
|
6019
|
+
const mcpServers = connectorServers(resumeConfig, entry.launch.connector, entry.launch.shareTools);
|
|
6017
6020
|
const envAllow = spawnEnvAllow(resumeConfig);
|
|
6018
6021
|
const launchOpts = {
|
|
6019
6022
|
space: this.space,
|
|
@@ -8705,7 +8708,7 @@ export class Manager {
|
|
|
8705
8708
|
console.error(`✓ static reconcile recovered alias=${item.alias} phase=retired uid=${item.lifecycleUid} attempts=${item.attempts}/${item.maxAttempts}`);
|
|
8706
8709
|
return;
|
|
8707
8710
|
}
|
|
8708
|
-
if (row
|
|
8711
|
+
if (ownedBySibling(row, this.managerInstanceId)) {
|
|
8709
8712
|
item.disposition = "refused";
|
|
8710
8713
|
item.remedy = "the owning manager must reconcile or an operator must perform an explicit CAS takeover";
|
|
8711
8714
|
item.lastError = `the durable slot belongs to manager instance ${row.ownerInstanceId}, not ${this.managerInstanceId}`;
|
|
@@ -8898,7 +8901,7 @@ export class Manager {
|
|
|
8898
8901
|
// hazard, now cross-instance). A legacy row (pre-3b-2, no owner recorded) predates multi-manager,
|
|
8899
8902
|
// so this manager is its legitimate single-manager-past successor and reconciles it. An orphaned
|
|
8900
8903
|
// sibling row is reclaimed only by an explicit operator CAS takeover (ruling 1), never here.
|
|
8901
|
-
if (row
|
|
8904
|
+
if (ownedBySibling(row, this.managerInstanceId)) {
|
|
8902
8905
|
preservedForeign++;
|
|
8903
8906
|
continue;
|
|
8904
8907
|
}
|