@cotal-ai/auth 0.91.0 → 0.92.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 (54) hide show
  1. package/dist/authority-client.d.ts +70 -3
  2. package/dist/authority-client.d.ts.map +1 -1
  3. package/dist/authority-client.js +109 -8
  4. package/dist/authority-client.js.map +1 -1
  5. package/dist/commands.d.ts.map +1 -1
  6. package/dist/commands.js +37 -10
  7. package/dist/commands.js.map +1 -1
  8. package/dist/manager-authority.d.ts +59 -3
  9. package/dist/manager-authority.d.ts.map +1 -1
  10. package/dist/manager-authority.js +185 -4
  11. package/dist/manager-authority.js.map +1 -1
  12. package/dist/native-service-gate.d.ts +14 -3
  13. package/dist/native-service-gate.d.ts.map +1 -1
  14. package/dist/native-service-gate.js +34 -20
  15. package/dist/native-service-gate.js.map +1 -1
  16. package/dist/native-service-name.d.ts +7 -0
  17. package/dist/native-service-name.d.ts.map +1 -1
  18. package/dist/native-service-name.js +15 -6
  19. package/dist/native-service-name.js.map +1 -1
  20. package/dist/platform-control.d.ts.map +1 -1
  21. package/dist/platform-control.js +1 -0
  22. package/dist/platform-control.js.map +1 -1
  23. package/dist/provider.d.ts.map +1 -1
  24. package/dist/provider.js +8 -0
  25. package/dist/provider.js.map +1 -1
  26. package/dist/run-cancel-observation.d.ts +50 -0
  27. package/dist/run-cancel-observation.d.ts.map +1 -0
  28. package/dist/run-cancel-observation.js +146 -0
  29. package/dist/run-cancel-observation.js.map +1 -0
  30. package/dist/service-custody.d.ts +29 -20
  31. package/dist/service-custody.d.ts.map +1 -1
  32. package/dist/service-custody.js +37 -167
  33. package/dist/service-custody.js.map +1 -1
  34. package/dist/service-endpoint-authority.d.ts +70 -0
  35. package/dist/service-endpoint-authority.d.ts.map +1 -0
  36. package/dist/service-endpoint-authority.js +257 -0
  37. package/dist/service-endpoint-authority.js.map +1 -0
  38. package/dist/service-host-contract.d.ts +158 -16
  39. package/dist/service-host-contract.d.ts.map +1 -1
  40. package/dist/service-host-contract.js +330 -1
  41. package/dist/service-host-contract.js.map +1 -1
  42. package/dist/service-host-ops.d.ts +550 -0
  43. package/dist/service-host-ops.d.ts.map +1 -0
  44. package/dist/service-host-ops.js +2180 -0
  45. package/dist/service-host-ops.js.map +1 -0
  46. package/dist/service-write.d.ts +141 -0
  47. package/dist/service-write.d.ts.map +1 -0
  48. package/dist/service-write.js +179 -0
  49. package/dist/service-write.js.map +1 -0
  50. package/dist/service.d.ts +54 -1
  51. package/dist/service.d.ts.map +1 -1
  52. package/dist/service.js +313 -14
  53. package/dist/service.js.map +1 -1
  54. package/package.json +9 -4
package/dist/service.js CHANGED
@@ -29,7 +29,8 @@ import { createToolResultConsentSigner, makeToolResultConsentRecorder } from "./
29
29
  * 127.0.0.1; TLS terminates at a reverse proxy — in-process TLS was rejected: it duplicates cert
30
30
  * renewal and forks deploy). It serves ONLY `GET /health`, `GET /jwks`, `POST /exchange`,
31
31
  * `POST /manager-service-authority`, and `GET /.well-known/cotal-mesh` (the generated discovery
32
- * bundle); everything else 404s. Its
32
+ * bundle), plus, only with `serviceEndpoints`, `/service-endpoint-authority` on both faces;
33
+ * everything else 404s. Its
33
34
  * `/exchange` demands NO capability — honestly: the 0600 cap is a same-uid file-ACL boundary with
34
35
  * no remote meaning, so requiring its bytes from a remote caller would prove nothing. The proof on
35
36
  * the public face is the credential itself: the human arm presents an EdDSA IdP JWT verified
@@ -62,7 +63,7 @@ import { resolve } from "node:path";
62
63
  import { connect, credsAuthenticator } from "@nats-io/transport-node";
63
64
  import { jetstream, jetstreamManager } from "@nats-io/jetstream";
64
65
  import { Kvm } from "@nats-io/kv";
65
- import { CotalEndpoint, deprovisionAgent, dlvDurable, dlvStream, dmDurable, dmStream, 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, sameManagedGrantCustody } from "@cotal-ai/core";
66
+ import { CotalEndpoint, deprovisionAgent, dlvDurable, dlvStream, dmDurable, dmStream, 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, parseServiceAuthorityEnvelope, SERVICE_AUTHORITY_MAX_BYTES, epgateKey, isCasLoss, sameManagedGrantCustody, parseServiceRegistrationWitness, serviceWitnessDigest } from "@cotal-ai/core";
66
67
  import { findCotalRoot, loadManagerInstanceIdentity, userAuthStateDir, workspaceSecretStore, createAuthInstanceIdentity, loadAuthInstanceIdentity } from "@cotal-ai/workspace";
67
68
  import { decodeJwt } from "jose";
68
69
  import { deriveOwnerForIdpSubject, platformControlOwner } from "./derive.js";
@@ -71,11 +72,11 @@ import { OwnedConnections } from "./owned-connections.js";
71
72
  import { makePlatformSupervisorAuthority, makePlatformSupervisorEnd, platformSupervisorEnded, requirePlatformSupervisorAdmin } from "./platform-supervisor.js";
