@cotal-ai/auth 0.57.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/auth-admin.d.ts +10 -0
  2. package/dist/auth-admin.d.ts.map +1 -1
  3. package/dist/auth-admin.js +162 -213
  4. package/dist/auth-admin.js.map +1 -1
  5. package/dist/auth-service-contract.d.ts +64 -0
  6. package/dist/auth-service-contract.d.ts.map +1 -0
  7. package/dist/auth-service-contract.js +131 -0
  8. package/dist/auth-service-contract.js.map +1 -0
  9. package/dist/authority-client.d.ts +16 -0
  10. package/dist/authority-client.d.ts.map +1 -1
  11. package/dist/authority-client.js +65 -1
  12. package/dist/authority-client.js.map +1 -1
  13. package/dist/barrier-evict.d.ts +7 -0
  14. package/dist/barrier-evict.d.ts.map +1 -1
  15. package/dist/barrier-evict.js +83 -21
  16. package/dist/barrier-evict.js.map +1 -1
  17. package/dist/callout.d.ts.map +1 -1
  18. package/dist/callout.js +9 -3
  19. package/dist/callout.js.map +1 -1
  20. package/dist/commands.js +18 -11
  21. package/dist/commands.js.map +1 -1
  22. package/dist/grant-command.d.ts +9 -8
  23. package/dist/grant-command.d.ts.map +1 -1
  24. package/dist/grant-command.js +9 -8
  25. package/dist/grant-command.js.map +1 -1
  26. package/dist/idp.d.ts +10 -1
  27. package/dist/idp.d.ts.map +1 -1
  28. package/dist/idp.js +11 -3
  29. package/dist/idp.js.map +1 -1
  30. package/dist/index.d.ts +3 -3
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +3 -3
  33. package/dist/index.js.map +1 -1
  34. package/dist/issuer.d.ts +3 -1
  35. package/dist/issuer.d.ts.map +1 -1
  36. package/dist/issuer.js +6 -2
  37. package/dist/issuer.js.map +1 -1
  38. package/dist/ledger.d.ts +6 -7
  39. package/dist/ledger.d.ts.map +1 -1
  40. package/dist/ledger.js +1 -1
  41. package/dist/ledger.js.map +1 -1
  42. package/dist/managed-agent-enrollment.d.ts +29 -1
  43. package/dist/managed-agent-enrollment.d.ts.map +1 -1
  44. package/dist/managed-agent-enrollment.js +45 -1
  45. package/dist/managed-agent-enrollment.js.map +1 -1
  46. package/dist/manager-authority.d.ts +102 -1
  47. package/dist/manager-authority.d.ts.map +1 -1
  48. package/dist/manager-authority.js +276 -16
  49. package/dist/manager-authority.js.map +1 -1
  50. package/dist/manager-contract.d.ts +4 -0
  51. package/dist/manager-contract.d.ts.map +1 -1
  52. package/dist/manager-contract.js +7 -1
  53. package/dist/manager-contract.js.map +1 -1
  54. package/dist/permissions.d.ts +15 -1
  55. package/dist/permissions.d.ts.map +1 -1
  56. package/dist/permissions.js +20 -1
  57. package/dist/permissions.js.map +1 -1
  58. package/dist/provider.d.ts.map +1 -1
  59. package/dist/provider.js +44 -11
  60. package/dist/provider.js.map +1 -1
  61. package/dist/service.d.ts +90 -2
  62. package/dist/service.d.ts.map +1 -1
  63. package/dist/service.js +628 -155
  64. package/dist/service.js.map +1 -1
  65. package/dist/session-redemption.d.ts +51 -0
  66. package/dist/session-redemption.d.ts.map +1 -0
  67. package/dist/session-redemption.js +61 -0
  68. package/dist/session-redemption.js.map +1 -0
  69. package/dist/token.d.ts +15 -2
  70. package/dist/token.d.ts.map +1 -1
  71. package/dist/token.js +30 -3
  72. package/dist/token.js.map +1 -1
  73. package/package.json +6 -6
package/dist/service.js CHANGED
@@ -54,27 +54,29 @@
54
54
  */
55
55
  import { createHash, randomBytes } from "node:crypto";
56
56
  import { createServer } from "node:http";
57
+ import { resolve } from "node:path";
57
58
  import { connect, credsAuthenticator } from "@nats-io/transport-node";
58
59
  import { jetstreamManager } from "@nats-io/jetstream";
59
60
  import { Kvm } from "@nats-io/kv";
60
- import { admissionMediatorGrants, assertDerivedOwnerToken, assertLifecycleToken, assertValidOwnerToken, authorizeTrustedServeSnapshot, commitSiblingIssuance, EpEnvelopeError, ensureAuthorityStores, epAuthBucket, isReachable, managedRetirementOpId, mintCreds, mintPublicUserJwt, newIdentity, parseEndpointGate, parseServiceSpec, parseServiceStatus, rawDigest, readSvcRecordLeader, reconcileEndpointGate, recordSpecKey, recordStatusKey, RECORD_KINDS, recordsBucket, remoteManagerActors, retirementFrontierStreams, serveIssuanceGateKv, standaloneConnectOpts, STANDING_RENEWABLE_TTL_SEC } from "@cotal-ai/core";
61
- import { findCotalRoot, loadManagerInstanceIdentity, userAuthStateDir, workspaceSecretStore } from "@cotal-ai/workspace";
61
+ import { contractRefToHex, contractStoreContext, fetchContractArtifact, verifyClusterManifest, verifyClusterRoot, admissionBucket, admissionMediatorGrants, assertDerivedOwnerToken, assertLifecycleToken, assertValidOwnerToken, authorizeTrustedServeSnapshot, commitSiblingIssuance, credsClaims, EpEnvelopeError, ensureAuthorityStores, epAuthBucket, isReachable, jwtFromCreds, managedRetirementOpId, mintCreds, mintPublicUserJwt, newIdentity, observeHostedRunAttempt, openRecordsBucket, parseEndpointGate, parseServiceSpec, parseServiceStatus, rawDigest, parseSecretStoreIdentity, readCheckpointStatus, readRunAdmission, readRunRecord, readSvcRecordLeader, reconcileEndpointGate, sameSecretStoreIdentity, recordSpecKey, recordStatusKey, RECORD_KINDS, recordsBucket, remoteManagerActors, retirementFrontierStreams, runDriverCaller, serveIssuanceGateKv, standaloneConnectOpts, STANDING_RENEWABLE_TTL_SEC, withIssuerSession, authorizeServeGrant, contractArtifactCanonicalBytes, DEV_OWNER, endpointRegistrationBarrier, mintLifecycleUid, principalKey, provisionEndpointGateOpen, publishContractArtifact, registerServiceInstance, SERVICE_READY, writeServiceStatus } from "@cotal-ai/core";
62
+ import { findCotalRoot, loadManagerInstanceIdentity, userAuthStateDir, workspaceSecretStore, createAuthInstanceIdentity, loadAuthInstanceIdentity } from "@cotal-ai/workspace";
62
63
  import { decodeJwt } from "jose";
63
64
  import { deriveOwnerForIdpSubject } from "./derive.js";
64
65
  import { startAuthCallout } from "./callout.js";
65
66
  import { createIdpBridge, verifyIdpToken } from "./idp.js";
66
67
  import { PUBLIC_EXCHANGE_VIEWS } from "./token.js";
68
+ import { grantCoordinates, verifySessionRedemption } from "./session-redemption.js";
67
69
  import { pinnedJwksResolver } from "./issuer.js";
68
70
  import { calloutPermissions } from "./permissions.js";
69
- import { issueRemoteManagerAuthority } from "./manager-authority.js";
71
+ import { admitRemoteRun, authorizeRemoteManagerRenewal, authorizeRemoteRunAttempt, issueRemoteManagerAuthority, parseRemoteRunAdmissionRequest, parseRemoteRunAttemptRequest } from "./manager-authority.js";
70
72
  import { authorizeRemoteRetainedAgentValidation, completeRemoteRetainedAgentValidation, remoteManagerCurrentRegistrationProof } from "./retained-manager-validation.js";
71
- import { authorizeRemoteManagedAgentEnrollment, authorizeRemoteManagedAgentPrepareRetirement } from "./managed-agent-enrollment.js";
73
+ import { authorizeRemoteManagedAgentEnrollment, authorizeRemoteManagedAgentPrepareRetirement, authorizeRemoteManagedAgentRuntimeCreate, authorizeRemoteManagedAgentRuntimeStatus } from "./managed-agent-enrollment.js";
72
74
  import { authorizeRemoteManagerGoalIndexScan, completeRemoteManagerGoalIndexScan } from "./manager-goal-index.js";
73
75
  import { authorizeRemoteManagerAdmin } from "./manager-admin-authorization.js";
74
76
  import { authorizeRemoteManagerMaintenance, completeRemoteManagerMaintenance } from "./manager-maintenance.js";
75
77
  import { validateRetainedManagedAgent } from "./continuity.js";
