@cotal-ai/auth 0.73.0 → 0.77.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 (70) hide show
  1. package/dist/auth-service-contract.d.ts +4 -4
  2. package/dist/auth-service-contract.d.ts.map +1 -1
  3. package/dist/auth-service-contract.js +7 -38
  4. package/dist/auth-service-contract.js.map +1 -1
  5. package/dist/barrier-evict.d.ts.map +1 -1
  6. package/dist/barrier-evict.js +17 -75
  7. package/dist/barrier-evict.js.map +1 -1
  8. package/dist/commands.js +11 -25
  9. package/dist/commands.js.map +1 -1
  10. package/dist/delivery-admin.d.ts +20 -0
  11. package/dist/delivery-admin.d.ts.map +1 -0
  12. package/dist/delivery-admin.js +46 -0
  13. package/dist/delivery-admin.js.map +1 -0
  14. package/dist/idp.d.ts +0 -2
  15. package/dist/idp.d.ts.map +1 -1
  16. package/dist/idp.js +3 -4
  17. package/dist/idp.js.map +1 -1
  18. package/dist/index.d.ts +3 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/ledger-scanner.d.ts +1 -1
  23. package/dist/ledger-scanner.d.ts.map +1 -1
  24. package/dist/ledger-scanner.js +7 -19
  25. package/dist/ledger-scanner.js.map +1 -1
  26. package/dist/ledger.d.ts.map +1 -1
  27. package/dist/ledger.js +7 -1
  28. package/dist/ledger.js.map +1 -1
  29. package/dist/manager-authority.d.ts +20 -1
  30. package/dist/manager-authority.d.ts.map +1 -1
  31. package/dist/manager-authority.js +62 -34
  32. package/dist/manager-authority.js.map +1 -1
  33. package/dist/manager-maintenance.d.ts.map +1 -1
  34. package/dist/manager-maintenance.js +6 -19
  35. package/dist/manager-maintenance.js.map +1 -1
  36. package/dist/permissions.d.ts +5 -3
  37. package/dist/permissions.d.ts.map +1 -1
  38. package/dist/permissions.js +5 -4
  39. package/dist/permissions.js.map +1 -1
  40. package/dist/plane-claim.d.ts +5 -0
  41. package/dist/plane-claim.d.ts.map +1 -1
  42. package/dist/plane-claim.js +40 -72
  43. package/dist/plane-claim.js.map +1 -1
  44. package/dist/platform-control.d.ts.map +1 -1
  45. package/dist/platform-control.js +2 -2
  46. package/dist/platform-control.js.map +1 -1
  47. package/dist/platform-supervisor.d.ts +51 -0
  48. package/dist/platform-supervisor.d.ts.map +1 -0
  49. package/dist/platform-supervisor.js +83 -0
  50. package/dist/platform-supervisor.js.map +1 -0
  51. package/dist/provider.d.ts.map +1 -1
  52. package/dist/provider.js +40 -19
  53. package/dist/provider.js.map +1 -1
  54. package/dist/records-scanner.d.ts +1 -1
  55. package/dist/records-scanner.d.ts.map +1 -1
  56. package/dist/records-scanner.js +3 -18
  57. package/dist/records-scanner.js.map +1 -1
  58. package/dist/service.d.ts +34 -9
  59. package/dist/service.d.ts.map +1 -1
  60. package/dist/service.js +207 -45
  61. package/dist/service.js.map +1 -1
  62. package/dist/store.d.ts +9 -0
  63. package/dist/store.d.ts.map +1 -1
  64. package/dist/store.js +32 -2
  65. package/dist/store.js.map +1 -1
  66. package/dist/token.d.ts +3 -2
  67. package/dist/token.d.ts.map +1 -1
  68. package/dist/token.js +6 -4
  69. package/dist/token.js.map +1 -1
  70. package/package.json +4 -4
package/dist/service.js CHANGED
@@ -25,8 +25,9 @@
25
25
  *
26
26
  * REMOTE EXCHANGE — the OPTIONAL second listener (`--exchange-public-port`, also binding
27
27
  * 127.0.0.1; TLS terminates at a reverse proxy — in-process TLS was rejected: it duplicates cert
28
- * renewal and forks deploy). It serves ONLY `GET /health`, `GET /jwks`, `POST /exchange`, and
29
- * `GET /.well-known/cotal-mesh` (the generated discovery bundle); everything else 404s. Its
28
+ * renewal and forks deploy). It serves ONLY `GET /health`, `GET /jwks`, `POST /exchange`,
29
+ * `POST /manager-service-authority`, and `GET /.well-known/cotal-mesh` (the generated discovery
30
+ * bundle); everything else 404s. Its
30
31
  * `/exchange` demands NO capability — honestly: the 0600 cap is a same-uid file-ACL boundary with
31
32
  * no remote meaning, so requiring its bytes from a remote caller would prove nothing. The proof on
32
33
  * the public face is the credential itself: the human arm presents an EdDSA IdP JWT verified
