@cotal-ai/manager 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/manager.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { createHash, randomUUID, randomBytes } from "node:crypto";
3
3
  import { hostname } from "node:os";
4
- import { connect, credsAuthenticator } from "@nats-io/transport-node";
4
+ import { credsAuthenticator } from "@nats-io/transport-node";
5
5
  import { existsSync, lstatSync, readFileSync, rmSync } from "node:fs";
6
6
  import { join, dirname, resolve } from "node:path";
7
- import { CotalEndpoint, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, agentFilePath, clearSpaceHistory, connectorServers, spawnEnvAllow, deprovisionAgent, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, mintCreds, mintLifecycleUid, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, parseShareSelection, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, epRequestSubject, epCallerReplyFilter, parseEpSubject, controlServiceSubject, eventChannelPrincipal, } from "@cotal-ai/core";
7
+ import { CotalEndpoint, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, mintCreds, mintLifecycleUid, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, parseShareSelection, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, epRequestSubject, epCallerReplyFilter, parseEpSubject, controlServiceSubject, eventChannelPrincipal, } from "@cotal-ai/core";
8
8
  import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, connectorInstallHint, DEFAULT_CONNECTOR, defaultAgentType, DELIVERY_CREDS_KIND, extensionConnectors, findCotalRoot, getSpaceAuth, hasUserAuthState, loadExtensionsManifest, loadManagerInstanceIdentity, loadMeshes, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KIND, mergeLaunchOptions, remintDaemonCreds, resolveOnPath, saveManagerInstanceIdentity, spaceMaterialKey, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
9
9
  import { createRuntime, requireRuntimeAdopt, } from "./runtime/index.js";
10
10
  import { AttachEndpoint } from "./attach-endpoint.js";