72
73
  import { startAuthCallout } from "./callout.js";
73
74
  import { createIdpBridge, verifyIdpToken, verifyToolResultHumanProof } from "./idp.js";
74
- import { PUBLIC_EXCHANGE_VIEWS, assertTransferWriterClaim } from "./token.js";
75
+ import { PUBLIC_EXCHANGE_VIEWS, assertTransferWriterClaim, validateUserToken } from "./token.js";
75
76
  import { grantCoordinates, verifySessionRedemption } from "./session-redemption.js";
76
77
  import { pinnedJwksResolver } from "./issuer.js";
77
78
  import { calloutPermissions } from "./permissions.js";
78
- import { admitRemoteRun, authorizeRemoteManagerRenewal, authorizeRemoteRunAttempt, holdsRunFence, issueRemoteManagerAuthority, observedRunRequest, parseRemoteRunAdmissionRequest, parseRemoteRunAttemptRequest, parseRemoteRunRevokeRequest, revokeRemoteRun } from "./manager-authority.js";
79
+ import { handleRemoteRunCancel, parseRemoteRunCancelRequest, admitRemoteRun, handleRemoteRunSubmission, authorizeRemoteManagerRenewal, authorizeRemoteRunAttempt, holdsRunFence, issueRemoteManagerAuthority, observedRunRequest, parseRemoteRunAdmissionRequest, parseRemoteRunAttemptRequest, parseRemoteRunRevokeRequest, revokeRemoteRun } from "./manager-authority.js";
79
80
  import { authorizeRemoteRetainedAgentValidation, completeRemoteRetainedAgentValidation, remoteManagerCurrentRegistrationProof } from "./retained-manager-validation.js";
80
81
  import { authorizeRemoteManagedAgentEnrollment, authorizeRemoteManagedAgentPrepareRetirement, authorizeRemoteManagedAgentRuntimeCreate, authorizeRemoteManagedAgentRuntimeStatus } from "./managed-agent-enrollment.js";
81
82
  import { authorizeRemoteManagerGoalIndexScan, completeRemoteManagerGoalIndexScan } from "./manager-goal-index.js";
@@ -83,12 +84,16 @@ import { authorizeRemoteManagerAdmin } from "./manager-admin-authorization.js";
83
84
  import { authorizeRemoteManagerMaintenance, completeRemoteManagerMaintenance } from "./manager-maintenance.js";
84
85
  import { validateRetainedManagedAgent } from "./continuity.js";
85
86
  import { reconstructRemoteManagerServeGrant, remoteManagerServeGrantFromCluster, remoteManagerSurface } from "./manager-contract.js";
86
- import { authorityBarrierGrants, authorityWriterGrants, openAuthorityClient, openSupervisedConnectReader, platformReadinessGrants, remoteManagerIssuerGrants, remoteManagerRegistrationProof, registrationExecutorGrants, servedRunRequestSubjects } from "./authority-client.js";
87
+ import { authorityBarrierGrants, authorityWriterGrants, openAuthorityClient, openSupervisedConnectReader, platformReadinessGrants, remoteManagerIssuerGrants, remoteManagerRegistrationProof, registrationExecutorGrants, servedRunRequestSubjects, servedRunCancelRequestSubject, serviceCalloutWriterGrants, serviceHostDiscoveryGrants } from "./authority-client.js";
88
+ import { createNativeServiceCalloutAuthority } from "./native-service-callout.js";
87
89
  import { authorizeConnectCredential } from "./connect-reader.js";
88
90
  import { chatAwareAclResolver, openChatSessionAuthority } from "./chat-session-authority.js";
89
91
  import { makeNativeIssuanceRecorder, pruneNativeIssuance } from "./native-issuance.js";
90
92
  import { ensureRootCredential } from "./root-credential.js";
91
93
  import { activateLifecycleAtUid, observeGate, openLifecycleRegistry, readLifecycleHeadForOperation } from "./lifecycle-registry.js";
94
+ import { openServiceHostEffect, openServiceHostOperations } from "./service-host-ops.js";
95
+ import { SERVICE_ENDPOINT_AUTHORITY_PATH, answerServiceAuthority, serviceAuthorityDenied, serviceAuthorityInfo } from "./service-endpoint-authority.js";
96
+ import { validateServiceRenewalProof } from "./service-renewal.js";
92
97
  import { openAuthLedgerScannerCandidate } from "./ledger-scanner.js";
93
98
  import { openRecordsScannerCandidate } from "./records-scanner.js";
94
99
  import { acquirePlaneClaim, makeDeliveryAdminPlaneOracle, makeDeliveryAdminPrincipalOracle, scannerDeathCopy, planeClaimRefusal } from "./plane-claim.js";
@@ -98,6 +103,8 @@ import { resumeAgentRetirement, runAgentRetirementBarrier } from "./retirement-b
98
103
  import { makeRetirementCleaners } from "./retirement-cleaner.js";
99
104
  import { makeDrainRepairers } from "./drain-repair.js";
100
105
  import { serializedFor } from "./serialized.js";
106
+ import { RunCancelObservations } from "./run-cancel-observation.js";
107
+ import { readCancellationAdmission } from "@cotal-ai/core";
101
108
  import { joinOrStartRetirement, openAuthAdminListener } from "./auth-admin.js";
102
109
  import { AUTH_SERVICE_ENDPOINT, authClusterArtifacts, authContractArtifactValues } from "./auth-service-contract.js";
103
110
  import { drainTargetForEndpoint, openAdmissionMediator } from "./admission-mediator.js";