@@ -59,19 +60,20 @@ import { resolve } from "node:path";
59
60
  import { connect, credsAuthenticator } from "@nats-io/transport-node";
60
61
  import { jetstream, jetstreamManager } from "@nats-io/jetstream";
61
62
  import { Kvm } from "@nats-io/kv";
62
- import { CotalEndpoint, deprovisionAgent, isConcreteChannel, provisionAgentDurables, contractDigest, contractRefToHex, contractStoreContext, endpointToken, fetchContractArtifact, verifyClusterManifest, verifyClusterRoot, admissionBucket, admissionMediatorGrants, assertDerivedOwnerToken, assertLifecycleToken, assertValidOwnerToken, authorizeTrustedServeSnapshot, commitSiblingIssuance, credsClaims, EpEnvelopeError, ensureAuthorityStores, epAuthBucket, isReachable, jwtFromCreds, managedRetirementOpId, mintCreds, mintPublicUserJwt, newIdentity, newTakeoverId, observeHostedRunAttempt, openRecordsBucket, parseEndpointGate, parseServiceSpec, parseServiceStatus, rawDigest, parseSecretStoreIdentity, readCheckpointStatus, readRunAdmission, readRunRecord, readSvcRecordLeader, reconcileEndpointGate, replayRunJournal, sameSecretStoreIdentity, recordSpecKey, recordStatusKey, RECORD_KINDS, recordsBucket, remoteManagerActors, retirementFrontierStreams, runDriverCaller, serveIssuanceGateKv, standaloneConnectOpts, STANDING_RENEWABLE_TTL_SEC, withIssuerSession, acceptedReadGrant, actorLedgerSource, connectionAcceptedToken, importNativeSubjectPermissions, mintGeneration, parseActorLedgerSource, writeAcceptedRow, invokeCommand, resolveService, contractArtifactCanonicalBytes, DEV_OWNER, endpointRegistrationBarrier, mintLifecycleUid, principalKey, provisionEndpointGateOpen, publishContractArtifact, registerServiceInstance, registerServingInstance } from "@cotal-ai/core";
63
+ import { CotalEndpoint, deprovisionAgent, isConcreteChannel, provisionAgentDurables, contractDigest, contractRefToHex, contractStoreContext, endpointToken, fetchContractArtifact, verifyClusterManifest, verifyClusterRoot, admissionBucket, admissionMediatorGrants, assertDerivedOwnerToken, assertLifecycleToken, assertValidOwnerToken, authorizeTrustedServeSnapshot, commitSiblingIssuance, credsClaims, EpEnvelopeError, ensureAuthorityStores, epAuthBucket, isReachable, jwtFromCreds, managedRetirementOpId, mintCreds, mintPublicUserJwt, newIdentity, newTakeoverId, observeHostedRunAttempt, openRecordsBucket, parseEndpointGate, parseServiceSpec, parseServiceStatus, rawDigest, parseSecretStoreIdentity, readCheckpointStatus, readRunAdmission, readRunRevocation, readRunRecord, readSvcRecordLeader, reconcileEndpointGate, replayRunJournal, sameSecretStoreIdentity, recordSpecKey, recordStatusKey, RECORD_KINDS, recordsBucket, remoteManagerActors, retirementFrontierStreams, runDriverCaller, serveIssuanceGateKv, standaloneConnectOpts, STANDING_RENEWABLE_TTL_SEC, withIssuerSession, acceptedReadGrant, actorLedgerSource, connectionAcceptedToken, importNativeSubjectPermissions, mintGeneration, parseActorLedgerSource, writeAcceptedRow, invokeCommand, resolveService, contractArtifactCanonicalBytes, DEV_OWNER, endpointRegistrationBarrier, mintLifecycleUid, principalKey, provisionEndpointGateOpen, publishContractArtifact, registerServiceInstance, registerServingInstance, epgateKey, isCasLoss } from "@cotal-ai/core";
63
64
  import { findCotalRoot, loadManagerInstanceIdentity, userAuthStateDir, workspaceSecretStore, createAuthInstanceIdentity, loadAuthInstanceIdentity } from "@cotal-ai/workspace";
64
65
  import { decodeJwt } from "jose";
65
66
  import { deriveOwnerForIdpSubject, platformControlOwner } from "./derive.js";
66
67
  import { makePlatformControlAuthority, makePlatformControlReadiness, requireManagerAuthorityHolder } from "./platform-control.js";
67
68
  import { OwnedConnections } from "./owned-connections.js";
69
+ import { makePlatformSupervisorAuthority, makePlatformSupervisorEnd, platformSupervisorEnded, requirePlatformSupervisorAdmin } from "./platform-supervisor.js";
68
70
  import { startAuthCallout } from "./callout.js";
69
71
  import { createIdpBridge, verifyIdpToken } from "./idp.js";
70
72
  import { PUBLIC_EXCHANGE_VIEWS, assertTransferWriterClaim } from "./token.js";
71
73
  import { grantCoordinates, verifySessionRedemption } from "./session-redemption.js";