76
- import { reconstructRemoteManagerServeGrant } from "./manager-contract.js";
77
- import { authorityBarrierGrants, authorityWriterGrants, openAuthorityClient, openSupervisedConnectReader, remoteManagerIssuerGrants, remoteManagerRegistrationProof } from "./authority-client.js";
78
+ import { reconstructRemoteManagerServeGrant, remoteManagerServeGrantFromCluster } from "./manager-contract.js";
79
+ import { authorityBarrierGrants, authorityWriterGrants, openAuthorityClient, openSupervisedConnectReader, remoteManagerIssuerGrants, remoteManagerRegistrationProof, authRegistrationExecutorGrants } from "./authority-client.js";
78
80
  import { authorizeConnectCredential } from "./connect-reader.js";
79
81
  import { ensureRootCredential } from "./root-credential.js";
80
82
  import { observeGate, openLifecycleRegistry, readLifecycleHeadForOperation } from "./lifecycle-registry.js";
@@ -82,11 +84,12 @@ import { openAuthLedgerScannerCandidate } from "./ledger-scanner.js";
82
84
  import { openRecordsScannerCandidate } from "./records-scanner.js";
83
85
  import { acquirePlaneClaim, makeDeliveryAdminPlaneOracle, makeDeliveryAdminPrincipalOracle, scannerDeathCopy } from "./plane-claim.js";
84
86
  import { enumerateOperationIntents, resumeAgentTakeover } from "./credential-ledger.js";
85
- import { makeDeliveryAdminEvictor } from "./barrier-evict.js";
87
+ import { makeDeliveryAdminEvictor, makeDeliveryAdminHolderEvictor } from "./barrier-evict.js";
86
88
  import { resumeAgentRetirement, runAgentRetirementBarrier } from "./retirement-barrier.js";
87
89
  import { makeRetirementCleaners } from "./retirement-cleaner.js";
88
90
  import { makeDrainRepairers } from "./drain-repair.js";
89
91
  import { joinOrStartRetirement, openAuthAdminListener } from "./auth-admin.js";
92
+ import { AUTH_SERVICE_ENDPOINT, authClusterArtifacts, authContractArtifactValues } from "./auth-service-contract.js";
90
93
  import { drainTargetForEndpoint, openAdmissionMediator } from "./admission-mediator.js";
91
94
  import { AGENT_BEARER_TTL_SEC, findInteractiveActor, findManagedActor, ledgerAclResolver, ledgerAuthorizeAgentExchange, ledgerAuthorizeConnect, ledgerAuthorizeGrant, } from "./ledger.js";
92
95
  import { AUTH_PROVIDER_NAME, clearAuthServiceInfo, loadCalloutAuth, loadIssuer, loadOwnerSecret, loadPinnedIdp, loadServiceKeys, saveAuthServiceInfo, spaceIssuer, } from "./store.js";
