@cotal-ai/manager 0.78.1 → 0.81.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/attach-endpoint.d.ts +10 -10
- package/dist/attach-endpoint.d.ts.map +1 -1
- package/dist/attach-endpoint.js +9 -9
- package/dist/attach-endpoint.js.map +1 -1
- package/dist/commands.js +2 -2
- package/dist/commands.js.map +1 -1
- package/dist/console/app.js +1 -1
- package/dist/console/index.html +1 -1
- package/dist/console-crypto-shim.d.ts +1 -1
- package/dist/console-crypto-shim.js +1 -1
- package/dist/console-session-entry.js +1 -1
- package/dist/endpoint-evict.d.ts +2 -2
- package/dist/endpoint-evict.js +3 -3
- package/dist/endpoint-evict.js.map +1 -1
- package/dist/manager-service-contract.d.ts +12 -13
- package/dist/manager-service-contract.d.ts.map +1 -1
- package/dist/manager-service-contract.js +22 -23
- package/dist/manager-service-contract.js.map +1 -1
- package/dist/manager.d.ts +57 -55
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +160 -136
- package/dist/manager.js.map +1 -1
- package/dist/runtime/custodial-pty.js +1 -1
- package/dist/runtime/custodial-pty.js.map +1 -1
- package/dist/runtime/pty.d.ts.map +1 -1
- package/dist/runtime/pty.js +6 -1
- package/dist/runtime/pty.js.map +1 -1
- package/dist/session/bridge.d.ts +3 -3
- package/dist/session/bridge.js +1 -1
- package/dist/session/bridge.js.map +1 -1
- package/dist/session/establish.d.ts +10 -10
- package/dist/session/establish.d.ts.map +1 -1
- package/dist/session/establish.js +3 -3
- package/dist/session/establish.js.map +1 -1
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/plane.js +5 -5
- package/package.json +12 -12
package/dist/manager.js
CHANGED
|
@@ -23,11 +23,11 @@ import { TranscriptReceiver } from "./transcript-receive.js";
|
|
|
23
23
|
import { jetstream, jetstreamManager } from "@nats-io/jetstream";
|
|
24
24
|
import { Kvm } from "@nats-io/kv";
|
|
25
25
|
import { recordsBucket, epAuthBucket, ensureAuthorityStores, ensureContractStore, createEndpointStreams, contractStoreContext, publishContractArtifact, contractArtifactCanonicalBytes, standaloneConnectOpts, rawDigest, STANDING_RENEWABLE_TTL_SEC as MANAGED_STATIC_TTL_SEC, newArtifactSigner, RotatingSigner, generationAnchor, sessionsBucket, SESSION_GRANT_MAX_TTL_MS, REMOTE_MANAGER_IDENTITY_NAMES, } from "@cotal-ai/core";
|
|
26
|
-
//
|
|
26
|
+
// The manager's ONE §13.6 session plane — offer mint + one-use redeem + PTY-bridge
|
|
27
27
|
// standup for `attach`, over a dedicated standing session-LEDGER connection (the byte rails ride
|
|
28
28
|
// per-session credentials on their own short-lived connections).
|
|
29
29
|
import { ManagerSessionPlane, openSessionLedgerKv } from "./session/index.js";
|
|
30
|
-
//
|
|
30
|
+
// The manager as an ordinary v0.4 `service` endpoint — the §13.1
|
|
31
31
|
// endpoint-serve credential subsystem (gate provisioning, registration barrier, mint fence) plus
|
|
32
32
|
// the register/authorize/serve seams, all driven over a scoped one-shot executor connection.
|
|
33
33
|
import { provisionEndpointGateOpen, endpointRegistrationBarrier, serveIssuanceGateKv, commitSiblingIssuance, markLedgerRowRevoked, epcredRowKey, epgateKey, parseEndpointGate, readEndpointGateGeneration, registerServingInstance, deregisterServiceInstance, serveEndpoint, bindGoal, createGoal, transitionGoal, commitGoalResult, requestGoalCancel, goalAlreadyTerminal, settleGoalUncertain, readGoalResult, readGoalStatus, readGoalSpec, recordGoalIndex, readGoalIndex, clearGoalIndex, listGoalIndex, mintCheckpoint, resumeCheckpoint, readCheckpointSettle, readCheckpointSpec, expireCheckpoint, GOAL_TERMINAL_STATES, goalRefOf, goalProgressTopic, epeSubject, submissionFingerprint, EpEnvelopeError, lifecycleBlocked, lifecycleBlockedFrom, renderLifecycleBlocked, } from "@cotal-ai/core";
|
|
@@ -255,10 +255,10 @@ function parseTurnNote(raw) {
|
|
|
255
255
|
return undefined;
|
|
256
256
|
return { payload: n.payload, deadlineAt: n.deadlineAt, holdEpoch: n.holdEpoch, owner: n.owner, ...(typeof n.holdToken === "string" ? { holdToken: n.holdToken } : {}), ...(typeof n.handoffFrom === "string" ? { handoffFrom: n.handoffFrom } : {}) };
|
|
257
257
|
}
|
|
258
|
-
/** A
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
function
|
|
258
|
+
/** A goal's key in the manager's maps: the caller triple and goal id the goal plane stores the
|
|
259
|
+
* goal under, because a goal id is the caller's own (SPEC 13.6 item 1) and two callers may pick
|
|
260
|
+
* the same one. Every part is a subject token, so the dotted join is unambiguous. */
|
|
261
|
+
function goalKey(ref) {
|
|
262
262
|
return `${ref.caller.owner}.${ref.caller.actor}.${ref.caller.uid}.${ref.goalId}`;
|
|
263
263
|
}
|
|
264
264
|
/** A turn hold's token, DERIVED (same recipe as the runtime's pause tokens) from the relay key, so
|
|
@@ -329,13 +329,13 @@ const STORE_CHALLENGE_TIMEOUT_MS = 5_000;
|
|
|
329
329
|
export class Manager {
|
|
330
330
|
space;
|
|
331
331
|
servers;
|
|
332
|
-
/**
|
|
332
|
+
/** The broker ws listener port (loopback) `cotal up` allocated, for the console session
|
|
333
333
|
* client's wsUrl. Undefined ⇒ no console session client (POST /session 503s). */
|
|
334
334
|
wsPort;
|
|
335
335
|
name;
|
|
336
336
|
workspaceRoot;
|
|
337
337
|
eventsRequired;
|
|
338
|
-
/**
|
|
338
|
+
/** The operator-set global live-session ceiling (see {@link ManagerOptions.maxSessions}). */
|
|
339
339
|
maxSessions;
|
|
340
340
|
/** The ONE secret store for every kind this manager touches (daemon-cred remint + agent kinds).
|
|
341
341
|
* See {@link ManagerOptions.secretStore}. */
|
|
@@ -401,7 +401,7 @@ export class Manager {
|
|
|
401
401
|
* never reused across restarts) — the endpoint's presence key AND the `managerInstance` audit
|
|
402
402
|
* coordinate every static activation records. */
|
|
403
403
|
managerLifecycleUid = mintLifecycleUid();
|
|
404
|
-
/** The persisted LOGICAL instance id (SPEC 13.6 item 7
|
|
404
|
+
/** The persisted LOGICAL instance id (SPEC 13.6 item 7): STABLE across restart, so a
|
|
405
405
|
* restart re-registers the SAME id with an ADVANCED epoch through the §13.1 gate (the successor
|
|
406
406
|
* fences the predecessor's epoch — the (i) fence bites on a real restart). It is the registration
|
|
407
407
|
* instanceId, the served-status id, the goal spec's executor.instanceId, the epe route, and the
|
|
@@ -412,19 +412,19 @@ export class Manager {
|
|
|
412
412
|
* {@link provisionEndpointGateOpen} idempotent (no core barrier change) and gives verified
|
|
413
413
|
* eviction a stable target (the predecessor's connections under this principal). Set in start(). */
|
|
414
414
|
managerServeIdentity;
|
|
415
|
-
/**
|
|
415
|
+
/** The manager's v0.4 service-endpoint serve state — the serve handle +
|
|
416
416
|
* its dedicated connection, the STABLE serve identity (renewals re-mint the same nkey), the
|
|
417
417
|
* branded serve grant, and the CURRENT credential (the connection's authenticator reads it on
|
|
418
418
|
* every (re)connect, so a renewal is adopted without re-registration). Absent on open meshes,
|
|
419
|
-
* in user mode
|
|
419
|
+
* in user mode, and before registration completes. */
|
|
420
420
|
serviceServe;
|
|
421
421
|
/** #1577: serve connections whose transport is down while the client reconnects them. Such a
|
|
422
422
|
* connection is open (`isClosed()` is false under unbounded reconnects) yet the broker holds
|
|
423
423
|
* none of its rails, so the liveness responder must not grade it `bound`. Keyed by the
|
|
424
424
|
* connection, so a re-dial that swaps in a fresh one starts from its own state. */
|
|
425
425
|
serveTransportDown = new WeakSet();
|
|
426
|
-
/**
|
|
427
|
-
* standing connection DISJOINT from the serve credential
|
|
426
|
+
/** The spawn-as-action SELF-MEDIATED goal-writer connection + ActionContext — a
|
|
427
|
+
* standing connection DISJOINT from the serve credential, scoped to exactly this endpoint's
|
|
428
428
|
* goal bind/terminal facts + goal-record writes ({@link goalWriterGrants}). Auth mode mints the
|
|
429
429
|
* `goal-writer` cred; an open mesh uses a bare connection (no credential system to mint from).
|
|
430
430
|
* `gate` (auth mode) is the own-issuance-gate READER for the own-gate currency belt — the
|
|
@@ -442,31 +442,31 @@ export class Manager {
|
|
|
442
442
|
* live) and stashed here for {@link startGoalWriter} to build the standing connection from. */
|
|
443
443
|
goalWriterIdentity;
|
|
444
444
|
goalWriterCreds;
|
|
445
|
-
/**
|
|
445
|
+
/** The manager's ONE §13.6 session plane — offer mint + one-use redeem + PTY-bridge
|
|
446
446
|
* standup for `attach`. The face's establisher and the CLI attach handler both call THIS one
|
|
447
447
|
* plane; the manager never constructs a second. Undefined until {@link startSessionPlane}. */
|
|
448
448
|
sessionPlane;
|
|
449
|
-
/**
|
|
449
|
+
/** The standing session-LEDGER connection + its mutable creds holder (the authenticator
|
|
450
450
|
* presents the refreshed cred on the next reconnect after a half-TTL renewal — the goal-writer
|
|
451
451
|
* precedent). Auth mode only; an open mesh runs the plane over a bare connection. */
|
|
452
452
|
sessionLedgerConn;
|
|
453
453
|
/** Renews the session signing key long before its window closes. A key that expires unattended
|
|
454
454
|
* takes the whole session plane down until the manager is restarted, which kills live sessions. */
|
|
455
455
|
sessionKeyRenewTimer;
|
|
456
|
-
/**
|
|
456
|
+
/** Maps credentialId → the nkey that credential was minted for, for the live per-session
|
|
457
457
|
* SERVING credentials. The §13.1 ledger row records the holder principal, and the row is written
|
|
458
458
|
* at stage time (after the mint), so the two steps need this one hop. Entries are dropped at
|
|
459
459
|
* revoke; a session that never staged drops its entry when the manager exits. */
|
|
460
460
|
sessionServingKeys = new Map();
|
|
461
|
-
/**
|
|
461
|
+
/** The STABLE session-LEDGER identity (auth mode) — minted once at registration
|
|
462
462
|
* alongside the serve + goal-writer identities; a renewal re-mints the SAME nkey with a fresh
|
|
463
463
|
* bounded exp and re-stages its distinct credId into the §13.1 revocation family. The current
|
|
464
464
|
* credential is minted INSIDE {@link registerManagerService}'s run block and stashed here. */
|
|
465
465
|
sessionLedgerIdentity;
|
|
466
466
|
sessionLedgerCreds;
|
|
467
|
-
/**
|
|
468
|
-
* idempotent same-goalId retry serves the IDENTICAL acceptance (same allocated name/triple)
|
|
469
|
-
* a second spawn. Durable cross-incarnation reconstruction rides the goal index; here the
|
|
467
|
+
/** The acceptance replied for each in-flight goal this incarnation accepted, by {@link goalKey},
|
|
468
|
+
* so an idempotent same-goalId retry serves the IDENTICAL acceptance (same allocated name/triple)
|
|
469
|
+
* without a second spawn. Durable cross-incarnation reconstruction rides the goal index; here the
|
|
470
470
|
* live map covers same-incarnation retries, with the committed result fact as the fallback. */
|
|
471
471
|
goalAcceptances = new Map();
|
|
472
472
|
/** The boot reconcile of the durable goal index runs ONCE at start (a
|
|
@@ -474,12 +474,12 @@ export class Manager {
|
|
|
474
474
|
* Spawn-as-action REFUSES to accept until it completes, so the sweep never races a live goal's
|
|
475
475
|
* acceptance (settling one mid-flight would steal its real terminal). */
|
|
476
476
|
goalReconcileDone = false;
|
|
477
|
-
/**
|
|
477
|
+
/** The live spawn goal ref for each managed agent name, so a despawn MID-GOAL
|
|
478
478
|
* drives the cancel path (transition -> cancel terminal). Cleared when the goal terminalizes. */
|
|
479
479
|
agentGoals = new Map();
|
|
480
480
|
/**
|
|
481
481
|
* The turn relay's same-incarnation idempotency map ({@link goalAcceptances}'s twin), by
|
|
482
|
-
* {@link
|
|
482
|
+
* {@link goalKey}: a same-goal retry serves the identical acceptance; cross-incarnation retries
|
|
483
483
|
* rebuild from the goal-index entry (its acceptance floor + note).
|
|
484
484
|
*
|
|
485
485
|
* One entry per turn this incarnation accepted: the acceptance a duplicate submission is served
|
|
@@ -501,7 +501,7 @@ export class Manager {
|
|
|
501
501
|
* window would hear `not-found` again.
|
|
502
502
|
*/
|
|
503
503
|
turnAcceptances = new Map();
|
|
504
|
-
/** Every relayed turn awaiting its seat's yield, by {@link
|
|
504
|
+
/** Every relayed turn awaiting its seat's yield, by {@link goalKey}. The seat's `turn-pending` pull
|
|
505
505
|
* scans it; the deadline sweep drives expiry; a seat reap stamps its entries `seatDiedAt`, which
|
|
506
506
|
* the deadline terminal carries as `agentDownAt` so the run reads that death as L4002.
|
|
507
507
|
* Rebuilt at boot from goal-index entries carrying a turn note. */
|
|
@@ -681,7 +681,7 @@ export class Manager {
|
|
|
681
681
|
this.attach = new AttachEndpoint(() => this.list(),
|
|
682
682
|
// Initial /feed replay for a connecting console: the current peer roster.
|
|
683
683
|
() => [{ event: "roster", data: this.ep?.getRoster() ?? [] }], opts.consolePort ?? 0,
|
|
684
|
-
//
|
|
684
|
+
// The console's mesh §13.6 session establisher — injected ONLY when a broker ws
|
|
685
685
|
// listener exists (cotal up allocated a wsPort). Never a second plane; it drives THE plane.
|
|
686
686
|
opts.wsPort !== undefined ? (name) => this.establishConsoleSession(name) : undefined,
|
|
687
687
|
// Loopback unless the OPERATOR said otherwise. A broker *dial* address is not a manager *bind*
|
|
@@ -889,7 +889,7 @@ export class Manager {
|
|
|
889
889
|
throw new Error(`space "${this.space}" has user-auth state on disk but no mesh registry entry - a user-mode manager needs the authoritative record (\`cotal up\` writes it before the control plane); \`cotal up --user-auth\` this space, or remove the stale ${userAuthStateDir(this.workspaceRoot, this.space)}`);
|
|
890
890
|
if (!this.remoteAuthority && this.userMode && !this.auth)
|
|
891
891
|
throw new Error(`space "${this.space}" has user-auth state but no auth.json under ${authDir(this.workspaceRoot)} - the pre-flip manager still needs the space trust bundle; re-run \`cotal up --user-auth\` here`);
|
|
892
|
-
//
|
|
892
|
+
// SPEC 13.6 item 7: the LOGICAL instance id + serve identity PERSIST across restart
|
|
893
893
|
// (a space-scoped manager identity file under .cotal). A restart re-registers the SAME id with an
|
|
894
894
|
// ADVANCED epoch (the successor fences the predecessor); a fresh mint over a malformed file is
|
|
895
895
|
// refused loud (no-fallbacks - a restart never silently becomes a fresh instance). A second
|
|
@@ -978,7 +978,7 @@ export class Manager {
|
|
|
978
978
|
this.ep.on("presence-view", () => this.logSeatMeshTransitions());
|
|
979
979
|
await this.ep.start();
|
|
980
980
|
await this.ep.setActivity(`supervisor (${this.runtimeKind})`);
|
|
981
|
-
// Per-instance liveness lease (
|
|
981
|
+
// Per-instance liveness lease (the old per-space singleton is DEMOTED). Acquire
|
|
982
982
|
// THIS logical instance's own key (atomic CAS create). A DIFFERENT instance (a second manager in a
|
|
983
983
|
// second workspace root) has a distinct id ⇒ a distinct key ⇒ it coexists; the create THROWS only
|
|
984
984
|
// when the SAME instance id is already live (a same-root double-start, or a restart racing the
|
|
@@ -1035,7 +1035,7 @@ export class Manager {
|
|
|
1035
1035
|
const startupReconcile = this.auth && !this.userMode ? this.reconcileStaticLifecycles() : undefined;
|
|
1036
1036
|
if (startupReconcile)
|
|
1037
1037
|
void startupReconcile.catch((e) => console.error(`! ${STARTUP_RECONCILING}: ${rejectionText(e)} - no per-alias retry could be planned; a later manager start re-reads unfinished durable terminals`));
|
|
1038
|
-
//
|
|
1038
|
+
// The manager serves NO ctl tiers - its whole control surface is the v0.4
|
|
1039
1039
|
// service endpoint registered below. The old three-tier rail (self/manager/admin) is deleted;
|
|
1040
1040
|
// `ctl.delivery`/`ctl.delivery-admin` (the delivery daemon) and `ctl.auth-admin` (the auth
|
|
1041
1041
|
// plane) are separate services and keep their rails.
|
|
@@ -1101,8 +1101,8 @@ export class Manager {
|
|
|
1101
1101
|
// the fence before it can publish a fresh service registration after shutdown.
|
|
1102
1102
|
if (this.staticReconcileStopping)
|
|
1103
1103
|
return;
|
|
1104
|
-
//
|
|
1105
|
-
// and serve its typed command surface on the ep rails -
|
|
1104
|
+
// Register the manager as an ordinary v0.4 `service` endpoint (SPEC §13.7/§13.9)
|
|
1105
|
+
// and serve its typed command surface on the ep rails - the ONLY control door, in
|
|
1106
1106
|
// EVERY mesh mode. Static + user meshes mint the scoped executor + endpoint-serve credential;
|
|
1107
1107
|
// an open mesh runs the same gate/registration ceremony over bare connections and never mints
|
|
1108
1108
|
// (there is no credential system - the broker enforces nothing, matching the old open-mesh ctl
|
|
@@ -1110,10 +1110,10 @@ export class Manager {
|
|
|
1110
1110
|
await this.registerManagerService();
|
|
1111
1111
|
if (this.staticReconcileStopping)
|
|
1112
1112
|
return;
|
|
1113
|
-
//
|
|
1113
|
+
// Stand up the standing goal-writer connection for spawn-as-action — AFTER
|
|
1114
1114
|
// registration (it writes this endpoint's goal facts/records), disjoint from the serve cred.
|
|
1115
1115
|
await this.startGoalWriter();
|
|
1116
|
-
//
|
|
1116
|
+
// Stand up the ONE §13.6 session plane for `attach` — AFTER registration too (it
|
|
1117
1117
|
// rides the serve grant's epoch + the family-staged session-ledger cred), on its own standing
|
|
1118
1118
|
// connection disjoint from both the serve and goal-writer creds.
|
|
1119
1119
|
await this.startSessionPlane();
|
|
@@ -1483,7 +1483,7 @@ export class Manager {
|
|
|
1483
1483
|
}
|
|
1484
1484
|
}
|
|
1485
1485
|
}
|
|
1486
|
-
//
|
|
1486
|
+
// The manager is the `endpoint-serve` renewal owner for its OWN
|
|
1487
1487
|
// service credential — re-mint the SAME serve identity with a fresh bounded exp THROUGH the
|
|
1488
1488
|
// §13.1 mint fence over a scoped one-shot executor (every renewal stages a distinct ledger
|
|
1489
1489
|
// row and wins the gate CAS; never the standing connection). The serve connection's
|
|
@@ -1529,7 +1529,7 @@ export class Manager {
|
|
|
1529
1529
|
console.error(`! goal-writer renewal: ${rejectionText(e)} - spawn-as-action stops accepting at this cred's expiry unless the manager restarts`);
|
|
1530
1530
|
}
|
|
1531
1531
|
}
|
|
1532
|
-
//
|
|
1532
|
+
// The manager is also the session-ledger's renewal owner — re-mint the SAME nkey
|
|
1533
1533
|
// with a fresh bounded exp AND re-stage its new credId into the §13.1 family, through the
|
|
1534
1534
|
// scoped executor. Without this the standing session-ledger connection dies at its TTL and
|
|
1535
1535
|
// `attach` stops establishing sessions until a restart. The connection's authenticator presents
|
|
@@ -2644,7 +2644,7 @@ export class Manager {
|
|
|
2644
2644
|
return { ok: false, error: rejectionText(e) };
|
|
2645
2645
|
}
|
|
2646
2646
|
}
|
|
2647
|
-
/** The ONE shared control-admission chokepoint
|
|
2647
|
+
/** The ONE shared control-admission chokepoint BOTH dispatch doors
|
|
2648
2648
|
* run — the v0.3 `ctl` door ({@link handle}) and the v0.4 `ep` service handlers
|
|
2649
2649
|
* ({@link serveGated}): the maintenance/resume fence (`beginLifecycle`: a resume-pending or
|
|
2650
2650
|
* non-active manager accepts no ordinary control work) and then the F5(a) membership gate
|
|
@@ -2679,7 +2679,7 @@ export class Manager {
|
|
|
2679
2679
|
admission.release();
|
|
2680
2680
|
}
|
|
2681
2681
|
}
|
|
2682
|
-
/** The ep door's ADMIN flag for a caller
|
|
2682
|
+
/** The ep door's ADMIN flag for a caller. Static mesh: `true` — the
|
|
2683
2683
|
* admin-grade rows (any-mode despawn/attach, the `manager.admin` family, `launch`) are minted
|
|
2684
2684
|
* only into operator instruments (§13.2: `any` is operator-policy-mintable; the agent/spawn
|
|
2685
2685
|
* rollups never carry them), so REACHING the handler is holding the admin tier, exactly as
|
|
@@ -2688,9 +2688,9 @@ export class Manager {
|
|
|
2688
2688
|
* subject-parsed caller tuple to its host-owned authorization callback, with no participant-ledger
|
|
2689
2689
|
* fallback. The same fresh authority drives {@link psOwnerFilter}, so a revoked scope demotes the
|
|
2690
2690
|
* very next call even on a still-valid bearer. Local ledger read failures authorize nothing;
|
|
2691
|
-
* remote host-state faults throw and fail the operation closed.
|
|
2692
|
-
*
|
|
2693
|
-
*
|
|
2691
|
+
* remote host-state faults throw and fail the operation closed.
|
|
2692
|
+
* NAMED RESIDUAL: the static `true` has no serve-time re-check — a LEAKED static admin
|
|
2693
|
+
* instrument keeps its reach until the credential's bounded
|
|
2694
2694
|
* TTL (the one-shot 5-minute profile), the same static-revoke≠reconnect-death class ruled
|
|
2695
2695
|
* across this campaign; static revocation is the TTL, not a ledger. */
|
|
2696
2696
|
async epAdminReach(caller) {
|
|
@@ -2796,15 +2796,15 @@ export class Manager {
|
|
|
2796
2796
|
if (accepted.by !== agent.name)
|
|
2797
2797
|
throw new EpEnvelopeError("permission-denied", `run-answer amend is allowed to ${agent.name} only for an answer recorded under its own name; this step's accepted answer is ${JSON.stringify(accepted.by)}'s`);
|
|
2798
2798
|
}
|
|
2799
|
-
/** The v0.4 typed command table
|
|
2799
|
+
/** The v0.4 typed command table: every ordinary handler runs the SHARED
|
|
2800
2800
|
* admission chokepoint ({@link serveGated}) and then delegates to the SAME op core the ctl
|
|
2801
|
-
* door dispatches (
|
|
2801
|
+
* door dispatches (one core, two thin doors). The resume/preservation family
|
|
2802
2802
|
* deliberately BYPASSES serveGated — exactly as it sits before {@link admitControl} on the ctl
|
|
2803
2803
|
* door (those ops must run while `resumeRequired` fences ordinary work) — riding its own state
|
|
2804
|
-
* fences; its ep gate is the admin-grade `manager.admin` capability grant (
|
|
2804
|
+
* fences; its ep gate is the admin-grade `manager.admin` capability grant (static
|
|
2805
2805
|
* admin-class commands are capability-gated + untargeted, never a fabricated ledger mode).
|
|
2806
2806
|
*
|
|
2807
|
-
* TIER SEMANTICS on the ep door
|
|
2807
|
+
* TIER SEMANTICS on the ep door: the tier lives in the CALLER'S
|
|
2808
2808
|
* GRANT, refined per-op exactly as the ctl doors refine their subject tier. Owner-mode
|
|
2809
2809
|
* `despawn`/`attach` keep the privileged semantics (`admin=false`, own-domain via
|
|
2810
2810
|
* {@link authorizeNamed}) — every spawn-capable agent holds those rows. ANY-mode requests are
|
|
@@ -2828,13 +2828,13 @@ export class Manager {
|
|
|
2828
2828
|
const callerOf = (ctx) => principalKey(ctx.subject.caller.owner, ctx.subject.caller.actor).key;
|
|
2829
2829
|
// A ctl-core failure reply becomes the §13.3 structured error the serve boundary publishes.
|
|
2830
2830
|
// The data half of a failure reply (e.g. a degraded resume result) rides the error MESSAGE
|
|
2831
|
-
// only — the
|
|
2831
|
+
// only — the action model gives failures a typed channel.
|
|
2832
2832
|
const unwrap = (r) => {
|
|
2833
2833
|
if (!r.ok)
|
|
2834
2834
|
throw new EpEnvelopeError("failed-precondition", r.error ?? "the operation failed");
|
|
2835
2835
|
return r.data;
|
|
2836
2836
|
};
|
|
2837
|
-
// The admin-family serve gate
|
|
2837
|
+
// The admin-family serve gate: every `manager.admin`-class
|
|
2838
2838
|
// command re-checks operator reach AT SERVE TIME - static: true (the mint boundary already
|
|
2839
2839
|
// gates the rows to instruments); user mesh: the caller's CURRENT ledger scope must still
|
|
2840
2840
|
// carry `admin` ({@link epAdminReach}'s fresh read), so a revoked scope demotes the very next
|
|
@@ -2942,8 +2942,8 @@ export class Manager {
|
|
|
2942
2942
|
throw new EpEnvelopeError(persona.error.startsWith("no persona") ? "not-found" : "failed-precondition", persona.error);
|
|
2943
2943
|
return { agent: persona.agent };
|
|
2944
2944
|
}),
|
|
2945
|
-
//
|
|
2946
|
-
// progress + terminal off-handler (no ~30s block). The blocking reply path is gone
|
|
2945
|
+
// `spawn` is an ACTION - accept a goal + reply the acceptance floor payload, drive
|
|
2946
|
+
// progress + terminal off-handler (no ~30s block). The blocking reply path is gone.
|
|
2947
2947
|
// #373: arming the event plane (events: true, or the omission that arms it by default)
|
|
2948
2948
|
// follows owner-domain reach on a user mesh. Resolve the caller's admin tier once for
|
|
2949
2949
|
// the residual cross-owner case; opStart decides before connector resolution or grant
|
|
@@ -3013,7 +3013,7 @@ export class Manager {
|
|
|
3013
3013
|
// deployer bearer would then bypass owner-equality (operator launch) despite the view holding
|
|
3014
3014
|
// no admin rows. Uniform owner-equality removes that divergence in the least-privilege
|
|
3015
3015
|
// direction.
|
|
3016
|
-
//
|
|
3016
|
+
// Manifest `launch` is an ACTION through the SAME chokepoint as spawn -
|
|
3017
3017
|
// the manifest resolve + owner-equality authz run in opLaunch's accept path, then the goal
|
|
3018
3018
|
// drives progress + terminal. The acceptance floor is the allocated identity + goal coords.
|
|
3019
3019
|
launch: (ctx) => this.serveGated(ctx, () => this.serveSpawnGoal(ctx, (h) => this.opLaunch(args(ctx), callerOf(ctx), false, h, ctx.subject.route))),
|
|
@@ -3624,7 +3624,7 @@ export class Manager {
|
|
|
3624
3624
|
rmSync(a.restart.sessionStatePath, { force: true });
|
|
3625
3625
|
if (a.seatHome)
|
|
3626
3626
|
rmSync(a.seatHome, { recursive: true, force: true });
|
|
3627
|
-
//
|
|
3627
|
+
// End any live §13.6 attach session bound to THIS incarnation with the honest
|
|
3628
3628
|
// `target-despawn` reason. Fires once per agent on every free path (despawn / self-stop / reap /
|
|
3629
3629
|
// exit) via the `agents` guard above; a no-op when no plane or no live session for the target.
|
|
3630
3630
|
this.sessionPlane?.endForTarget(a.name, a.lifecycleUid, "target-despawn");
|
|
@@ -3973,9 +3973,14 @@ export class Manager {
|
|
|
3973
3973
|
console.error(`despawn ${a.name}: the agent's retirement completed; the name is free for reuse`);
|
|
3974
3974
|
}
|
|
3975
3975
|
}
|
|
3976
|
-
else {
|
|
3976
|
+
else if (cur) {
|
|
3977
3977
|
console.error(`despawn ${a.name}: retirement confirmed for a prior lifecycle of "${a.name}"; the current hold is left intact`);
|
|
3978
3978
|
}
|
|
3979
|
+
else {
|
|
3980
|
+
// No hold stands, as after an orphaned spawn's rollback: that lifecycle was never despawned and
|
|
3981
|
+
// has no successor, so this line names neither.
|
|
3982
|
+
console.error(`retire ${a.name}: lifecycle ${a.lifecycleUid} retired; the name had no hold to clear`);
|
|
3983
|
+
}
|
|
3979
3984
|
}
|
|
3980
3985
|
/** The teardown's ASYNC BROKER PHASE: mint the ephemeral target-pinned deprovisioner cred and
|
|
3981
3986
|
* delete the agent's broker footprint (dm_/dlv_ durables + read-ACL row). Split from
|
|
@@ -4397,7 +4402,7 @@ export class Manager {
|
|
|
4397
4402
|
}
|
|
4398
4403
|
return live;
|
|
4399
4404
|
}
|
|
4400
|
-
/** THE single name-liveness predicate both the hard-pinned collision refuse
|
|
4405
|
+
/** THE single name-liveness predicate both the hard-pinned collision refuse and
|
|
4401
4406
|
* uniqueName's numbering consult, so they can never drift: a name is taken if this manager
|
|
4402
4407
|
* reserves/manages/retires it OR a roster-live occupant already holds it. Pass a pre-built
|
|
4403
4408
|
* {@link liveRosterNames} set when checking many names in one allocation. */
|
|
@@ -4671,9 +4676,9 @@ export class Manager {
|
|
|
4671
4676
|
}
|
|
4672
4677
|
/** The launch fields the host supplies rather than the seat. Spawn and resume both take them from
|
|
4673
4678
|
* here, so a resumed seat launches under the same host policy, `eventsRequired` included, as the
|
|
4674
|
-
* same seat spawned fresh.
|
|
4675
|
-
|
|
4676
|
-
|
|
4679
|
+
* same seat spawned fresh. The caller passes the cotal config it judged the launch by, so the
|
|
4680
|
+
* servers and env allowlist come from the contents its checks read. */
|
|
4681
|
+
hostLaunchFields(cotalConfig, agent, shareTools, harness) {
|
|
4677
4682
|
return {
|
|
4678
4683
|
space: this.space,
|
|
4679
4684
|
servers: this.servers,
|
|
@@ -5024,6 +5029,17 @@ export class Manager {
|
|
|
5024
5029
|
if (this.userMode)
|
|
5025
5030
|
return { ok: false, error: "supervise is a restart policy this host cannot enforce: a user-mode seat has no static slot to keep the incarnation owned across a process death" };
|
|
5026
5031
|
}
|
|
5032
|
+
// One read of the cotal config per spawn: the delegated refusal, the model policy and the launch
|
|
5033
|
+
// fields below all take this value, so the awaits between the policy check and the launch cannot
|
|
5034
|
+
// admit the seat under one file and launch it with another's servers and env allowlist. Read per
|
|
5035
|
+
// spawn, so a policy edit reaches the next spawn without a restart.
|
|
5036
|
+
let cotalConfig;
|
|
5037
|
+
try {
|
|
5038
|
+
cotalConfig = loadCotalConfig(this.workspaceRoot);
|
|
5039
|
+
}
|
|
5040
|
+
catch (e) {
|
|
5041
|
+
return { ok: false, error: rejectionText(e) };
|
|
5042
|
+
}
|
|
5027
5043
|
// A delegated seat starts outside this host from values alone, so a choice only this host can
|
|
5028
5044
|
// honour is refused here, before enrollment, and named rather than dropped.
|
|
5029
5045
|
if (this.requiredRuntime.spawnDelegated) {
|
|
@@ -5042,7 +5058,7 @@ export class Manager {
|
|
|
5042
5058
|
return { ok: false, error: `${delegatedBy}, so it cannot reopen a session held on this host (continuity: exact)` };
|
|
5043
5059
|
if (typeof opts.cwd === "string" && opts.cwd !== "")
|
|
5044
5060
|
return { ok: false, error: `${delegatedBy}, so it cannot run in a directory on this host's filesystem (cwd); the seat runs in its runtime resource's own directory` };
|
|
5045
|
-
const shared = Object.keys(connectorServers(
|
|
5061
|
+
const shared = Object.keys(connectorServers(cotalConfig, agent, opts.shareTools));
|
|
5046
5062
|
if (shared.length)
|
|
5047
5063
|
return { ok: false, error: `${delegatedBy}, so it cannot share MCP servers that run on this host (${shared.join(", ")}); pass --share-tools none` };
|
|
5048
5064
|
}
|
|
@@ -5160,22 +5176,16 @@ export class Manager {
|
|
|
5160
5176
|
// #581: the operator's per-role model allowlist, judged on the EFFECTIVE role and model resolved
|
|
5161
5177
|
// above, before any reserve or mint. This is where the launch is performed, so no caller can
|
|
5162
5178
|
// skip it, and a definition pinned to a stale model (or to none) is refused here instead of
|
|
5163
|
-
// serving on it.
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
launchOptions,
|
|
5174
|
-
});
|
|
5175
|
-
}
|
|
5176
|
-
catch (e) {
|
|
5177
|
-
return { ok: false, error: rejectionText(e) };
|
|
5178
|
-
}
|
|
5179
|
+
// serving on it.
|
|
5180
|
+
const policyRefusal = modelPolicyRefusal(cotalConfig, {
|
|
5181
|
+
persona: opts.resolved ? `manifest agent "${ref}"` : `persona "${ref}" (${configPath})`,
|
|
5182
|
+
role, model, variant,
|
|
5183
|
+
// A manifest launch carries its own declared model/variant in these options
|
|
5184
|
+
// (launchAgentToStartOpts), so there they are the definition's field, never a flag.
|
|
5185
|
+
modelFlag: !opts.resolved && opts.model !== undefined,
|
|
5186
|
+
variantFlag: !opts.resolved && opts.variant !== undefined,
|
|
5187
|
+
launchOptions,
|
|
5188
|
+
});
|
|
5179
5189
|
if (policyRefusal)
|
|
5180
5190
|
return { ok: false, error: policyRefusal };
|
|
5181
5191
|
// The alias-reuse gate (#29 piece 3): a name whose previous agent is still retiring REFUSES
|
|
@@ -5230,9 +5240,9 @@ export class Manager {
|
|
|
5230
5240
|
// unconditional: a half-wired endpoint without the seam must fail loud here, not silently
|
|
5231
5241
|
// allocate off a pre-snapshot roster.
|
|
5232
5242
|
await this.ep.waitForPresenceSnapshot();
|
|
5233
|
-
//
|
|
5243
|
+
// Spawn-as-action: a HARD-PINNED name — an imperative `--name`/identity override
|
|
5234
5244
|
// or a manifest-declared name (opts.resolved) — that collides with a LIVE/provisioning/reserved
|
|
5235
|
-
// incarnation REFUSES loud at accept, BEFORE any reserve/mint/bind
|
|
5245
|
+
// incarnation REFUSES loud at accept, BEFORE any reserve/mint/bind, never a silent `_2`
|
|
5236
5246
|
// suffix (so an address-by-triple caller's pinned name can't be re-pointed). A PERSONA-DERIVED
|
|
5237
5247
|
// base name (no pin) keeps uniqueName's collision numbering, so multi-peer `spawn reviewer` twice
|
|
5238
5248
|
// still yields reviewer + reviewer_2. The alias-reuse gate above is orthogonal and already fired.
|
|
@@ -5243,7 +5253,7 @@ export class Manager {
|
|
|
5243
5253
|
// this manager's agents/reserved/retiring PLUS the roster-live set) - a hard-pinned name
|
|
5244
5254
|
// colliding with ANY live incarnation (managed, unmanaged foreground/connector, or another
|
|
5245
5255
|
// manager's agent) refuses cleanly at accept, rather than minting the collision and black-
|
|
5246
|
-
// holing on the broker/auth refusal (
|
|
5256
|
+
// holing on the broker/auth refusal (a pinned name live under another manager MUST refuse).
|
|
5247
5257
|
if (this.nameInUse(identityName))
|
|
5248
5258
|
return { ok: false, error: `the name "${identityName}" is hard-pinned (${opts.resolved ? "manifest-declared" : "--name/identity override"}) but is already held by a live incarnation (managed here, an unmanaged foreground/connector session, or another manager's agent); a pinned same-name collision refuses at accept - pick another name or despawn the existing one` };
|
|
5249
5259
|
name = identityName;
|
|
@@ -5285,11 +5295,11 @@ export class Manager {
|
|
|
5285
5295
|
// before anything recorded how to address them, so a crash in that window left a live seat
|
|
5286
5296
|
// outside every manager and the successor retired the slot over it.
|
|
5287
5297
|
const custody = this.reserveCustody();
|
|
5288
|
-
// ACCEPT SEAM (
|
|
5298
|
+
// ACCEPT SEAM (spawn-as-action): the incarnation identity is minted and NOTHING has
|
|
5289
5299
|
// been provisioned yet — the action serve path binds the goal + replies the acceptance HERE. A
|
|
5290
5300
|
// throw (bind conflict / duplicate goalId) aborts the spawn before provisioning: the catch below
|
|
5291
5301
|
// returns the failure and the finally releases the reserve, so a refused accept leaves zero
|
|
5292
|
-
// footprint
|
|
5302
|
+
// footprint. The hosted enrollment arm is the exception, below. Blocking callers
|
|
5293
5303
|
// (roster boot) pass no hooks and this is a no-op.
|
|
5294
5304
|
// The ALLOCATED agent's addressing triple (the acceptance floor names what was actually
|
|
5295
5305
|
// allocated, never the requested-but-unallocated name). Static/open key on DEV_OWNER + the
|
|
@@ -5462,7 +5472,7 @@ export class Manager {
|
|
|
5462
5472
|
: undefined;
|
|
5463
5473
|
const manifestSha256 = manifestPath ? this.fileDigest(manifestPath) : undefined;
|
|
5464
5474
|
const launchOpts = {
|
|
5465
|
-
...this.hostLaunchFields(agent, opts.shareTools, harness),
|
|
5475
|
+
...this.hostLaunchFields(cotalConfig, agent, opts.shareTools, harness),
|
|
5466
5476
|
name,
|
|
5467
5477
|
role,
|
|
5468
5478
|
// User mode: the principal IS the identity (the endpoint derives card.id from owner+actor);
|
|
@@ -5521,7 +5531,7 @@ export class Manager {
|
|
|
5521
5531
|
spec.env = { ...spec.env, COTAL_MANAGER_INSTANCE: this.managerInstanceId };
|
|
5522
5532
|
handle = await this.spawnCustodied(name, spec, cwd, custody);
|
|
5523
5533
|
}
|
|
5524
|
-
hooks?.onLaunched?.(); //
|
|
5534
|
+
hooks?.onLaunched?.(); // The "launched" progress edge (process spawned, pre-presence)
|
|
5525
5535
|
const managed = {
|
|
5526
5536
|
name,
|
|
5527
5537
|
role,
|
|
@@ -5688,7 +5698,7 @@ export class Manager {
|
|
|
5688
5698
|
// OMIT an absent role: the goal terminal commits this data through the strict
|
|
5689
5699
|
// canonicalJson (undefined never coerces to null, SPEC 13.6), so a role-less spawn would
|
|
5690
5700
|
// otherwise fail its succeeded terminal. The CLI/connector already render an absent role as
|
|
5691
|
-
// "no role", so dropping the key preserves the reply (
|
|
5701
|
+
// "no role", so dropping the key preserves the reply (surfaced by readiness:live).
|
|
5692
5702
|
const okData = { name, agent, id: managed.id, mode: handle.kind, lifecycleUid, ...(role !== undefined ? { role } : {}), ...(opts.eventsNotice !== undefined ? { eventsNotice: opts.eventsNotice } : {}) };
|
|
5693
5703
|
await hooks?.onOutcome?.({ kind: "succeeded", data: okData });
|
|
5694
5704
|
return { ok: true, data: okData };
|
|
@@ -6179,7 +6189,7 @@ export class Manager {
|
|
|
6179
6189
|
}
|
|
6180
6190
|
try {
|
|
6181
6191
|
const launchOpts = {
|
|
6182
|
-
...this.hostLaunchFields(entry.launch.connector, entry.launch.shareTools, harness),
|
|
6192
|
+
...this.hostLaunchFields(loadCotalConfig(this.workspaceRoot), entry.launch.connector, entry.launch.shareTools, harness),
|
|
6183
6193
|
name: entry.name,
|
|
6184
6194
|
role: entry.role,
|
|
6185
6195
|
id: authority.id,
|
|
@@ -6635,7 +6645,7 @@ export class Manager {
|
|
|
6635
6645
|
return { ok: false, error: `no agent "${name}"` };
|
|
6636
6646
|
return this.despawnCore(a, caller, admin, args.graceful !== false);
|
|
6637
6647
|
}
|
|
6638
|
-
/** The ONE named-terminal core both doors share
|
|
6648
|
+
/** The ONE named-terminal core both doors share: the ctl named `stop`
|
|
6639
6649
|
* and the v0.4 targeted `despawn` are the same terminal — authorize by the shared policy
|
|
6640
6650
|
* ({@link authorizeNamed}: own-child / owner-domain on privileged, any on admin), stop, track.
|
|
6641
6651
|
* The ep door runs the SAME two pieces separately so a policy denial surfaces as the §13.3
|
|
@@ -6653,14 +6663,15 @@ export class Manager {
|
|
|
6653
6663
|
despawnAuthorized(a, graceful, trackNonAdmin, caller) {
|
|
6654
6664
|
this.stopHandle(a, graceful);
|
|
6655
6665
|
this.trackStoppedHandle(a, trackNonAdmin, { kind: "stopped-requested", requester: caller });
|
|
6656
|
-
void this.cancelAgentGoal(a.name, graceful ? "graceful" : "terminate"); //
|
|
6666
|
+
void this.cancelAgentGoal(a.name, graceful ? "graceful" : "terminate"); // Cancel a live spawn goal
|
|
6657
6667
|
return { ok: true, data: { name: a.name, stopped: true, graceful } };
|
|
6658
6668
|
}
|
|
6659
6669
|
/** Retire one agent this manager launched under a delegated user intent, through the host
|
|
6660
6670
|
* (SPEC 13.16). It sends the user's retirement intent for the UID of the running slot or its
|
|
6661
|
-
* retirement hold, and only on `retired: true` for that exact target stops the slot
|
|
6662
|
-
* name; every other answer leaves the slot running or
|
|
6663
|
-
*
|
|
6671
|
+
* retirement hold, and only on `retired: true` for that exact target stops the slot, shreds the
|
|
6672
|
+
* seat's local secret family and frees the name; every other answer leaves the slot running or
|
|
6673
|
+
* the name held, and a failed shred keeps the name held. It never calls the holder's own prepare
|
|
6674
|
+
* or requester. */
|
|
6664
6675
|
async retireDelegatedAgent(name, intentId) {
|
|
6665
6676
|
const execute = this.remoteAuthority?.executeDelegatedUserIntent;
|
|
6666
6677
|
if (!execute)
|
|
@@ -6696,6 +6707,17 @@ export class Manager {
|
|
|
6696
6707
|
const slot = this.agents.get(name);
|
|
6697
6708
|
if (slot?.lifecycleUid === target.lifecycleUid)
|
|
6698
6709
|
this.despawnAuthorized(slot, true, false, `delegated retirement ${intentId}`);
|
|
6710
|
+
// The host owns the grant and the footprint, but the secret family enrollUserAgent wrote here
|
|
6711
|
+
// is this participant's, and no teardown arm of a delegated seat removes it.
|
|
6712
|
+
const unshredded = [];
|
|
6713
|
+
await this.shredUserSecrets(unshredded, (live ?? held?.target)?.secretPaths ?? agentLifecycleSecretFilePaths(this.workspaceRoot, this.space, name, target.lifecycleUid));
|
|
6714
|
+
if (unshredded.length) {
|
|
6715
|
+
const error = `the host retired "${name}" at ${target.lifecycleUid}, but cleanup failed: ${unshredded.join("; ")}; the name stays held until a retried retirement intent removes it`;
|
|
6716
|
+
const hold = this.retiring.get(name);
|
|
6717
|
+
if (hold?.target.lifecycleUid === target.lifecycleUid)
|
|
6718
|
+
hold.lastError = error;
|
|
6719
|
+
return { ok: false, error };
|
|
6720
|
+
}
|
|
6699
6721
|
this.confirmRetirement({ id: principalKey(target.owner, target.actor).key, name, lifecycleUid: target.lifecycleUid });
|
|
6700
6722
|
return { ok: true, data: { name, lifecycleUid: target.lifecycleUid, retired: true } };
|
|
6701
6723
|
}
|
|
@@ -6808,7 +6830,7 @@ export class Manager {
|
|
|
6808
6830
|
await nc.drain().catch(() => nc.close());
|
|
6809
6831
|
}
|
|
6810
6832
|
}
|
|
6811
|
-
/**
|
|
6833
|
+
/** The open-mesh counterpart of {@link withEndpointServeExecutor}: an OPEN mesh has no
|
|
6812
6834
|
* credential system, so there is no scoped executor to mint - the same §13.1 gate/records
|
|
6813
6835
|
* writes ride a bare one-shot connection (the broker enforces nothing on an open mesh; the
|
|
6814
6836
|
* ceremony still produces the real gate, epoch, and registration the serve rails run on). */
|
|
@@ -6829,7 +6851,7 @@ export class Manager {
|
|
|
6829
6851
|
await nc.drain().catch(() => nc.close());
|
|
6830
6852
|
}
|
|
6831
6853
|
}
|
|
6832
|
-
/** The served manager-level health summary
|
|
6854
|
+
/** The served manager-level health summary. */
|
|
6833
6855
|
managerStatusData() {
|
|
6834
6856
|
return {
|
|
6835
6857
|
instanceId: this.managerInstanceId,
|
|
@@ -6936,7 +6958,7 @@ export class Manager {
|
|
|
6936
6958
|
})().catch(() => this.serveTransportDown.add(nc));
|
|
6937
6959
|
let handle;
|
|
6938
6960
|
try {
|
|
6939
|
-
// The
|
|
6961
|
+
// The typed surface + the derived `describe`. The descriptor stays PUBLIC in static
|
|
6940
6962
|
// mode: the broker grant (who holds each command's request-publish row) is the
|
|
6941
6963
|
// load-bearing authority tier, and a static single-operator mesh leaks nothing by listing
|
|
6942
6964
|
// command names; the trusted per-caller `view(caller)` scoping joins the user-mode
|
|
@@ -7029,8 +7051,8 @@ export class Manager {
|
|
|
7029
7051
|
await this.stop();
|
|
7030
7052
|
process.exit(1);
|
|
7031
7053
|
}
|
|
7032
|
-
/**
|
|
7033
|
-
* command surface on the ep rails -
|
|
7054
|
+
/** Register the manager as an ordinary v0.4 `service` endpoint and serve its typed
|
|
7055
|
+
* command surface on the ep rails - the manager's ONLY control door. On an AUTH mesh
|
|
7034
7056
|
* the whole credential path is the SAME one an ordinary endpoint traverses (the enforcement
|
|
7035
7057
|
* test that keeps "ordinary" honest): provision the §13.1 issuance gate, drive the
|
|
7036
7058
|
* registration BARRIER's gate CAS, then release the serve credential only on the mint FENCE's
|
|
@@ -7065,7 +7087,7 @@ export class Manager {
|
|
|
7065
7087
|
console.error(`remote manager service endpoint activated: ${MANAGER_ENDPOINT}/${this.managerInstanceId} (epoch ${state.grant.epoch})`);
|
|
7066
7088
|
return;
|
|
7067
7089
|
}
|
|
7068
|
-
// The §13.7 contract store is REGISTRATION's dependency, ensured here MODE-NEUTRALLY
|
|
7090
|
+
// The §13.7 contract store is REGISTRATION's dependency, ensured here MODE-NEUTRALLY:
|
|
7069
7091
|
// it used to ride the static-only lifecycle reconcile, so a USER-mode manager registered
|
|
7070
7092
|
// against an absent stream and its artifact publish died no-responders (live-repro'd). A
|
|
7071
7093
|
// provisioner one-shot creates-or-verifies it (config-B immutability incl. the shadowed-legacy
|
|
@@ -7075,7 +7097,7 @@ export class Manager {
|
|
|
7075
7097
|
const provCreds = auth ? await mintCreds(auth, newIdentity(), "provisioner") : undefined;
|
|
7076
7098
|
const provNc = await this.dial({ ...standaloneConnectOpts({ creds: provCreds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
|
|
7077
7099
|
try {
|
|
7078
|
-
//
|
|
7100
|
+
// The manager now WRITES goal facts (EPF) + progress events (EPE), so the §13.12
|
|
7079
7101
|
// endpoint streams must exist. Nothing provisioned them before spawn-as-action (no endpoint
|
|
7080
7102
|
// wrote to EPF/EPE), so the manager ensures the full set here over the provisioner (whose
|
|
7081
7103
|
// STREAM.CREATE now covers them), idempotently - createEndpointStreams is a superset of
|
|
@@ -7103,7 +7125,7 @@ export class Manager {
|
|
|
7103
7125
|
const authority = {
|
|
7104
7126
|
authorize: (name, owner) => ({ authorized: name === MANAGER_ENDPOINT && owner === DEV_OWNER, revision: 0 }),
|
|
7105
7127
|
};
|
|
7106
|
-
// The STABLE serve identity
|
|
7128
|
+
// The STABLE serve identity: the PERSISTED serve nkey, reused across restart so the
|
|
7107
7129
|
// gate binds the SAME principal (§13.1 serving-principal binding) - provisionEndpointGateOpen
|
|
7108
7130
|
// stays idempotent and verified eviction has a stable target. Renewals re-mint the same nkey
|
|
7109
7131
|
// with a fresh bounded exp; a restart re-provisions the same (idempotent) gate + re-registers.
|
|
@@ -7114,7 +7136,7 @@ export class Manager {
|
|
|
7114
7136
|
// The STABLE goal-writer identity — a SIBLING credential in the same §13.1 family
|
|
7115
7137
|
// (not the gate's bound serving principal), so the run block can family-stage it.
|
|
7116
7138
|
this.goalWriterIdentity = siblings.goalWriter;
|
|
7117
|
-
//
|
|
7139
|
+
// The STABLE session-LEDGER identity — another SIBLING in the SAME §13.1 family, so
|
|
7118
7140
|
// the takeover barrier revokes a deposed manager's ledger cred alongside its goal-writer. The
|
|
7119
7141
|
// per-session serving creds join the same family, each with its own fresh identity.
|
|
7120
7142
|
this.sessionLedgerIdentity = siblings.sessionLedger;
|
|
@@ -7135,17 +7157,17 @@ export class Manager {
|
|
|
7135
7157
|
}
|
|
7136
7158
|
};
|
|
7137
7159
|
const publishAndProvision = async ({ authKv, nc: execNc }) => {
|
|
7138
|
-
// §13.7 contract-artifact publication
|
|
7160
|
+
// §13.7 contract-artifact publication: every schema root + its closure manifest, plus
|
|
7139
7161
|
// the cluster document + ITS manifest, land in the EPC store BEFORE the registration that
|
|
7140
7162
|
// advertises their digests — so a caller can always fetch-verify-compile a registered
|
|
7141
|
-
// digest (the
|
|
7163
|
+
// digest (the generic-invoke read path). Create-only + content-addressed: a retry
|
|
7142
7164
|
// or a same-artifact republish is an idempotent lost-CAS. The registration itself still
|
|
7143
7165
|
// verifies against the in-memory copies (the manager is the author).
|
|
7144
7166
|
const storeCtx = await contractStoreContext(execNc, this.space);
|
|
7145
7167
|
for (const value of [...managerContractArtifactValues(), artifacts.document, artifacts.manifest])
|
|
7146
7168
|
await publishContractArtifact(storeCtx, contractArtifactCanonicalBytes(value));
|
|
7147
|
-
// §13.1 pre-registration
|
|
7148
|
-
// principal — provisioned ONCE, on the FIRST registration. On a RESTART
|
|
7169
|
+
// §13.1 pre-registration: the issuance gate, born open@gen0 bound to the serve
|
|
7170
|
+
// principal — provisioned ONCE, on the FIRST registration. On a RESTART the gate
|
|
7149
7171
|
// already EXISTS (advanced past gen0 by the prior registration), so re-provisioning the gen0
|
|
7150
7172
|
// row would conflict "foreign content"; the persisted instanceId makes this a TAKEOVER, and
|
|
7151
7173
|
// registerServiceInstance below freezes + re-registers the EXISTING gate (advancing the epoch
|
|
@@ -7156,7 +7178,7 @@ export class Manager {
|
|
|
7156
7178
|
};
|
|
7157
7179
|
const completeRegistration = async ({ recordsKv, authKv, nc: execNc }) => {
|
|
7158
7180
|
await publishAndProvision({ recordsKv, authKv, nc: execNc });
|
|
7159
|
-
//
|
|
7181
|
+
// On an AUTH mesh a RE-registration (restart of the persisted instanceId)
|
|
7160
7182
|
// must VERIFY-EVICT the superseded serve family BEFORE the epoch advances (§13.1 "old authority
|
|
7161
7183
|
// dies before new authority is visible"). Inject the SCOPED delivery-admin evictor; the OPEN
|
|
7162
7184
|
// mesh mints no serve family, so no evictor (the empty-family path never consults it and a
|
|
@@ -7186,7 +7208,7 @@ export class Manager {
|
|
|
7186
7208
|
// authKv (the fence is live), so its credId lands in `epcred.<e>.<iid>` and the takeover
|
|
7187
7209
|
// barrier revokes it. Open mesh: no mint (no credential system; the goal-writer conn is bare).
|
|
7188
7210
|
const goalWriterCreds = auth ? await this.mintAndStageGoalWriter(authKv) : undefined;
|
|
7189
|
-
//
|
|
7211
|
+
// Mint + family-stage the session-LEDGER credential HERE too (same executor, same
|
|
7190
7212
|
// fence, same §13.1 family), so takeover revokes a deposed manager's ledger connection. The
|
|
7191
7213
|
// per-session SERVING credentials are minted later, one per redemption, into the SAME family.
|
|
7192
7214
|
// Open mesh: no mint (no credential system; the ledger connection stays bare).
|
|
@@ -7249,7 +7271,7 @@ export class Manager {
|
|
|
7249
7271
|
const auth = this.auth;
|
|
7250
7272
|
const identity = this.goalWriterIdentity;
|
|
7251
7273
|
// The issuance gate + §13.1 revocation family are keyed by the REGISTRATION instanceId
|
|
7252
|
-
// (the persisted logical id
|
|
7274
|
+
// (the persisted logical id), NOT the per-process lifecycleUid — the barrier
|
|
7253
7275
|
// enumerates `epcred.<e>.<managerInstanceId>`, so the goal-writer must stage into that family.
|
|
7254
7276
|
const iid = this.managerInstanceId;
|
|
7255
7277
|
const creds = await mintCreds(auth, identity, "goal-writer", { goalWriter: { endpoint: MANAGER_ENDPOINT } });
|
|
@@ -7287,8 +7309,8 @@ export class Manager {
|
|
|
7287
7309
|
});
|
|
7288
7310
|
nc.closed().then((err) => { void this.onGoalWriterConnectionClosed(err ?? undefined); });
|
|
7289
7311
|
gw.nc = nc;
|
|
7290
|
-
// The (i) fence resolver (SPEC 13.6
|
|
7291
|
-
// epoch. This manager reconciles only ITS OWN goals
|
|
7312
|
+
// The (i) fence resolver (SPEC 13.6): resolve an executing instance's CURRENT gate
|
|
7313
|
+
// epoch. This manager reconciles only ITS OWN goals, so it resolves its own
|
|
7292
7314
|
// registration instanceId to this incarnation's serve-grant epoch; a foreign/retired instance
|
|
7293
7315
|
// is `null` (no current terminal to surface). A successor incarnation carries an ADVANCED epoch
|
|
7294
7316
|
// here, so its reads pick the current-epoch subject and the predecessor's terminal is fenced out.
|
|
@@ -7341,7 +7363,7 @@ export class Manager {
|
|
|
7341
7363
|
return;
|
|
7342
7364
|
console.error("! manager goal-writer could not be re-dialed; leaving the credential to the next renewal pass");
|
|
7343
7365
|
}
|
|
7344
|
-
/**
|
|
7366
|
+
/** Spawn-as-action: stand up the standing self-mediated goal-writer connection +
|
|
7345
7367
|
* ActionContext. Mode-dual, mirroring {@link registerManagerService}: an AUTH mesh uses the
|
|
7346
7368
|
* scoped `goal-writer` credential already minted + family-STAGED inside registration's run block
|
|
7347
7369
|
* ({@link mintAndStageGoalWriter} — DISJOINT grant from the serve cred, SHARED §13.1 revocation
|
|
@@ -7375,7 +7397,7 @@ export class Manager {
|
|
|
7375
7397
|
catch { /* best effort */ }
|
|
7376
7398
|
}
|
|
7377
7399
|
}
|
|
7378
|
-
/**
|
|
7400
|
+
/** Mint the standing `session-ledger` credential and STAGE it into this instance's
|
|
7379
7401
|
* §13.1 revocation family (`epcred.<e>.<iid>`), over the passed executor's `authKv` — EXACTLY the
|
|
7380
7402
|
* {@link mintAndStageGoalWriter} pattern, under the same open-and-commit fence.
|
|
7381
7403
|
*
|
|
@@ -7388,7 +7410,7 @@ export class Manager {
|
|
|
7388
7410
|
async mintAndStageSessionLedger(authKv) {
|
|
7389
7411
|
const auth = this.auth;
|
|
7390
7412
|
const identity = this.sessionLedgerIdentity;
|
|
7391
|
-
// Registration instanceId (
|
|
7413
|
+
// Registration instanceId (the persisted logical id), not the per-process lifecycleUid:
|
|
7392
7414
|
// the barrier enumerates `epcred.<e>.<managerInstanceId>`, so the ledger cred joins that family.
|
|
7393
7415
|
const iid = this.managerInstanceId;
|
|
7394
7416
|
const fence = serveIssuanceGateKv(authKv, this.space, { endpoint: MANAGER_ENDPOINT, instanceId: iid });
|
|
@@ -7455,7 +7477,7 @@ export class Manager {
|
|
|
7455
7477
|
return;
|
|
7456
7478
|
console.error("! manager session-ledger could not be re-dialed; leaving the credential to the next renewal pass");
|
|
7457
7479
|
}
|
|
7458
|
-
/**
|
|
7480
|
+
/** Stand up the ONE §13.6 session plane on its own standing connection. Mode-dual,
|
|
7459
7481
|
* mirroring {@link startGoalWriter}: an AUTH mesh presents the scoped `session-ledger` cred
|
|
7460
7482
|
* already minted + family-staged inside registration's run block ({@link mintAndStageSessionLedger});
|
|
7461
7483
|
* an OPEN mesh uses a bare connection (no credential system to mint from). The connection presents
|
|
@@ -7506,10 +7528,10 @@ export class Manager {
|
|
|
7506
7528
|
const keyId = rotating.current().keyId;
|
|
7507
7529
|
this.sessionPlane = new ManagerSessionPlane({
|
|
7508
7530
|
space: this.space,
|
|
7509
|
-
// The session's serving identity is the persisted REGISTRATION instanceId
|
|
7531
|
+
// The session's serving identity is the persisted REGISTRATION instanceId, not the
|
|
7510
7532
|
// per-process lifecycleUid: a restarted manager re-registers the SAME logical instanceId with
|
|
7511
7533
|
// an ADVANCED epoch, so a client re-attaches by the same instance while the epoch fences the
|
|
7512
|
-
// old incarnation's sessions
|
|
7534
|
+
// old incarnation's sessions.
|
|
7513
7535
|
serving: { instanceId: this.managerInstanceId, epoch: serveEpoch },
|
|
7514
7536
|
// Read through the rotator on EVERY use rather than capturing a key: signing takes the newest
|
|
7515
7537
|
// generation, and resolution accepts any generation still inside its overlap so an artifact
|
|
@@ -7538,7 +7560,7 @@ export class Manager {
|
|
|
7538
7560
|
console.error(`manager session plane standing (endpoint ${MANAGER_ENDPOINT}, epoch ${serveEpoch}, ${(this.auth || this.remoteAuthority) ? "scoped session-ledger cred, §13.1 family-staged" : "open/bare"})`);
|
|
7539
7561
|
}
|
|
7540
7562
|
/**
|
|
7541
|
-
* The per-session SERVING credential seam (
|
|
7563
|
+
* The per-session SERVING credential seam (SPEC 13.6): the manager mints, gate-stages,
|
|
7542
7564
|
* connects and revokes ONE credential per live session, replacing a standing credential that held
|
|
7543
7565
|
* `eps.manager.*.<epoch>.{in,out}` and so reached every live session's bytes at its epoch.
|
|
7544
7566
|
*
|
|
@@ -7713,11 +7735,12 @@ export class Manager {
|
|
|
7713
7735
|
await nc.drain().catch(() => nc.close());
|
|
7714
7736
|
}
|
|
7715
7737
|
}
|
|
7716
|
-
// Single-manager
|
|
7717
|
-
// at a time), so all are reconciled. The `iid` field is the hook
|
|
7738
|
+
// Single-manager: EVERY inherited entry belongs to a DEAD predecessor (only one manager
|
|
7739
|
+
// at a time), so all are reconciled. The `iid` field is the hook the multi-instance sweep
|
|
7718
7740
|
// filters on (skip a goal whose accepting `iid` is a still-LIVE sibling — never settle its goal).
|
|
7719
7741
|
for (const entry of entries) {
|
|
7720
|
-
|
|
7742
|
+
const key = goalKey(entry.ref);
|
|
7743
|
+
if (this.goalAcceptances.has(key) || this.turnAcceptances.has(key))
|
|
7721
7744
|
continue; // never settle a goal THIS incarnation drives
|
|
7722
7745
|
try {
|
|
7723
7746
|
// A note marks a TURN entry: its relay is rebuilt (the hold is its bounded ending), never
|
|
@@ -7788,9 +7811,9 @@ export class Manager {
|
|
|
7788
7811
|
const t = setTimeout(() => { settle().catch((e) => console.error(`! goal reconcile settle ${ref.goalId}: ${rejectionText(e)}`)); }, remaining + 100);
|
|
7789
7812
|
t.unref?.();
|
|
7790
7813
|
}
|
|
7791
|
-
/**
|
|
7814
|
+
/** Publish a goal PROGRESS event on the caller-scoped epe subtree, over the SERVE
|
|
7792
7815
|
* connection (which holds the `epe.<e>.<iid>.<epoch>.>` egress grant; the goal-writer deliberately
|
|
7793
|
-
* does not). The terminal rides a final event `phase:"terminal"` (
|
|
7816
|
+
* does not). The terminal rides a final event `phase:"terminal"` (the caller follows epe to
|
|
7794
7817
|
* the terminal; the durable result fact + inspect/ps are the reconcile authority). A dropped event
|
|
7795
7818
|
* is non-fatal (the terminal is authoritative in the journal). */
|
|
7796
7819
|
emitGoalProgress(ref, epoch, event) {
|
|
@@ -7838,13 +7861,13 @@ export class Manager {
|
|
|
7838
7861
|
if (observed.processEpoch !== epoch)
|
|
7839
7862
|
throw new EpEnvelopeError("expired", `the manager's issuance gate epoch is ${observed.processEpoch} but this goal was accepted under epoch ${epoch}; a superseded incarnation never commits a goal terminal (SPEC 13.6)`);
|
|
7840
7863
|
}
|
|
7841
|
-
/** Serve `spawn`/`launch` as an ACTION
|
|
7864
|
+
/** Serve `spawn`/`launch` as an ACTION. Authz already ran in {@link serveGated}. The
|
|
7842
7865
|
* accept path runs INLINE on the handler ({@link startAgent} with hooks): the goal binds + the
|
|
7843
|
-
* acceptance replies the moment the identity is minted, BEFORE any provision
|
|
7866
|
+
* acceptance replies the moment the identity is minted, BEFORE any provision, or on the
|
|
7844
7867
|
* hosted enrollment arm once the host has enrolled the agent; progress and the terminal are driven
|
|
7845
7868
|
* OFF-handler, so the ~30s readiness wait no longer blocks the reply.
|
|
7846
7869
|
* Returns the acceptance floor payload {name, owner, actor, uid, goalId, fingerprint, executor}
|
|
7847
|
-
* (the ALLOCATED identity). goalId = the request id (env.id
|
|
7870
|
+
* (the ALLOCATED identity). goalId = the request id (env.id). */
|
|
7848
7871
|
async serveSpawnGoal(ctx, run) {
|
|
7849
7872
|
if (this.execution === "none")
|
|
7850
7873
|
throw new EpEnvelopeError("unimplemented", this.executionRefusal());
|
|
@@ -7858,14 +7881,15 @@ export class Manager {
|
|
|
7858
7881
|
const goalId = ctx.request.id;
|
|
7859
7882
|
const { fingerprint } = submissionFingerprint(ctx.request, ctx.subject);
|
|
7860
7883
|
const ref = goalRefOf(ctx.subject, goalId);
|
|
7884
|
+
const key = goalKey(ref);
|
|
7861
7885
|
const executor = { lifecycleUid: this.managerInstanceId, epoch: this.serviceServe?.grant.epoch ?? 0 };
|
|
7862
7886
|
const epoch = executor.epoch;
|
|
7863
7887
|
const acceptedAt = Date.now();
|
|
7864
7888
|
// Idempotent same-goalId retry (a client re-send): serve the IDENTICAL acceptance without
|
|
7865
|
-
// re-running the accept path — so a HARD-PINNED retry does not trip the
|
|
7889
|
+
// re-running the accept path — so a HARD-PINNED retry does not trip the same-name refuse and no
|
|
7866
7890
|
// name is re-allocated. Same-incarnation rides the live map; the create-only bindGoal in onAccepted
|
|
7867
7891
|
// still fences a CONCURRENT same-goalId race (the loser aborts and serves the winner's acceptance).
|
|
7868
|
-
const prior = this.goalAcceptances.get(
|
|
7892
|
+
const prior = this.goalAcceptances.get(key);
|
|
7869
7893
|
if (prior !== undefined) {
|
|
7870
7894
|
if (prior.fingerprint !== fingerprint)
|
|
7871
7895
|
throw new EpEnvelopeError("failed-precondition", `goal "${goalId}" was accepted under a different submission; one goalId never carries two specs (SPEC 13.6)`);
|
|
@@ -7969,15 +7993,15 @@ export class Manager {
|
|
|
7969
7993
|
// No claim needed here: `ownsGoal` is still false, so the commit path refuses this attempt.
|
|
7970
7994
|
throw new EpEnvelopeError("failed-precondition", `goal "${goalId}" was accepted by instance "${idx.existing.iid}"; that instance's acceptance is served and this attempt provisions nothing (SPEC 13.6)`);
|
|
7971
7995
|
}
|
|
7972
|
-
// Bind AFTER the accept-path checks (
|
|
7973
|
-
// a create-only CAS per goalId (
|
|
7996
|
+
// Bind AFTER the accept-path checks (name/capacity/persona) + identity mint, BEFORE any provision:
|
|
7997
|
+
// a create-only CAS per goalId (a refused accept above left zero bind, zero reserve).
|
|
7974
7998
|
const b = await bindGoal(gw.ctx, ref, fingerprint);
|
|
7975
7999
|
if (!b.bound) {
|
|
7976
8000
|
if (b.existing.fingerprint !== fingerprint)
|
|
7977
8001
|
throw new EpEnvelopeError("failed-precondition", `goal "${goalId}" is already bound to a different submission; one goalId never carries two specs (SPEC 13.6)`);
|
|
7978
8002
|
// Lost a concurrent same-goalId race: serve the winner's acceptance and abort THIS provision
|
|
7979
8003
|
// (no second spawn). The throw unwinds to the finally, which releases this attempt's reserve.
|
|
7980
|
-
acceptance = this.goalAcceptances.get(
|
|
8004
|
+
acceptance = this.goalAcceptances.get(key) ?? await this.cachedSpawnAcceptance(ref, goalId, fingerprint, executor);
|
|
7981
8005
|
resolveAccept(acceptance);
|
|
7982
8006
|
// No claim needed here either. This attempt lost the CAS, so `ownsGoal` is still false and
|
|
7983
8007
|
// the single check in `onOutcome` refuses it. Before that check existed this site had to
|
|
@@ -8001,8 +8025,8 @@ export class Manager {
|
|
|
8001
8025
|
readinessDeadlineMs: readinessTimeoutMs,
|
|
8002
8026
|
});
|
|
8003
8027
|
acceptance = { name, owner: agentTriple.owner, actor: agentTriple.actor, uid: agentTriple.uid, goalId, fingerprint, readinessDeadlineMs: readinessTimeoutMs, executor };
|
|
8004
|
-
this.goalAcceptances.set(
|
|
8005
|
-
this.agentGoals.set(name, ref); //
|
|
8028
|
+
this.goalAcceptances.set(key, acceptance);
|
|
8029
|
+
this.agentGoals.set(name, ref); // A despawn of this name mid-goal drives the cancel path
|
|
8006
8030
|
resolveAccept(acceptance);
|
|
8007
8031
|
this.emitGoalProgress(ref, epoch, { phase: "handoff" });
|
|
8008
8032
|
},
|
|
@@ -8015,7 +8039,7 @@ export class Manager {
|
|
|
8015
8039
|
onTerminalDeferred: () => { terminalEntered = true; },
|
|
8016
8040
|
});
|
|
8017
8041
|
bg.then((reply) => {
|
|
8018
|
-
// Refused BEFORE onAccepted (
|
|
8042
|
+
// Refused BEFORE onAccepted (hard-pin collision, capacity, persona-not-found) — no goal bound.
|
|
8019
8043
|
if (acceptance === undefined) {
|
|
8020
8044
|
const details = reply.details;
|
|
8021
8045
|
rejectAccept(new EpEnvelopeError("failed-precondition", reply.error ?? "spawn refused at accept", details));
|
|
@@ -8107,7 +8131,7 @@ export class Manager {
|
|
|
8107
8131
|
const goalId = ctx.request.id;
|
|
8108
8132
|
const { fingerprint } = submissionFingerprint(ctx.request, ctx.subject);
|
|
8109
8133
|
const ref = goalRefOf(ctx.subject, goalId);
|
|
8110
|
-
const key =
|
|
8134
|
+
const key = goalKey(ref);
|
|
8111
8135
|
const executor = { lifecycleUid: this.managerInstanceId, epoch: this.serviceServe?.grant.epoch ?? 0 };
|
|
8112
8136
|
const acceptedAt = Date.now();
|
|
8113
8137
|
const prior = this.turnAcceptances.get(key)?.acceptance;
|
|
@@ -8366,7 +8390,7 @@ export class Manager {
|
|
|
8366
8390
|
const raw = (ctx.request.args ?? {});
|
|
8367
8391
|
const goalId = String(raw.goalId);
|
|
8368
8392
|
const ref = goalRefOf(ctx.subject, goalId);
|
|
8369
|
-
const key =
|
|
8393
|
+
const key = goalKey(ref);
|
|
8370
8394
|
const p = this.pendingTurns.get(key);
|
|
8371
8395
|
if (p === undefined) {
|
|
8372
8396
|
const ended = await readGoalResult(gw.ctx, ref);
|
|
@@ -8407,7 +8431,7 @@ export class Manager {
|
|
|
8407
8431
|
const gw = this.goalWriter;
|
|
8408
8432
|
if (!gw)
|
|
8409
8433
|
return;
|
|
8410
|
-
const key =
|
|
8434
|
+
const key = goalKey(p.ref);
|
|
8411
8435
|
if (!this.pendingTurns.delete(key))
|
|
8412
8436
|
return;
|
|
8413
8437
|
this.markTurnLatchDropped(key);
|
|
@@ -8581,7 +8605,7 @@ export class Manager {
|
|
|
8581
8605
|
console.error(`turn reconcile ${p.goalId}: its deadline hold was never minted (the accept crashed before it); left unsettled`);
|
|
8582
8606
|
return;
|
|
8583
8607
|
}
|
|
8584
|
-
const key =
|
|
8608
|
+
const key = goalKey(p.ref);
|
|
8585
8609
|
this.pendingTurns.set(key, p);
|
|
8586
8610
|
// THE ACCEPTANCE TOO, or the adopted turn's answer is never remembered: `rememberSettledTurn`
|
|
8587
8611
|
// writes only against an acceptance this incarnation holds, and adoption rebuilt the pending
|
|
@@ -8605,12 +8629,12 @@ export class Manager {
|
|
|
8605
8629
|
console.error(`turn reconcile ${p.goalId}: pending relay to ${p.seat.name}/${p.seat.uid} adopted (deadline ${p.deadlineAt})`);
|
|
8606
8630
|
this.ensureTurnSweep();
|
|
8607
8631
|
}
|
|
8608
|
-
/**
|
|
8632
|
+
/** Settle race: a despawn MID-GOAL drives the goal's cancel terminal - transition to
|
|
8609
8633
|
* `cancelling`, then commit the `cancel` cause on the goal-writer connection. First-terminal-fact
|
|
8610
8634
|
* wins: if the readiness outcome already committed (succeeded/failed/uncertain) the transition or
|
|
8611
8635
|
* the create-only commit loses gracefully and the readiness terminal stands. Fire-and-forget from
|
|
8612
8636
|
* despawn (the process teardown is authoritative for the agent; this settles the GOAL honestly).
|
|
8613
|
-
* Cancel rides the despawn's own authorizeNamed reach
|
|
8637
|
+
* Cancel rides the despawn's own authorizeNamed reach - there is no cancel-by-goalId. */
|
|
8614
8638
|
async cancelAgentGoal(name, mode) {
|
|
8615
8639
|
const gw = this.goalWriter;
|
|
8616
8640
|
const ref = this.agentGoals.get(name);
|
|
@@ -9161,12 +9185,12 @@ export class Manager {
|
|
|
9161
9185
|
retiredSlots++;
|
|
9162
9186
|
continue;
|
|
9163
9187
|
}
|
|
9164
|
-
//
|
|
9188
|
+
// RECONCILE OWNERSHIP (multi-manager-per-space): a manager adjudicates ONLY the non-retired
|
|
9165
9189
|
// rows THIS logical instance owns. A SIBLING manager's active/provisioning row is LEFT UNTOUCHED —
|
|
9166
9190
|
// sweep-terminalizing it would destroy the sibling's live agent (the historical all-agents-kill
|
|
9167
|
-
// hazard, now cross-instance). A legacy row (
|
|
9191
|
+
// hazard, now cross-instance). A legacy row (no owner recorded) predates multi-manager,
|
|
9168
9192
|
// so this manager is its legitimate single-manager-past successor and reconciles it. An orphaned
|
|
9169
|
-
// sibling row is reclaimed only by an explicit operator CAS takeover
|
|
9193
|
+
// sibling row is reclaimed only by an explicit operator CAS takeover, never here.
|
|
9170
9194
|
if (ownedBySibling(row, this.managerInstanceId)) {
|
|
9171
9195
|
preservedForeign++;
|
|
9172
9196
|
continue;
|
|
@@ -9608,7 +9632,7 @@ export class Manager {
|
|
|
9608
9632
|
throw new EpEnvelopeError("unavailable", `input for seat "${a.name}" failed: runtime accepted ${String(bytes)} of ${intendedBytes} bytes`);
|
|
9609
9633
|
return { name: a.name, bytes };
|
|
9610
9634
|
}
|
|
9611
|
-
/** The post-authorization attach effect
|
|
9635
|
+
/** The post-authorization attach effect: mint the holder-bound §13.6 offer, redeem it
|
|
9612
9636
|
* through the ONE session plane (one-use CAS + presenter-equality), and stand up the PTY bridge —
|
|
9613
9637
|
* atomically. The reply is the SIGNED grant (no ws:// URL, non-bearer, never logged); the caller
|
|
9614
9638
|
* redeems it over the mesh with a per-session rails-only cred it mints itself. Only streamable
|
|
@@ -9650,7 +9674,7 @@ export class Manager {
|
|
|
9650
9674
|
const carried = a.launch.resumed?.host;
|
|
9651
9675
|
return { ok: true, data: { grant, ...(carried ? { resumedFrom: { host: carried, source: a.launch.resumed.source } } : {}) } };
|
|
9652
9676
|
}
|
|
9653
|
-
/**
|
|
9677
|
+
/** The console's mesh §13.6 session establisher (backing `POST /session/<name>` on the
|
|
9654
9678
|
* loopback face). Drives THE ONE plane — same establishAttach as the ep `attach` command — with
|
|
9655
9679
|
* the loopback OPERATOR as holder (same-host trust boundary), then hands the browser everything it
|
|
9656
9680
|
* needs to open the caller rail over the broker ws listener: the holder-bound grant, a per-session
|