@cotal-ai/manager 0.124.3 → 0.126.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 +10 -2
- package/dist/manager-service-contract.d.ts.map +1 -1
- package/dist/manager-service-contract.js +24 -3
- package/dist/manager-service-contract.js.map +1 -1
- package/dist/manager.d.ts +5 -0
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +100 -15
- package/dist/manager.js.map +1 -1
- package/package.json +12 -12
package/dist/manager.js
CHANGED
|
@@ -3,6 +3,7 @@ import { hostname } from "node:os";
|
|
|
3
3
|
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
|
+
import { parse as parseYaml } from "yaml";
|
|
6
7
|
import Headless from "@xterm/headless";
|
|
7
8
|
import { CotalEndpoint, canonicalJson, MANAGED_HANDOFF_KIND, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, MANAGER_LEASE_TTL_MS, awaitLeaseExpiry, leaseRemainingMs, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, injectedSecretStoreIdentity, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, fenceLifecycleAcl, DeprovisionError, addDeprovisionAccounting, isConcreteChannel, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, identityFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, modelPolicyRefusal, mintCreds, mintGeneration, mintAcceptedToken, bindIssuedStores, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, oneLine, actionContext, parsePrincipalKey, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, resolveReadAcl, runAgentBearer, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannelPrincipal, resolveService, invokeCommand, digestIdToken, } from "@cotal-ai/core";
|
|
8
9
|
import { loadLaunchPersonaFile, PersonaSourceError } from "@cotal-ai/workspace";
|
|
@@ -3306,6 +3307,12 @@ export class Manager {
|
|
|
3306
3307
|
cancel: (ctx) => this.serveGated(ctx, () => this.serveTurnCancel(ctx)),
|
|
3307
3308
|
stopSelf: (ctx) => this.serveGated(ctx, () => unwrap(this.opStopSelf(callerOf(ctx), args(ctx)))),
|
|
3308
3309
|
definePersona: (ctx) => this.serveGated(ctx, () => unwrap(this.opDefinePersona(args(ctx), callerOf(ctx), false))),
|
|
3310
|
+
removePersona: (ctx) => this.serveGated(ctx, () => {
|
|
3311
|
+
const r = this.opRemovePersona(args(ctx), callerOf(ctx), false);
|
|
3312
|
+
if (!r.ok)
|
|
3313
|
+
throw new EpEnvelopeError((r.code ?? "failed-precondition"), r.error ?? "the operation failed", r.details, "not-executed");
|
|
3314
|
+
return r.data;
|
|
3315
|
+
}),
|
|
3309
3316
|
// Reach is the broker's: only operator instruments carry these rows. The serve gate is the
|
|
3310
3317
|
// maintenance fence, so neither runs while a shutdown or resume holds control.
|
|
3311
3318
|
chatSessionOpen: (ctx) => this.serveGated(ctx, () => this.openStaticChatSession(args(ctx))),
|
|
@@ -4443,10 +4450,14 @@ export class Manager {
|
|
|
4443
4450
|
throw new Error(`cannot read connector session state ${path}: ${rejectionText(error)}`);
|
|
4444
4451
|
}
|
|
4445
4452
|
const state = parsed;
|
|
4446
|
-
if (state.version
|
|
4447
|
-
(state.status
|
|
4448
|
-
|
|
4449
|
-
|
|
4453
|
+
if (state.version === 1 && typeof state.sessionId === "string" && state.sessionId.trim() && state.sessionId.length <= 4096 &&
|
|
4454
|
+
(state.status === "running" || state.status === "quit"))
|
|
4455
|
+
return { sessionId: state.sessionId, status: state.status };
|
|
4456
|
+
if (state.version === 2 && state.status === "prompt" && typeof state.connector === "string" && /^[a-z0-9-]{1,64}$/.test(state.connector) &&
|
|
4457
|
+
typeof state.prompt === "string" && state.prompt.trim() && state.prompt.length <= 1024 &&
|
|
4458
|
+
typeof state.cwd === "string" && state.cwd.trim() && state.cwd.length <= 4096)
|
|
4459
|
+
return { connector: state.connector, status: "prompt", prompt: state.prompt, cwd: state.cwd };
|
|
4460
|
+
throw new Error(`connector session state ${path} is malformed`);
|
|
4450
4461
|
}
|
|
4451
4462
|
readManagedSessionState(a) {
|
|
4452
4463
|
const path = a.restart?.sessionStatePath;
|
|
@@ -4463,7 +4474,10 @@ export class Manager {
|
|
|
4463
4474
|
return undefined;
|
|
4464
4475
|
const path = join(this.workspaceRoot, ".cotal", "pi-sessions", `${entry.name}-${entry.identity.lifecycleUid}.json`);
|
|
4465
4476
|
try {
|
|
4466
|
-
|
|
4477
|
+
const state = this.readSessionStatePath(path);
|
|
4478
|
+
if (state.status === "prompt")
|
|
4479
|
+
throw new Error(`${state.connector} is waiting at prompt ${JSON.stringify(state.prompt)} for directory ${JSON.stringify(state.cwd)}`);
|
|
4480
|
+
return state.sessionId;
|
|
4467
4481
|
}
|
|
4468
4482
|
catch (error) {
|
|
4469
4483
|
throw new Error(`retained ${connector.name} agent ${entry.name} has no sessionId in its older inventory and no usable upgrade state at ${path} ` +
|
|
@@ -4535,7 +4549,10 @@ export class Manager {
|
|
|
4535
4549
|
this.freeSlot(a, true, "carried-fork-refused");
|
|
4536
4550
|
}
|
|
4537
4551
|
readManagedSession(a) {
|
|
4538
|
-
|
|
4552
|
+
const state = this.readManagedSessionState(a);
|
|
4553
|
+
if (state.status === "prompt")
|
|
4554
|
+
throw new Error(`${state.connector} is waiting at prompt ${JSON.stringify(state.prompt)} for directory ${JSON.stringify(state.cwd)}`);
|
|
4555
|
+
return state.sessionId;
|
|
4539
4556
|
}
|
|
4540
4557
|
/** Re-record an exact seat's assignment at a proven session, so crash recovery, a preserved resume
|
|
4541
4558
|
* and the next manifest launch never pick different sessions. */
|
|
@@ -4558,18 +4575,26 @@ export class Manager {
|
|
|
4558
4575
|
async awaitManagedSessionState(a) {
|
|
4559
4576
|
const deadline = Date.now() + 15_000;
|
|
4560
4577
|
let last = "session state not written yet";
|
|
4578
|
+
let prompt;
|
|
4579
|
+
const promptFailure = () => prompt
|
|
4580
|
+
? `${prompt.connector} is waiting at prompt ${JSON.stringify(prompt.prompt)} for directory ${JSON.stringify(prompt.cwd)}`
|
|
4581
|
+
: undefined;
|
|
4561
4582
|
while (Date.now() < deadline) {
|
|
4562
|
-
if (a.handle.status() === "exited")
|
|
4563
|
-
throw new Error("process exited before writing session state");
|
|
4564
4583
|
try {
|
|
4565
|
-
|
|
4584
|
+
const state = this.readManagedSessionState(a);
|
|
4585
|
+
if (state.status === "prompt")
|
|
4586
|
+
prompt = state;
|
|
4587
|
+
else
|
|
4588
|
+
return state;
|
|
4566
4589
|
}
|
|
4567
4590
|
catch (error) {
|
|
4568
4591
|
last = rejectionText(error);
|
|
4569
4592
|
}
|
|
4593
|
+
if (a.handle.status() === "exited")
|
|
4594
|
+
throw new Error(promptFailure() ?? "process exited before writing session state");
|
|
4570
4595
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4571
4596
|
}
|
|
4572
|
-
throw new Error(`connector did not write session state (${last})`);
|
|
4597
|
+
throw new Error(promptFailure() ?? `connector did not write session state (${last})`);
|
|
4573
4598
|
}
|
|
4574
4599
|
/** Bind a continuation-capable connector only after its process is ready. The file proves the
|
|
4575
4600
|
* latest in-process session (including Pi /resume); the authenticated socket proves this process
|
|
@@ -5000,22 +5025,26 @@ export class Manager {
|
|
|
5000
5025
|
catch (error) {
|
|
5001
5026
|
const reason = rejectionText(error);
|
|
5002
5027
|
this.bootNotes.push(`! manager boot: connector inventory unavailable - ${reason}`);
|
|
5003
|
-
this.connectorStatuses = [{ agent: "(inventory)", state: "unavailable", binaries: {}, reason }];
|
|
5028
|
+
this.connectorStatuses = [{ agent: "(inventory)", state: "unavailable", binaries: {}, eventPlane: false, reason }];
|
|
5004
5029
|
this.bootNotes.push("! manager: no agent application is available here, so a spawn without --on goes to another manager in this space");
|
|
5005
5030
|
return;
|
|
5006
5031
|
}
|
|
5007
5032
|
}
|
|
5008
5033
|
else {
|
|
5009
|
-
declared = registry.all("connector").map((connector) => ({
|
|
5034
|
+
declared = registry.all("connector").map((connector) => ({
|
|
5035
|
+
name: connector.name,
|
|
5036
|
+
requires: connector.requires ?? [],
|
|
5037
|
+
eventPlane: connector.eventChannel !== undefined,
|
|
5038
|
+
}));
|
|
5010
5039
|
}
|
|
5011
|
-
for (const { name, requires } of declared) {
|
|
5040
|
+
for (const { name, requires, eventPlane } of declared) {
|
|
5012
5041
|
const { binaries, reason } = resolveHarness(name, requires);
|
|
5013
5042
|
if (reason) {
|
|
5014
|
-
rows.push({ agent: name, state: "unavailable", binaries, reason });
|
|
5043
|
+
rows.push({ agent: name, state: "unavailable", binaries, eventPlane, reason });
|
|
5015
5044
|
this.bootNotes.push(`! manager boot: connector ${name} unavailable - ${reason}`);
|
|
5016
5045
|
}
|
|
5017
5046
|
else {
|
|
5018
|
-
rows.push({ agent: name, state: "available", binaries });
|
|
5047
|
+
rows.push({ agent: name, state: "available", binaries, eventPlane });
|
|
5019
5048
|
}
|
|
5020
5049
|
}
|
|
5021
5050
|
this.connectorStatuses = rows.sort((a, b) => a.agent.localeCompare(b.agent));
|
|
@@ -10594,6 +10623,62 @@ export class Manager {
|
|
|
10594
10623
|
}
|
|
10595
10624
|
return { ok: true, data: { name, path } };
|
|
10596
10625
|
}
|
|
10626
|
+
/** Remove one machine-local persona definition without touching any running lifecycle that was
|
|
10627
|
+
* launched from it. Removal accepts the catalog's safe filename alphabet without applying the
|
|
10628
|
+
* current spawn-name length or user-mode actor rules, so an obsolete invalid-name entry remains
|
|
10629
|
+
* repairable. Ownership matches redefine: the recorded owner, or admin for an ownerless file. */
|
|
10630
|
+
opRemovePersona(args, caller, admin) {
|
|
10631
|
+
if (this.execution === "none")
|
|
10632
|
+
throw new EpEnvelopeError("unimplemented", this.executionRefusal());
|
|
10633
|
+
const name = String(args.name ?? "").trim();
|
|
10634
|
+
if (!name)
|
|
10635
|
+
return { ok: false, code: "bad-request", error: "name required" };
|
|
10636
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name))
|
|
10637
|
+
return { ok: false, code: "bad-request", error: `unsafe persona name ${JSON.stringify(name)} (allowed: letters, digits, _ -)` };
|
|
10638
|
+
const path = agentFilePath(this.workspaceRoot, name);
|
|
10639
|
+
if (!existsSync(path))
|
|
10640
|
+
return { ok: false, code: "not-found", error: `no persona "${name}"` };
|
|
10641
|
+
let owner;
|
|
10642
|
+
try {
|
|
10643
|
+
owner = loadAgentFile(path).owner;
|
|
10644
|
+
}
|
|
10645
|
+
catch {
|
|
10646
|
+
// A catalog entry may be invalid for launch solely because an older release admitted a name
|
|
10647
|
+
// above today's identity limit. Read only its frontmatter owner without asking the launch
|
|
10648
|
+
// parser to accept the identity. This does not make the file spawnable or readable.
|
|
10649
|
+
try {
|
|
10650
|
+
const source = readFileSync(path, "utf8").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
10651
|
+
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(source);
|
|
10652
|
+
if (!match)
|
|
10653
|
+
throw new Error("missing frontmatter");
|
|
10654
|
+
const frontmatter = parseYaml(match[1]);
|
|
10655
|
+
if (!frontmatter || typeof frontmatter !== "object" || Array.isArray(frontmatter))
|
|
10656
|
+
throw new Error("frontmatter is not a mapping");
|
|
10657
|
+
const fields = frontmatter;
|
|
10658
|
+
if (String(fields.name ?? "") !== name)
|
|
10659
|
+
throw new Error("frontmatter name differs from the catalog entry");
|
|
10660
|
+
if (fields.owner !== undefined && typeof fields.owner !== "string")
|
|
10661
|
+
throw new Error("owner is not a string");
|
|
10662
|
+
owner = fields.owner;
|
|
10663
|
+
}
|
|
10664
|
+
catch {
|
|
10665
|
+
return { ok: false, code: "not-found", error: `no persona "${name}"` };
|
|
10666
|
+
}
|
|
10667
|
+
}
|
|
10668
|
+
if (!admin && owner !== caller) {
|
|
10669
|
+
const heldBy = owner ? `owned by ${owner}` : "operator-owned (legacy file - no agent owner)";
|
|
10670
|
+
return { ok: false, code: "permission-denied", error: `not authorized to remove ${name}: ${heldBy}; only its owner or an operator can` };
|
|
10671
|
+
}
|
|
10672
|
+
try {
|
|
10673
|
+
rmSync(path);
|
|
10674
|
+
}
|
|
10675
|
+
catch (e) {
|
|
10676
|
+
if (!existsSync(path))
|
|
10677
|
+
return { ok: false, code: "not-found", error: `no persona "${name}"` };
|
|
10678
|
+
return { ok: false, error: rejectionText(e) };
|
|
10679
|
+
}
|
|
10680
|
+
return { ok: true, data: { name } };
|
|
10681
|
+
}
|
|
10597
10682
|
/** The owner a fresh wire-defined persona records. A user-auth caller's principal is stable, and
|
|
10598
10683
|
* so is one of this manager's own seats for its life. Any other static caller, such as the CLI's
|
|
10599
10684
|
* operator instrument minted for one call, would leave an owner no later call matches, so it
|