@cotal-ai/manager 0.70.0 → 0.70.1
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-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 +95 -48
- package/dist/manager.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) {
|
|
@@ -252,6 +238,20 @@ function foreignEventChannels(channels, owner, actor) {
|
|
|
252
238
|
return p !== null && !(p.owner === owner && p.actor === actor);
|
|
253
239
|
});
|
|
254
240
|
}
|
|
241
|
+
/** One connector's declared harness binaries resolved on PATH now. The refusal sentence is built only
|
|
242
|
+
* here, so the boot row and the check for a connector registered after boot can never word it apart. */
|
|
243
|
+
function resolveHarness(name, requires) {
|
|
244
|
+
const binaries = {};
|
|
245
|
+
const missing = [];
|
|
246
|
+
for (const bin of requires) {
|
|
247
|
+
const path = resolveOnPath(bin);
|
|
248
|
+
if (path)
|
|
249
|
+
binaries[bin] = path;
|
|
250
|
+
else
|
|
251
|
+
missing.push(bin);
|
|
252
|
+
}
|
|
253
|
+
return missing.length ? { binaries, reason: `${name} harness needs ${missing.join(", ")} on PATH - not found` } : { binaries };
|
|
254
|
+
}
|
|
255
255
|
function injectedManagerStoreIdentity(store) {
|
|
256
256
|
if (store.identity !== undefined)
|
|
257
257
|
return parseSecretStoreIdentity(store.identity);
|
|
@@ -2875,6 +2875,9 @@ export class Manager {
|
|
|
2875
2875
|
// turns addressed to ITS incarnation and yields them; no reach beyond itself exists here.
|
|
2876
2876
|
turnPending: (ctx) => this.serveGated(ctx, () => this.turnPendingFor(ctx.subject.caller)),
|
|
2877
2877
|
turnYield: (ctx) => this.serveGated(ctx, () => this.serveTurnYield(ctx.subject.caller, args(ctx))),
|
|
2878
|
+
// The reserved goal cancel (SPEC 13.6 item 4), served for the relay: the goal ref is the
|
|
2879
|
+
// authenticated caller's own, so a run withdraws only the turns it submitted.
|
|
2880
|
+
cancel: (ctx) => this.serveGated(ctx, () => this.serveTurnCancel(ctx)),
|
|
2878
2881
|
stopSelf: (ctx) => this.serveGated(ctx, () => unwrap(this.opStopSelf(callerOf(ctx), args(ctx)))),
|
|
2879
2882
|
definePersona: (ctx) => this.serveGated(ctx, () => unwrap(this.opDefinePersona(args(ctx), callerOf(ctx), false))),
|
|
2880
2883
|
listPersonas: (ctx) => this.serveGated(ctx, () => unwrap(this.opListPersonas(callerOf(ctx), false))),
|
|
@@ -3248,7 +3251,9 @@ export class Manager {
|
|
|
3248
3251
|
"--token-file", tokenPath,
|
|
3249
3252
|
"--health-file", healthPath,
|
|
3250
3253
|
];
|
|
3251
|
-
|
|
3254
|
+
// One bearer run before the launch is the end-to-end auth preflight (state dir, daemon, ledger
|
|
3255
|
+
// row, secret); its failure sentence is the spawn refusal.
|
|
3256
|
+
await runAgentBearer(bearerCmd);
|
|
3252
3257
|
return { owner, lifecycleUid: opts.lifecycleUid, files, launch: { owner, actor: name, sentinelCredsPath: sentinelPath, bearerCmd } };
|
|
3253
3258
|
}
|
|
3254
3259
|
catch (e) {
|
|
@@ -3350,7 +3355,7 @@ export class Manager {
|
|
|
3350
3355
|
"--token-file", files.actorToken,
|
|
3351
3356
|
"--health-file", files.health,
|
|
3352
3357
|
];
|
|
3353
|
-
await
|
|
3358
|
+
await runAgentBearer(bearerCmd);
|
|
3354
3359
|
return {
|
|
3355
3360
|
owner: material.owner,
|
|
3356
3361
|
lifecycleUid: material.lifecycleUid,
|
|
@@ -4496,19 +4501,9 @@ export class Manager {
|
|
|
4496
4501
|
else {
|
|
4497
4502
|
declared = registry.all("connector").map((connector) => ({ name: connector.name, requires: connector.requires ?? [] }));
|
|
4498
4503
|
}
|
|
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`;
|
|
4504
|
+
for (const { name, requires } of declared) {
|
|
4505
|
+
const { binaries, reason } = resolveHarness(name, requires);
|
|
4506
|
+
if (reason) {
|
|
4512
4507
|
rows.push({ agent: name, state: "unavailable", binaries, reason });
|
|
4513
4508
|
console.error(`! manager boot: connector ${name} unavailable - ${reason}`);
|
|
4514
4509
|
}
|
|
@@ -4540,18 +4535,17 @@ export class Manager {
|
|
|
4540
4535
|
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
4536
|
}
|
|
4542
4537
|
/** The harness binaries a managed launch runs, as boot resolved them, or the refusal naming what is
|
|
4543
|
-
* missing. Spawn and
|
|
4544
|
-
* starts it
|
|
4538
|
+
* missing. Spawn, resume and the model catalog all take it, so a seat launches from the same files
|
|
4539
|
+
* whichever path starts it and the catalog never disagrees with a launch about whether the harness
|
|
4540
|
+
* is there. A connector registered after boot has no inventory row and is checked on PATH now. */
|
|
4545
4541
|
launchHarness(connector) {
|
|
4546
4542
|
const row = this.connectorStatuses.find((status) => status.agent === connector.name);
|
|
4547
4543
|
if (row?.state === "unavailable")
|
|
4548
4544
|
return { refusal: row.reason ?? `${connector.name} harness is unavailable` };
|
|
4549
4545
|
if (row)
|
|
4550
4546
|
return { binaries: row.binaries };
|
|
4551
|
-
const
|
|
4552
|
-
|
|
4553
|
-
return { refusal: `${connector.name} harness needs ${missing.join(", ")} on PATH - not found` };
|
|
4554
|
-
return {};
|
|
4547
|
+
const { reason } = resolveHarness(connector.name, connector.requires ?? []);
|
|
4548
|
+
return reason ? { refusal: reason } : {};
|
|
4555
4549
|
}
|
|
4556
4550
|
/** The refusal for a launch choice the connector cannot honour. Spawn and resume both ask before
|
|
4557
4551
|
* any reserve or mint, so a rule changed here holds on both paths. */
|
|
@@ -4573,14 +4567,9 @@ export class Manager {
|
|
|
4573
4567
|
const one = async (connector) => {
|
|
4574
4568
|
if (!connector.listModels)
|
|
4575
4569
|
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
|
-
};
|
|
4570
|
+
const harness = this.launchHarness(connector);
|
|
4571
|
+
if ("refusal" in harness)
|
|
4572
|
+
return { agent: connector.name, supported: true, models: [], error: harness.refusal };
|
|
4584
4573
|
try {
|
|
4585
4574
|
const catalog = await connector.listModels({ refresh });
|
|
4586
4575
|
return { agent: connector.name, supported: true, ...catalog };
|
|
@@ -8176,6 +8165,58 @@ export class Manager {
|
|
|
8176
8165
|
this.maybeStopTurnSweep();
|
|
8177
8166
|
return { goalId, state: fact.state };
|
|
8178
8167
|
}
|
|
8168
|
+
/** The caller withdraws a turn it submitted (the reserved `cancel`, SPEC 13.6 item 4), so the
|
|
8169
|
+
* seat is not shown it again and a late yield is answered from the cancelled terminal. Only a
|
|
8170
|
+
* relay this incarnation holds is moved to `cancelling`, because only its holder can commit the
|
|
8171
|
+
* terminal; an ended goal is refused with its cached outcome, which keeps a repeated cancel
|
|
8172
|
+
* idempotent. The pending entry is the latch the yield and the deadline sweep race on, taken
|
|
8173
|
+
* before the first await so a concurrent cancel finds nothing to take. A failed cancel puts it
|
|
8174
|
+
* back so the relay still ends at its deadline, unless something ended the relay while it was
|
|
8175
|
+
* held: a yield that read the entry first, or this commit with only its reply lost. An entry
|
|
8176
|
+
* whose outcome cannot be read back stays out and converges through the goal index at the
|
|
8177
|
+
* next boot, as a failed deadline commit does. */
|
|
8178
|
+
async serveTurnCancel(ctx) {
|
|
8179
|
+
if (!this.goalReconcileDone)
|
|
8180
|
+
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)");
|
|
8181
|
+
const gw = this.goalWriter;
|
|
8182
|
+
if (!gw)
|
|
8183
|
+
throw new EpEnvelopeError("unavailable", "the manager goal-writer connection is not standing (SPEC 13.6)");
|
|
8184
|
+
const raw = (ctx.request.args ?? {});
|
|
8185
|
+
const goalId = String(raw.goalId);
|
|
8186
|
+
const ref = goalRefOf(ctx.subject, goalId);
|
|
8187
|
+
const p = this.pendingTurns.get(goalId);
|
|
8188
|
+
const c = p?.ref.caller;
|
|
8189
|
+
if (p === undefined || c?.owner !== ref.caller.owner || c.actor !== ref.caller.actor || c.uid !== ref.caller.uid) {
|
|
8190
|
+
const ended = await readGoalResult(gw.ctx, ref);
|
|
8191
|
+
if (ended !== undefined)
|
|
8192
|
+
throw goalAlreadyTerminal(goalId, ended);
|
|
8193
|
+
throw new EpEnvelopeError("failed-precondition", `no turn "${goalId}" of this caller is pending on this manager; cancel withdraws a relayed turn (SPEC 13.6)`);
|
|
8194
|
+
}
|
|
8195
|
+
this.pendingTurns.delete(goalId);
|
|
8196
|
+
const epoch = this.serviceServe?.grant.epoch ?? 0;
|
|
8197
|
+
let fact;
|
|
8198
|
+
try {
|
|
8199
|
+
await requestGoalCancel(gw.ctx, { request: ctx.subject, goalId, mode: (raw.mode ?? "graceful") });
|
|
8200
|
+
await this.assertGoalWriterEpochCurrent(epoch);
|
|
8201
|
+
({ fact } = await commitGoalResult(gw.ctx, { ref, now: Date.now(), cause: "cancel", data: { cancelledBy: "caller" }, committer: { instanceId: this.managerInstanceId, epoch } }));
|
|
8202
|
+
}
|
|
8203
|
+
catch (e) {
|
|
8204
|
+
// The remembered answer is read after the durable one, with no await before the put back:
|
|
8205
|
+
// a yield that has dropped the entry remembered its answer, and one still committing drops
|
|
8206
|
+
// the entry it finds. An ended relay is stamped so its acceptance still ages off.
|
|
8207
|
+
if ((await readGoalResult(gw.ctx, ref)) !== undefined)
|
|
8208
|
+
this.markTurnLatchDropped(goalId);
|
|
8209
|
+
else if (this.turnAcceptances.get(goalId)?.settled === undefined)
|
|
8210
|
+
this.pendingTurns.set(goalId, p);
|
|
8211
|
+
throw e;
|
|
8212
|
+
}
|
|
8213
|
+
this.emitGoalProgress(ref, epoch, { phase: "terminal", state: fact.state, ...(fact.data !== undefined ? { data: fact.data } : {}) });
|
|
8214
|
+
this.markTurnLatchDropped(goalId);
|
|
8215
|
+
this.rememberSettledTurn(goalId, fact.state, Date.now());
|
|
8216
|
+
await clearGoalIndex(gw.ctx, ref);
|
|
8217
|
+
this.maybeStopTurnSweep();
|
|
8218
|
+
return { goalId, state: fact.state };
|
|
8219
|
+
}
|
|
8179
8220
|
/** Commit the deadline terminal for one pending turn whose hold settled EXPIRED: the deny's
|
|
8180
8221
|
* predicate is that recorded settle, verified by core against the spec's goal binding. The map
|
|
8181
8222
|
* delete is the idempotency latch (yield-loss and sweep race here); a commit failure after it
|
|
@@ -8537,11 +8578,17 @@ export class Manager {
|
|
|
8537
8578
|
console.error(`static retirement ${a.name}: retired ${retired} issuance(s) bound to uid ${a.lifecycleUid}`);
|
|
8538
8579
|
const secrets = this.secrets;
|
|
8539
8580
|
const files = a.secretPaths ?? agentLifecycleSecretFilePaths(this.workspaceRoot, this.space, a.name, a.lifecycleUid);
|
|
8581
|
+
// The broker teardown mints its own credential, so a creds file that cannot be removed must not
|
|
8582
|
+
// skip it. The failures are thrown together, so the terminal still stops at an incomplete cleanup.
|
|
8583
|
+
const failed = [];
|
|
8540
8584
|
if (files.creds) {
|
|
8541
|
-
|
|
8542
|
-
|
|
8585
|
+
const creds = files.creds;
|
|
8586
|
+
await attemptCleanup(failed, `secret ${creds}`, () => secrets.delete(agentSecretKeyForFile(creds, this.space)));
|
|
8587
|
+
await attemptCleanup(failed, `file ${creds}`, () => rmSync(creds, { force: true }));
|
|
8543
8588
|
}
|
|
8544
|
-
await this.deprovisionBroker(a);
|
|
8589
|
+
await attemptCleanup(failed, "broker teardown", () => this.deprovisionBroker(a));
|
|
8590
|
+
if (failed.length)
|
|
8591
|
+
throw new Error(`cleanup failed: ${failed.join("; ")}`);
|
|
8545
8592
|
};
|
|
8546
8593
|
// The process goes before its footprint: reap the orphan seat by reference, then tear down.
|
|
8547
8594
|
const reapThenCleanup = async () => {
|