@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/commands.js +2 -2
- package/dist/commands.js.map +1 -1
- package/dist/manager.d.ts +2 -0
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +56 -25
- package/dist/manager.js.map +1 -1
- package/dist/pending-enrollment.d.ts +24 -0
- package/dist/pending-enrollment.d.ts.map +1 -0
- package/dist/pending-enrollment.js +37 -0
- package/dist/pending-enrollment.js.map +1 -0
- package/dist/remote-authority.d.ts +1 -1
- package/dist/remote-authority.d.ts.map +1 -1
- package/dist/remote-authority.js +2 -2
- package/dist/remote-authority.js.map +1 -1
- package/package.json +13 -13
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
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
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
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
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
|
-
//
|
|
4354
|
-
//
|
|
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
|
-
|
|
4363
|
-
|
|
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
|