@@ -462,6 +462,17 @@ export class Manager {
462
462
  resumeDurableCommitToken;
463
463
  resumedAgentNames = new Set();
464
464
  remoteAuthority;
465
+ /** Every broker dial this manager makes, through ONE transport decision.
466
+ *
467
+ * `this.servers` is whatever the mesh record held (`cotal supervise` reads it from there), so it
468
+ * can be a `ws://`/`wss://` broker published through an HTTPS edge. The raw node transport
469
+ * refuses such a URL instead of dialing it, so the scheme picks the dialer here rather than at
470
+ * thirteen call sites. `dialerFor` also owns the websocket transport's options, which is why
471
+ * callers below still pass their own auth options unchanged. */
472
+ dial(opts) {
473
+ const servers = this.servers ?? DEFAULT_SERVER;
474
+ return dialerFor(servers)({ ...opts, servers });
475
+ }
465
476
  constructor(opts) {
466
477
  this.space = opts.space;
467
478
  this.servers = opts.servers;
@@ -1154,8 +1165,6 @@ export class Manager {
1154
1165
  async awaitHandleExit(handle) {
1155
1166
  if (!handle.waitForExit)
1156
1167
  throw new Error(`runtime "${handle.kind}" cannot prove child exit (AgentHandle.waitForExit is not implemented)`);
1157
- if (handle.status() === "exited")
1158
- return;
1159
1168
  await withTimeout(handle.waitForExit(), this.preserveStopTimeoutMs, `child did not exit within ${this.preserveStopTimeoutMs}ms`);
1160
1169
  if (handle.status() !== "exited")
1161
1170
  throw new Error(`runtime "${handle.kind}" reported exit completion but status is still running`);
@@ -2540,7 +2549,7 @@ export class Manager {
2540
2549
  const creds = await mintCreds(this.auth, newIdentity(), "retirement-requester", {
2541
2550
  retirementRequester: { ...caller, target: { owner: target.owner, actor: target.actor, lifecycleUid: a.lifecycleUid } },
2542
2551
  });
2543
- const nc = await connect({ servers: this.servers ?? DEFAULT_SERVER, authenticator: credsAuthenticator(new TextEncoder().encode(creds)), maxReconnectAttempts: 0 });
2552
+ const nc = await this.dial({ authenticator: credsAuthenticator(new TextEncoder().encode(creds)), maxReconnectAttempts: 0 });
2544
2553
  try {
2545
2554
  // §13.2 nonce: >=128 bits of CSPRNG entropy, base64url (the `endpoint-invoke` idiom).
2546
2555
  const nonce = randomBytes(24).toString("base64url");
@@ -4444,7 +4453,12 @@ export class Manager {
4444
4453
  // └──────────────────────────────────────────────────────────────────────────────────────┘
4445
4454
  const deliberate = a.terminalizing === true;
4446
4455
  void (async () => {
4447
- const tail = this.tail(await s.backlog());
4456
+ // waitForExit may close the attach stream before this snapshot
4457
+ let tail = "";
4458
+ try {
4459
+ tail = this.tail(await s.backlog());
4460
+ }
4461
+ catch { }
4448
4462
  if (opts.reapOnExit !== false)
4449
4463
  this.onAgentExit(a);
4450
4464
  // A DELIBERATE STOP IS NOT A LAUNCH FAILURE. The despawn path owns this goal's terminal
@@ -4569,7 +4583,7 @@ export class Manager {
4569
4583
  const creds = await mintCreds(this.auth, identity, "lifecycle-executor", {
4570
4584
  lifecycleExecutor: { owner: pin.owner, actor: pin.actor, lifecycleUid: pin.lifecycleUid, alias: pin.alias },
4571
4585
  });
4572
- const nc = await connect({ servers: this.servers ?? DEFAULT_SERVER, ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
4586
+ const nc = await this.dial({ ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
4573
4587
  try {
4574
4588
  const kvm = new Kvm(nc);
4575
4589
  const recordsKv = await kvm.open(recordsBucket(this.space));
@@ -4594,7 +4608,7 @@ export class Manager {
4594
4608
  : undefined);
4595
4609
  if (!creds)
4596
4610
  throw new Error("withEndpointServeExecutor: no scoped executor authority (an open mesh must use the bare path)");
4597
- const nc = await connect({ servers: this.servers ?? DEFAULT_SERVER, ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
4611
+ const nc = await this.dial({ ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
4598
4612
  try {
4599
4613
  const kvm = new Kvm(nc);
4600
4614
  return await fn({ recordsKv: await kvm.open(recordsBucket(this.space)), authKv: await kvm.open(epAuthBucket(this.space)), nc });
@@ -4610,7 +4624,7 @@ export class Manager {
4610
4624
  async withOpenServeConnection(fn) {
4611
4625
  if (this.auth)
4612
4626
  throw new Error("withOpenServeConnection: an auth mesh must use the scoped endpoint-serve executor");
4613
- const nc = await connect({ servers: this.servers ?? DEFAULT_SERVER, maxReconnectAttempts: 0 });
4627
+ const nc = await this.dial({ maxReconnectAttempts: 0 });
4614
4628
  try {
4615
4629
  const kvm = new Kvm(nc);
4616
4630
  // An open mesh may be a RAW broker (no `cotal up` provisioning ran), and `Kvm.open` binds
@@ -4629,7 +4643,7 @@ export class Manager {
4629
4643
  return {
4630
4644
  instanceId: this.managerInstanceId,
4631
4645
  runtime: this.runtime.kind,
4632
- custody: "legacy",
4646
+ custody: process.platform === "linux" && this.runtime.kind === "pty" ? "custodied" : "legacy",
4633
4647
  agentCount: this.agents.size,
4634
4648
  uptimeMs: Date.now() - this.startedAtMs,
4635
4649
  connectors: this.connectorStatuses.map((row) => ({ ...row, binaries: { ...row.binaries } })),
@@ -4710,8 +4724,7 @@ export class Manager {
4710
4724
  creds: remote.serveCreds,
4711
4725
  };
4712
4726
  const enc = new TextEncoder();
4713
- const nc = await connect({
4714
- servers: this.servers ?? DEFAULT_SERVER,
4727
+ const nc = await this.dial({
4715
4728
  authenticator: (nonce) => credsAuthenticator(enc.encode(state.creds))(nonce),
4716
4729
  inboxPrefix: `_INBOX_${state.identity.id}`,
4717
4730
  maxReconnectAttempts: -1,
@@ -4744,7 +4757,7 @@ export class Manager {
4744
4757
  {
4745
4758
  // Open mesh: the bare connection holds the rights (there is no credential system to mint from).
4746
4759
  const provCreds = auth ? await mintCreds(auth, newIdentity(), "provisioner") : undefined;
4747
- const provNc = await connect({ servers: this.servers ?? DEFAULT_SERVER, ...standaloneConnectOpts({ creds: provCreds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
4760
+ const provNc = await this.dial({ ...standaloneConnectOpts({ creds: provCreds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
4748
4761
  try {
4749
4762
  // P2 item 2: the manager now WRITES goal facts (EPF) + progress events (EPE), so the §13.12
4750
4763
  // endpoint streams must exist. Nothing provisioned them before spawn-as-action (no endpoint
@@ -4889,8 +4902,7 @@ export class Manager {
4889
4902
  // registered surface for its whole incarnation.
4890
4903
  const state = { handle: undefined, nc: undefined, identity: serveIdentity, grant, creds };
4891
4904
  const enc = new TextEncoder();
4892
- const nc = await connect({
4893
- servers: this.servers ?? DEFAULT_SERVER,
4905
+ const nc = await this.dial({
4894
4906
  // Open mesh: a bare serve connection (no credential exists; the broker enforces nothing).
4895
4907
  ...(creds !== undefined ? { authenticator: (nonce) => credsAuthenticator(enc.encode(state.creds))(nonce) } : {}),
4896
4908
  inboxPrefix: `_INBOX_${serveIdentity.id}`,
@@ -4985,8 +4997,7 @@ export class Manager {
4985
4997
  // The mutable holder captured by the authenticator (mirrors the serve connection): a half-TTL
4986
4998
  // renewal updates `gw.creds` and the next (re)connect presents the refreshed credential.
4987
4999
  const gw = { nc: undefined, ctx: undefined, creds: (this.auth || this.remoteAuthority) ? this.goalWriterCreds : undefined, identity };
4988
- const nc = await connect({
4989
- servers: this.servers ?? DEFAULT_SERVER,
5000
+ const nc = await this.dial({
4990
5001
  ...((this.auth || this.remoteAuthority) ? { authenticator: (nonce) => credsAuthenticator(enc.encode(gw.creds))(nonce) } : {}),
4991
5002
  inboxPrefix: `_INBOX_${identity.id}`,
4992
5003
  maxReconnectAttempts: -1,
@@ -5091,8 +5102,7 @@ export class Manager {
5091
5102
  // The mutable holder captured by the authenticator (mirrors the goal-writer): a half-TTL renewal
5092
5103
  // updates `sw.creds` and the next (re)connect presents the refreshed credential.
5093
5104
  const sw = { nc: undefined, creds: (this.auth || this.remoteAuthority) ? this.sessionLedgerCreds : undefined };
5094
- const nc = await connect({
5095
- servers: this.servers ?? DEFAULT_SERVER,
5105
+ const nc = await this.dial({
5096
5106
  ...((this.auth || this.remoteAuthority) ? { authenticator: (nonce) => credsAuthenticator(enc.encode(sw.creds))(nonce) } : {}),
5097
5107
  inboxPrefix: `_INBOX_${identity.id}`,
5098
5108
  maxReconnectAttempts: -1,
@@ -5263,7 +5273,7 @@ export class Manager {
5263
5273
  // FAIL LOUD: there is deliberately no shared connection to fall back to. Serving a session
5264
5274
  // without its own credential is exactly the standing-writer shape this design removes.
5265
5275
  const opts = (this.auth || this.remoteAuthority) ? standaloneConnectOpts({ creds: cred.creds, /* not yet wired to a recorded transport */ tls: false }) : {};
5266
- return connect({ servers: this.servers ?? DEFAULT_SERVER, ...opts, maxReconnectAttempts: -1 });
5276
+ return this.dial({ ...opts, maxReconnectAttempts: -1 });
5267
5277
  },
5268
5278
  revoke: async (credentialId) => {
5269
5279
  if (!this.auth && !this.remoteAuthority)
@@ -5318,8 +5328,8 @@ export class Manager {
5318
5328
  try {
5319
5329
  let entries = [];
5320
5330
  const nc = this.auth
5321
- ? await connect({ servers: this.servers ?? DEFAULT_SERVER, ...standaloneConnectOpts({ creds: await mintCreds(this.auth, newIdentity(), "provisioner"), /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 })
5322
- : await connect({ servers: this.servers ?? DEFAULT_SERVER, maxReconnectAttempts: 0 });
5331
+ ? await this.dial({ ...standaloneConnectOpts({ creds: await mintCreds(this.auth, newIdentity(), "provisioner"), /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 })
5332
+ : await this.dial({ maxReconnectAttempts: 0 });
5323
5333
  try {
5324
5334
  const kvm = new Kvm(nc);
5325
5335
  await ensureAuthorityStores(await jetstreamManager(nc), kvm, this.space);
@@ -6281,7 +6291,7 @@ export class Manager {
6281
6291
  return;
6282
6292
  const identity = newIdentity();
6283
6293
  const creds = await mintCreds(this.auth, identity, "provisioner");
6284
- const nc = await connect({ servers: this.servers ?? DEFAULT_SERVER, ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
6294
+ const nc = await this.dial({ ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
6285
6295
  const slotRows = [];
6286
6296
  try {
6287
6297
  const jsm = await jetstreamManager(nc);