72
74
  import { pinnedJwksResolver } from "./issuer.js";
73
75
  import { calloutPermissions } from "./permissions.js";
74
- import { admitRemoteRun, authorizeRemoteManagerRenewal, authorizeRemoteRunAttempt, issueRemoteManagerAuthority, observedRunRequest, parseRemoteRunAdmissionRequest, parseRemoteRunAttemptRequest } from "./manager-authority.js";
76
+ import { admitRemoteRun, authorizeRemoteManagerRenewal, authorizeRemoteRunAttempt, holdsRunFence, issueRemoteManagerAuthority, observedRunRequest, parseRemoteRunAdmissionRequest, parseRemoteRunAttemptRequest, parseRemoteRunRevokeRequest, revokeRemoteRun } from "./manager-authority.js";
75
77
  import { authorizeRemoteRetainedAgentValidation, completeRemoteRetainedAgentValidation, remoteManagerCurrentRegistrationProof } from "./retained-manager-validation.js";
76
78
  import { authorizeRemoteManagedAgentEnrollment, authorizeRemoteManagedAgentPrepareRetirement, authorizeRemoteManagedAgentRuntimeCreate, authorizeRemoteManagedAgentRuntimeStatus } from "./managed-agent-enrollment.js";
77
79
  import { authorizeRemoteManagerGoalIndexScan, completeRemoteManagerGoalIndexScan } from "./manager-goal-index.js";
@@ -96,7 +98,7 @@ import { joinOrStartRetirement, openAuthAdminListener } from "./auth-admin.js";
96
98
  import { AUTH_SERVICE_ENDPOINT, authClusterArtifacts, authContractArtifactValues } from "./auth-service-contract.js";
97
99
  import { drainTargetForEndpoint, openAdmissionMediator } from "./admission-mediator.js";
98
100
  import { AGENT_BEARER_TTL_SEC, assertWithinSpawnerGrant, findInteractiveActor, findManagedActor, findActorUnified, grantManagedActor, ledgerAclResolver, ledgerActorSourceIsLive, ledgerAuthorizeAgentExchange, ledgerAuthorizeConnect, ledgerAuthorizeGrant, revokeManagedActorAt, } from "./ledger.js";
99
- import { AUTH_PROVIDER_NAME, clearAuthServiceInfo, loadCalloutAuth, loadIssuer, loadOwnerSecret, loadPinnedIdp, loadServiceKeys, saveAuthServiceInfo, spaceIssuer, } from "./store.js";
101
+ import { AUTH_PROVIDER_NAME, claimAuthInstanceIdentity, clearAuthServiceInfo, loadCalloutAuth, loadIssuer, loadOwnerSecret, loadPinnedIdp, loadServiceKeys, saveAuthServiceInfo, spaceIssuer, } from "./store.js";
100
102
  /** JWKS max-age seconds — the cache contract's knob. Exported so a rotation tool can compute the
101
103
  * retire floor (max-age + max bearer TTL) from it. */
102
104
  export const JWKS_MAX_AGE_SEC = 300;
@@ -187,6 +189,24 @@ async function registerSelfAuthorizedInstance(nc, args, register) {
187
189
  throw new EpEnvelopeError("conflict", `the issuance gate of ${endpoint}/${instanceId} is no longer open at this registration (process epoch ${processEpoch}, revision ${registrationRevision}); a later barrier fenced this start (SPEC 13.1)`);
188
190
  return registered;
189
191
  }
