@cotal-ai/manager 0.157.6 → 0.157.7

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
@@ -18,6 +18,7 @@ import { authorizeLaunch, authorizeNamedControl } from "./authorize.js";
18
18
  import { controlShutdown } from "./control-shutdown.js";
19
19
  import { RunHosting, servedRequestSubject } from "./run-hosting.js";
20
20
  import { enrolledAcl } from "./remote-authority.js";
21
+ import { pendingEnrollment } from "./pending-enrollment.js";
21
22
  import { rejectionText, attemptCleanup, describeCleanupFailures } from "@cotal-ai/core";
22
23
  import { controlSession } from "./control-session.js";
23
24
  import { MAX_AGENTS, parseResumeCommitArgs, parseResumeControlArgs, parseResumeFinalizeArgs, readReleasedSeatReference, readSeatRecord, removeReleasedSeatReference, removeSeatRecord, supervisePolicy, writeReleasedSeatReference, writeSeatRecord } from "./resume.js";
@@ -4282,33 +4283,54 @@ export class Manager {
4282
4283
  return { error: rejectionText(e) };
4283
4284
  }
4284
4285
  const secrets = this.secrets;
4285
- // The token the agent will present to `POST /exchange` for the rest of its life. Generated
4286
- // here, hashed here, and never sent: the request carries `tokenHash` alone.
4287
- const actorToken = randomBytes(32).toString("base64url");
4286
+ const input = {
4287
+ target: {
4288
+ actor: name,
4289
+ ...(opts.role !== undefined ? { role: opts.role } : {}),
4290
+ label: opts.label,
4291
+ ...(opts.capabilities !== undefined ? { capabilities: opts.capabilities } : {}),
4292
+ ...(opts.subscribe !== undefined ? { subscribe: opts.subscribe } : {}),
4293
+ allowSubscribe: opts.allowSubscribe,
4294
+ ...(opts.allowPublish !== undefined ? { allowPublish: opts.allowPublish } : {}),
4295
+ },
4296
+ ...(opts.spawnRequest !== undefined ? { spawn: opts.spawnRequest } : {}),
4297
+ };
4298
+ // An unanswered enrollment may already hold the host's alias. Keep its request and standing
4299
+ // token together until preflight succeeds, rather than minting a conflicting successor.
4300
+ let pending;
4301
+ if (!opts.delegatedIntent) {
4302
+ try {
4303
+ pending = await pendingEnrollment({ store: secrets, space: this.space,
4304
+ instanceId: this.managerInstanceId, actor: name, input,
4305
+ authority: { target: input.target, owner: this.remoteAuthority.owner,
4306
+ managerLifecycleUid: this.remoteAuthority.lifecycleUid, specOwner: opts.specOwner, spawner: opts.spawner,
4307
+ caller: opts.enrollmentCaller, runId: opts.spawnRequest?.runId } });
4308
+ }
4309
+ catch (e) {
4310
+ return { error: `agent auth preflight failed for "${name}": ${rejectionText(e)}` };
4311
+ }
4312
+ }
4313
+ const actorToken = pending?.record.actorToken ?? randomBytes(32).toString("base64url");
4288
4314
  const tokenHash = createHash("sha256").update(actorToken, "utf8").digest("hex");
4289
4315
  // Provisional paths under the participant's OWN uid: the token has to be on disk before the
4290
4316
  // request (a host that granted against a digest whose plaintext was lost would leave an agent
4291
4317
  // that can never exchange), and the host's uid is not known until the request answers. The
4292
4318
  // family is re-keyed to the host's uid below.
4293
- const staged = agentLifecycleSecretFilePaths(this.workspaceRoot, this.space, name, opts.lifecycleUid);
4319
+ const staged = agentLifecycleSecretFilePaths(this.workspaceRoot, this.space, name, pending?.record.lifecycleUid ?? opts.lifecycleUid);
4294
4320
  let files = staged;
4295
4321
  let material;