@@ -254,6 +261,28 @@ function runtimeSelection(options) {
254
261
  export async function openAuthAuthorityPlane(opts) {
255
262
  return await openAuthAuthorityPlaneWithCustody(opts);
256
263
  }
264
+ /** The hosted door over a plane's service endpoint host: refused before entry unless the context is
265
+ * ready, and mapped to the same engine so the internal accessor still finds it. */
266
+ function hostedServiceEndpoints(host, ready) {
267
+ const door = async (request) => { ready(); return await host(request); };
268
+ const engine = SERVICE_HOST_ENGINES.get(host), callout = SERVICE_CALLOUTS.get(host);
269
+ if (engine !== undefined)
270
+ SERVICE_HOST_ENGINES.set(door, engine);
271
+ if (callout !== undefined)
272
+ SERVICE_CALLOUTS.set(door, callout);
273
+ return door;
274
+ }
275
+ /** Package-internal (not exported from the package index): the engine behind a plane's service
276
+ * endpoint host, whose register admission only a later owner-verifying route may call. */
277
+ const SERVICE_HOST_ENGINES = new WeakMap();
278
+ export function serviceHostEngineOf(host) {
279
+ return SERVICE_HOST_ENGINES.get(host);
280
+ }
281
+ /** Package-internal: the native service callout authority composed with a plane's service host. */
282
+ const SERVICE_CALLOUTS = new WeakMap();
283
+ export function serviceHostCalloutOf(host) {
284
+ return SERVICE_CALLOUTS.get(host);
285
+ }
257
286
  /** The context's custody callback is private. Failed construction unwinds from one place. */
258
287
  async function openAuthAuthorityPlaneWithCustody(opts, onConnection) {
259
288
  const unwind = [];
@@ -270,6 +299,7 @@ async function openAuthAuthorityPlaneWithCustody(opts, onConnection) {
270
299
  async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
271
300
  const interactiveManagerSelection = runtimeSelection(opts);
272
301
  const { server, space, dataAccount, log } = opts;
302
+ const cancelObservations = new RunCancelObservations(space, opts.runCancelLimits);
273
303
  const standingTtl = opts.standingRenewableTtlSeconds ?? STANDING_RENEWABLE_TTL_SEC;
274
304
  if (!Number.isSafeInteger(standingTtl) || standingTtl < 5 || standingTtl > STANDING_RENEWABLE_TTL_SEC) {
275
305
  throw new Error(`standingRenewableTtlSeconds must be an integer between 5 and ${STANDING_RENEWABLE_TTL_SEC} seconds (got ${standingTtl})`);
@@ -367,12 +397,17 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
367
397
  // Mid-life disconnect = the FENCING event (security req: an owned scanner that dies must not
368
398
  // leave the plane half-running). First death wins: fence the guard with the ux state-3 copy,
369
399
  // close the sibling so a successor's reclaim isn't blocked by a half-dead pair, log loud.
400
+ let serviceHost;
370
401
  const fenceOnDeath = (role, sibling) => {
371
- if (closing)
372
- return;
373
402
  const copy = scannerDeathCopy(space, role);
403
+ // A closing plane does not go fatal, but a service host call it is still draining stops here.
404
+ if (closing) {
405
+ serviceHost?.trip(copy);
406
+ return;
407
+ }
374
408
  hold?.fence(copy);
375
409
  fatalReason = fatalReason ?? copy;
410
+ serviceHost?.trip(copy);
376
411
  fireFatal(copy);
377
412
  log(`auth-plane: ${copy}`);
378
413
  void sibling().catch(() => { });
@@ -477,11 +512,15 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
477
512
  return;
478
513
  pruneTimer = setTimeout(() => {
479
514
  pruneFlight = pruneNativeIssuance(registry, Math.floor(Date.now() / 1000)).then(schedulePrune, (e) => {
480
- if (closing)
481
- return;
482
515
  const copy = `native-issuance cleanup failed: ${e instanceof Error ? e.message : String(e)}`;
516
+ // As for a scanner death: a closing plane does not go fatal, but a call it is draining stops.
517
+ if (closing) {
518
+ serviceHost?.trip(copy);
519
+ return;
520
+ }
483
521
  hold?.fence(copy);
484
522
  fatalReason = fatalReason ?? copy;
523
+ serviceHost?.trip(copy);
485
524
  fireFatal(copy);
486
525
  log(`auth-plane: ${copy}`);
487
526
  });
@@ -490,6 +529,84 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
490
529
  const stopPrune = async () => { clearTimeout(pruneTimer); await pruneFlight; };
491
530
  unwind.push(async () => { closing = true; await stopPrune(); });
492
531
  schedulePrune();
532
+ // The service endpoint host, default off. Its discovery client only reads, on the ordinary
533
+ // renewing profile: a broker loss makes its reads fail until it reconnects, and a renewal failure
534
+ // closes it for good, after which decide and revoke answer unavailable until the plane is opened
535
+ // again. The engine shares this plane's hold; both fatal sites above trip it.
536
+ let discoveryClient, calloutWriter;
537
+ // Every effect connection the host opened whose close has not succeeded, kept past its call's
538
+ // settlement: plane close and a failed construction close it again and fail while any remains.
539
+ const openEffects = new Set();
540
+ const closeEffects = async () => {
541
+ const failed = [];
542
+ for (const e of [...openEffects]) {
543
+ try {
544
+ await e.close();
545
+ }
546
+ catch (err) {
547
+ failed.push(err);
548
+ }
549
+ }
550
+ if (failed.length > 0)
551
+ throw new AggregateError(failed, `${failed.length} service host effect connection(s) did not close`);
552
+ };
553
+ if (opts.serviceEndpoints !== undefined) {
554
+ const label = `cotal:service-host-discovery:${space}`;
555
+ const d = discoveryClient = await openAuthorityClient({ onConnection, server, space, dataAccount, label, grants: (id) => serviceHostDiscoveryGrants(space, id), log,
556
+ onRenewalFailure: () => {
557
+ log(`${label}: credential renewal failed - service host discovery is closed until the auth plane is opened again`);
558
+ void discoveryClient?.close().catch(() => { });
559
+ } });
560
+ unwind.push(() => d.close());
561
+ unwind.push(() => closeEffects());
562
+ const discovery = await openLifecycleRegistry(d.nc, space);
563
+ await opts.probeServiceHost?.discovery?.({ registry: discovery, kill: () => d.close() });
564
+ const probeEffect = opts.probeServiceHost?.effect;
565
+ // Serving reads registered closures from the contract store on the issuer's connection, which
566
+ // holds only the store's exact-subject Direct Get.
567
+ const contractStore = await contractStoreContext(remoteIssuer.nc, space);
568
+ const readContractArtifact = async (digest) => {
569
+ const bytes = await fetchContractArtifact(contractStore, contractRefToHex(digest));
570
+ if (!bytes)
571
+ throw new EpEnvelopeError("failed-precondition", `the registered contract artifact ${digest} is not readable`);
572
+ return JSON.parse(new TextDecoder().decode(bytes));
573
+ };
574
+ const engine = await openServiceHostOperations({
575
+ discovery, hold: hold, accountPublicKey: dataAccount.pub, ledgerDir: opts.dir, startup: opts.serviceEndpoints.startup,
576
+ serving: { readContractArtifact },
577
+ openEffect: async (scope) => {
578
+ const opened = await openServiceHostEffect({ onConnection, server, space, dataAccount, log }, scope);
579
+ const effect = { ...opened, close: async () => { await opened.close(); openEffects.delete(effect); } };
580
+ openEffects.add(effect);
581
+ if (probeEffect !== undefined) {
582
+ try {
583
+ await probeEffect(effect, scope);
584
+ }
585
+ catch (e) {
586
+ await effect.close().catch(() => { });
587
+ throw e;
588
+ }
589
+ }
590
+ return effect;
591
+ },
592
+ });
593
+ unwind.push(() => engine.close());
594
+ serviceHost = engine;
595
+ SERVICE_HOST_ENGINES.set(engine.handle, engine);
596
+ // A fence while the engine was opening latches it now.
597
+ if (fatalReason !== undefined)
598
+ engine.trip(fatalReason);
599
+ // The native service callout: a service bearer's connect mints only for the current bound
600
+ // assignment its witness names, read from the engine, on a writer that stages and touches the
601
+ // endpoint gate and credential rows. A fenced plane or a latched engine refuses it.
602
+ const calloutLabel = `cotal:service-callout:${space}`;
603
+ const cw = calloutWriter = await openAuthorityClient({ onConnection, server, space, dataAccount, label: calloutLabel, grants: (id) => serviceCalloutWriterGrants(space, id), log });
604
+ unwind.push(() => cw.close());
605
+ SERVICE_CALLOUTS.set(engine.handle, createNativeServiceCalloutAuthority({
606
+ registry: await openLifecycleRegistry(cw.nc, space), reader: () => reader.current(), dataAccount,
607
+ resolveAssignment: (witness) => engine.serviceBinding(witness), readClusterArtifact: readContractArtifact, assertOpen: () => refuseIfFenced(),
608
+ }));
609
+ }
493
610
  const recordsJsm = await jetstreamManager(remoteIssuer.nc);
494
611
  const authKv = await new Kvm(remoteIssuer.nc).open(epAuthBucket(space));
495
612
  const managerIssuanceGate = (instanceId) => serveIssuanceGateKv(authKv, space, { endpoint: "manager", instanceId });
@@ -633,6 +750,12 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
633
750
  // envelope asked (SPEC 14.8): the forward's coordinates are the manager's word. The manager
634
751
  // forwards from inside the handler serving the request, so an observation older than the window
635
752
  // has no forward left to bind; the window also bounds the table.
753
+ remoteIssuer.nc.subscribe(servedRunCancelRequestSubject(space), {
754
+ callback: (error, message) => {
755
+ if (!error)
756
+ cancelObservations.observe(message.subject, message.data);
757
+ },
758
+ });
636
759
  const observedRunRequests = new Map();
637
760
  for (const subject of servedRunRequestSubjects(space))
638
761
  remoteIssuer.nc.subscribe(subject, {
@@ -1457,6 +1580,34 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
1457
1580
  observeManagerGate: observeLegacyManagerGate,
1458
1581
  });
1459
1582
  },
1583
+ submitManagerRun: async ({ request, ...holder }) => {
1584
+ refuseIfFenced();
1585
+ const { owner } = holder;
1586
+ requireManagerAuthorityHolder(holder, String(request?.instanceId), 'keyed run submission needs scope "supervise"');
1587
+ return withIssuerSession({ servers: server, space, auth: issuerAuth(), tls: false }, session => handleRemoteRunSubmission({
1588
+ request, owner, space, accountPublicKey: dataAccount.pub, proofSecret: dataAccount.signingSeed, endpoint: "manager",
1589
+ observeManagerGate: observeLegacyManagerGate, issued: session.store, sourceIsLive: composedSourceIsLive(session),
1590
+ takeObserved: takeObservedRunRequest,
1591
+ registeredCommand: async (instanceId, registrationRevision, command) => {
1592
+ const registered = remoteManagerSurface(await registeredManagerCluster(owner, instanceId, { registrationRevision }))[command];
1593
+ if (!registered)
1594
+ throw new EpEnvelopeError("failed-precondition", "registered manager has no keyed contract");
1595
+ return registered;
1596
+ },
1597
+ withStore: async (runId, submission, work) => {
1598
+ const nc = await connect({ servers: server, ...standaloneConnectOpts({
1599
+ creds: await mintCreds(issuerAuth(), newIdentity(), "run-admitter", { runAdmitter: { endpoint: "manager", runId, submission }, expiresInSeconds: 60 }), tls: false,
1600
+ }), maxReconnectAttempts: 0 });
1601
+ onConnection?.(nc, `cotal:keyed-admitter:${space}`);
1602
+ try {
1603
+ return await work(await new Kvm(nc).open(admissionBucket(space)), await jetstreamManager(nc));
1604
+ }
1605
+ finally {
1606
+ await nc.drain().catch(() => nc.close());
1607
+ }
1608
+ },
1609
+ }));
1610
+ },
1460
1611
  admitManagerRun: async ({ request, ...holder }) => {
1461
1612
  refuseIfFenced();
1462
1613
  const { owner } = holder;
@@ -1489,6 +1640,46 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
1489
1640
  }
1490
1641
  });
1491
1642
  },
1643
+ cancelManagerRun: async ({ request, ...holder }) => {
1644
+ refuseIfFenced();
1645
+ const req = parseRemoteRunCancelRequest(request);
1646
+ requireManagerAuthorityHolder(holder, req.instanceId, 'manager run cancellation needs scope "supervise"');
1647
+ return withIssuerSession({ servers: server, space, auth: issuerAuth(), tls: false }, session => handleRemoteRunCancel({
1648
+ request: req, owner: holder.owner, space, endpoint: "manager", accountPublicKey: dataAccount.pub,
1649
+ proofSecret: dataAccount.signingSeed, observeManagerGate: observeLegacyManagerGate,
1650
+ observations: cancelObservations, flush: () => remoteIssuer.nc.flush(), assertServing: refuseIfFenced,
1651
+ issued: session.store, sourceIsLive: composedSourceIsLive(session),
1652
+ interactiveScope: async (caller, sources) => {
1653
+ if (sources.length !== 1 || sources[0].space !== space)
1654
+ return undefined;
1655
+ const source = parseActorLedgerSource(sources[0]);
1656
+ if (!source || source.owner !== caller.owner || source.actor !== caller.actor || source.lifecycleUid !== caller.uid)
1657
+ return undefined;
1658
+ const row = findActorUnified(opts.dir, caller.owner, caller.actor);
1659
+ return row?.kind === "interactive" && row.lifecycleUid === caller.uid ? [...row.scope] : undefined;
1660
+ },
1661
+ registeredCommand: async (instanceId, registrationRevision, command) => {
1662
+ const registered = remoteManagerSurface(await registeredManagerCluster(holder.owner, instanceId, { registrationRevision }))[command];
1663
+ if (!registered)
1664
+ throw new EpEnvelopeError("failed-precondition", "registered manager has no run-cancel contract");
1665
+ return registered;
1666
+ },
1667
+ readAdmission: runId => readCancellationAdmission(recordsJsm, space, "manager", runId),
1668
+ readRevocation: runId => readRunRevocation(recordsJsm, space, "manager", runId),
1669
+ withStore: async (runId, work) => {
1670
+ const nc = await connect({ servers: server, ...standaloneConnectOpts({
1671
+ creds: await mintCreds(issuerAuth(), newIdentity(), "run-admitter", { runAdmitter: { endpoint: "manager", runId }, expiresInSeconds: 60 }), tls: false,
1672
+ }), maxReconnectAttempts: 0 });
1673
+ onConnection?.(nc, `cotal:run-cancel:${space}`);
1674
+ try {
1675
+ return await work(await new Kvm(nc).open(admissionBucket(space)));
1676
+ }
1677
+ finally {
1678
+ await nc.drain().catch(() => nc.close());
1679
+ }
1680
+ },
1681
+ }));
1682
+ },
1492
1683
  revokeManagerRun: async ({ request, ...holder }) => {
1493
1684
  refuseIfFenced();
1494
1685
  const req = parseRemoteRunRevokeRequest(request);
@@ -1658,14 +1849,24 @@ async function buildAuthAuthorityPlane(opts, unwind, onConnection) {
1658
1849
  return await serveIssuanceGateKv(authKv, space, { endpoint, instanceId }).observe();
1659
1850
  },
1660
1851
  fenced,
1852
+ ...(serviceHost !== undefined ? { serviceEndpointHost: serviceHost.handle } : {}),
1661
1853
  close: async () => {
1662
1854
  // Clean-close order (SPEC 13.13): the rail stops answering first, then scan-capable
1663
1855
  // clients down, then `held → released` (never released while either scanner can still
1664
1856
  // act), then the barrier that wrote it. An unreachable release keeps the barrier alive
1665
1857
  // for a later close retry; successful scanner teardown is not run a second time.
1666
1858
  closing = true;
1859
+ // Before the first await: no service host call enters once close has begun.
1860
+ serviceHost?.refuse();
1667
1861
  await stopPrune();
1668
1862
  await stopChatSweep();
1863
+ // Every service host call that entered returns, discovery and effects included, then every
1864
+ // effect connection that did not close is closed again, before discovery closes and before
1865
+ // the claim is released. A failure here rejects with the claim still held; close retries it.
1866
+ await serviceHost?.close();
1867
+ await closeEffects();
1868
+ await calloutWriter?.close();
1869
+ await discoveryClient?.close();
1669
1870
  const scannerFlight = closeScanners ??= (async () => {
1670
1871
  await authAdmin?.close();
1671
1872
  const results = await Promise.allSettled([reader.close(), recordsScanner.close(), scanner.close()]);
@@ -1851,6 +2052,7 @@ export async function runAuthService(args, store, options = {}) {
1851
2052
  // selection exactly as before. A hosted context passes no local manager at all.
1852
2053
  localManager: () => loadManagerInstanceIdentity(root, space),
1853
2054
  ...(interactiveManagerSelection !== undefined ? { interactiveManagerSelection } : {}),
2055
+ ...(options.runCancelLimits !== undefined ? { runCancelLimits: options.runCancelLimits } : {}),
1854
2056
  });
1855
2057
  // All planes bound — NOW write the discovery file (its existence is the readiness signal).
1856
2058
  const { url, publicUrl, cap } = started.handle;
@@ -1955,6 +2157,7 @@ export async function startAuthService(inputs) {
1955
2157
  ...(inputs.platformSupervisor !== undefined ? { platformSupervisor: inputs.platformSupervisor } : {}),
1956
2158
  ...(inputs.standingRenewableTtlSeconds !== undefined ? { standingRenewableTtlSeconds: inputs.standingRenewableTtlSeconds } : {}),
1957
2159
  ...(inputs.toolResultConsents !== undefined ? { toolResultConsents: inputs.toolResultConsents } : {}),
2160
+ ...(inputs.serviceEndpoints !== undefined ? { serviceEndpoints: inputs.serviceEndpoints } : {}),
1958
2161
  });
1959
2162
  return { ...started.handle, readiness: () => ({ ...started.status(), context: inputs.context }) };
1960
2163
  }
@@ -1995,7 +2198,9 @@ async function startAuthContext(o) {
1995
2198
  log: (l) => console.error(l),
1996
2199
  localManager: o.localManager,
1997
2200
  ...(o.interactiveManagerSelection !== undefined ? { interactiveManagerSelection: o.interactiveManagerSelection } : {}),
2201
+ ...(o.runCancelLimits !== undefined ? { runCancelLimits: o.runCancelLimits } : {}),
1998
2202
  ...(o.standingRenewableTtlSeconds !== undefined ? { standingRenewableTtlSeconds: o.standingRenewableTtlSeconds } : {}),
2203
+ ...(o.serviceEndpoints !== undefined ? { serviceEndpoints: o.serviceEndpoints } : {}),
1999
2204
  }, onConnection);
2000
2205
  let nc;
2001
2206
  let http;
@@ -2020,7 +2225,9 @@ async function startAuthContext(o) {
2020
2225
  name: `cotal:auth-service:${space}`,
2021
2226
  });
2022
2227
  onConnection(nc, `cotal:auth-service:${space}`);
2228
+ const serviceCallout = plane.serviceEndpointHost !== undefined ? serviceHostCalloutOf(plane.serviceEndpointHost) : undefined;
2023
2229
  startAuthCallout(nc, {
2230
+ ...(serviceCallout !== undefined ? { serviceAuthority: serviceCallout } : {}),
2024
2231
  xkeySeed: callout.xkey.seed,
2025
2232
  authAccount: { pub: callout.account.pub, signingSeed: callout.account.signingSeed },
2026
2233
  dataAccount: { pub: keys.dataAccount.pub, signingSeed: keys.dataAccount.signingSeed },
@@ -2057,8 +2264,10 @@ async function startAuthContext(o) {
2057
2264
  space, accountPublicKey: keys.dataAccount.pub, dir, store: secrets, idp: bridgeIdp, ownerSecret,
2058
2265
  signer: o.toolResultConsents, record: plane.recordToolResultConsent,
2059
2266
  });
2267
+ const serviceEngine = plane.serviceEndpointHost !== undefined ? serviceHostEngineOf(plane.serviceEndpointHost) : undefined;
2060
2268
  const ctx = {
2061
2269
  ...(issueToolResultConsent ? { issueToolResultConsent } : {}),
2270
+ ...(serviceEngine !== undefined ? { serviceAuthority: { space, accountPublicKey: keys.dataAccount.pub, engine: serviceEngine, material: issuer } } : {}),
2062
2271
  issuer,
2063
2272
  bridge,
2064
2273
  bridgeIdp,
@@ -2078,7 +2287,9 @@ async function startAuthContext(o) {
2078
2287
  scanManagerGoalIndex: plane.scanManagerGoalIndex,
2079
2288
  authorizeManagerAdmin: plane.authorizeManagerAdmin,
2080
2289
  admitManagerRun: plane.admitManagerRun,
2290
+ submitManagerRun: plane.submitManagerRun,
2081
2291
  revokeManagerRun: plane.revokeManagerRun,
2292
+ cancelManagerRun: plane.cancelManagerRun,
2082
2293
  issueManagerRunAttempt: plane.issueManagerRunAttempt,
2083
2294
  secrets,
2084
2295
  retireInteractiveLifecycle: plane.retireInteractiveLifecycle,
@@ -2263,6 +2474,7 @@ async function startAuthContext(o) {
2263
2474
  cap,
2264
2475
  closed: owned.closed,
2265
2476
  connections: () => owned.connections(),
2477
+ ...(plane.serviceEndpointHost !== undefined ? { serviceEndpointHost: hostedServiceEndpoints(plane.serviceEndpointHost, refuseUnlessReady) } : {}),
2266
2478
  ...(o.platformSupervisor !== undefined ? {
2267
2479
  platformSupervisorAuthority: async (owner) => {
2268
2480
  refuseUnlessReady();
@@ -2293,6 +2505,7 @@ async function startAuthContext(o) {
2293
2505
  case "manager-goal-index-scan": return plane.scanManagerGoalIndex({ ...holder, request });
2294
2506
  case "manager-admin-authorization": return plane.authorizeManagerAdmin({ ...holder, request });
2295
2507
  case "manager-run-admission": return plane.admitManagerRun({ ...holder, request });
2508
+ case "manager-run-submission": return plane.submitManagerRun({ ...holder, request });
2296
2509
  case "manager-run-attempt": return plane.issueManagerRunAttempt({ ...holder, request });
2297
2510
  case "manager-retained-agent-validation": return validateRetainedForHolder(holder, request);
2298
2511
  }
@@ -2389,6 +2602,16 @@ export async function dispatchManagerAuthorityRequest(ctx, owner, body) {
2389
2602
  return ctx.authorizeManagerAdmin({ owner, scope: row.scope ?? [], request: body.request });
2390
2603
  if (request.kind === "manager-run-admission")
2391
2604
  return ctx.admitManagerRun({ owner, scope: row.scope ?? [], request: body.request });
2605
+ if (request.kind === "manager-run-submission") {
2606
+ if (!ctx.submitManagerRun)
2607
+ throw new EpEnvelopeError("unimplemented", "issuing host has no keyed submission handler");
2608
+ return ctx.submitManagerRun({ owner, scope: row.scope ?? [], request: body.request });
2609
+ }
2610
+ if (request.kind === "manager-run-cancel") {
2611
+ if (!ctx.cancelManagerRun)
2612
+ throw new EpEnvelopeError("unimplemented", "this issuing host has no run-cancel handler");
2613
+ return ctx.cancelManagerRun({ owner, scope: row.scope ?? [], request: body.request });
2614
+ }
2392
2615
  if (request.kind === "manager-run-revoke") {
2393
2616
  if (ctx.revokeManagerRun === undefined)
2394
2617
  throw new EpEnvelopeError("unimplemented", "this issuing host has no run revoke handler");
@@ -2503,6 +2726,7 @@ const ROUTES = new Map([
2503
2726
  ["/exchange", (req, res, ctx) => handleExchange(req, res, ctx, LOOPBACK_POLICY)],
2504
2727
  ["/manager-service-authority", (req, res, ctx) => handleManagerServiceAuthority(req, res, ctx, LOOPBACK_POLICY)],
2505
2728
  ["/manager-instances", (req, res, ctx) => handleManagerInstances(req, res, ctx, LOOPBACK_POLICY)],
2729
+ [SERVICE_ENDPOINT_AUTHORITY_PATH, (req, res, ctx) => handleServiceEndpointAuthority(req, res, ctx, LOOPBACK_POLICY)],
2506
2730
  [INTERACTIVE_RETIRE_PATH, handleInteractiveLifecycleRetirement],
2507
2731
  [MANAGED_RETIRE_PATH, handleManagedLifecycleRetirement],
2508
2732
  [VERIFY_ENROLLMENT_PATH, handleVerifyManagedAgentEnrollment],
@@ -2988,6 +3212,80 @@ async function handleManagerInstances(req, res, ctx, policy) {
2988
3212
  return send(res, 503, { error: `the manager instance list is unavailable: ${e instanceof Error ? e.message : String(e)}` });
2989
3213
  }
2990
3214
  }
3215
+ /** A service renewal or access proof, verified against this context's own signing keys: the
3216
+ * signed witness it binds. A request that carries a witness must name the proof's; a status query
3217
+ * carries none, so its proof's own witness is the expected one. Current native state is the
3218
+ * engine's check, never the proof's. */
3219
+ async function verifyServiceProof(proof, request, issuer, space) {
3220
+ const key = issuer.localKeySet();
3221
+ if (proof.kind === "service-renewal") {
3222
+ const expected = "witness" in request ? request.witness : parseServiceRegistrationWitness(decodeJwt(proof.token).witness);
3223
+ return (await validateServiceRenewalProof(proof.token, { issuer: issuer.issuer, key, witness: expected })).witness;
3224
+ }
3225
+ const token = await validateUserToken(proof.token, { issuer: issuer.issuer, audience: space, key });
3226
+ if (token.act.view !== "service-endpoint" || token.act.service?.duty !== "serve")
3227
+ throw new Error("the access proof is not a service bearer");
3228
+ const witness = parseServiceRegistrationWitness(token.act.service.witness);
3229
+ if (token.owner !== witness.principal.owner || token.act.owner !== witness.principal.owner || token.act.actor !== witness.principal.actor
3230
+ || token.act.lifecycleUid !== witness.principal.lifecycleUid || token.credentialId !== witness.rootCredentialId)
3231
+ throw new Error("the access proof does not bind its witness");
3232
+ if ("witness" in request && serviceWitnessDigest(request.witness) !== serviceWitnessDigest(witness))
3233
+ throw new Error("the access proof binds another witness");
3234
+ return witness;
3235
+ }
3236
+ /** The public service endpoint authority route, on both faces and only with `serviceEndpoints`: GET
3237
+ * reads the account info, POST takes one `{request, proof}` envelope. The proof is the credential on
3238
+ * either face; the loopback capability is never read or accepted here. An IdP proof verifies against
3239
+ * the pinned issuer and exact audience, and its subject derives the owner. A service renewal or
3240
+ * access proof verifies against this context's own issuer and binds a witness, never an owner's
3241
+ * authority. A refused proof counts against the face's refusal budget; a valid proof does not,
3242
+ * whatever the answer. */
3243
+ async function handleServiceEndpointAuthority(req, res, ctx, policy) {
3244
+ const door = ctx.serviceAuthority;
3245
+ if (door === undefined)
3246
+ return send(res, 404, { error: "service endpoint authority is not enabled for this space" });
3247
+ if (req.headers.origin !== undefined)
3248
+ return send(res, 403, { error: "browser-origin requests are not served here" });
3249
+ const noStore = { "cache-control": "no-store" };
3250
+ if (req.method === "GET")
3251
+ return send(res, 200, serviceAuthorityInfo(door), noStore);
3252
+ if (req.method !== "POST")
3253
+ return send(res, 405, { error: "GET or POST only" });
3254
+ if (!/^application\/json\b/.test(req.headers["content-type"] ?? ""))
3255
+ return send(res, 415, { error: "content-type must be application/json" });
3256
+ const peer = policy.peerKey(req);
3257
+ const peerThrottled = policy.throttled(ctx, peer);
3258
+ let envelope;
3259
+ try {
3260
+ envelope = parseServiceAuthorityEnvelope(await readJsonBody(req, SERVICE_AUTHORITY_MAX_BYTES));
3261
+ }
3262
+ catch (e) {
3263
+ return sendRequestError(res, e);
3264
+ }
3265
+ const { request, proof } = envelope;
3266
+ const answer = (r) => send(res, r.status, r.body, noStore);
3267
+ if (request.space !== door.space || request.accountPublicKey !== door.accountPublicKey)
3268
+ return answer(serviceAuthorityDenied(request, "account-mismatch"));
3269
+ let caller;
3270
+ try {
3271
+ caller = proof.kind === "idp"
3272
+ ? { kind: "idp", owner: deriveOwnerForIdpSubject(ctx.ownerSecret, ctx.bridgeIdp.issuer, (await verifyIdpToken(proof.token, ctx.bridgeIdp)).sub) }
3273
+ : { kind: "service", witness: await verifyServiceProof(proof, request, ctx.issuer, door.space) };
3274
+ }
3275
+ catch (e) {
3276
+ policy.recordFailure(ctx, peer);
3277
+ console.error(`auth-service: refused a service endpoint authority proof: ${e instanceof Error ? e.message : String(e)}`);
3278
+ return answer(serviceAuthorityDenied(request, peerThrottled ? "rate-limited" : "unauthenticated"));
3279
+ }
3280
+ try {
3281
+ return answer(await answerServiceAuthority(door, request, caller));
3282
+ }
3283
+ catch (e) {
3284
+ // After the proof, a register may already be bound: an unexpected failure is an unknown effect.
3285
+ console.error(`auth-service: service endpoint authority ${request.operation} failed: ${e instanceof Error ? e.message : String(e)}`);
3286
+ return send(res, 503, { error: "service endpoint authority did not complete; the request may be bound, so retain its request identity" }, noStore);
3287
+ }
3288
+ }
2991
3289
  /** The typed manager authority exchange, served on both faces. The face's policy decides whether the
2992
3290
  * loopback capability is demanded; on the public face the verified IdP JWT is the proof, and the
2993
3291
  * dispatcher reads the caller's scope from the ledger either way. */
@@ -3194,6 +3492,7 @@ function makePublicHandler(ctx, policy, bundle) {
3194
3492
  ["/exchange", (req, res, c) => handleExchange(req, res, c, policy)],
3195
3493
  ["/manager-service-authority", (req, res, c) => handleManagerServiceAuthority(req, res, c, policy)],
3196
3494
  ["/manager-instances", (req, res, c) => handleManagerInstances(req, res, c, policy)],
3495
+ [SERVICE_ENDPOINT_AUTHORITY_PATH, (req, res, c) => handleServiceEndpointAuthority(req, res, c, policy)],
3197
3496
  [CHAT_SESSION_PATHS.open, (req, res, c) => handleChatSession(req, res, c, policy, "open")],
3198
3497
  [CHAT_SESSION_PATHS.bearer, (req, res, c) => handleChatSession(req, res, c, policy, "bearer")],
3199
3498
  [CHAT_SESSION_PATHS.retire, (req, res, c) => handleChatSession(req, res, c, policy, "retire")],
@@ -3240,16 +3539,16 @@ function makePublicHandler(ctx, policy, bundle) {
3240
3539
  };
3241
3540
  }
3242
3541
  /** Read + parse a small JSON body, bounded — the exchange payload is an IdP JWT plus an actor name;
3243
- * 64 KB clears any sane JWT while keeping the loopback surface un-floodable. */
3244
- function readJsonBody(req) {
3245
- const MAX = 64 * 1024;
3542
+ * 64 KB clears any sane JWT while keeping the loopback surface un-floodable. A route with a larger
3543
+ * protocol envelope passes its own bound. */
3544
+ function readJsonBody(req, max = 64 * 1024) {
3246
3545
  return new Promise((resolve, reject) => {
3247
3546
  let size = 0;
3248
3547
  let tooLarge = false;
3249
3548
  const chunks = [];
3250
3549
  req.on("data", (c) => {
3251
3550
  size += c.length;
3252
- if (size > MAX) {
3551
+ if (size > max) {
3253
3552
  // Stop retaining bytes immediately, but keep draining the request so the client receives
3254
3553
  // the explicit 413 JSON response. Destroying the socket hid the server's decision behind
3255
3554
  // an undici transport error and made the size-bound smoke unable to prove this guard.
@@ -3262,7 +3561,7 @@ function readJsonBody(req) {
3262
3561
  });
3263
3562
  req.on("end", () => {
3264
3563
  if (tooLarge)
3265
- return reject(new RequestBodyError(413, "request body too large (maximum 65536 bytes)"));
3564
+ return reject(new RequestBodyError(413, `request body too large (maximum ${max} bytes)`));
3266
3565
  try {
3267
3566
  resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3268
3567
  }