192
+ /** The accepted-row token one platform supervisor duty's issued generation is recorded under. */
193
+ function supervisorAcceptedToken(owner, instanceId, lifecycleUid, duty) {
194
+ return connectionAcceptedToken(`platform-supervisor:${owner}:${instanceId}:${lifecycleUid}:${duty}`);
195
+ }
196
+ /** The issued generation recorded for one platform supervisor duty, or undefined when none was.
197
+ * The accepted store is write-once per key, so the first row is the only row. */
198
+ async function supervisorGeneration(session, space, owner, instanceId, lifecycleUid, duty) {
199
+ const response = await session.nc.request(acceptedReadGrant(space, supervisorAcceptedToken(owner, instanceId, lifecycleUid, duty)), new Uint8Array(0), { timeout: 3000 });
200
+ const status = response.headers?.get("Status");
201
+ if (response.headers?.code === 404 || status?.startsWith("404"))
202
+ return undefined;
203
+ if (status)
204
+ throw new EpEnvelopeError("unavailable", "platform supervisor accepted-row read failed");
205
+ const accepted = JSON.parse(new TextDecoder().decode(response.data));
206
+ if (accepted.version !== 1 || accepted.ref.space !== space || accepted.ref.owner !== owner || accepted.ref.actor !== remoteManagerActors(instanceId)[duty] || accepted.ref.uid !== lifecycleUid)
207
+ throw new EpEnvelopeError("permission-denied", "platform supervisor accepted row names another owner or lifecycle");
208
+ return accepted.ref;
209
+ }
190
210
  /**
191
211
  * Open the authority plane — the PRODUCTION connect/exchange composition (exported so the live
192
212
  * deny-new smoke exercises exactly what the daemon runs). Boot order is the readiness contract:
@@ -228,17 +248,16 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
228
248
  // #399 M2: register the auth plane itself as an ordinary `auth` service endpoint — the SAME
229
249
  // §13.7 registration ceremony the manager runs (`manager.ts:6562-6690`), before the listener
230
250
  // below serves a single request. The persisted instance id + serve nkey (in `identityRoot`,
231
- // hardened the way the manager's own instance identity is) so a restart re-registers the
232
- // SAME instance: `registerServiceInstance` advances the process epoch on a re-registration and
233
- // fences the predecessor; a first registration stays at epoch 0.
251
+ // hardened the way the manager's own instance identity is, or in a hosted context's store) so a
252
+ // restart re-registers the SAME instance: `registerServiceInstance` advances the process epoch
253
+ // on a re-registration and fences the predecessor; a first registration stays at epoch 0.
234
254
  let authServeInstanceId;
235
255
  let authServeGrant;
236
256
  {
237
- const persisted = loadAuthInstanceIdentity(opts.identityRoot, space);
238
- const authIdentity = persisted ?? createAuthInstanceIdentity(opts.identityRoot, space, {
239
- instanceId: mintLifecycleUid(),
240
- serveIdentity: newIdentity(),
241
- });
257
+ const mint = () => ({ instanceId: mintLifecycleUid(), serveIdentity: newIdentity() });
258
+ const authIdentity = opts.identityStore
259
+ ? await claimAuthInstanceIdentity(opts.identityStore, space, opts.identityRoot, mint)
260
+ : loadAuthInstanceIdentity(opts.identityRoot, space) ?? createAuthInstanceIdentity(opts.identityRoot, space, mint());
242
261
  const iid = authIdentity.instanceId;
243
262
  const artifacts = authClusterArtifacts();
244
263
  const values = [...authContractArtifactValues(), artifacts.document, artifacts.manifest];
@@ -683,23 +702,47 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
683
702
  issueManagerServiceAuthority: async (holder) => {
684
703
  refuseIfFenced();
685
704
  const { owner } = holder;
686
- return issueRemoteManagerAuthority({
705
+ // The ended-assignment fence (SPEC 13.16): read on every call, before anything is signed. The
706
+ // retired gate and the retired generations are terminal rows, so no host view reopens them.
707
+ if (holder.supervisorExpiresAt !== undefined) {
708
+ const gate = await observeManagerGate(holder.request.instanceId);
709
+ if (gate?.state === "retired")
710
+ throw platformSupervisorEnded(owner);
711
+ await withIssuerSession({ servers: server, space, auth: issuerAuth(), tls: false }, async (session) => {
712
+ for (const duty of ["supervisor", "executor"]) {
713
+ const ref = await supervisorGeneration(session, space, owner, holder.request.instanceId, holder.request.managerLifecycleUid, duty);
714
+ if (ref === undefined)
715
+ continue;
716
+ try {
717
+ await session.store.resolve(ref, session.sourceIsLive);
718
+ }
719
+ catch (error) {
720
+ throw platformSupervisorEnded(owner, error instanceof Error ? error.message : String(error));
721
+ }
722
+ }
723
+ });
724
+ }
725
+ const observeManagerRun = async (r, runId) => observeHostedRunAttempt(await openRecordsBucket(remoteIssuer.nc, space), "manager", runId, { supervisorId: r.identities.supervisor.id, instanceId: r.instanceId });
726
+ const material = await issueRemoteManagerAuthority({
687
727
  ...holder,
688
728
  authorizeRenewal: ({ owner: o, request: r }) => authorizeRemoteManagerRenewal({
689
729
  request: r, owner: o, space, accountPublicKey: dataAccount.pub, proofSecret: dataAccount.signingSeed,
690
730
  observeManagerGate,
691
- observeRun: async (runId) => {
692
- const recordsKv = await openRecordsBucket(remoteIssuer.nc, space);
693
- return await observeHostedRunAttempt(recordsKv, "manager", runId, {
694
- supervisorId: r.identities.supervisor.id,
695
- instanceId: r.instanceId,
696
- });
697
- },
731
+ observeRun: (runId) => observeManagerRun(r, runId),
698
732
  }),
699
733
  issue: async ({ actors, request: r }) => {
700
734
  const signer = { space, account: dataAccount };
735
+ const bounded = (opts) => {
736
+ if (holder.supervisorExpiresAt === undefined)
737
+ return opts;
738
+ const now = Math.floor(Date.now() / 1000);
739
+ if (holder.supervisorExpiresAt <= now)
740
+ throw new EpEnvelopeError("permission-denied", `platform supervisor lifecycle has ended for owner ${owner}`);
741
+ const { expiresInSeconds, expiresAt, ...rest } = opts ?? {};
742
+ return { ...rest, expiresAt: Math.min(holder.supervisorExpiresAt, expiresAt ?? now + (expiresInSeconds ?? standingTtl)) };
743
+ };
701
744
  const registration = { space: r.space, instanceId: r.instanceId, lifecycleUid: r.managerLifecycleUid, identities: r.identities };
702
- const credential = async (key, profile, actor, opts) => mintPublicUserJwt(signer, r.identities[key].id, profile, { ...opts, principal: { owner, actor }, lifecycleUid: r.managerLifecycleUid });
745
+ const credential = async (key, profile, actor, opts) => mintPublicUserJwt(signer, r.identities[key].id, profile, bounded({ ...opts, principal: { owner, actor }, lifecycleUid: r.managerLifecycleUid }));
703
746
  const credentials = {};
704
747
  const siblingOn = (gate, observed) => async (key, profile, actor) => {
705
748
  const issued = await credential(key, profile, actor, profile === "goal-writer" ? { goalWriter: { endpoint: "manager" }, expiresInSeconds: standingTtl } : { expiresInSeconds: standingTtl });
@@ -803,13 +846,13 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
803
846
  remoteManager: { instanceId: r.instanceId, owner, actor: actors.executor },
804
847
  expiresInSeconds: 5 * 60,
805
848
  });
806
- credentials.serve = await mintPublicUserJwt(signer, r.identities.serve.id, "endpoint-serve", {
849
+ credentials.serve = await mintPublicUserJwt(signer, r.identities.serve.id, "endpoint-serve", bounded({
807
850
  principal: { owner, actor: actors.serve },
808
851
  lifecycleUid: r.managerLifecycleUid,
809
852
  expiresInSeconds: standingTtl,
810
853
  endpointServe: remoteManagerServeGrantFromCluster(r, owner, await registeredManagerCluster(owner, r.instanceId, observed), observed),
811
854
  serveIssuance: gate,
812
- });
855
+ }));
813
856
  const sibling = siblingOn(gate, observed);
814
857
  credentials.goalWriter = await sibling("goalWriter", "goal-writer", actors.goalWriter);
815
858
  credentials.sessionLedger = await sibling("sessionLedger", "session-ledger", actors.sessionLedger);
@@ -820,14 +863,7 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
820
863
  const observed = await gate.observe();
821
864
  if (!observed || observed.state !== "open" || observed.processEpoch !== r.processEpoch)
822
865
  throw new EpEnvelopeError("conflict", "manager-service run renewal gate moved before issuance");
823
- const recordsKv = await openRecordsBucket(remoteIssuer.nc, space);
824
- const run = await observeHostedRunAttempt(recordsKv, "manager", r.run.runId, {
825
- supervisorId: r.identities.supervisor.id,
826
- instanceId: r.instanceId,
827
- });
828
- if (!run || run.state !== "running" || run.instanceId !== r.instanceId ||
829
- run.holder !== r.run.holder || run.takeoverId !== r.run.takeoverId ||
830
- run.epoch !== r.run.epoch || run.fencingToken !== r.run.fencingToken)
866
+ if (!holdsRunFence(await observeManagerRun(r, r.run.runId), r))
831
867
  throw new EpEnvelopeError("conflict", "manager-service run renewal authority moved before issuance");
832
868
  const caller = runDriverCaller(r.run.runId, owner);
833
869
  const binding = {
@@ -838,19 +874,19 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
838
874
  epoch: r.run.epoch,
839
875
  owner,
840
876
  };
841
- credentials.runDriver = await mintPublicUserJwt(signer, r.run.driverId, "run-driver", {
877
+ credentials.runDriver = await mintPublicUserJwt(signer, r.run.driverId, "run-driver", bounded({
842
878
  principal: { owner, actor: caller.actor },
843
879
  lifecycleUid: r.managerLifecycleUid,
844
880
  runDriver: binding,
845
881
  expiresInSeconds: standingTtl,
846
- });
847
- credentials.runMediator = await mintPublicUserJwt(signer, r.run.mediatorId, "run-mediator", {
882
+ }));
883
+ credentials.runMediator = await mintPublicUserJwt(signer, r.run.mediatorId, "run-mediator", bounded({
848
884
  principal: { owner, actor: caller.actor },
849
885
  lifecycleUid: r.managerLifecycleUid,
850
886
  // Renewed with the placement the attempt was issued with (authorizeRemoteRunAttempt).
851
887
  runMediator: { ...binding, placement: { instanceId: r.instanceId } },
852
888
  expiresInSeconds: standingTtl,
853
- });
889
+ }));
854
890
  return { credentials };
855
891
  }
856
892
  if (r.operation === "activate") {
@@ -866,13 +902,13 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
866
902
  const observed = await gate.observe();
867
903
  if (!observed)
868
904
  throw new EpEnvelopeError("failed-precondition", "manager-service activation found no issuance gate");
869
- credentials.serve = await mintPublicUserJwt(signer, r.identities.serve.id, "endpoint-serve", {
905
+ credentials.serve = await mintPublicUserJwt(signer, r.identities.serve.id, "endpoint-serve", bounded({
870
906
  principal: { owner, actor: actors.serve },
871
907
  lifecycleUid: r.managerLifecycleUid,
872
908
  expiresInSeconds: standingTtl,
873
909
  endpointServe: reconstructRemoteManagerServeGrant(r, owner, observed),
874
910
  serveIssuance: gate,
875
- });
911
+ }));
876
912
  const sibling = siblingOn(gate, observed);
877
913
  credentials.goalWriter = await sibling("goalWriter", "goal-writer", actors.goalWriter);
878
914
  credentials.sessionLedger = await sibling("sessionLedger", "session-ledger", actors.sessionLedger);
@@ -887,6 +923,82 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
887
923
  return { credentials };
888
924
  },
889
925
  });
926
+ if (holder.supervisorExpiresAt !== undefined) {
927
+ await withIssuerSession({ servers: server, space, auth: issuerAuth(), tls: false }, async (session) => {
928
+ for (const duty of ["supervisor", "executor"]) {
929
+ const credential = material.credentials[duty];
930
+ if (!credential)
931
+ continue;
932
+ const payload = decodeJwt(credential.jwt);
933
+ const permissions = importNativeSubjectPermissions({ pub: payload.nats?.pub, sub: payload.nats?.sub });
934
+ const actor = remoteManagerActors(holder.request.instanceId)[duty];
935
+ if (!actor || payload.sub !== holder.request.identities[duty]?.id)
936
+ throw new EpEnvelopeError("internal", "platform supervisor issued a duty outside its fixed standing identities");
937
+ const acceptedToken = supervisorAcceptedToken(owner, holder.request.instanceId, holder.request.managerLifecycleUid, duty);
938
+ const existing = await supervisorGeneration(session, space, owner, holder.request.instanceId, holder.request.managerLifecycleUid, duty);
939
+ if (existing === undefined) {
940
+ const ref = { space, owner, actor, uid: holder.request.managerLifecycleUid, generation: mintGeneration() };
941
+ const prepared = await session.store.stage({ version: 1, ref, sources: [], permissions, expiresAt: holder.supervisorExpiresAt });
942
+ await session.store.release(prepared, async () => { });
943
+ await writeAcceptedRow(session.accepted, acceptedToken, ref);
944
+ }
945
+ else {
946
+ await session.store.resolve(existing, session.sourceIsLive);
947
+ await session.store.confirm(existing, permissions);
948
+ }
949
+ }
950
+ });
951
+ }
952
+ return material;
953
+ },
954
+ endPlatformSupervisor: async ({ owner, instanceId, lifecycleUid }) => {
955
+ refuseIfFenced();
956
+ // Retire the issued generations first, then the gate. Each step is idempotent, so a retry after
957
+ // a partial end completes it. The retired gate alone refuses a lifecycle that issued nothing.
958
+ await withIssuerSession({ servers: server, space, auth: issuerAuth(), tls: false }, async (session) => {
959
+ for (const duty of ["supervisor", "executor"]) {
960
+ const ref = await supervisorGeneration(session, space, owner, instanceId, lifecycleUid, duty);
961
+ if (ref !== undefined)
962
+ await session.store.retire(ref);
963
+ }
964
+ });
965
+ const key = epgateKey("manager", instanceId);
966
+ const opId = managedRetirementOpId(lifecycleUid);
967
+ for (let attempt = 0; attempt < 4; attempt++) {
968
+ const entry = await authKv.get(key);
969
+ if (entry === null) {
970
+ // No gate yet: create it retired, so a later registration finds the terminal row.
971
+ const row = { state: "retired", generation: 0, processEpoch: 0, registrationRevision: 0, nameAuthorityRevision: 0, principal: `${owner}.${remoteManagerActors(instanceId).serve}`, op: { opId, kind: "retirement" } };
972
+ parseEndpointGate(new TextEncoder().encode(JSON.stringify(row)), key);
973
+ try {
974
+ await authKv.create(key, new TextEncoder().encode(JSON.stringify(row)));
975
+ return;
976
+ }
977
+ catch (error) {
978
+ if (isCasLoss(error))
979
+ continue;
980
+ throw error;
981
+ }
982
+ }
983
+ if (entry.operation !== "PUT")
984
+ throw new EpEnvelopeError("failed-precondition", `the manager gate ${key} carries a ${entry.operation} marker; a gate is never deleted (SPEC 13.12)`);
985
+ const row = parseEndpointGate(entry.value, key);
986
+ if (row.principal !== `${owner}.${remoteManagerActors(instanceId).serve}`)
987
+ throw new EpEnvelopeError("permission-denied", `the manager gate ${key} belongs to ${row.principal}, not owner ${owner}`);
988
+ if (row.state === "retired")
989
+ return;
990
+ const { op: _op, ...rest } = row;
991
+ void _op;
992
+ try {
993
+ await authKv.update(key, new TextEncoder().encode(JSON.stringify({ ...rest, state: "retired", op: { opId, kind: "retirement" } })), entry.revision);
994
+ return;
995
+ }
996
+ catch (error) {
997
+ if (!isCasLoss(error))
998
+ throw error;
999
+ }
1000
+ }
1001
+ throw new EpEnvelopeError("conflict", `ending the platform supervisor of ${owner} lost repeated CAS attempts on ${key}; retry`);
890
1002
  },
891
1003
  maintainRemoteManager: async (holder) => {
892
1004
  refuseIfFenced();
@@ -1147,6 +1259,31 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
1147
1259
  }
1148
1260
  });
1149
1261
  },
1262
+ revokeManagerRun: async ({ request, ...holder }) => {
1263
+ refuseIfFenced();
1264
+ const req = parseRemoteRunRevokeRequest(request);
1265
+ const admitterNc = await connect({
1266
+ servers: server,
1267
+ ...standaloneConnectOpts({
1268
+ creds: await mintCreds(issuerAuth(), newIdentity(), "run-admitter", { runAdmitter: { endpoint: "manager", runId: req.revoke.runId }, expiresInSeconds: 60 }),
1269
+ tls: false,
1270
+ }),
1271
+ maxReconnectAttempts: 0,
1272
+ });
1273
+ onConnection?.(admitterNc, `cotal:run-revoker:${space}`);
1274
+ try {
1275
+ return await revokeRemoteRun({
1276
+ ...holder, request: req, space, accountPublicKey: dataAccount.pub, proofSecret: dataAccount.signingSeed, endpoint: "manager",
1277
+ observeManagerGate,
1278
+ readAdmission: (runId) => readRunAdmission(recordsJsm, space, "manager", runId),
1279
+ readRevocation: (runId) => readRunRevocation(recordsJsm, space, "manager", runId),
1280
+ admissions: await new Kvm(admitterNc).open(admissionBucket(space)),
1281
+ });
1282
+ }
1283
+ finally {
1284
+ await admitterNc.drain().catch(() => admitterNc.close());
1285
+ }
1286
+ },
1150
1287
  issueManagerRunAttempt: async ({ request, ...holder }) => {
1151
1288
  refuseIfFenced();
1152
1289
  const { owner } = holder;
@@ -1551,6 +1688,7 @@ export async function startAuthService(inputs) {
1551
1688
  context: inputs.context,
1552
1689
  ...(inputs.publicFace !== undefined ? { publicFace: inputs.publicFace } : {}),
1553
1690
  ...(inputs.platformControl !== undefined ? { platformControl: inputs.platformControl } : {}),
1691
+ ...(inputs.platformSupervisor !== undefined ? { platformSupervisor: inputs.platformSupervisor } : {}),
1554
1692
  ...(inputs.standingRenewableTtlSeconds !== undefined ? { standingRenewableTtlSeconds: inputs.standingRenewableTtlSeconds } : {}),
1555
1693
  });
1556
1694
  return { ...started.handle, readiness: () => ({ ...started.status(), context: inputs.context }) };
@@ -1587,6 +1725,7 @@ async function startAuthContext(o) {
1587
1725
  space,
1588
1726
  dir,
1589
1727
  identityRoot: o.identityRoot,
1728
+ ...(o.context !== undefined ? { identityStore: secrets } : {}),
1590
1729
  dataAccount: { pub: keys.dataAccount.pub, signingSeed: keys.dataAccount.signingSeed },
1591
1730
  log: (l) => console.error(l),
1592
1731
  localManager: o.localManager,
@@ -1664,6 +1803,7 @@ async function startAuthContext(o) {
1664
1803
  scanManagerGoalIndex: plane.scanManagerGoalIndex,
1665
1804
  authorizeManagerAdmin: plane.authorizeManagerAdmin,
1666
1805
  admitManagerRun: plane.admitManagerRun,
1806
+ revokeManagerRun: plane.revokeManagerRun,
1667
1807
  issueManagerRunAttempt: plane.issueManagerRunAttempt,
1668
1808
  secrets,
1669
1809
  retireInteractiveLifecycle: plane.retireInteractiveLifecycle,
@@ -1844,6 +1984,25 @@ async function startAuthContext(o) {
1844
1984
  cap,
1845
1985
  closed: owned.closed,
1846
1986
  connections: () => owned.connections(),
1987
+ ...(o.platformSupervisor !== undefined ? {
1988
+ platformSupervisorAuthority: async (owner) => {
1989
+ refuseUnlessReady();
1990
+ assertDerivedOwnerToken(owner);
1991
+ const input = o.platformSupervisor;
1992
+ await requirePlatformSupervisorAdmin(input, owner);
1993
+ return makePlatformSupervisorAuthority({
1994
+ ...input, owner, space, accountPublicKey: keys.dataAccount.pub, ready: refuseUnlessReady,
1995
+ observePrincipal: async (instanceId) => (await plane.observeManagerInstance(instanceId)).gate?.principal,
1996
+ issue: (boundOwner, supervisorExpiresAt, request) => plane.issueManagerServiceAuthority({
1997
+ owner: boundOwner, scope: ["supervise"], supervisorExpiresAt, request,
1998
+ }),
1999
+ });
2000
+ },
2001
+ endPlatformSupervisorAssignment: makePlatformSupervisorEnd({
2002
+ ...o.platformSupervisor, space, accountPublicKey: keys.dataAccount.pub, ready: refuseUnlessReady,
2003
+ end: (owner, instanceId, lifecycleUid) => plane.endPlatformSupervisor({ owner, instanceId, lifecycleUid }),
2004
+ }),
2005
+ } : {}),
1847
2006
  ...(platformDeps !== undefined ? {
1848
2007
  platformControlAuthority: makePlatformControlAuthority({
1849
2008
  ...platformDeps,
@@ -1951,6 +2110,11 @@ export async function dispatchManagerAuthorityRequest(ctx, owner, body) {
1951
2110
  return ctx.authorizeManagerAdmin({ owner, scope: row.scope ?? [], request: body.request });
1952
2111
  if (request.kind === "manager-run-admission")
1953
2112
  return ctx.admitManagerRun({ owner, scope: row.scope ?? [], request: body.request });
2113
+ if (request.kind === "manager-run-revoke") {
2114
+ if (ctx.revokeManagerRun === undefined)
2115
+ throw new EpEnvelopeError("unimplemented", "this issuing host has no run revoke handler");
2116
+ return ctx.revokeManagerRun({ owner, scope: row.scope ?? [], request: body.request });
2117
+ }
1954
2118
  if (request.kind === "manager-run-attempt")
1955
2119
  return ctx.issueManagerRunAttempt({ owner, scope: row.scope ?? [], request: body.request });
1956
2120
  if (request.kind === "manager-service-maintenance")
@@ -1962,7 +2126,6 @@ export async function dispatchManagerAuthorityRequest(ctx, owner, body) {
1962
2126
  const LOOPBACK_POLICY = {
1963
2127
  requireCapability: true,
1964
2128
  refuseViews: false,
1965
- allowManagerAuthority: true,
1966
2129
  peerKey: (req) => req.socket.remoteAddress ?? "loopback",
1967
2130
  throttled: (ctx) => {
1968
2131
  const now = Date.now();
@@ -1996,7 +2159,6 @@ function makePublicPolicy(trustedProxy) {
1996
2159
  return {
1997
2160
  requireCapability: false,
1998
2161
  refuseViews: true,
1999
- allowManagerAuthority: true,
2000
2162
  peerKey: (req) => {
2001
2163
  if (trustedProxy) {
2002
2164
  const xff = req.headers["x-forwarded-for"];
@@ -2384,8 +2546,9 @@ async function handleExchange(req, res, ctx, policy) {
2384
2546
  return send(res, 401, { error: reason });
2385
2547
  }
2386
2548
  }
2387
- /** Loopback/operator-only typed manager authority exchange. The public route table never includes
2388
- * this path, and the loopback capability is checked here in addition to the route separation. */
2549
+ /** The typed manager authority exchange, served on both faces. The face's policy decides whether the
2550
+ * loopback capability is demanded; on the public face the verified IdP JWT is the proof, and the
2551
+ * dispatcher reads the caller's scope from the ledger either way. */
2389
2552
  export async function handleManagerServiceAuthority(req, res, ctx, policy) {
2390
2553
  if (req.method !== "POST")
2391
2554
  return send(res, 405, { error: "POST only" });
@@ -2393,8 +2556,6 @@ export async function handleManagerServiceAuthority(req, res, ctx, policy) {
2393
2556
  return send(res, 403, { error: "browser-origin requests are not served here" });
2394
2557
  if (!/^application\/json\b/.test(req.headers["content-type"] ?? ""))
2395
2558
  return send(res, 415, { error: "content-type must be application/json" });
2396
- if (!policy.allowManagerAuthority)
2397
- return send(res, 403, { error: "manager-service authority is not enabled on this exchange face" });
2398
2559
  if (policy.requireCapability && req.headers.authorization !== `Bearer ${ctx.cap}`)
2399
2560
  return send(res, 401, { error: "missing/invalid exchange capability - manager-service authority requires the operator exchange capability on this face" });
2400
2561
  const body = await readJsonBody(req);
@@ -2489,7 +2650,8 @@ export function checkAgentProvisioningUrl(raw) {
2489
2650
  return undefined;
2490
2651
  }
2491
2652
  /** Build the PUBLIC listener's request handler: its own closed route table (/health, /jwks,
2492
- * /exchange under the public policy, /.well-known/cotal-mesh — 404 everything else), behind a
2653
+ * /exchange and /manager-service-authority under the public policy, /.well-known/cotal-mesh — 404
2654
+ * everything else), behind a
2493
2655
  * global concurrent-admission cap and a hard wall-clock deadline. All of it is public-face-local
2494
2656
  * state: nothing here touches the loopback face's windows. */
2495
2657
  function makePublicHandler(ctx, policy, bundle) {