4322
+ let sent = false;
4296
4323
  try {
4297
4324
  await secrets.put(agentSecretKeyForFile(staged.actorToken, this.space), actorToken);
4298
4325
  await materializeSecretToFile(secrets, agentSecretKeyForFile(staged.actorToken, this.space), staged.actorToken);
4299
- material = await enroll({
4300
- target: {
4301
- actor: name,
4302
- tokenHash,
4303
- ...(opts.role !== undefined ? { role: opts.role } : {}),
4304
- label: opts.label,
4305
- ...(opts.capabilities !== undefined ? { capabilities: opts.capabilities } : {}),
4306
- ...(opts.subscribe !== undefined ? { subscribe: opts.subscribe } : {}),
4307
- allowSubscribe: opts.allowSubscribe,
4308
- ...(opts.allowPublish !== undefined ? { allowPublish: opts.allowPublish } : {}),
4309
- },
4310
- ...(opts.spawnRequest !== undefined ? { spawn: opts.spawnRequest } : {}),
4311
- });
4326
+ const saved = pending?.record.input ?? input;
4327
+ if (canonicalJson(saved.target) !== canonicalJson(input.target))
4328
+ throw new Error(`pending enrollment for "${name}" names different target authority`);
4329
+ sent = true;
4330
+ // The original observation may have expired, or vanished on a host restart before commit.
4331
+ // Same-authority retries keep the enrollment identity but carry this freshly served spawn.
4332
+ material = await enroll({ ...input, target: { ...saved.target, tokenHash },
4333
+ ...(pending ? { requestId: pending.record.requestId } : {}) });
4312
4334
  if (material.actor !== name)
4313
4335
  throw new Error(`the host enrolled actor "${material.actor}", not the requested "${name}"`);
4314
4336
  const specOwner = opts.specOwner ?? (opts.spawner ? parsePrincipalKey(opts.spawner)?.owner : undefined);
@@ -4341,6 +4363,8 @@ export class Manager {
4341
4363
  "--health-file", files.health,
4342
4364
  ];
4343
4365
  await runAgentBearer(bearerCmd);
4366
+ if (pending)
4367
+ await secrets.delete(pending.key);
4344
4368
  return {
4345
4369
  owner: material.owner,
4346
4370
  lifecycleUid: material.lifecycleUid,
@@ -4350,21 +4374,27 @@ export class Manager {
4350
4374
  };
4351
4375
  }
4352
4376
  catch (e) {
4353
- // Shred every local secret this attempt materialized. The HOST's rows are the host's to
4354
- // reconcile: a participant holds no writer, so it must not pretend to roll back a grant or a
4355
- // durable it never authored. A retry with the same requestId re-enters the same enrollment.
4377
+ // Remove launch-file materializations, but retain the pending token and request when the
4378
+ // host may have committed. The next same-authority spawn replays that enrollment.
4356
4379
  // A failed removal joins the refusal instead of throwing, so every removal is attempted and
4357
4380
  // the refusal still reaches the caller.
4358
4381
  const unshredded = [];
4359
4382
  for (const family of files.actorToken === staged.actorToken ? [staged] : [staged, files])
4360
4383
  await this.shredUserSecrets(unshredded, family);
4384
+ const rejected = material === undefined ? caughtEnvelope(e) : undefined;
4385
+ // Only a first, definitively refused request proves there was no earlier commit. A refusal
4386
+ // during replay cannot prove that an earlier unanswered attempt enrolled nothing.
4387
+ const discard = pending?.created && (!sent || rejected?.outcome === "not-executed");
4388
+ if (discard && pending) {
4389
+ const key = pending.key;
4390
+ await attemptCleanup(unshredded, "pending enrollment", () => secrets.delete(key));
4391
+ }
4392
+ const retained = pending && !discard;
4361
4393
  const leftover = unshredded.length ? `; cleanup failed: ${describeCleanupFailures(unshredded)}` : "";
4362
- // A host that refused the enrollment enrolled nothing, so once every staged secret is gone
4363
- // this attempt left nothing behind. A failure after the host answered, or a host that did not
4364
- // answer, may have left a grant there.
4365
- const refusal = material === undefined && !unshredded.length ? caughtEnvelope(e) : undefined;
4394
+ const remedy = retained ? `; the name "${name}" is held by this manager's earlier enrollment. Retry this persona with the same caller to replay it. Once it launches, stop it before changing its authority` : "";
4395
+ const refusal = !retained && material === undefined && !unshredded.length ? rejected : undefined;
4366
4396
  return {
4367
- error: `agent auth preflight failed for "${name}": ${rejectionText(e)}${leftover}`,
4397
+ error: `agent auth preflight failed for "${name}": ${rejectionText(e)}${leftover}${remedy}`,
4368
4398
  ...(refusal?.outcome === "not-executed" ? { refused: refusal.code, ...(refusal.details ? { details: refusal.details } : {}) } : {}),
4369
4399
  };
4370
4400
  }
@@ -6809,6 +6839,7 @@ export class Manager {
6809
6839
  capabilities,
6810
6840
  label: ref,
6811
6841
  lifecycleUid,
6842
+ enrollmentCaller: authenticatedCaller ? { owner: authenticatedCaller.owner, actor: authenticatedCaller.actor } : undefined,
6812
6843
  delegatedIntent: opts.delegatedIntent,
6813
6844
  ...(opts.served !== undefined ? { spawnRequest: this.servedSpawnRequest(opts.served, authenticatedCaller) } : {}),
6814
6845
  // Hold the name at the UID the host enrolled for the intent's user before any later local