@@ -143,6 +146,10 @@ export function authorizeRemoteManagerRetirement(args) {
143
146
  */
144
147
  export async function openAuthAuthorityPlane(opts) {
145
148
  const { server, space, dataAccount, log } = opts;
149
+ const standingTtl = opts.standingRenewableTtlSeconds ?? STANDING_RENEWABLE_TTL_SEC;
150
+ if (!Number.isSafeInteger(standingTtl) || standingTtl < 5 || standingTtl > STANDING_RENEWABLE_TTL_SEC) {
151
+ throw new Error(`standingRenewableTtlSeconds must be an integer between 5 and ${STANDING_RENEWABLE_TTL_SEC} seconds (got ${standingTtl})`);
152
+ }
146
153
  const writer = await openAuthorityClient({ server, space, dataAccount, label: `cotal:auth-mint:${space}`, grants: (id) => authorityWriterGrants(space, id), log });
147
154
  let registry;
148
155
  try {
@@ -153,6 +160,87 @@ export async function openAuthAuthorityPlane(opts) {
153
160
  await writer.close();
154
161
  throw e;
155
162
  }
163
+ // #399 M2: register the auth plane itself as an ordinary `auth` service endpoint — the SAME
164
+ // §13.7 registration ceremony the manager runs (`manager.ts:6562-6690`), before the listener
165
+ // below serves a single request. The persisted instance id + serve nkey (under the plane's
166
+ // `dir`, hardened the way the manager's own instance identity is) so a restart re-registers the
167
+ // SAME instance: `registerServiceInstance` advances the process epoch on a re-registration and
168
+ // fences the predecessor; a first registration stays at epoch 0.
169
+ let authServeInstanceId;
170
+ let authServeGrant;
171
+ try {
172
+ const persisted = loadAuthInstanceIdentity(opts.dir, space);
173
+ const authIdentity = persisted ?? createAuthInstanceIdentity(opts.dir, space, {
174
+ instanceId: mintLifecycleUid(),
175
+ serveIdentity: newIdentity(),
176
+ });
177
+ const iid = authIdentity.instanceId;
178
+ const artifacts = authClusterArtifacts();
179
+ const store = new Map([
180
+ [artifacts.rootDigest, artifacts.document],
181
+ [artifacts.closureDigest, artifacts.manifest],
182
+ ]);
183
+ const readClusterArtifact = (digest) => store.get(digest);
184
+ // §13.9 name authority, static mode: the auth plane holds the space signing seed, so it
185
+ // self-authorizes exactly its own name ("auth") for exactly DEV_OWNER — mirrors the manager's
186
+ // own self-authorization for "manager".
187
+ const authority = {
188
+ authorize: (name, owner) => ({ authorized: name === AUTH_SERVICE_ENDPOINT && owner === DEV_OWNER, revision: 0 }),
189
+ };
190
+ const servePrincipal = principalKey(DEV_OWNER, authIdentity.serveIdentity.id).key;
191
+ const registrationOpId = mintLifecycleUid();
192
+ const regClient = await openAuthorityClient({
193
+ server, space, dataAccount, label: `cotal:auth-registration:${space}`,
194
+ grants: (id) => authRegistrationExecutorGrants(space, id, iid),
195
+ log,
196
+ });
197
+ try {
198
+ const authKv = await new Kvm(regClient.nc).open(epAuthBucket(space));
199
+ const recordsKv = await new Kvm(regClient.nc).open(recordsBucket(space));
200
+ // §13.7 contract-artifact publication BEFORE the registration that advertises the digests.
201
+ const storeCtx = await contractStoreContext(regClient.nc, space);
202
+ for (const value of [...authContractArtifactValues(), artifacts.document, artifacts.manifest])
203
+ await publishContractArtifact(storeCtx, contractArtifactCanonicalBytes(value));
204
+ // §13.1 pre-registration: the issuance gate, born open@gen0, provisioned ONCE on the FIRST
205
+ // registration. On a restart the gate already exists; `registerServiceInstance` below
206
+ // freezes + re-registers it (advancing the epoch).
207
+ if ((await serveIssuanceGateKv(authKv, space, { endpoint: AUTH_SERVICE_ENDPOINT, instanceId: iid }).observe()) === null)
208
+ await provisionEndpointGateOpen(authKv, { endpoint: AUTH_SERVICE_ENDPOINT, instanceId: iid, principal: servePrincipal });
209
+ const barrier = endpointRegistrationBarrier(authKv, space, { endpoint: AUTH_SERVICE_ENDPOINT, instanceId: iid, opId: registrationOpId });
210
+ const spec = { endpoint: AUTH_SERVICE_ENDPOINT, owner: DEV_OWNER, clusterDigests: [artifacts.closureDigest], protocol: { v: 1 } };
211
+ const { registrationRevision } = await registerServiceInstance(recordsKv, {
212
+ space, spec, instanceId: iid, registrant: { owner: DEV_OWNER }, authority, barrier, readClusterArtifact,
213
+ });
214
+ const fence = serveIssuanceGateKv(authKv, space, { endpoint: AUTH_SERVICE_ENDPOINT, instanceId: iid });
215
+ const observed = await fence.observe();
216
+ if (observed === null)
217
+ throw new Error(`the issuance gate for ${AUTH_SERVICE_ENDPOINT}/${iid} vanished after registration`);
218
+ const readProcessEpoch = async () => {
219
+ const g = await fence.observe();
220
+ if (g === null)
221
+ throw new Error(`no issuance gate for ${AUTH_SERVICE_ENDPOINT}/${iid}`);
222
+ return g.processEpoch;
223
+ };
224
+ const grant = await authorizeServeGrant(recordsKv, {
225
+ space, endpoint: AUTH_SERVICE_ENDPOINT, instanceId: iid, epoch: observed.processEpoch,
226
+ holder: { owner: DEV_OWNER }, authority, readClusterArtifact, readProcessEpoch,
227
+ });
228
+ await writeServiceStatus(recordsKv, {
229
+ endpoint: AUTH_SERVICE_ENDPOINT, instanceId: iid, epoch: observed.processEpoch,
230
+ status: { state: SERVICE_READY, epoch: observed.processEpoch, observedSpecRevision: registrationRevision },
231
+ readProcessEpoch,
232
+ });
233
+ authServeInstanceId = iid;
234
+ authServeGrant = grant;
235
+ }
236
+ finally {
237
+ await regClient.close();
238
+ }
239
+ }
240
+ catch (e) {
241
+ await writer.close();
242
+ throw e;
243
+ }
156
244
  let reader;
157
245
  try {
158
246
  reader = await openSupervisedConnectReader({ server, space, dataAccount, log });
@@ -325,7 +413,7 @@ export async function openAuthAuthorityPlane(opts) {
325
413
  // the FRESH space-manager-lease holder check) and dispatches.
326
414
  let authAdmin;
327
415
  try {
328
- authAdmin = await openAuthAdminListener({ server, space, dataAccount, reg: barrierReg, retirement, barrierFlight: retirementFlights, log });
416
+ authAdmin = await openAuthAdminListener({ server, space, dataAccount, reg: barrierReg, retirement, barrierFlight: retirementFlights, instanceId: authServeInstanceId, epoch: authServeGrant.epoch, grant: authServeGrant, log });
329
417
  }
330
418
  catch (e) {
331
419
  closing = true;
@@ -340,9 +428,34 @@ export async function openAuthAuthorityPlane(opts) {
340
428
  }
341
429
  const fileArm = ledgerAuthorizeConnect(opts.dir);
342
430
  const recordsJsm = await jetstreamManager(remoteIssuer.nc);
343
- const root = findCotalRoot();
431
+ const loadLocalManager = opts.localManager ?? (() => undefined);
432
+ // Standing renewal re-derives the serve surface from the REGISTERED service spec at the gate's
433
+ // registration revision: spec (leader read) -> closure manifest -> root document, each verified
434
+ // against its content digest. The request never selects the surface.
435
+ const registeredManagerCluster = async (owner, instanceId, observed) => {
436
+ const rec = await readSvcRecordLeader(recordsJsm, space, recordSpecKey(RECORD_KINDS.svc, ["manager", instanceId]));
437
+ if (!rec || "deleted" in rec || rec.revision !== observed.registrationRevision)
438
+ throw new EpEnvelopeError("failed-precondition", "manager-service renewal found no service spec at the gate's registration revision");
439
+ const spec = parseServiceSpec(rec.value, { endpoint: "manager" });
440
+ if (spec.owner !== owner)
441
+ throw new EpEnvelopeError("permission-denied", "manager-service renewal spec belongs to another owner");
442
+ const store = await contractStoreContext(remoteIssuer.nc, space);
443
+ const read = async (digest) => {
444
+ const bytes = await fetchContractArtifact(store, contractRefToHex(digest));
445
+ if (!bytes)
446
+ throw new EpEnvelopeError("failed-precondition", `manager-service renewal cannot read registered contract artifact ${digest}`);
447
+ return JSON.parse(new TextDecoder().decode(bytes));
448
+ };
449
+ for (const closure of spec.clusterDigests) {
450
+ const { root } = verifyClusterManifest(closure, await read(closure));
451
+ const document = await read(root);
452
+ if (verifyClusterRoot(root, document).urn === "ai.cotal.manager")
453
+ return document;
454
+ }
455
+ throw new EpEnvelopeError("failed-precondition", "manager-service renewal spec registers no manager cluster");
456
+ };
344
457
  const managerGate = async (owner, instanceId) => {
345
- const localManager = loadManagerInstanceIdentity(root, space);
458
+ const localManager = loadLocalManager();
346
459
  const gate = await serveIssuanceGateKv(await new Kvm(remoteIssuer.nc).open(epAuthBucket(space)), space, { endpoint: "manager", instanceId }).observe();
347
460
  if (!gate)
348
461
  return "unknown";
@@ -379,7 +492,7 @@ export async function openAuthAuthorityPlane(opts) {
379
492
  const remoteInstances = [];
380
493
  const remoteLive = [];
381
494
  let localCandidate;
382
- const localManager = loadManagerInstanceIdentity(root, space);
495
+ const localManager = loadLocalManager();
383
496
  for (const raw of await scanner.scanManagerGates()) {
384
497
  if (raw.op !== undefined)
385
498
  continue;
@@ -434,6 +547,18 @@ export async function openAuthAuthorityPlane(opts) {
434
547
  if (verdict !== "candidate")
435
548
  throw new Error(`manager instance ${instanceId} is ${verdict} at the permission mint`);
436
549
  },
550
+ verifySession: async (principal, claim) => {
551
+ refuseIfFenced();
552
+ return verifySessionRedemption({
553
+ jsm: recordsJsm,
554
+ space,
555
+ managerGate: async (owner, instanceId) => {
556
+ const verdict = await managerGate(owner, assertLifecycleToken(instanceId, "serving.instanceId"));
557
+ const gate = await serveIssuanceGateKv(await new Kvm(remoteIssuer.nc).open(epAuthBucket(space)), space, { endpoint: "manager", instanceId }).observe();
558
+ return { verdict, ...(gate ? { processEpoch: gate.processEpoch } : {}) };
559
+ },
560
+ }, principal, claim);
561
+ },
437
562
  retireInteractiveLifecycle: async (args) => {
438
563
  refuseIfFenced();
439
564
  const owner = assertDerivedOwnerToken(args.owner);
@@ -494,13 +619,43 @@ export async function openAuthAuthorityPlane(opts) {
494
619
  },
495
620
  issueManagerServiceAuthority: async ({ owner, scope, request }) => {
496
621
  refuseIfFenced();
622
+ const observeManagerGate = async (instanceId) => (await serveIssuanceGateKv(await new Kvm(remoteIssuer.nc).open(epAuthBucket(space)), space, { endpoint: "manager", instanceId }).observe()) ?? null;
497
623
  return issueRemoteManagerAuthority({
498
624
  owner,
499
625
  scope,
500
626
  request,
627
+ authorizeRenewal: ({ owner: o, request: r }) => authorizeRemoteManagerRenewal({
628
+ request: r, owner: o, space, accountPublicKey: dataAccount.pub, proofSecret: dataAccount.signingSeed,
629
+ observeManagerGate,
630
+ observeRun: async (runId) => {
631
+ const recordsKv = await openRecordsBucket(remoteIssuer.nc, space);
632
+ return await observeHostedRunAttempt(recordsKv, "manager", runId, {
633
+ supervisorId: r.identities.supervisor.id,
634
+ instanceId: r.instanceId,
635
+ });
636
+ },
637
+ }),
501
638
  issue: async ({ actors, request: r }) => {
502
639
  const credential = async (key, profile, actor, opts) => mintPublicUserJwt({ space, account: { pub: dataAccount.pub, signingSeed: dataAccount.signingSeed } }, r.identities[key].id, profile, { ...opts, principal: { owner, actor }, lifecycleUid: r.managerLifecycleUid });
503
640
  const credentials = {};
641
+ const siblingOn = (gate, observed) => async (key, profile, actor) => {
642
+ const issued = await credential(key, profile, actor, profile === "goal-writer" ? { goalWriter: { endpoint: "manager" }, expiresInSeconds: standingTtl } : { expiresInSeconds: standingTtl });
643
+ await commitSiblingIssuance(gate, observed, {
644
+ credentialId: rawDigest(issued.jwt).replace("sha256:", "sha256-"),
645
+ credentialKey: r.identities[key].id,
646
+ holderPrincipal: `${owner}.${actor}`,
647
+ endpoint: "manager",
648
+ lifecycleUid: r.instanceId,
649
+ sourceChain: ["root"],
650
+ state: "active",
651
+ exp: issued.exp,
652
+ generation: observed.generation,
653
+ processEpoch: observed.processEpoch,
654
+ registrationRevision: observed.registrationRevision,
655
+ nameAuthorityRevision: observed.nameAuthorityRevision,
656
+ });
657
+ return issued;
658
+ };
504
659
  if (r.operation === "renew") {
505
660
  const gate = serveIssuanceGateKv(await new Kvm(remoteIssuer.nc).open(epAuthBucket(space)), space, { endpoint: "manager", instanceId: r.instanceId });
506
661
  const observed = await gate.observe();
@@ -516,7 +671,7 @@ export async function openAuthAuthorityPlane(opts) {
516
671
  if (r.operation === "prepare" || r.operation === "renew") {
517
672
  credentials.supervisor = await credential("supervisor", "remote-manager", actors.supervisor, {
518
673
  remoteManager: { instanceId: r.instanceId, owner, actor: actors.supervisor },
519
- expiresInSeconds: STANDING_RENEWABLE_TTL_SEC,
674
+ expiresInSeconds: standingTtl,
520
675
  });
521
676
  // The executor receives the exact instance-scoped registration/maintenance surface under
522
677
  // a separate nkey and bounded five-minute lifetime. The participant retains it for the
@@ -564,6 +719,71 @@ export async function openAuthAuthorityPlane(opts) {
564
719
  });
565
720
  return { credentials };
566
721
  }
722
+ if (r.operation === "renewStandingBundle") {
723
+ // authorizeRenewal fresh-checked account, owner, epoch and the current-registration proof
724
+ // (which binds the held nkeys and lifecycle). Re-observe so every row binds one revision.
725
+ const gate = serveIssuanceGateKv(await new Kvm(remoteIssuer.nc).open(epAuthBucket(space)), space, { endpoint: "manager", instanceId: r.instanceId });
726
+ const observed = await gate.observe();
727
+ if (!observed || observed.state !== "open" || observed.processEpoch !== r.processEpoch)
728
+ throw new EpEnvelopeError("conflict", "manager-service standing renewal gate moved before issuance");
729
+ credentials.supervisor = await credential("supervisor", "remote-manager", actors.supervisor, {
730
+ remoteManager: { instanceId: r.instanceId, owner, actor: actors.supervisor },
731
+ expiresInSeconds: standingTtl,
732
+ });
733
+ credentials.executor = await credential("executor", "remote-manager", actors.executor, {
734
+ remoteManager: { instanceId: r.instanceId, owner, actor: actors.executor },
735
+ expiresInSeconds: 5 * 60,
736
+ });
737
+ credentials.serve = await mintPublicUserJwt({ space, account: { pub: dataAccount.pub, signingSeed: dataAccount.signingSeed } }, r.identities.serve.id, "endpoint-serve", {
738
+ principal: { owner, actor: actors.serve },
739
+ lifecycleUid: r.managerLifecycleUid,
740
+ expiresInSeconds: standingTtl,
741
+ endpointServe: remoteManagerServeGrantFromCluster(r, owner, await registeredManagerCluster(owner, r.instanceId, observed), observed),
742
+ serveIssuance: gate,
743
+ });
744
+ const sibling = siblingOn(gate, observed);
745
+ credentials.goalWriter = await sibling("goalWriter", "goal-writer", actors.goalWriter);
746
+ credentials.sessionLedger = await sibling("sessionLedger", "session-ledger", actors.sessionLedger);
747
+ return { credentials };
748
+ }
749
+ if (r.operation === "renewRunDriver") {
750
+ const gate = serveIssuanceGateKv(await new Kvm(remoteIssuer.nc).open(epAuthBucket(space)), space, { endpoint: "manager", instanceId: r.instanceId });
751
+ const observed = await gate.observe();
752
+ if (!observed || observed.state !== "open" || observed.processEpoch !== r.processEpoch)
753
+ throw new EpEnvelopeError("conflict", "manager-service run renewal gate moved before issuance");
754
+ const recordsKv = await openRecordsBucket(remoteIssuer.nc, space);
755
+ const run = await observeHostedRunAttempt(recordsKv, "manager", r.run.runId, {
756
+ supervisorId: r.identities.supervisor.id,
757
+ instanceId: r.instanceId,
758
+ });
759
+ if (!run || run.state !== "running" || run.instanceId !== r.instanceId ||
760
+ run.holder !== r.run.holder || run.takeoverId !== r.run.takeoverId ||
761
+ run.epoch !== r.run.epoch || run.fencingToken !== r.run.fencingToken)
762
+ throw new EpEnvelopeError("conflict", "manager-service run renewal authority moved before issuance");
763
+ const caller = runDriverCaller(r.run.runId, owner);
764
+ const binding = {
765
+ endpoint: "manager",
766
+ runId: r.run.runId,
767
+ takeoverId: r.run.takeoverId,
768
+ instanceId: r.instanceId,
769
+ epoch: r.run.epoch,
770
+ owner,
771
+ };
772
+ const auth = { space, account: { pub: dataAccount.pub, signingSeed: dataAccount.signingSeed } };
773
+ credentials.runDriver = await mintPublicUserJwt(auth, r.run.driverId, "run-driver", {
774
+ principal: { owner, actor: caller.actor },
775
+ lifecycleUid: r.managerLifecycleUid,
776
+ runDriver: binding,
777
+ expiresInSeconds: standingTtl,
778
+ });
779
+ credentials.runMediator = await mintPublicUserJwt(auth, r.run.mediatorId, "run-mediator", {
780
+ principal: { owner, actor: caller.actor },
781
+ lifecycleUid: r.managerLifecycleUid,
782
+ runMediator: binding,
783
+ expiresInSeconds: standingTtl,
784
+ });
785
+ return { credentials };
786
+ }
567
787
  if (r.operation === "activate") {
568
788
  const expectedProof = remoteManagerRegistrationProof(owner, r);
569
789
  if (r.registrationProof !== expectedProof)
@@ -580,30 +800,13 @@ export async function openAuthAuthorityPlane(opts) {
580
800
  credentials.serve = await mintPublicUserJwt({ space, account: { pub: dataAccount.pub, signingSeed: dataAccount.signingSeed } }, r.identities.serve.id, "endpoint-serve", {
581
801
  principal: { owner, actor: actors.serve },
582
802
  lifecycleUid: r.managerLifecycleUid,
583
- expiresInSeconds: STANDING_RENEWABLE_TTL_SEC,
803
+ expiresInSeconds: standingTtl,
584
804
  endpointServe: reconstructRemoteManagerServeGrant(r, owner, actors.serve, observed),
585
805
  serveIssuance: gate,
586
806
  });
587
807
  if (!observed)
588
808
  throw new EpEnvelopeError("failed-precondition", "manager-service activation found no issuance gate");
589
- const sibling = async (key, profile, actor) => {
590
- const issued = await credential(key, profile, actor, profile === "goal-writer" ? { goalWriter: { endpoint: "manager" }, expiresInSeconds: STANDING_RENEWABLE_TTL_SEC } : { expiresInSeconds: STANDING_RENEWABLE_TTL_SEC });
591
- await commitSiblingIssuance(gate, observed, {
592
- credentialId: rawDigest(issued.jwt).replace("sha256:", "sha256-"),
593
- credentialKey: r.identities[key].id,
594
- holderPrincipal: `${owner}.${actor}`,
595
- endpoint: "manager",
596
- lifecycleUid: r.instanceId,
597
- sourceChain: ["root"],
598
- state: "active",
599
- exp: issued.exp,
600
- generation: observed.generation,
601
- processEpoch: observed.processEpoch,
602
- registrationRevision: observed.registrationRevision,
603
- nameAuthorityRevision: observed.nameAuthorityRevision,
604
- });
605
- return issued;
606
- };
809
+ const sibling = siblingOn(gate, observed);
607
810
  credentials.goalWriter = await sibling("goalWriter", "goal-writer", actors.goalWriter);
608
811
  credentials.sessionLedger = await sibling("sessionLedger", "session-ledger", actors.sessionLedger);
609
812
  return {
@@ -664,7 +867,7 @@ export async function openAuthAuthorityPlane(opts) {
664
867
  const result = await principalOracle(principal);
665
868
  return { state: result.state, detail: result.note ?? `delivery-daemon principal sweep, sweepComplete=${String(result.sweepComplete)}` };
666
869
  },
667
- evict: async (principal) => (await evict(principal)).verifiedGone,
870
+ evictHolders: makeDeliveryAdminHolderEvictor({ space, server, dataAccount, log }),
668
871
  log,
669
872
  });
670
873
  }
@@ -715,6 +918,22 @@ export async function openAuthAuthorityPlane(opts) {
715
918
  },
716
919
  });
717
920
  },
921
+ verifyManagedAgentRuntime: async ({ owner, request }) => {
922
+ refuseIfFenced();
923
+ const args = {
924
+ owner,
925
+ dir: opts.dir,
926
+ proofSecret: dataAccount.signingSeed,
927
+ space,
928
+ observeManagerGate: async (instanceId) => {
929
+ const gate = serveIssuanceGateKv(await new Kvm(remoteIssuer.nc).open(epAuthBucket(space)), space, { endpoint: "manager", instanceId });
930
+ return gate.observe();
931
+ },
932
+ };
933
+ return request.kind === "manager-managed-agent-runtime-status"
934
+ ? authorizeRemoteManagedAgentRuntimeStatus({ ...args, request })
935
+ : authorizeRemoteManagedAgentRuntimeCreate({ ...args, request });
936
+ },
718
937
  scanManagerGoalIndex: async ({ owner, scope, request }) => {
719
938
  refuseIfFenced();
720
939
  const authorized = await authorizeRemoteManagerGoalIndexScan({
@@ -741,6 +960,124 @@ export async function openAuthAuthorityPlane(opts) {
741
960
  },
742
961
  });
743
962
  },
963
+ admitManagerRun: async ({ owner, scope, request }) => {
964
+ refuseIfFenced();
965
+ if (!scope.includes("supervise"))
966
+ throw new EpEnvelopeError("permission-denied", 'manager run admission needs scope "supervise"; spawn/admin do not imply it');
967
+ const runId = parseRemoteRunAdmissionRequest(request).run.runId;
968
+ const hostAuth = {
969
+ space,
970
+ operator: { seed: "", jwt: "" },
971
+ account: { pub: dataAccount.pub, seed: "", jwt: "", signingSeed: dataAccount.signingSeed, signingPub: "" },
972
+ sys: { pub: "", jwt: "" },
973
+ };
974
+ return withIssuerSession({ servers: server, space, auth: hostAuth, tls: false }, async (session) => {
975
+ const admitterNc = await connect({
976
+ servers: server,
977
+ ...standaloneConnectOpts({
978
+ creds: await mintCreds(hostAuth, newIdentity(), "run-admitter", { runAdmitter: { endpoint: "manager", runId }, expiresInSeconds: 60 }),
979
+ tls: false,
980
+ }),
981
+ maxReconnectAttempts: 0,
982
+ });
983
+ try {
984
+ return await admitRemoteRun({
985
+ request, owner, space, accountPublicKey: dataAccount.pub, proofSecret: dataAccount.signingSeed, endpoint: "manager",
986
+ observeManagerGate: async (instanceId) => {
987
+ const gate = serveIssuanceGateKv(await new Kvm(remoteIssuer.nc).open(epAuthBucket(space)), space, { endpoint: "manager", instanceId });
988
+ return gate.observe();
989
+ },
990
+ issued: session.store,
991
+ sourceIsLive: (source) => session.sourceIsLive(source),
992
+ admissions: await new Kvm(admitterNc).open(admissionBucket(space)),
993
+ });
994
+ }
995
+ finally {
996
+ await admitterNc.drain().catch(() => admitterNc.close());
997
+ }
998
+ });
999
+ },
1000
+ issueManagerRunAttempt: async ({ owner, scope, request }) => {
1001
+ refuseIfFenced();
1002
+ if (!scope.includes("supervise"))
1003
+ throw new EpEnvelopeError("permission-denied", 'manager run attempt needs scope "supervise"; spawn/admin do not imply it');
1004
+ const req = parseRemoteRunAttemptRequest(request);
1005
+ const recordsKv = await openRecordsBucket(remoteIssuer.nc, space);
1006
+ const grant = await authorizeRemoteRunAttempt({
1007
+ request,
1008
+ owner,
1009
+ space,
1010
+ accountPublicKey: dataAccount.pub,
1011
+ proofSecret: dataAccount.signingSeed,
1012
+ endpoint: "manager",
1013
+ observeManagerGate: async (instanceId) => {
1014
+ const gate = serveIssuanceGateKv(await new Kvm(remoteIssuer.nc).open(epAuthBucket(space)), space, { endpoint: "manager", instanceId });
1015
+ return gate.observe();
1016
+ },
1017
+ readAdmission: (runId) => readRunAdmission(recordsJsm, space, "manager", runId),
1018
+ readRunStatus: async (runId) => (await readRunRecord(recordsKv, "manager", runId))?.status?.value,
1019
+ checkpointWaiting: async (token) => (await readCheckpointStatus(recordsKv, { endpoint: "manager", token }))?.value.state === "waiting",
1020
+ });
1021
+ const hostAuth = {
1022
+ space,
1023
+ operator: { seed: "", jwt: "" },
1024
+ account: { pub: dataAccount.pub, seed: "", jwt: "", signingSeed: dataAccount.signingSeed, signingPub: "" },
1025
+ sys: { pub: "", jwt: "" },
1026
+ };
1027
+ if (grant.kind === "attempt") {
1028
+ const caller = runDriverCaller(grant.driver.runDriver.runId, owner);
1029
+ const driver = await mintPublicUserJwt(hostAuth, grant.driver.id, "run-driver", {
1030
+ principal: { owner, actor: caller.actor },
1031
+ lifecycleUid: req.managerLifecycleUid,
1032
+ runDriver: grant.driver.runDriver,
1033
+ expiresInSeconds: standingTtl,
1034
+ });
1035
+ const mediator = await mintPublicUserJwt(hostAuth, grant.mediator.id, "run-mediator", {
1036
+ principal: { owner, actor: caller.actor },
1037
+ lifecycleUid: req.managerLifecycleUid,
1038
+ runMediator: grant.mediator.runMediator,
1039
+ expiresInSeconds: standingTtl,
1040
+ });
1041
+ return {
1042
+ v: 1,
1043
+ kind: "manager-run-attempt",
1044
+ space,
1045
+ owner,
1046
+ actor: req.actor,
1047
+ instanceId: req.instanceId,
1048
+ managerLifecycleUid: req.managerLifecycleUid,
1049
+ requestId: req.requestId,
1050
+ registrationProof: req.registrationProof,
1051
+ accountPublicKey: req.accountPublicKey,
1052
+ processEpoch: req.processEpoch,
1053
+ identities: req.identities,
1054
+ attempt: req.attempt,
1055
+ credentials: { driver, mediator },
1056
+ };
1057
+ }
1058
+ const operator = await mintPublicUserJwt(hostAuth, grant.operator.id, "run-operator", {
1059
+ principal: { owner, actor: req.actor },
1060
+ lifecycleUid: req.managerLifecycleUid,
1061
+ runOperator: grant.operator.runOperator,
1062
+ expiresInSeconds: 60,
1063
+ });
1064
+ return {
1065
+ v: 1,
1066
+ kind: "manager-run-attempt",
1067
+ space,
1068
+ owner,
1069
+ actor: req.actor,
1070
+ instanceId: req.instanceId,
1071
+ managerLifecycleUid: req.managerLifecycleUid,
1072
+ requestId: req.requestId,
1073
+ registrationProof: req.registrationProof,
1074
+ accountPublicKey: req.accountPublicKey,
1075
+ processEpoch: req.processEpoch,
1076
+ identities: req.identities,
1077
+ operator: req.operator,
1078
+ credentials: { operator },
1079
+ };
1080
+ },
744
1081
  fenced,
745
1082
  close: async () => {
746
1083
  // Clean-close order (SPEC 13.13): the rail stops answering first, then scan-capable
@@ -894,6 +1231,77 @@ export async function runAuthService(args, store) {
894
1231
  await new Promise((r) => setTimeout(r, hold));
895
1232
  }
896
1233
  }
1234
+ const started = await startAuthContext({
1235
+ space,
1236
+ server,
1237
+ port,
1238
+ dir,
1239
+ secrets,
1240
+ hostedStore: store !== undefined,
1241
+ ...(publicPort !== undefined ? { publicFace: { port: publicPort, urlFlag: publicUrlFlag, trustedProxy, advertisedServer, agentProvisioningUrl } } : {}),
1242
+ // The CLI composition's local manager is this workspace root's persisted instance, re-read per
1243
+ // selection exactly as before. A hosted context passes no local manager at all.
1244
+ localManager: () => loadManagerInstanceIdentity(root, space),
1245
+ });
1246
+ // All planes bound — NOW write the discovery file (its existence is the readiness signal).
1247
+ saveAuthServiceInfo(dir, { url: started.url, pid: process.pid, cap: started.cap, ...(started.publicUrl !== undefined ? { publicUrl: started.publicUrl } : {}) });
1248
+ console.log(`✓ auth service up (space ${space}) - callout on ${server}, exchange/JWKS at ${started.url}${started.publicUrl !== undefined ? `, public exchange at ${started.publicUrl}` : ""}`);
1249
+ // The CLI wrapper, not the context, owns process signals and exit codes.
1250
+ const stop = async () => {
1251
+ clearAuthServiceInfo(dir); // a dead service must not satisfy the next start's readiness poll
1252
+ await started.handle.close();
1253
+ process.exit(0);
1254
+ };
1255
+ process.on("SIGINT", () => void stop());
1256
+ process.on("SIGTERM", () => void stop());
1257
+ // A dropped broker connection is fatal-loud, not a zombie: the supervising `up`/`down` lifecycle
1258
+ // owns restarts, and a callout that silently stopped answering would hang every user connect.
1259
+ // A FENCED plane is equally fatal (SPEC 13.13): its scanners are no longer whole, every authority
1260
+ // operation already refuses, and a daemon that stayed up would look healthy while a successor
1261
+ // reclaims — down the whole service instead.
1262
+ const ended = await started.ended;
1263
+ clearAuthServiceInfo(dir);
1264
+ if (ended.kind === "fenced") {
1265
+ console.error(`✗ auth-service: ${ended.cause} - exiting`);
1266
+ process.exit(1);
1267
+ }
1268
+ if (ended.cause !== undefined) {
1269
+ console.error(`✗ auth-service: broker connection closed (${ended.cause}) - exiting`);
1270
+ process.exit(1);
1271
+ }
1272
+ process.exit(0);
1273
+ }
1274
+ /** Start one account-scoped auth-service context. Every input is explicit: the state dir, the
1275
+ * injected store with its proved identity, the assigned data account and the broker. The context
1276
+ * installs no process signal handler, never exits the process, never selects a root from cwd,
1277
+ * and has no local manager. It returns only after the authority plane, the callout subscription
1278
+ * and the loopback listener are bound. A failed start releases what it acquired and throws. A
1279
+ * mid-life fence or broker loss makes only this context `unavailable` and closes its resources. */
1280
+ export async function startAuthService(inputs) {
1281
+ if (!inputs.context?.accountPublicKey || !inputs.context.lifecycleUid || !inputs.space || !inputs.servers || !inputs.stateDir)
1282
+ throw new Error("auth-service: hosted context needs an account, lifecycle, space, server and stateDir");
1283
+ if (inputs.store.identity === undefined)
1284
+ throw new Error("auth-service: hosted SecretStore must declare a stable identity");
1285
+ const actual = parseSecretStoreIdentity(inputs.store.identity);
1286
+ if (actual.kind !== "injected" || !sameSecretStoreIdentity(actual, inputs.storeIdentity))
1287
+ throw new Error("auth-service: hosted SecretStore identity does not match the assigned store identity");
1288
+ const port = inputs.port ?? 0;
1289
+ if (!Number.isInteger(port) || port < 0 || port > 65535)
1290
+ throw new Error(`auth-service: port must be a port number, got ${port}`);
1291
+ const started = await startAuthContext({
1292
+ space: inputs.space,
1293
+ server: inputs.servers,
1294
+ port,
1295
+ dir: resolve(inputs.stateDir),
1296
+ secrets: inputs.store,
1297
+ hostedStore: true,
1298
+ localManager: () => undefined,
1299
+ context: inputs.context,
1300
+ });
1301
+ return started.handle;
1302
+ }
1303
+ async function startAuthContext(o) {
1304
+ const { space, server, dir, secrets, port } = o;
897
1305
  const keys = await loadServiceKeys(secrets, space);
898
1306
  const callout = await loadCalloutAuth(secrets, space);
899
1307
  const issuer = await loadIssuer(secrets, space);
@@ -907,10 +1315,12 @@ export async function runAuthService(args, store) {
907
1315
  ...(ownerSecret ? [] : ["owner secret"]),
908
1316
  ...(idp ? [] : [`IdP pin under ${dir}`]),
909
1317
  ];
910
- throw new Error(`auth-service: user-auth material is missing (${missing.join(", ")}) - ${store ? "the hosted composition must provision the secret store before starting this daemon" : "enable it with `cotal up --user-auth --idp <url>`"}`);
1318
+ throw new Error(`auth-service: user-auth material is missing (${missing.join(", ")}) - ${o.hostedStore ? "the hosted composition must provision the secret store before starting this daemon" : "enable it with `cotal up --user-auth --idp <url>`"}`);
911
1319
  }
912
1320
  if (issuer.issuer !== spaceIssuer(space))
913
1321
  throw new Error(`auth-service: issuer pin ${issuer.issuer} does not match space "${space}"`);
1322
+ if (o.context !== undefined && keys.dataAccount.pub !== o.context.accountPublicKey)
1323
+ throw new Error("auth-service: the store's data account does not match the assigned hosted context");
914
1324
  if (!(await isReachable(server, { creds: callout.calloutCreds })))
915
1325
  throw new Error(`auth-service: can't reach the broker at ${server} with the callout creds - is the mesh up (with the callout account preloaded)?`);
916
1326
  // ---- The authority plane (R1): stores ensured + registry + supervised reader, BEFORE the
@@ -921,132 +1331,172 @@ export async function runAuthService(args, store) {
921
1331
  dir,
922
1332
  dataAccount: { pub: keys.dataAccount.pub, signingSeed: keys.dataAccount.signingSeed },
923
1333
  log: (l) => console.error(l),
1334
+ localManager: o.localManager,
924
1335
  });
925
- // ---- Plane 2: the callout, on its own callout-account connection ----
926
- const nc = await connect({
927
- servers: server,
928
- authenticator: credsAuthenticator(new TextEncoder().encode(callout.calloutCreds)),
929
- name: `cotal:auth-service:${space}`,
930
- });
931
- startAuthCallout(nc, {
932
- xkeySeed: callout.xkey.seed,
933
- authAccount: { pub: callout.account.pub, signingSeed: callout.account.signingSeed },
934
- dataAccount: { pub: keys.dataAccount.pub, signingSeed: keys.dataAccount.signingSeed },
935
- space,
936
- token: { key: issuer.localKeySet(), issuer: issuer.issuer },
937
- authorizeActor: plane.authorizeConnect,
938
- permissionsFor: calloutPermissions(ledgerAclResolver(dir), plane.authorizeManagerCaller),
939
- log: (l) => console.error(l),
940
- });
941
- // The subscription must be ON the broker before readiness is signaled — an `up` that recorded a
942
- // usable user mesh while the SUB was still in flight would intermittently deny first connects.
943
- await nc.flush();
944
- // ---- Plane 1: the exchange + JWKS, loopback HTTP ----
945
- const bridgeIdp = { issuer: idp.issuer, audience: idp.audience, key: pinnedJwksResolver(idp.jwksUri) };
946
- const bridge = createIdpBridge({
947
- idp: bridgeIdp,
948
- space,
949
- spaceSecret: ownerSecret,
950
- issuer,
951
- authorizeActor: ledgerAuthorizeGrant(dir),
952
- mintConnectCredential: plane.mintConnectCredential,
953
- });
954
- const cap = randomBytes(32).toString("hex"); // per-start exchange capability (rotates with the daemon)
955
- const failures = []; // rolling-window timestamps of REFUSED exchanges
956
- const badCaps = []; // rolling-window timestamps of invalid-capability attempts
957
- const ctx = {
958
- issuer,
959
- bridge,
960
- bridgeIdp,
961
- ownerSecret,
962
- managerServiceAuthority: plane.issueManagerServiceAuthority,
963
- maintainRemoteManager: plane.maintainRemoteManager,
964
- validateRetainedAgent: plane.validateRetainedAgent,
965
- verifyManagedAgentEnrollment: plane.verifyManagedAgentEnrollment,
966
- verifyManagedAgentPrepareRetirement: plane.verifyManagedAgentPrepareRetirement,
967
- scanManagerGoalIndex: plane.scanManagerGoalIndex,
968
- authorizeManagerAdmin: plane.authorizeManagerAdmin,
969
- secrets,
970
- retireInteractiveLifecycle: plane.retireInteractiveLifecycle,
971
- retireManagedLifecycle: plane.retireManagedLifecycle,
972
- cap,
973
- failures,
974
- badCaps,
975
- space,
976
- dir,
977
- mintConnectCredential: plane.mintConnectCredential,
978
- selectManagerInstance: plane.selectManagerInstance,
979
- };
980
- const http = createServer((req, res) => void handle(req, res, ctx));
981
- await new Promise((resolvePort, reject) => {
982
- http.once("error", reject);
983
- http.listen(port, "127.0.0.1", () => resolvePort());
984
- });
985
- const addr = http.address();
986
- const boundPort = typeof addr === "object" && addr ? addr.port : port;
987
- const url = `http://127.0.0.1:${boundPort}`;
988
- // The optional PUBLIC face: its own server, its own closed route table, its own budgets — also
989
- // loopback-bound (the operator's reverse proxy terminates TLS and forwards here).
1336
+ let nc;
1337
+ let http;
990
1338
  let publicHttp;
991
- let publicUrl;
992
- if (publicPort !== undefined) {
993
- // The discovery bundle is GENERATED from the daemon's own recorded config — the pinned IdP,
994
- // the flags, the callout material — so it cannot drift from what this process enforces.
995
- // `endpoints.url` is finalized AFTER bind (the closure sees the mutation): with `--port 0`
996
- // the pre-bind port would advertise an address nothing listens on.
997
- const bundle = composeUserBundle({
1339
+ // Stop admission, let accepted requests settle within the public request deadline, then cut
1340
+ // whatever is still open so a stuck client cannot hold this context's close forever.
1341
+ const closeServer = async (srv) => {
1342
+ if (srv === undefined || !srv.listening)
1343
+ return;
1344
+ const closed = new Promise((r) => srv.close(() => r()));
1345
+ let timer;
1346
+ await Promise.race([closed, new Promise((r) => { timer = setTimeout(r, PUBLIC_DEADLINE_MS); })]);
1347
+ clearTimeout(timer);
1348
+ srv.closeAllConnections();
1349
+ await closed;
1350
+ };
1351
+ try {
1352
+ // ---- Plane 2: the callout, on its own callout-account connection ----
1353
+ nc = await connect({
1354
+ servers: server,
1355
+ authenticator: credsAuthenticator(new TextEncoder().encode(callout.calloutCreds)),
1356
+ name: `cotal:auth-service:${space}`,
1357
+ });
1358
+ startAuthCallout(nc, {
1359
+ xkeySeed: callout.xkey.seed,
1360
+ authAccount: { pub: callout.account.pub, signingSeed: callout.account.signingSeed },
1361
+ dataAccount: { pub: keys.dataAccount.pub, signingSeed: keys.dataAccount.signingSeed },
998
1362
  space,
999
- // What participants DIAL, not what the callout dials: the daemon reaches the broker on its
1000
- // loopback/LAN address (--server), which is meaningless off this machine. --advertised-server
1001
- // is the publicly dialable address (e.g. wss://… through the reverse proxy).
1002
- server: advertisedServer ?? server,
1003
- idp: { url: idp.url, issuer: idp.issuer, audience: idp.audience },
1004
- sentinelCreds: callout.sentinelCreds,
1005
- ...(agentProvisioningUrl ? { agentProvisioningUrl } : {}),
1363
+ token: { key: issuer.localKeySet(), issuer: issuer.issuer },
1364
+ authorizeActor: plane.authorizeConnect,
1365
+ permissionsFor: calloutPermissions(ledgerAclResolver(dir), plane.authorizeManagerCaller, plane.verifySession),
1366
+ log: (l) => console.error(l),
1006
1367
  });
1007
- publicHttp = createServer(makePublicHandler(ctx, makePublicPolicy(trustedProxy), bundle));
1368
+ // The subscription must be ON the broker before readiness is signaled — an `up` that recorded a
1369
+ // usable user mesh while the SUB was still in flight would intermittently deny first connects.
1370
+ await nc.flush();
1371
+ // ---- Plane 1: the exchange + JWKS, loopback HTTP ----
1372
+ const bridgeIdp = { issuer: idp.issuer, audience: idp.audience, key: pinnedJwksResolver(idp.jwksUri) };
1373
+ const bridge = createIdpBridge({
1374
+ idp: bridgeIdp,
1375
+ space,
1376
+ spaceSecret: ownerSecret,
1377
+ issuer,
1378
+ authorizeActor: ledgerAuthorizeGrant(dir),
1379
+ mintConnectCredential: plane.mintConnectCredential,
1380
+ });
1381
+ const cap = randomBytes(32).toString("hex"); // per-start exchange capability (rotates with the daemon)
1382
+ const failures = []; // rolling-window timestamps of REFUSED exchanges
1383
+ const badCaps = []; // rolling-window timestamps of invalid-capability attempts
1384
+ const ctx = {
1385
+ issuer,
1386
+ bridge,
1387
+ bridgeIdp,
1388
+ ownerSecret,
1389
+ managerServiceAuthority: plane.issueManagerServiceAuthority,
1390
+ maintainRemoteManager: plane.maintainRemoteManager,
1391
+ validateRetainedAgent: plane.validateRetainedAgent,
1392
+ verifyManagedAgentEnrollment: plane.verifyManagedAgentEnrollment,
1393
+ verifyManagedAgentPrepareRetirement: plane.verifyManagedAgentPrepareRetirement,
1394
+ verifyManagedAgentRuntime: plane.verifyManagedAgentRuntime,
1395
+ scanManagerGoalIndex: plane.scanManagerGoalIndex,
1396
+ authorizeManagerAdmin: plane.authorizeManagerAdmin,
1397
+ admitManagerRun: plane.admitManagerRun,
1398
+ issueManagerRunAttempt: plane.issueManagerRunAttempt,
1399
+ secrets,
1400
+ retireInteractiveLifecycle: plane.retireInteractiveLifecycle,
1401
+ retireManagedLifecycle: plane.retireManagedLifecycle,
1402
+ cap,
1403
+ failures,
1404
+ badCaps,
1405
+ space,
1406
+ dir,
1407
+ mintConnectCredential: plane.mintConnectCredential,
1408
+ selectManagerInstance: plane.selectManagerInstance,
1409
+ verifySession: plane.verifySession,
1410
+ };
1411
+ const loopback = createServer((req, res) => void handle(req, res, ctx));
1412
+ http = loopback;
1008
1413
  await new Promise((resolvePort, reject) => {
1009
- publicHttp.once("error", reject);
1010
- publicHttp.listen(publicPort, "127.0.0.1", () => resolvePort());
1414
+ loopback.once("error", reject);
1415
+ loopback.listen(port, "127.0.0.1", () => resolvePort());
1011
1416
  });
1012
- const paddr = publicHttp.address();
1013
- const boundPublic = typeof paddr === "object" && paddr ? paddr.port : publicPort;
1014
- publicUrl = publicUrlFlag ?? `http://127.0.0.1:${boundPublic}`;
1015
- finalizeUserBundleEndpoint(bundle, publicUrl);
1417
+ const addr = loopback.address();
1418
+ const boundPort = typeof addr === "object" && addr ? addr.port : port;
1419
+ const url = `http://127.0.0.1:${boundPort}`;
1420
+ // The optional PUBLIC face: its own server, its own closed route table, its own budgets — also
1421
+ // loopback-bound (the operator's reverse proxy terminates TLS and forwards here).
1422
+ let publicUrl;
1423
+ if (o.publicFace !== undefined) {
1424
+ const face = o.publicFace;
1425
+ // The discovery bundle is GENERATED from the daemon's own recorded config — the pinned IdP,
1426
+ // the flags, the callout material — so it cannot drift from what this process enforces.
1427
+ // `endpoints.url` is finalized AFTER bind (the closure sees the mutation): with `--port 0`
1428
+ // the pre-bind port would advertise an address nothing listens on.
1429
+ const bundle = composeUserBundle({
1430
+ space,
1431
+ // What participants DIAL, not what the callout dials: the daemon reaches the broker on its
1432
+ // loopback/LAN address (--server), which is meaningless off this machine. --advertised-server
1433
+ // is the publicly dialable address (e.g. wss://… through the reverse proxy).
1434
+ server: face.advertisedServer ?? server,
1435
+ idp: { url: idp.url, issuer: idp.issuer, audience: idp.audience },
1436
+ sentinelCreds: callout.sentinelCreds,
1437
+ ...(face.agentProvisioningUrl ? { agentProvisioningUrl: face.agentProvisioningUrl } : {}),
1438
+ });
1439
+ const pub = createServer(makePublicHandler(ctx, makePublicPolicy(face.trustedProxy), bundle));
1440
+ publicHttp = pub;
1441
+ await new Promise((resolvePort, reject) => {
1442
+ pub.once("error", reject);
1443
+ pub.listen(face.port, "127.0.0.1", () => resolvePort());
1444
+ });
1445
+ const paddr = pub.address();
1446
+ const boundPublic = typeof paddr === "object" && paddr ? paddr.port : face.port;
1447
+ publicUrl = face.urlFlag ?? `http://127.0.0.1:${boundPublic}`;
1448
+ finalizeUserBundleEndpoint(bundle, publicUrl);
1449
+ }
1450
+ const callNc = nc;
1451
+ let state = "ready";
1452
+ let cause;
1453
+ let closePromise;
1454
+ const close = () => {
1455
+ if (closePromise !== undefined)
1456
+ return closePromise;
1457
+ if (state === "ready")
1458
+ state = "draining";
1459
+ closePromise = (async () => {
1460
+ await Promise.all([closeServer(http), closeServer(publicHttp)]);
1461
+ await plane.close().catch(() => { });
1462
+ await callNc.close().catch(() => { });
1463
+ })();
1464
+ return closePromise;
1465
+ };
1466
+ const ended = Promise.race([
1467
+ callNc.closed().then((err) => ({ kind: "broker", ...(err ? { cause: err.message } : {}) })),
1468
+ plane.fenced.then((reason) => ({ kind: "fenced", cause: reason })),
1469
+ ]).then((end) => {
1470
+ // A clean broker close after this context's own close() is the orderly end, not a failure.
1471
+ if (end.kind === "fenced" || end.cause !== undefined || closePromise === undefined) {
1472
+ state = "unavailable";
1473
+ cause = end.kind === "fenced" ? end.cause : `broker connection closed${end.cause !== undefined ? ` (${end.cause})` : ""}`;
1474
+ void close();
1475
+ }
1476
+ return end;
1477
+ });
1478
+ const context = o.context ?? { accountPublicKey: keys.dataAccount.pub, lifecycleUid: "" };
1479
+ const service = {
1480
+ url,
1481
+ ...(publicUrl !== undefined ? { publicUrl } : {}),
1482
+ readiness() {
1483
+ if (state === "unavailable")
1484
+ return { state, context, cause: cause ?? "auth-service context is unavailable" };
1485
+ return { state: closePromise === undefined ? "ready" : "draining", context };
1486
+ },
1487
+ drain: close,
1488
+ close,
1489
+ };
1490
+ return { url, ...(publicUrl !== undefined ? { publicUrl } : {}), cap, handle: service, ended };
1016
1491
  }
1017
- // All planes bound — NOW write the discovery file (its existence is the readiness signal).
1018
- saveAuthServiceInfo(dir, { url, pid: process.pid, cap, ...(publicUrl !== undefined ? { publicUrl } : {}) });
1019
- console.log(`✓ auth service up (space ${space}) - callout on ${server}, exchange/JWKS at ${url}${publicUrl !== undefined ? `, public exchange at ${publicUrl}` : ""}`);
1020
- const stop = async () => {
1021
- clearAuthServiceInfo(dir); // a dead service must not satisfy the next start's readiness poll
1022
- http.close();
1023
- publicHttp?.close();
1492
+ catch (e) {
1493
+ // A failed start releases only what THIS context acquired, in reverse order.
1494
+ await closeServer(publicHttp).catch(() => { });
1495
+ await closeServer(http).catch(() => { });
1496
+ await nc?.close().catch(() => { });
1024
1497
  await plane.close().catch(() => { });
1025
- await nc.close().catch(() => { });
1026
- process.exit(0);
1027
- };
1028
- process.on("SIGINT", () => void stop());
1029
- process.on("SIGTERM", () => void stop());
1030
- // A dropped broker connection is fatal-loud, not a zombie: the supervising `up`/`down` lifecycle
1031
- // owns restarts, and a callout that silently stopped answering would hang every user connect.
1032
- // A FENCED plane is equally fatal (SPEC 13.13): its scanners are no longer whole, every authority
1033
- // operation already refuses, and a daemon that stayed up would look healthy while a successor
1034
- // reclaims — down the whole service instead.
1035
- await Promise.race([
1036
- nc.closed().then((err) => {
1037
- clearAuthServiceInfo(dir);
1038
- if (err) {
1039
- console.error(`✗ auth-service: broker connection closed (${err.message}) - exiting`);
1040
- process.exit(1);
1041
- }
1042
- process.exit(0);
1043
- }),
1044
- plane.fenced.then((reason) => {
1045
- clearAuthServiceInfo(dir);
1046
- console.error(`✗ auth-service: ${reason} - exiting`);
1047
- process.exit(1);
1048
- }),
1049
- ]);
1498
+ throw e;
1499
+ }
1050
1500
  }
1051
1501
  /** Dispatch one already-authenticated manager-authority body through the fixed host validator. */
1052
1502
  export async function dispatchManagerAuthorityRequest(ctx, owner, body) {
@@ -1063,6 +1513,9 @@ export async function dispatchManagerAuthorityRequest(ctx, owner, body) {
1063
1513
  // would answer a manager-lifecycle phase for an agent-lifecycle request.
1064
1514
  if (request.kind === "manager-managed-agent-enrollment" || request.kind === "manager-managed-agent-prepare-retirement")
1065
1515
  throw new EpEnvelopeError("unimplemented", "managed agent enrollment and retirement preparation must be handled by host platform interception");
1516
+ // The hosted runtime kinds read and drive host-owned intent state that stock does not hold.
1517
+ if (request.kind === "manager-managed-agent-runtime-create" || request.kind === "manager-managed-agent-runtime-status")
1518
+ throw new EpEnvelopeError("unimplemented", "managed agent runtime create and status must be handled by host platform interception");
1066
1519
  if (request.kind === "manager-retained-agent-validation") {
1067
1520
  const retained = await ctx.validateRetainedAgent({
1068
1521
  owner,
@@ -1084,6 +1537,10 @@ export async function dispatchManagerAuthorityRequest(ctx, owner, body) {
1084
1537
  return ctx.scanManagerGoalIndex({ owner, scope: row.scope ?? [], request: body.request });
1085
1538
  if (request.kind === "manager-admin-authorization")
1086
1539
  return ctx.authorizeManagerAdmin({ owner, scope: row.scope ?? [], request: body.request });
1540
+ if (request.kind === "manager-run-admission")
1541
+ return ctx.admitManagerRun({ owner, scope: row.scope ?? [], request: body.request });
1542
+ if (request.kind === "manager-run-attempt")
1543
+ return ctx.issueManagerRunAttempt({ owner, scope: row.scope ?? [], request: body.request });
1087
1544
  if (request.kind === "manager-service-maintenance")
1088
1545
  return ctx.maintainRemoteManager({ owner, scope: row.scope ?? [], request: body.request });
1089
1546
  return ctx.managerServiceAuthority({ owner, scope: row.scope ?? [], request: body.request });
@@ -1308,8 +1765,9 @@ async function handleVerifyManagedAgentEnrollment(req, res, ctx) {
1308
1765
  if (typeof raw.owner !== "string" || raw.request === null || typeof raw.request !== "object" || Array.isArray(raw.request))
1309
1766
  return send(res, 400, { error: "managed agent enrollment verification needs a string owner and an object request" });
1310
1767
  const request = raw.request;
1311
- if (request.kind !== "manager-managed-agent-enrollment" && request.kind !== "manager-managed-agent-prepare-retirement")
1312
- return send(res, 400, { error: 'managed agent enrollment verification serves only kind "manager-managed-agent-enrollment" or "manager-managed-agent-prepare-retirement"' });
1768
+ const runtimeKind = request.kind === "manager-managed-agent-runtime-create" || request.kind === "manager-managed-agent-runtime-status";
1769
+ if (request.kind !== "manager-managed-agent-enrollment" && request.kind !== "manager-managed-agent-prepare-retirement" && !runtimeKind)
1770
+ return send(res, 400, { error: 'managed agent enrollment verification serves only kind "manager-managed-agent-enrollment", "manager-managed-agent-prepare-retirement", "manager-managed-agent-runtime-create" or "manager-managed-agent-runtime-status"' });
1313
1771
  if (typeof request.actor !== "string")
1314
1772
  return send(res, 400, { error: "managed agent enrollment verification request requires an actor" });
1315
1773
  try {
@@ -1319,6 +1777,14 @@ async function handleVerifyManagedAgentEnrollment(req, res, ctx) {
1319
1777
  // caller. An ungranted actor throws the ledger's own operator-exact sentence.
1320
1778
  const row = ledgerAuthorizeGrant(ctx.dir)(owner, request.actor);
1321
1779
  const scope = row.scope ?? [];
1780
+ if (runtimeKind) {
1781
+ // The plane re-reads the same ledger row itself: the runtime decision never takes a scope.
1782
+ const decision = await ctx.verifyManagedAgentRuntime({
1783
+ owner,
1784
+ request: raw.request,
1785
+ });
1786
+ return send(res, 200, { authorized: true, ...decision });
1787
+ }
1322
1788
  if (request.kind === "manager-managed-agent-enrollment") {
1323
1789
  const verified = await ctx.verifyManagedAgentEnrollment({
1324
1790
  owner,
@@ -1396,7 +1862,9 @@ async function handleExchange(req, res, ctx, policy) {
1396
1862
  const peer = policy.peerKey(req);
1397
1863
  const peerThrottled = policy.throttled(ctx, peer);
1398
1864
  const body = await readJsonBody(req);
1399
- const { idpToken, actor, actorToken, owner, ttlSec, view, managerInstanceId } = body;
1865
+ const { idpToken, actor, actorToken, owner, ttlSec, view, managerInstanceId, sessionGrant } = body;
1866
+ if ((view === "session-caller") !== (sessionGrant !== undefined))
1867
+ return send(res, 400, { error: 'view "session-caller" and sessionGrant come together or not at all' });
1400
1868
  if (ttlSec !== undefined && typeof ttlSec !== "number")
1401
1869
  return send(res, 400, { error: "ttlSec must be a number" });
1402
1870
  if (view !== undefined && typeof view !== "string")
@@ -1475,11 +1943,16 @@ async function handleExchange(req, res, ctx, policy) {
1475
1943
  const selectedManagerInstanceId = view === "manager-caller"
1476
1944
  ? await ctx.selectManagerInstance(deriveOwnerForIdpSubject(ctx.ownerSecret, ctx.bridgeIdp.issuer, (await verifyIdpToken(idpToken, ctx.bridgeIdp)).sub), managerInstanceId)
1477
1945
  : undefined;
1946
+ // #2312: a session-caller bearer is signed only once the identity plane has decided that THIS
1947
+ // principal holds THAT redeemed session. The bridge's `verifySession` hook runs the decision on
1948
+ // the principal it derived, before signing; the callout re-runs it at the mint.
1949
+ const session = view === "session-caller" ? grantCoordinates(sessionGrant) : undefined;
1478
1950
  const r = await ctx.bridge.exchange(idpToken, {
1479
1951
  actor,
1480
1952
  ttlSec,
1481
1953
  view: view,
1482
1954
  managerInstanceId: selectedManagerInstanceId,
1955
+ ...(session ? { verifySession: (p) => ctx.verifySession(p, session) } : {}),
1483
1956
  });
1484
1957
  return send(res, 200, r);
1485
1958
  }
@@ -1496,7 +1969,7 @@ async function handleExchange(req, res, ctx, policy) {
1496
1969
  }
1497
1970
  /** Loopback/operator-only typed manager authority exchange. The public route table never includes
1498
1971
  * this path, and the loopback capability is checked here in addition to the route separation. */
1499
- async function handleManagerServiceAuthority(req, res, ctx, policy) {
1972
+ export async function handleManagerServiceAuthority(req, res, ctx, policy) {
1500
1973
  if (req.method !== "POST")
1501
1974
  return send(res, 405, { error: "POST only" });
1502
1975
  if (req.headers.origin !== undefined)