@cotal-ai/manager 0.70.0 → 0.71.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/launch.js +2 -2
- package/dist/launch.js.map +1 -1
- package/dist/manager-service-contract.d.ts +6 -1
- package/dist/manager-service-contract.d.ts.map +1 -1
- package/dist/manager-service-contract.js +18 -2
- package/dist/manager-service-contract.js.map +1 -1
- package/dist/manager.d.ts +14 -2
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +126 -64
- package/dist/manager.js.map +1 -1
- package/dist/resume.d.ts.map +1 -1
- package/dist/resume.js +17 -41
- package/dist/resume.js.map +1 -1
- package/dist/roster.d.ts +2 -0
- package/dist/roster.d.ts.map +1 -1
- package/dist/roster.js +9 -0
- package/dist/roster.js.map +1 -1
- package/dist/runtime/pty.d.ts +1 -1
- package/dist/runtime/pty.d.ts.map +1 -1
- package/dist/runtime/pty.js +11 -3
- package/dist/runtime/pty.js.map +1 -1
- package/package.json +12 -12
package/dist/manager.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { createHash, randomUUID, randomBytes } from "node:crypto";
|
|
3
2
|
import { hostname } from "node:os";
|
|
4
3
|
import { credsAuthenticator } from "@nats-io/transport-node";
|
|
5
4
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
6
5
|
import { isAbsolute, join, dirname, resolve } from "node:path";
|
|
7
6
|
import Headless from "@xterm/headless";
|
|
8
|
-
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,
|
|
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, runAgentBearer, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannelPrincipal, resolveService, invokeCommand, } from "@cotal-ai/core";
|
|
9
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";
|
|
10
9
|
import { createRuntime, isCustodialRuntime, requireRuntimeAdopt, requireRuntimeReap, RuntimeReapUnproven, } from "./runtime/index.js";
|
|
11
10
|
import { AttachEndpoint } from "./attach-endpoint.js";
|
|
@@ -31,7 +30,7 @@ import { ManagerSessionPlane, openSessionLedgerKv } from "./session/index.js";
|
|
|
31
30
|
// P2 item 1 (1a-serve): the manager as an ordinary v0.4 `service` endpoint — the §13.1
|
|
32
31
|
// endpoint-serve credential subsystem (gate provisioning, registration barrier, mint fence) plus
|
|
33
32
|
// the register/authorize/serve seams, all driven over a scoped one-shot executor connection.
|
|
34
|
-
import { provisionEndpointGateOpen, endpointRegistrationBarrier, serveIssuanceGateKv, commitSiblingIssuance, markLedgerRowRevoked, epcredRowKey, epgateKey, parseEndpointGate, readEndpointGateGeneration, registerServingInstance, deregisterServiceInstance, serveEndpoint, bindGoal, createGoal, transitionGoal, commitGoalResult, settleGoalUncertain, readGoalResult, readGoalStatus, readGoalSpec, recordGoalIndex, readGoalIndex, clearGoalIndex, listGoalIndex, mintCheckpoint, resumeCheckpoint, readCheckpointSettle, readCheckpointSpec, expireCheckpoint, GOAL_TERMINAL_STATES, goalRefOf, goalProgressTopic, epeSubject, submissionFingerprint, EpEnvelopeError, lifecycleBlocked, lifecycleBlockedFrom, renderLifecycleBlocked, } from "@cotal-ai/core";
|
|
33
|
+
import { provisionEndpointGateOpen, endpointRegistrationBarrier, serveIssuanceGateKv, commitSiblingIssuance, markLedgerRowRevoked, epcredRowKey, epgateKey, parseEndpointGate, readEndpointGateGeneration, registerServingInstance, deregisterServiceInstance, serveEndpoint, bindGoal, createGoal, transitionGoal, commitGoalResult, requestGoalCancel, goalAlreadyTerminal, 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";
|
|
35
34
|
import { MANAGER_ENDPOINT, managerClusterArtifacts, managerCommandDefs, managerContractArtifactValues } from "./manager-service-contract.js";
|
|
36
35
|
import { staticLifecycleTransport, activateStaticLifecycle, runStaticTerminal, readStaticSlot, casStaticSlot, recordSlotCredential, appendStaticCredentialRow, planStaticSlotResume, ownedBySibling, observeStaticSlot, listStaticSlotObservations, renderStaticSlotObservation, StaticSlotReadError, STATIC_SLOT_READ_FAILED_DETAIL, } from "./static-lifecycle.js";
|
|
37
36
|
/** Minimum slot lifetime for rate-flooring (P4c). A slot freed (by despawn OR natural exit/reap)
|
|
@@ -148,19 +147,6 @@ async function attemptCleanup(failed, what, step) {
|
|
|
148
147
|
failed.push(`${what}: ${rejectionText(err)}`);
|
|
149
148
|
}
|
|
150
149
|
}
|
|
151
|
-
/** Run the agent's bearer argv once, pre-launch — the end-to-end auth preflight (state dir, daemon,
|
|
152
|
-
* ledger row, secret). Its stderr is the provider command's operator-exact sentence; surface it
|
|
153
|
-
* verbatim as the spawn refusal. */
|
|
154
|
-
function execBearerPreflight(argv) {
|
|
155
|
-
const timeout = 30_000;
|
|
156
|
-
return new Promise((res, rej) => {
|
|
157
|
-
execFile(argv[0], argv.slice(1), { timeout, maxBuffer: 64 * 1024 }, (err, _stdout, stderr) => {
|
|
158
|
-
if (err)
|
|
159
|
-
return rej(bearerCommandFailure(err, stderr, timeout));
|
|
160
|
-
res();
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
150
|
/** Reject `p` with `Error(msg)` if it hasn't settled within `ms`; clears the timer when `p` settles so it
|
|
165
151
|
* never keeps the loop alive. Used to bound the detached deprovision so its fail-loud log is guaranteed. */
|
|
166
152
|
function withTimeout(p, ms, msg) {
|
|
@@ -173,6 +159,18 @@ function withTimeout(p, ms, msg) {
|
|
|
173
159
|
function sameStrings(a, b) {
|
|
174
160
|
return JSON.stringify([...(a ?? [])].sort()) === JSON.stringify([...(b ?? [])].sort());
|
|
175
161
|
}
|
|
162
|
+
/** The caller keeps the arrays it passed, so a launch form is admitted with its own copies: a later
|
|
163
|
+
* write to the caller's array cannot change what the manager grants, shares or preserves. */
|
|
164
|
+
function ownLists(form) {
|
|
165
|
+
return {
|
|
166
|
+
...form,
|
|
167
|
+
...(form.subscribe && { subscribe: [...form.subscribe] }),
|
|
168
|
+
...(form.allowSubscribe && { allowSubscribe: [...form.allowSubscribe] }),
|
|
169
|
+
...(form.allowPublish && { allowPublish: [...form.allowPublish] }),
|
|
170
|
+
...(form.capabilities && { capabilities: [...form.capabilities] }),
|
|
171
|
+
...(form.shareTools && { shareTools: [...form.shareTools] }),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
176
174
|
/** Operator-facing phrasing per cause. Kept beside the union so adding a member without a sentence
|
|
177
175
|
* is a type error rather than a blank in the log. The stop family's sentences carry the principal
|
|
178
176
|
* from the cause, so they render through {@link STOP_CAUSE_TEXT} under the same exhaustive-Record
|
|
@@ -252,6 +250,20 @@ function foreignEventChannels(channels, owner, actor) {
|
|
|
252
250
|
return p !== null && !(p.owner === owner && p.actor === actor);
|
|
253
251
|
});
|
|
254
252
|
}
|
|
253
|
+
/** One connector's declared harness binaries resolved on PATH now. The refusal sentence is built only
|
|
254
|
+
* here, so the boot row and the check for a connector registered after boot can never word it apart. */
|
|
255
|
+
function resolveHarness(name, requires) {
|
|
256
|
+
const binaries = {};
|
|
257
|
+
const missing = [];
|
|
258
|
+
for (const bin of requires) {
|
|
259
|
+
const path = resolveOnPath(bin);
|
|
260
|
+
if (path)
|
|
261
|
+
binaries[bin] = path;
|
|
262
|
+
else
|
|
263
|
+
missing.push(bin);
|
|
264
|
+
}
|
|
265
|
+
return missing.length ? { binaries, reason: `${name} harness needs ${missing.join(", ")} on PATH - not found` } : { binaries };
|
|
266
|
+
}
|
|
255
267
|
function injectedManagerStoreIdentity(store) {
|
|
256
268
|
if (store.identity !== undefined)
|
|
257
269
|
return parseSecretStoreIdentity(store.identity);
|
|
@@ -2875,6 +2887,9 @@ export class Manager {
|
|
|
2875
2887
|
// turns addressed to ITS incarnation and yields them; no reach beyond itself exists here.
|
|
2876
2888
|
turnPending: (ctx) => this.serveGated(ctx, () => this.turnPendingFor(ctx.subject.caller)),
|
|
2877
2889
|
turnYield: (ctx) => this.serveGated(ctx, () => this.serveTurnYield(ctx.subject.caller, args(ctx))),
|
|
2890
|
+
// The reserved goal cancel (SPEC 13.6 item 4), served for the relay: the goal ref is the
|
|
2891
|
+
// authenticated caller's own, so a run withdraws only the turns it submitted.
|
|
2892
|
+
cancel: (ctx) => this.serveGated(ctx, () => this.serveTurnCancel(ctx)),
|
|
2878
2893
|
stopSelf: (ctx) => this.serveGated(ctx, () => unwrap(this.opStopSelf(callerOf(ctx), args(ctx)))),
|
|
2879
2894
|
definePersona: (ctx) => this.serveGated(ctx, () => unwrap(this.opDefinePersona(args(ctx), callerOf(ctx), false))),
|
|
2880
2895
|
listPersonas: (ctx) => this.serveGated(ctx, () => unwrap(this.opListPersonas(callerOf(ctx), false))),
|
|
@@ -3248,7 +3263,9 @@ export class Manager {
|
|
|
3248
3263
|
"--token-file", tokenPath,
|
|
3249
3264
|
"--health-file", healthPath,
|
|
3250
3265
|
];
|
|
3251
|
-
|
|
3266
|
+
// One bearer run before the launch is the end-to-end auth preflight (state dir, daemon, ledger
|
|
3267
|
+
// row, secret); its failure sentence is the spawn refusal.
|
|
3268
|
+
await runAgentBearer(bearerCmd);
|
|
3252
3269
|
return { owner, lifecycleUid: opts.lifecycleUid, files, launch: { owner, actor: name, sentinelCredsPath: sentinelPath, bearerCmd } };
|
|
3253
3270
|
}
|
|
3254
3271
|
catch (e) {
|
|
@@ -3350,7 +3367,7 @@ export class Manager {
|
|
|
3350
3367
|
"--token-file", files.actorToken,
|
|
3351
3368
|
"--health-file", files.health,
|
|
3352
3369
|
];
|
|
3353
|
-
await
|
|
3370
|
+
await runAgentBearer(bearerCmd);
|
|
3354
3371
|
return {
|
|
3355
3372
|
owner: material.owner,
|
|
3356
3373
|
lifecycleUid: material.lifecycleUid,
|
|
@@ -4330,6 +4347,8 @@ export class Manager {
|
|
|
4330
4347
|
// but a raw control message could otherwise slip an empty value through and silently start fresh.
|
|
4331
4348
|
if (args.resume !== undefined && !String(args.resume).trim())
|
|
4332
4349
|
return Promise.resolve({ ok: false, error: "resume: session id must not be empty" });
|
|
4350
|
+
if (args.model !== undefined && !String(args.model).trim())
|
|
4351
|
+
return Promise.resolve({ ok: false, error: "model: must not be empty" });
|
|
4333
4352
|
if (args.variant !== undefined && !String(args.variant).trim())
|
|
4334
4353
|
return Promise.resolve({ ok: false, error: "variant: must not be empty" });
|
|
4335
4354
|
if (args.defaultAgent !== undefined && !String(args.defaultAgent).trim())
|
|
@@ -4496,19 +4515,9 @@ export class Manager {
|
|
|
4496
4515
|
else {
|
|
4497
4516
|
declared = registry.all("connector").map((connector) => ({ name: connector.name, requires: connector.requires ?? [] }));
|
|
4498
4517
|
}
|
|
4499
|
-
for (const
|
|
4500
|
-
const
|
|
4501
|
-
|
|
4502
|
-
const missing = [];
|
|
4503
|
-
for (const bin of connector.requires) {
|
|
4504
|
-
const path = resolveOnPath(bin);
|
|
4505
|
-
if (path)
|
|
4506
|
-
binaries[bin] = path;
|
|
4507
|
-
else
|
|
4508
|
-
missing.push(bin);
|
|
4509
|
-
}
|
|
4510
|
-
if (missing.length) {
|
|
4511
|
-
const reason = `${name} harness needs ${missing.join(", ")} on PATH - not found`;
|
|
4518
|
+
for (const { name, requires } of declared) {
|
|
4519
|
+
const { binaries, reason } = resolveHarness(name, requires);
|
|
4520
|
+
if (reason) {
|
|
4512
4521
|
rows.push({ agent: name, state: "unavailable", binaries, reason });
|
|
4513
4522
|
console.error(`! manager boot: connector ${name} unavailable - ${reason}`);
|
|
4514
4523
|
}
|
|
@@ -4540,18 +4549,17 @@ export class Manager {
|
|
|
4540
4549
|
return `${reason}. This manager cannot launch that harness. The space may have other managers; pin one with --on <instance> (the whole id, as ps prints it)`;
|
|
4541
4550
|
}
|
|
4542
4551
|
/** The harness binaries a managed launch runs, as boot resolved them, or the refusal naming what is
|
|
4543
|
-
* missing. Spawn and
|
|
4544
|
-
* starts it
|
|
4552
|
+
* missing. Spawn, resume and the model catalog all take it, so a seat launches from the same files
|
|
4553
|
+
* whichever path starts it and the catalog never disagrees with a launch about whether the harness
|
|
4554
|
+
* is there. A connector registered after boot has no inventory row and is checked on PATH now. */
|
|
4545
4555
|
launchHarness(connector) {
|
|
4546
4556
|
const row = this.connectorStatuses.find((status) => status.agent === connector.name);
|
|
4547
4557
|
if (row?.state === "unavailable")
|
|
4548
4558
|
return { refusal: row.reason ?? `${connector.name} harness is unavailable` };
|
|
4549
4559
|
if (row)
|
|
4550
4560
|
return { binaries: row.binaries };
|
|
4551
|
-
const
|
|
4552
|
-
|
|
4553
|
-
return { refusal: `${connector.name} harness needs ${missing.join(", ")} on PATH - not found` };
|
|
4554
|
-
return {};
|
|
4561
|
+
const { reason } = resolveHarness(connector.name, connector.requires ?? []);
|
|
4562
|
+
return reason ? { refusal: reason } : {};
|
|
4555
4563
|
}
|
|
4556
4564
|
/** The refusal for a launch choice the connector cannot honour. Spawn and resume both ask before
|
|
4557
4565
|
* any reserve or mint, so a rule changed here holds on both paths. */
|
|
@@ -4573,14 +4581,9 @@ export class Manager {
|
|
|
4573
4581
|
const one = async (connector) => {
|
|
4574
4582
|
if (!connector.listModels)
|
|
4575
4583
|
return { agent: connector.name, supported: false, models: [] };
|
|
4576
|
-
const
|
|
4577
|
-
if (
|
|
4578
|
-
return {
|
|
4579
|
-
agent: connector.name,
|
|
4580
|
-
supported: true,
|
|
4581
|
-
models: [],
|
|
4582
|
-
error: `${connector.name} harness needs ${missing.join(", ")} on PATH - not found`,
|
|
4583
|
-
};
|
|
4584
|
+
const harness = this.launchHarness(connector);
|
|
4585
|
+
if ("refusal" in harness)
|
|
4586
|
+
return { agent: connector.name, supported: true, models: [], error: harness.refusal };
|
|
4584
4587
|
try {
|
|
4585
4588
|
const catalog = await connector.listModels({ refresh });
|
|
4586
4589
|
return { agent: connector.name, supported: true, ...catalog };
|
|
@@ -4729,9 +4732,10 @@ export class Manager {
|
|
|
4729
4732
|
}
|
|
4730
4733
|
}
|
|
4731
4734
|
async startAgentActive(opts, spawner, hooks) {
|
|
4732
|
-
//
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
+
// Before the first await, so the grant, the launch and the retained form all read the lists as admitted.
|
|
4736
|
+
opts = ownLists(opts);
|
|
4737
|
+
if (opts.resolved)
|
|
4738
|
+
opts.resolved = ownLists(opts.resolved);
|
|
4735
4739
|
if (opts.delegatedIntent) {
|
|
4736
4740
|
if (!this.remoteAuthority?.executeDelegatedUserIntent)
|
|
4737
4741
|
return { ok: false, error: `"${opts.name}" names a delegated user intent, and this manager has no host execution for one` };
|
|
@@ -4869,7 +4873,7 @@ export class Manager {
|
|
|
4869
4873
|
return { ok: false, error: `${delegatedBy}, so it cannot reopen a session held on this host (continuity: exact)` };
|
|
4870
4874
|
if (typeof opts.cwd === "string" && opts.cwd !== "")
|
|
4871
4875
|
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` };
|
|
4872
|
-
const shared = Object.keys(connectorServers(loadCotalConfig(this.workspaceRoot), agent, shareTools));
|
|
4876
|
+
const shared = Object.keys(connectorServers(loadCotalConfig(this.workspaceRoot), agent, opts.shareTools));
|
|
4873
4877
|
if (shared.length)
|
|
4874
4878
|
return { ok: false, error: `${delegatedBy}, so it cannot share MCP servers that run on this host (${shared.join(", ")}); pass --share-tools none` };
|
|
4875
4879
|
}
|
|
@@ -4905,7 +4909,7 @@ export class Manager {
|
|
|
4905
4909
|
if (opts.resolved) {
|
|
4906
4910
|
// A manifest launch is the access + identity authority: imperative overrides arriving
|
|
4907
4911
|
// alongside `resolved` are a caller contract error, not something to merge (no fallbacks).
|
|
4908
|
-
if (opts.subscribe || opts.allowSubscribe || opts.allowPublish || opts.prompt || shareTools || opts.identity)
|
|
4912
|
+
if (opts.subscribe || opts.allowSubscribe || opts.allowPublish || opts.prompt || opts.shareTools || opts.identity)
|
|
4909
4913
|
return { ok: false, error: "a manifest launch (resolved) rejects imperative overrides (identity/subscribe/allow*/prompt/shareTools)" };
|
|
4910
4914
|
const r = opts.resolved;
|
|
4911
4915
|
identityName = r.name;
|
|
@@ -4952,12 +4956,12 @@ export class Manager {
|
|
|
4952
4956
|
catch (e) {
|
|
4953
4957
|
return { ok: false, error: opts.resolved ? `launch agent: ${e.message}` : `persona ${configPath}: ${e.message}` };
|
|
4954
4958
|
}
|
|
4955
|
-
//
|
|
4956
|
-
//
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
if (
|
|
4960
|
-
|
|
4959
|
+
// Checked after every path has resolved its selectors: a direct `startAgent` call skips the
|
|
4960
|
+
// `start` op's checks, and no connector may receive a blank one (#2862).
|
|
4961
|
+
if (model !== undefined && !model.trim())
|
|
4962
|
+
return { ok: false, error: "model: must not be empty" };
|
|
4963
|
+
if (variant !== undefined && !variant.trim())
|
|
4964
|
+
return { ok: false, error: "variant: must not be empty" };
|
|
4961
4965
|
const idErr = this.nameError(identityName);
|
|
4962
4966
|
if (idErr)
|
|
4963
4967
|
return { ok: false, error: opts.resolved ? `launch agent: ${idErr}` : `persona ${configPath}: ${idErr}` };
|
|
@@ -5294,7 +5298,7 @@ export class Manager {
|
|
|
5294
5298
|
// (cotal config; default none → isolated, the memory-safe default this guards), narrowed by
|
|
5295
5299
|
// an optional --share-tools selection (absent → all declared, the pre-merge behavior).
|
|
5296
5300
|
const cotalConfig = loadCotalConfig(this.workspaceRoot);
|
|
5297
|
-
const mcpServers = connectorServers(cotalConfig, agent, shareTools);
|
|
5301
|
+
const mcpServers = connectorServers(cotalConfig, agent, opts.shareTools);
|
|
5298
5302
|
// The operator's spawn-env policy travels the same route: absent means no extras (the OS
|
|
5299
5303
|
// allow-list + operator knobs + connector-declared inputs), present means those names too.
|
|
5300
5304
|
// A connector never reads the config itself.
|
|
@@ -5421,7 +5425,7 @@ export class Manager {
|
|
|
5421
5425
|
allowPublish,
|
|
5422
5426
|
capabilities,
|
|
5423
5427
|
events,
|
|
5424
|
-
shareTools,
|
|
5428
|
+
shareTools: opts.shareTools,
|
|
5425
5429
|
forkSource: opts.resume,
|
|
5426
5430
|
...(opts.resume !== undefined && spec?.resumeRecordPath ? { resumeRecordPath: spec.resumeRecordPath } : {}),
|
|
5427
5431
|
...(carried ? { carried: { transcriptSha256: carried.sha256, host: carried.sourceHost, transferredAt: carried.stagedAt } } : {}),
|
|
@@ -5598,9 +5602,9 @@ export class Manager {
|
|
|
5598
5602
|
return { ok: false, agents: [], error: `unsupported manager resume inventory version ${String(inventory.version)}` };
|
|
5599
5603
|
if (inventory.space !== this.space)
|
|
5600
5604
|
return { ok: false, agents: [], error: `resume inventory belongs to space "${inventory.space}", not "${this.space}"` };
|
|
5601
|
-
// The caller keeps its inventory, so
|
|
5602
|
-
//
|
|
5603
|
-
const entries = inventory.agents
|
|
5605
|
+
// The caller keeps its inventory, so it is copied before the first await: a later write to it
|
|
5606
|
+
// cannot change what a resumed seat launches with or what the next preservation retains.
|
|
5607
|
+
const entries = structuredClone(inventory.agents);
|
|
5604
5608
|
const seen = new Set();
|
|
5605
5609
|
const principals = new Set();
|
|
5606
5610
|
const orphans = [];
|
|
@@ -8176,6 +8180,58 @@ export class Manager {
|
|
|
8176
8180
|
this.maybeStopTurnSweep();
|
|
8177
8181
|
return { goalId, state: fact.state };
|
|
8178
8182
|
}
|
|
8183
|
+
/** The caller withdraws a turn it submitted (the reserved `cancel`, SPEC 13.6 item 4), so the
|
|
8184
|
+
* seat is not shown it again and a late yield is answered from the cancelled terminal. Only a
|
|
8185
|
+
* relay this incarnation holds is moved to `cancelling`, because only its holder can commit the
|
|
8186
|
+
* terminal; an ended goal is refused with its cached outcome, which keeps a repeated cancel
|
|
8187
|
+
* idempotent. The pending entry is the latch the yield and the deadline sweep race on, taken
|
|
8188
|
+
* before the first await so a concurrent cancel finds nothing to take. A failed cancel puts it
|
|
8189
|
+
* back so the relay still ends at its deadline, unless something ended the relay while it was
|
|
8190
|
+
* held: a yield that read the entry first, or this commit with only its reply lost. An entry
|
|
8191
|
+
* whose outcome cannot be read back stays out and converges through the goal index at the
|
|
8192
|
+
* next boot, as a failed deadline commit does. */
|
|
8193
|
+
async serveTurnCancel(ctx) {
|
|
8194
|
+
if (!this.goalReconcileDone)
|
|
8195
|
+
throw new EpEnvelopeError("unavailable", "the manager is still reconciling accepted goals at boot; the pending-turn index is not rebuilt yet, retry (SPEC 13.6)");
|
|
8196
|
+
const gw = this.goalWriter;
|
|
8197
|
+
if (!gw)
|
|
8198
|
+
throw new EpEnvelopeError("unavailable", "the manager goal-writer connection is not standing (SPEC 13.6)");
|
|
8199
|
+
const raw = (ctx.request.args ?? {});
|
|
8200
|
+
const goalId = String(raw.goalId);
|
|
8201
|
+
const ref = goalRefOf(ctx.subject, goalId);
|
|
8202
|
+
const p = this.pendingTurns.get(goalId);
|
|
8203
|
+
const c = p?.ref.caller;
|
|
8204
|
+
if (p === undefined || c?.owner !== ref.caller.owner || c.actor !== ref.caller.actor || c.uid !== ref.caller.uid) {
|
|
8205
|
+
const ended = await readGoalResult(gw.ctx, ref);
|
|
8206
|
+
if (ended !== undefined)
|
|
8207
|
+
throw goalAlreadyTerminal(goalId, ended);
|
|
8208
|
+
throw new EpEnvelopeError("failed-precondition", `no turn "${goalId}" of this caller is pending on this manager; cancel withdraws a relayed turn (SPEC 13.6)`);
|
|
8209
|
+
}
|
|
8210
|
+
this.pendingTurns.delete(goalId);
|
|
8211
|
+
const epoch = this.serviceServe?.grant.epoch ?? 0;
|
|
8212
|
+
let fact;
|
|
8213
|
+
try {
|
|
8214
|
+
await requestGoalCancel(gw.ctx, { request: ctx.subject, goalId, mode: (raw.mode ?? "graceful") });
|
|
8215
|
+
await this.assertGoalWriterEpochCurrent(epoch);
|
|
8216
|
+
({ fact } = await commitGoalResult(gw.ctx, { ref, now: Date.now(), cause: "cancel", data: { cancelledBy: "caller" }, committer: { instanceId: this.managerInstanceId, epoch } }));
|
|
8217
|
+
}
|
|
8218
|
+
catch (e) {
|
|
8219
|
+
// The remembered answer is read after the durable one, with no await before the put back:
|
|
8220
|
+
// a yield that has dropped the entry remembered its answer, and one still committing drops
|
|
8221
|
+
// the entry it finds. An ended relay is stamped so its acceptance still ages off.
|
|
8222
|
+
if ((await readGoalResult(gw.ctx, ref)) !== undefined)
|
|
8223
|
+
this.markTurnLatchDropped(goalId);
|
|
8224
|
+
else if (this.turnAcceptances.get(goalId)?.settled === undefined)
|
|
8225
|
+
this.pendingTurns.set(goalId, p);
|
|
8226
|
+
throw e;
|
|
8227
|
+
}
|
|
8228
|
+
this.emitGoalProgress(ref, epoch, { phase: "terminal", state: fact.state, ...(fact.data !== undefined ? { data: fact.data } : {}) });
|
|
8229
|
+
this.markTurnLatchDropped(goalId);
|
|
8230
|
+
this.rememberSettledTurn(goalId, fact.state, Date.now());
|
|
8231
|
+
await clearGoalIndex(gw.ctx, ref);
|
|
8232
|
+
this.maybeStopTurnSweep();
|
|
8233
|
+
return { goalId, state: fact.state };
|
|
8234
|
+
}
|
|
8179
8235
|
/** Commit the deadline terminal for one pending turn whose hold settled EXPIRED: the deny's
|
|
8180
8236
|
* predicate is that recorded settle, verified by core against the spec's goal binding. The map
|
|
8181
8237
|
* delete is the idempotency latch (yield-loss and sweep race here); a commit failure after it
|
|
@@ -8537,11 +8593,17 @@ export class Manager {
|
|
|
8537
8593
|
console.error(`static retirement ${a.name}: retired ${retired} issuance(s) bound to uid ${a.lifecycleUid}`);
|
|
8538
8594
|
const secrets = this.secrets;
|
|
8539
8595
|
const files = a.secretPaths ?? agentLifecycleSecretFilePaths(this.workspaceRoot, this.space, a.name, a.lifecycleUid);
|
|
8596
|
+
// The broker teardown mints its own credential, so a creds file that cannot be removed must not
|
|
8597
|
+
// skip it. The failures are thrown together, so the terminal still stops at an incomplete cleanup.
|
|
8598
|
+
const failed = [];
|
|
8540
8599
|
if (files.creds) {
|
|
8541
|
-
|
|
8542
|
-
|
|
8600
|
+
const creds = files.creds;
|
|
8601
|
+
await attemptCleanup(failed, `secret ${creds}`, () => secrets.delete(agentSecretKeyForFile(creds, this.space)));
|
|
8602
|
+
await attemptCleanup(failed, `file ${creds}`, () => rmSync(creds, { force: true }));
|
|
8543
8603
|
}
|
|
8544
|
-
await this.deprovisionBroker(a);
|
|
8604
|
+
await attemptCleanup(failed, "broker teardown", () => this.deprovisionBroker(a));
|
|
8605
|
+
if (failed.length)
|
|
8606
|
+
throw new Error(`cleanup failed: ${failed.join("; ")}`);
|
|
8545
8607
|
};
|
|
8546
8608
|
// The process goes before its footprint: reap the orphan seat by reference, then tear down.
|
|
8547
8609
|
const reapThenCleanup = async () => {
|