@cotal-ai/manager 0.126.3 → 0.127.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/commands.d.ts.map +1 -1
- package/dist/commands.js +8 -1
- package/dist/commands.js.map +1 -1
- package/dist/manager-service-contract.d.ts +5 -1
- package/dist/manager-service-contract.d.ts.map +1 -1
- package/dist/manager-service-contract.js +21 -2
- package/dist/manager-service-contract.js.map +1 -1
- package/dist/manager.d.ts +22 -0
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +124 -18
- package/dist/manager.js.map +1 -1
- package/dist/remote-authority.d.ts +7 -1
- package/dist/remote-authority.d.ts.map +1 -1
- package/dist/remote-authority.js +42 -33
- package/dist/remote-authority.js.map +1 -1
- package/package.json +12 -12
package/dist/manager.js
CHANGED
|
@@ -261,6 +261,10 @@ function stopCauseText(cause) {
|
|
|
261
261
|
default: return cause;
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
|
+
const TURN_PARTICIPATION_TTL_MS = 5 * 60_000;
|
|
265
|
+
function participantKey(owner, actor, uid) {
|
|
266
|
+
return `${owner}.${actor}.${uid}`;
|
|
267
|
+
}
|
|
264
268
|
function parseTurnNote(raw) {
|
|
265
269
|
let o;
|
|
266
270
|
try {
|
|
@@ -544,6 +548,7 @@ export class Manager {
|
|
|
544
548
|
* window would hear `not-found` again.
|
|
545
549
|
*/
|
|
546
550
|
turnAcceptances = new Map();
|
|
551
|
+
participants = new Map();
|
|
547
552
|
/** Every relayed turn awaiting its seat's yield, by {@link goalKey}. The seat's `turn-pending` pull
|
|
548
553
|
* scans it; the deadline sweep drives expiry; a seat reap stamps its entries `seatDiedAt`, which
|
|
549
554
|
* the deadline terminal carries as `agentDownAt` so the run reads that death as L4002.
|
|
@@ -699,7 +704,7 @@ export class Manager {
|
|
|
699
704
|
throw new Error("execution: none requires boolean pooled and a non-null remote authority object when supplied");
|
|
700
705
|
if (opts.execution === "none" && (opts.runtime !== undefined || (!!opts.pooled !== !!opts.remoteAuthority)))
|
|
701
706
|
throw new Error("execution: none requires pooled signerless remote authority for remote composition, or genuine local authority, and no runtime option");
|
|
702
|
-
if (opts.execution === "none" && opts.remoteAuthority && ["renewExecutor", "renewStandingBundle", "mintSessionServing", "mintRetirementRequester", "prepareAgentRetirement", "validateRetainedAgent", "scanGoalIndex", "authorizeAdmin"].some((key) => typeof opts.remoteAuthority?.[key] !== "function"))
|
|
707
|
+
if (opts.execution === "none" && opts.remoteAuthority && ["renewExecutor", "renewStandingBundle", "mintSessionServing", "mintRetirementRequester", "prepareAgentRetirement", "validateRetainedAgent", "scanGoalIndex", "authorizeAdmin", "authorizeParticipation"].some((key) => typeof opts.remoteAuthority?.[key] !== "function"))
|
|
703
708
|
throw new Error("execution: none requires complete closed remote authority callbacks before construction");
|
|
704
709
|
if (opts.pooled && (!opts.remoteAuthority || (opts.execution !== "none" && (opts.runtime === undefined || opts.runtime === "auto" || opts.runtime === "pty"))))
|
|
705
710
|
throw new Error("pooled control requires signerless remote authority and an explicit non-PTY runtime; local custodial execution is forbidden");
|
|
@@ -3089,6 +3094,83 @@ export class Manager {
|
|
|
3089
3094
|
if (p.run !== args.runId || p.step !== args.stepKey || addressedToken !== open.token)
|
|
3090
3095
|
throw new EpEnvelopeError("permission-denied", `run-answer is allowed to ${agent.name} only for the open pause named by its pending ask or escalation relay; ${args.runId} ${args.stepKey} is not that pause`);
|
|
3091
3096
|
}
|
|
3097
|
+
/** Find one exact live presence row. Presence is not authority, but a participation lease with no
|
|
3098
|
+
* live addressee would authorize a durable turn nobody can receive. Duplicate exact rows are
|
|
3099
|
+
* refused rather than selected by order. */
|
|
3100
|
+
liveParticipantPresence(caller) {
|
|
3101
|
+
const principal = principalKey(caller.owner, caller.actor).key;
|
|
3102
|
+
const rows = this.ep.getRoster().filter((p) => p.card.id === principal && p.lifecycleUid === caller.uid && p.status !== "offline");
|
|
3103
|
+
if (rows.length !== 1)
|
|
3104
|
+
throw new EpEnvelopeError("expired", rows.length === 0
|
|
3105
|
+
? `personal participant ${principal} (lifecycle ${caller.uid}) has no exact live presence`
|
|
3106
|
+
: `personal participant ${principal} (lifecycle ${caller.uid}) has multiple exact live presence rows; no row was selected`, undefined, "not-executed");
|
|
3107
|
+
return rows[0];
|
|
3108
|
+
}
|
|
3109
|
+
/** Authorize and renew one personal native participant. Local user-auth authority reads the exact
|
|
3110
|
+
* managed foreground row fresh; a signerless manager asks its registered host. The lease is held
|
|
3111
|
+
* only in memory and is therefore renewed after reconnect or manager restart. */
|
|
3112
|
+
async serveTurnParticipation(caller) {
|
|
3113
|
+
if (this.execution === "none")
|
|
3114
|
+
throw new EpEnvelopeError("unimplemented", this.executionRefusal(), undefined, "not-executed");
|
|
3115
|
+
if (!/^pa_[a-z2-7]{26}$/.test(caller.actor))
|
|
3116
|
+
throw new EpEnvelopeError("permission-denied", "workflow turn participation is reserved for approved personal conversation actors", undefined, "not-executed");
|
|
3117
|
+
if (!this.remoteAuthority)
|
|
3118
|
+
throw new EpEnvelopeError("unimplemented", "personal workflow participation requires a registered issuing host with fresh lifecycle authority", undefined, "not-executed");
|
|
3119
|
+
if (!this.remoteAuthority.authorizeParticipation)
|
|
3120
|
+
throw new EpEnvelopeError("unimplemented", "the issuing host supplies no closed personal participation authorization", undefined, "not-executed");
|
|
3121
|
+
const authorized = await this.remoteAuthority.authorizeParticipation({ owner: caller.owner, actor: caller.actor, lifecycleUid: caller.uid });
|
|
3122
|
+
if (!authorized)
|
|
3123
|
+
throw new EpEnvelopeError("permission-denied", "the exact personal lifecycle is not currently foreground-authorized with run scope", undefined, "not-executed");
|
|
3124
|
+
const presence = this.liveParticipantPresence(caller);
|
|
3125
|
+
const expiresAt = Date.now() + TURN_PARTICIPATION_TTL_MS;
|
|
3126
|
+
const generation = caller.generation;
|
|
3127
|
+
const participant = {
|
|
3128
|
+
name: presence.card.name, owner: caller.owner, actor: caller.actor, uid: caller.uid,
|
|
3129
|
+
...(generation !== undefined ? { generation } : {}),
|
|
3130
|
+
expiresAt,
|
|
3131
|
+
};
|
|
3132
|
+
this.participants.set(participantKey(caller.owner, caller.actor, caller.uid), participant);
|
|
3133
|
+
return { owner: caller.owner, actor: caller.actor, lifecycleUid: caller.uid, name: participant.name,
|
|
3134
|
+
managerInstanceId: this.managerInstanceId, expiresAt };
|
|
3135
|
+
}
|
|
3136
|
+
currentParticipant(owner, actor, uid) {
|
|
3137
|
+
const key = participantKey(owner, actor, uid);
|
|
3138
|
+
const p = this.participants.get(key);
|
|
3139
|
+
if (!p)
|
|
3140
|
+
return undefined;
|
|
3141
|
+
if (p.expiresAt <= Date.now()) {
|
|
3142
|
+
this.participants.delete(key);
|
|
3143
|
+
return undefined;
|
|
3144
|
+
}
|
|
3145
|
+
try {
|
|
3146
|
+
this.liveParticipantPresence({ owner, actor, uid });
|
|
3147
|
+
}
|
|
3148
|
+
catch {
|
|
3149
|
+
this.participants.delete(key);
|
|
3150
|
+
return undefined;
|
|
3151
|
+
}
|
|
3152
|
+
return p;
|
|
3153
|
+
}
|
|
3154
|
+
/** A managed seat or a finite authorized personal participant. Only the first carries custody. */
|
|
3155
|
+
turnTarget(ctx) {
|
|
3156
|
+
if (this.execution === "none")
|
|
3157
|
+
throw new EpEnvelopeError("unimplemented", this.executionRefusal());
|
|
3158
|
+
const t = ctx.request.target;
|
|
3159
|
+
const managed = this.findManagedByTarget(t);
|
|
3160
|
+
if (managed)
|
|
3161
|
+
return { kind: "managed", agent: managed, name: managed.name };
|
|
3162
|
+
const participant = this.currentParticipant(t.owner, t.actor, t.lifecycleUid);
|
|
3163
|
+
if (participant)
|
|
3164
|
+
return { kind: "personal", participant, name: participant.name };
|
|
3165
|
+
throw new EpEnvelopeError("expired", `target ${t.owner}.${t.actor} (lifecycle ${t.lifecycleUid}) is neither a live managed agent nor a current personal participant of this manager`, undefined, "not-executed");
|
|
3166
|
+
}
|
|
3167
|
+
/** Resolve whether an exact turn executor lifecycle is current. */
|
|
3168
|
+
turnExecutorEpoch(name, owner, actor, uid) {
|
|
3169
|
+
const managed = this.agents.get(name);
|
|
3170
|
+
if (managed && this.managedPrincipal(managed) === principalKey(owner, actor).key && managed.lifecycleUid === uid)
|
|
3171
|
+
return 0;
|
|
3172
|
+
return this.currentParticipant(owner, actor, uid)?.name === name ? 0 : null;
|
|
3173
|
+
}
|
|
3092
3174
|
/** A baseline seat may amend only an answer recorded under its own name: it changes its own
|
|
3093
3175
|
* position, never another answerer's. Explicit `run` holders and unmanaged operator instruments
|
|
3094
3176
|
* keep the reach they have to answer, as {@link authorizeRunAnswer} does. */
|
|
@@ -3292,12 +3374,33 @@ export class Manager {
|
|
|
3292
3374
|
// caller must hold owner-equality or admin over the TARGET seat — written out for the same
|
|
3293
3375
|
// reason `input` is (a shared policy, not a shared body).
|
|
3294
3376
|
turn: (ctx) => this.serveGated(ctx, async () => {
|
|
3295
|
-
const
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3377
|
+
const target = this.turnTarget(ctx);
|
|
3378
|
+
if (target.kind === "managed") {
|
|
3379
|
+
const denied = await this.authorizeNamed(target.agent, callerOf(ctx), await this.epAnyModeAdmin(ctx), ctx.subject.caller);
|
|
3380
|
+
if (denied)
|
|
3381
|
+
throw new EpEnvelopeError("permission-denied", denied, undefined, "not-executed");
|
|
3382
|
+
}
|
|
3383
|
+
else {
|
|
3384
|
+
const admitted = this.runHosting?.admittedCaller(ctx.subject.caller);
|
|
3385
|
+
if (!admitted || admitted.owner !== target.participant.owner || admitted.actor !== target.participant.actor || admitted.uid !== target.participant.uid)
|
|
3386
|
+
throw new EpEnvelopeError("permission-denied", "a personal participant may be turned only by the workflow run admitted for that exact owner, actor and lifecycle", undefined, "not-executed");
|
|
3387
|
+
if (!this.remoteAuthority?.authorizeParticipation)
|
|
3388
|
+
throw new EpEnvelopeError("unimplemented", "the issuing host supplies no closed personal participation authorization", undefined, "not-executed");
|
|
3389
|
+
const authorized = await this.remoteAuthority.authorizeParticipation({
|
|
3390
|
+
owner: target.participant.owner,
|
|
3391
|
+
actor: target.participant.actor,
|
|
3392
|
+
lifecycleUid: target.participant.uid,
|
|
3393
|
+
});
|
|
3394
|
+
if (!authorized) {
|
|
3395
|
+
this.participants.delete(participantKey(target.participant.owner, target.participant.actor, target.participant.uid));
|
|
3396
|
+
throw new EpEnvelopeError("permission-denied", "the exact personal lifecycle is no longer foreground-authorized with run scope", undefined, "not-executed");
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
return this.serveTurnGoal(ctx, target);
|
|
3300
3400
|
}),
|
|
3401
|
+
// A personal native lifecycle must opt in explicitly. The command is self-targeted and returns
|
|
3402
|
+
// only a finite relay lease. It never inserts a ManagedAgent or grants stop/attach/input custody.
|
|
3403
|
+
turnParticipate: (ctx) => this.serveGated(ctx, () => this.serveTurnParticipation(ctx.subject.caller)),
|
|
3301
3404
|
// The seat's own half of the relay (`manager.self`, stop-self's tier): a seat pulls the
|
|
3302
3405
|
// turns addressed to ITS incarnation and yields them; no reach beyond itself exists here.
|
|
3303
3406
|
turnPending: (ctx) => this.serveGated(ctx, () => this.turnPendingFor(ctx.subject.caller)),
|
|
@@ -7592,7 +7695,8 @@ export class Manager {
|
|
|
7592
7695
|
for (const a of this.agents.values())
|
|
7593
7696
|
if (a.id === key)
|
|
7594
7697
|
return { lifecycleUid: a.lifecycleUid, mappingRevision: 0 };
|
|
7595
|
-
|
|
7698
|
+
const participant = [...this.participants.values()].find((p) => p.owner === t.owner && p.actor === t.actor && this.currentParticipant(p.owner, p.actor, p.uid) === p);
|
|
7699
|
+
return participant ? { lifecycleUid: participant.uid, mappingRevision: 0 } : undefined;
|
|
7596
7700
|
}
|
|
7597
7701
|
if (t.owner === DEV_OWNER) {
|
|
7598
7702
|
for (const a of this.agents.values())
|
|
@@ -8785,7 +8889,7 @@ export class Manager {
|
|
|
8785
8889
|
* path (idempotent retry map, index-CAS-before-bind, create-only bindGoal) but runs it INLINE:
|
|
8786
8890
|
* there is no launch to hand off, so the accept either completes durably or unwinds its own
|
|
8787
8891
|
* bound goal with a `failed` terminal before refusing. */
|
|
8788
|
-
async serveTurnGoal(ctx,
|
|
8892
|
+
async serveTurnGoal(ctx, target) {
|
|
8789
8893
|
const gw = this.goalWriter;
|
|
8790
8894
|
if (!gw)
|
|
8791
8895
|
throw new EpEnvelopeError("unavailable", "the manager goal-writer connection is not standing; turn-as-action cannot accept (SPEC 13.6)");
|
|
@@ -8795,6 +8899,7 @@ export class Manager {
|
|
|
8795
8899
|
if (!this.goalReconcileDone)
|
|
8796
8900
|
throw new EpEnvelopeError("unavailable", "the manager is still reconciling accepted goals at boot; retry shortly (SPEC 13.6)");
|
|
8797
8901
|
const t = ctx.request.target; // targeted command: the serve boundary enforced presence + currency
|
|
8902
|
+
const name = target.name;
|
|
8798
8903
|
const goalId = ctx.request.id;
|
|
8799
8904
|
const { fingerprint } = submissionFingerprint(ctx.request, ctx.subject);
|
|
8800
8905
|
const ref = goalRefOf(ctx.subject, goalId);
|
|
@@ -8808,7 +8913,7 @@ export class Manager {
|
|
|
8808
8913
|
return prior;
|
|
8809
8914
|
}
|
|
8810
8915
|
// Refused here, before anything durable, so the caller's goal id stays free for another seat.
|
|
8811
|
-
this.assertSeatTurnFree(
|
|
8916
|
+
this.assertSeatTurnFree(name, t, goalId);
|
|
8812
8917
|
const raw = (ctx.request.args ?? {});
|
|
8813
8918
|
const payload = String(raw.payload);
|
|
8814
8919
|
const deadlineAt = raw.deadlineAt === undefined ? acceptedAt + Number(raw.deadlineMs) : Number(raw.deadlineAt);
|
|
@@ -8829,7 +8934,7 @@ export class Manager {
|
|
|
8829
8934
|
throw new EpEnvelopeError("failed-precondition", `goal "${goalId}" already ended ${ended.state}; a bound goal is never accepted again (SPEC 13.6)`);
|
|
8830
8935
|
// Index-CAS-before-bind, exactly as spawn: the entry (floor + note) is the durable relay
|
|
8831
8936
|
// record a successor rebuilds from, so it must exist before the acceptance is servable.
|
|
8832
|
-
const idx = await recordGoalIndex(gw.ctx, ref, executor.lifecycleUid, { name
|
|
8937
|
+
const idx = await recordGoalIndex(gw.ctx, ref, executor.lifecycleUid, { name, owner: t.owner, actor: t.actor, uid: t.lifecycleUid, readinessDeadlineMs: deadlineMs }, note);
|
|
8833
8938
|
if (!idx.recorded && idx.existing.iid !== executor.lifecycleUid)
|
|
8834
8939
|
throw new EpEnvelopeError("failed-precondition", `goal "${goalId}" was accepted by instance "${idx.existing.iid}"; that instance owns its relay and this attempt provisions nothing (SPEC 13.6)`);
|
|
8835
8940
|
const b = await bindGoal(gw.ctx, ref, fingerprint);
|
|
@@ -8877,7 +8982,7 @@ export class Manager {
|
|
|
8877
8982
|
});
|
|
8878
8983
|
// Again after the awaits, with no await before the maps are written: another caller's
|
|
8879
8984
|
// accept of this id to this seat may have finished meanwhile, and this unwinds the loser.
|
|
8880
|
-
this.assertSeatTurnFree(
|
|
8985
|
+
this.assertSeatTurnFree(name, t, goalId);
|
|
8881
8986
|
}
|
|
8882
8987
|
catch (e) {
|
|
8883
8988
|
// The accept is inline (no launch closure to fail later), so a post-bind throw unwinds HERE:
|
|
@@ -8894,7 +8999,7 @@ export class Manager {
|
|
|
8894
8999
|
ref, now: Date.now(), cause: "complete", state: "failed", data: { error: msg },
|
|
8895
9000
|
committer: { instanceId: this.managerInstanceId, epoch: executor.epoch },
|
|
8896
9001
|
executor: { lifecycleUid: t.lifecycleUid, epoch: 0 },
|
|
8897
|
-
resolveCurrentEpoch: (
|
|
9002
|
+
resolveCurrentEpoch: (current) => this.turnExecutorEpoch(name, t.owner, t.actor, current.lifecycleUid),
|
|
8898
9003
|
});
|
|
8899
9004
|
await clearGoalIndex(gw.ctx, ref);
|
|
8900
9005
|
}
|
|
@@ -8905,7 +9010,7 @@ export class Manager {
|
|
|
8905
9010
|
}
|
|
8906
9011
|
const pending = {
|
|
8907
9012
|
ref, goalId,
|
|
8908
|
-
seat: { name
|
|
9013
|
+
seat: { name, owner: t.owner, actor: t.actor, uid: t.lifecycleUid },
|
|
8909
9014
|
payload, acceptedAt, deadlineAt,
|
|
8910
9015
|
holdToken, holdEpoch: executor.epoch,
|
|
8911
9016
|
...(handoffFrom !== undefined ? { handoffFrom } : {}),
|
|
@@ -8913,7 +9018,7 @@ export class Manager {
|
|
|
8913
9018
|
this.pendingTurns.set(key, pending);
|
|
8914
9019
|
this.ensureTurnSweep();
|
|
8915
9020
|
this.noticeSeatTurns(pending.seat);
|
|
8916
|
-
const acceptance = { name
|
|
9021
|
+
const acceptance = { name, owner: t.owner, actor: t.actor, uid: t.lifecycleUid, goalId, fingerprint, deadlineAt, executor };
|
|
8917
9022
|
this.turnAcceptances.set(key, { acceptance, ref });
|
|
8918
9023
|
this.emitGoalProgress(ref, executor.epoch, { phase: "relayed" });
|
|
8919
9024
|
return acceptance;
|
|
@@ -8954,7 +9059,8 @@ export class Manager {
|
|
|
8954
9059
|
return;
|
|
8955
9060
|
// An issued seat credential reads only the versioned rail, under the generation it was issued.
|
|
8956
9061
|
const a = this.agents.get(seat.name);
|
|
8957
|
-
const
|
|
9062
|
+
const participant = this.currentParticipant(seat.owner, seat.actor, seat.uid);
|
|
9063
|
+
const generation = a?.lifecycleUid === seat.uid ? a.issued?.generation : participant?.generation;
|
|
8958
9064
|
const caller = { owner: seat.owner, actor: seat.actor, uid: seat.uid, ...(generation !== undefined ? { generation } : {}) };
|
|
8959
9065
|
try {
|
|
8960
9066
|
serve.nc.publish(turnNoticeSubject(this.space, this.managerInstanceId, serve.grant.epoch, caller), new TextEncoder().encode(JSON.stringify({ v: 1 })));
|
|
@@ -8965,9 +9071,9 @@ export class Manager {
|
|
|
8965
9071
|
}
|
|
8966
9072
|
/** A seat names a turn by goal id alone when it yields, so one incarnation holding two callers'
|
|
8967
9073
|
* turns under one id could not say which of the two it answered. */
|
|
8968
|
-
assertSeatTurnFree(
|
|
9074
|
+
assertSeatTurnFree(name, t, goalId) {
|
|
8969
9075
|
if (this.seatTurnKey({ owner: t.owner, actor: t.actor, uid: t.lifecycleUid }, goalId) !== undefined)
|
|
8970
|
-
throw new EpEnvelopeError("failed-precondition", `seat "${
|
|
9076
|
+
throw new EpEnvelopeError("failed-precondition", `seat "${name}" already holds another caller's turn "${goalId}"; a seat yields by goal id, so it is never handed two turns under one (SPEC 13.6)`);
|
|
8971
9077
|
}
|
|
8972
9078
|
/** The relay key of the turn addressed to seat incarnation `c` under `goalId`, pending or kept
|
|
8973
9079
|
* for a retried yield. The accept path never hands one incarnation two turns under one goal
|
|
@@ -9051,7 +9157,7 @@ export class Manager {
|
|
|
9051
9157
|
ref: p.ref, now: at, cause: "complete", state: "succeeded", data,
|
|
9052
9158
|
committer: { instanceId: this.managerInstanceId, epoch },
|
|
9053
9159
|
executor: { lifecycleUid: p.seat.uid, epoch: 0 },
|
|
9054
|
-
resolveCurrentEpoch: (target) => this.
|
|
9160
|
+
resolveCurrentEpoch: (target) => this.turnExecutorEpoch(p.seat.name, p.seat.owner, p.seat.actor, target.lifecycleUid),
|
|
9055
9161
|
});
|
|
9056
9162
|
this.emitGoalProgress(p.ref, epoch, { phase: "terminal", state: fact.state, ...(fact.data !== undefined ? { data: fact.data } : {}) });
|
|
9057
9163
|
await clearGoalIndex(gw.ctx, p.ref);
|