@cotal-ai/auth 0.61.0 → 0.63.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.
package/dist/service.js CHANGED
@@ -59,29 +59,29 @@ import { resolve } from "node:path";
59
59
  import { connect, credsAuthenticator } from "@nats-io/transport-node";
60
60
  import { jetstreamManager } from "@nats-io/jetstream";
61
61
  import { Kvm } from "@nats-io/kv";
62
- 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 { 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, acceptedReadGrant, actorLedgerSource, connectionAcceptedToken, importNativeSubjectPermissions, mintGeneration, parseActorLedgerSource, writeAcceptedRow, authorizeServeGrant, invokeCommand, resolveService, contractArtifactCanonicalBytes, DEV_OWNER, endpointRegistrationBarrier, mintLifecycleUid, principalKey, provisionEndpointGateOpen, publishContractArtifact, registerServiceInstance, SERVICE_READY, writeServiceStatus } from "@cotal-ai/core";
63
63
  import { findCotalRoot, loadManagerInstanceIdentity, userAuthStateDir, workspaceSecretStore, createAuthInstanceIdentity, loadAuthInstanceIdentity } from "@cotal-ai/workspace";
64
64
  import { decodeJwt } from "jose";
65
65
  import { deriveOwnerForIdpSubject, platformControlOwner } from "./derive.js";
66
- import { makePlatformControlAuthority, requireManagerAuthorityHolder } from "./platform-control.js";
66
+ import { makePlatformControlAuthority, makePlatformControlReadiness, requireManagerAuthorityHolder } from "./platform-control.js";
67
67
  import { startAuthCallout } from "./callout.js";
68
68
  import { createIdpBridge, verifyIdpToken } from "./idp.js";
69
69
  import { PUBLIC_EXCHANGE_VIEWS } from "./token.js";
70
70
  import { grantCoordinates, verifySessionRedemption } from "./session-redemption.js";
71
71
  import { pinnedJwksResolver } from "./issuer.js";
72
72
  import { calloutPermissions } from "./permissions.js";
73
- import { admitRemoteRun, authorizeRemoteManagerRenewal, authorizeRemoteRunAttempt, issueRemoteManagerAuthority, parseRemoteRunAdmissionRequest, parseRemoteRunAttemptRequest } from "./manager-authority.js";
73
+ import { admitRemoteRun, authorizeRemoteManagerRenewal, authorizeRemoteRunAttempt, issueRemoteManagerAuthority, observedRunRequest, parseRemoteRunAdmissionRequest, parseRemoteRunAttemptRequest } from "./manager-authority.js";
74
74
  import { authorizeRemoteRetainedAgentValidation, completeRemoteRetainedAgentValidation, remoteManagerCurrentRegistrationProof } from "./retained-manager-validation.js";
75
75
  import { authorizeRemoteManagedAgentEnrollment, authorizeRemoteManagedAgentPrepareRetirement, authorizeRemoteManagedAgentRuntimeCreate, authorizeRemoteManagedAgentRuntimeStatus } from "./managed-agent-enrollment.js";
76
76
  import { authorizeRemoteManagerGoalIndexScan, completeRemoteManagerGoalIndexScan } from "./manager-goal-index.js";
77
77
  import { authorizeRemoteManagerAdmin } from "./manager-admin-authorization.js";
78
78
  import { authorizeRemoteManagerMaintenance, completeRemoteManagerMaintenance } from "./manager-maintenance.js";
79
79
  import { validateRetainedManagedAgent } from "./continuity.js";
80
- import { reconstructRemoteManagerServeGrant, remoteManagerServeGrantFromCluster } from "./manager-contract.js";
81
- import { authorityBarrierGrants, authorityWriterGrants, openAuthorityClient, openSupervisedConnectReader, remoteManagerIssuerGrants, remoteManagerRegistrationProof, authRegistrationExecutorGrants } from "./authority-client.js";
80
+ import { reconstructRemoteManagerServeGrant, remoteManagerServeGrantFromCluster, remoteManagerSurface } from "./manager-contract.js";
81
+ import { authorityBarrierGrants, authorityWriterGrants, openAuthorityClient, openSupervisedConnectReader, platformReadinessGrants, remoteManagerIssuerGrants, remoteManagerRegistrationProof, authRegistrationExecutorGrants, servedRunRequestSubjects } from "./authority-client.js";
82
82
  import { authorizeConnectCredential } from "./connect-reader.js";
83
83
  import { ensureRootCredential } from "./root-credential.js";
84
- import { observeGate, openLifecycleRegistry, readLifecycleHeadForOperation } from "./lifecycle-registry.js";
84
+ import { activateLifecycleAtUid, observeGate, openLifecycleRegistry, readLifecycleHeadForOperation } from "./lifecycle-registry.js";
85
85
  import { openAuthLedgerScannerCandidate } from "./ledger-scanner.js";
86
86
  import { openRecordsScannerCandidate } from "./records-scanner.js";
87
87
  import { acquirePlaneClaim, makeDeliveryAdminPlaneOracle, makeDeliveryAdminPrincipalOracle, scannerDeathCopy } from "./plane-claim.js";
@@ -93,7 +93,7 @@ import { makeDrainRepairers } from "./drain-repair.js";
93
93
  import { joinOrStartRetirement, openAuthAdminListener } from "./auth-admin.js";
94
94
  import { AUTH_SERVICE_ENDPOINT, authClusterArtifacts, authContractArtifactValues } from "./auth-service-contract.js";
95
95
  import { drainTargetForEndpoint, openAdmissionMediator } from "./admission-mediator.js";
96
- import { AGENT_BEARER_TTL_SEC, findInteractiveActor, findManagedActor, ledgerAclResolver, ledgerAuthorizeAgentExchange, ledgerAuthorizeConnect, ledgerAuthorizeGrant, } from "./ledger.js";
96
+ import { AGENT_BEARER_TTL_SEC, findInteractiveActor, findManagedActor, findActorUnified, ledgerAclResolver, ledgerActorSourceIsLive, ledgerAuthorizeAgentExchange, ledgerAuthorizeConnect, ledgerAuthorizeGrant, } from "./ledger.js";
97
97
  import { AUTH_PROVIDER_NAME, clearAuthServiceInfo, loadCalloutAuth, loadIssuer, loadOwnerSecret, loadPinnedIdp, loadServiceKeys, saveAuthServiceInfo, spaceIssuer, } from "./store.js";
98
98
  /** JWKS max-age seconds — the cache contract's knob. Exported so a rotation tool can compute the
99
99
  * retire floor (max-age + max bearer TTL) from it. */
@@ -121,6 +121,7 @@ const PUBLIC_FAILED_PER_MIN = 30; // per-PEER refused-exchange window (rolling m
121
121
  const PUBLIC_PEER_BUCKETS_MAX = 1024; // bounded LRU of per-peer failure buckets
122
122
  const PUBLIC_MAX_IN_FLIGHT = 64; // global concurrent-admission cap on the public listener
123
123
  const PUBLIC_DEADLINE_MS = 10_000; // hard wall-clock deadline per public request
124
+ const OBSERVED_RUN_REQUEST_WINDOW_MS = 5 * 60_000; // how long an observed served resume or answer can still be forwarded
124
125
  /** Package-internal policy step for the hosted retirement requester. The HTTP production door calls
125
126
  * this after deriving the owner and manager actors; broker-free smokes exercise the same decision. */
126
127
  export function authorizeRemoteManagerRetirement(args) {
@@ -431,21 +432,21 @@ export async function openAuthAuthorityPlane(opts) {
431
432
  const fileArm = ledgerAuthorizeConnect(opts.dir);
432
433
  const recordsJsm = await jetstreamManager(remoteIssuer.nc);
433
434
  const loadLocalManager = opts.localManager ?? (() => undefined);
434
- // Standing renewal re-derives the serve surface from the REGISTERED service spec at the gate's
435
- // registration revision: spec (leader read) -> closure manifest -> root document, each verified
436
- // against its content digest. The request never selects the surface.
435
+ // Standing renewal and a served resume or answer read the manager surface from the REGISTERED
436
+ // service spec at the gate's registration revision: spec (leader read) -> closure manifest -> root
437
+ // document, each verified against its content digest. The request never selects the surface.
437
438
  const registeredManagerCluster = async (owner, instanceId, observed) => {
438
439
  const rec = await readSvcRecordLeader(recordsJsm, space, recordSpecKey(RECORD_KINDS.svc, ["manager", instanceId]));
439
440
  if (!rec || "deleted" in rec || rec.revision !== observed.registrationRevision)
440
- throw new EpEnvelopeError("failed-precondition", "manager-service renewal found no service spec at the gate's registration revision");
441
+ throw new EpEnvelopeError("failed-precondition", "found no manager service spec at the gate's registration revision");
441
442
  const spec = parseServiceSpec(rec.value, { endpoint: "manager" });
442
443
  if (spec.owner !== owner)
443
- throw new EpEnvelopeError("permission-denied", "manager-service renewal spec belongs to another owner");
444
+ throw new EpEnvelopeError("permission-denied", "the manager service spec belongs to another owner");
444
445
  const store = await contractStoreContext(remoteIssuer.nc, space);
445
446
  const read = async (digest) => {
446
447
  const bytes = await fetchContractArtifact(store, contractRefToHex(digest));
447
448
  if (!bytes)
448
- throw new EpEnvelopeError("failed-precondition", `manager-service renewal cannot read registered contract artifact ${digest}`);
449
+ throw new EpEnvelopeError("failed-precondition", `cannot read registered manager contract artifact ${digest}`);
449
450
  return JSON.parse(new TextDecoder().decode(bytes));
450
451
  };
451
452
  for (const closure of spec.clusterDigests) {
@@ -454,7 +455,7 @@ export async function openAuthAuthorityPlane(opts) {
454
455
  if (verifyClusterRoot(root, document).urn === "ai.cotal.manager")
455
456
  return document;
456
457
  }
457
- throw new EpEnvelopeError("failed-precondition", "manager-service renewal spec registers no manager cluster");
458
+ throw new EpEnvelopeError("failed-precondition", "the manager service spec registers no manager cluster");
458
459
  };
459
460
  const managerGate = async (owner, instanceId) => {
460
461
  const localManager = loadLocalManager();
@@ -532,6 +533,47 @@ export async function openAuthAuthorityPlane(opts) {
532
533
  if (fatalReason !== undefined)
533
534
  throw new EpEnvelopeError("unavailable", `the auth service for space "${space}" is momentarily unavailable (it detected a fault and is restarting); retry shortly`);
534
535
  };
536
+ // The issuer window's signer: the data account's signing key, the same `hostAuth` shape the run
537
+ // admission builds.
538
+ const issuerAuth = () => ({
539
+ space,
540
+ operator: { seed: "", jwt: "" },
541
+ account: { pub: dataAccount.pub, seed: "", jwt: "", signingSeed: dataAccount.signingSeed, signingPub: "" },
542
+ sys: { pub: "", jwt: "" },
543
+ });
544
+ // The source check the issuing host resolves with: an actor-ledger row is attested here, from the
545
+ // ledger this service owns; every other coordinate goes to core's check (SPEC 13.15).
546
+ const composedSourceIsLive = (session) => (source) => parseActorLedgerSource(source) ? Promise.resolve(ledgerActorSourceIsLive(opts.dir)(source)) : session.sourceIsLive(source);
547
+ // A served resume or answer is issued only for a request this host saw its caller publish, only
548
+ // once, and only for what that request's envelope asked (SPEC 14.8): the forward's coordinates
549
+ // are the manager's word. The manager forwards from inside the handler serving the request, so an
550
+ // observation older than the window has no forward left to bind; the window also bounds the table.
551
+ const observedRunRequests = new Map();
552
+ for (const subject of servedRunRequestSubjects(space))
553
+ remoteIssuer.nc.subscribe(subject, {
554
+ callback: (_err, msg) => {
555
+ const now = Date.now();
556
+ for (const [seen, { expires }] of observedRunRequests) {
557
+ if (expires > now)
558
+ break;
559
+ observedRunRequests.delete(seen);
560
+ }
561
+ const request = observedRunRequest(msg.subject, msg.data);
562
+ if (request === undefined)
563
+ return;
564
+ // Re-inserting keeps the table in expiry order, which the prune above stops on.
565
+ observedRunRequests.delete(msg.subject);
566
+ observedRunRequests.set(msg.subject, { expires: now + OBSERVED_RUN_REQUEST_WINDOW_MS, request });
567
+ },
568
+ });
569
+ const takeObservedRunRequest = async (subject) => {
570
+ // The broker queued the request to this connection before the manager could read it, and a
571
+ // flush returns only after this connection has read everything its server queued before it.
572
+ await remoteIssuer.nc.flush();
573
+ const observed = observedRunRequests.get(subject);
574
+ observedRunRequests.delete(subject);
575
+ return observed !== undefined && observed.expires > Date.now() ? observed.request : undefined;
576
+ };
535
577
  return {
536
578
  authorizeConnect: async (t) => {
537
579
  refuseIfFenced();
@@ -549,6 +591,40 @@ export async function openAuthAuthorityPlane(opts) {
549
591
  if (verdict !== "candidate")
550
592
  throw new Error(`manager instance ${instanceId} is ${verdict} at the permission mint`);
551
593
  },
594
+ issueUserCaller: async ({ t, connId, mint }) => {
595
+ refuseIfFenced();
596
+ const owner = assertDerivedOwnerToken(t.owner);
597
+ const actor = t.act.actor;
598
+ const uid = assertLifecycleToken(t.act.lifecycleUid ?? "", "lifecycleUid");
599
+ const acceptedToken = connectionAcceptedToken(connId);
600
+ return withIssuerSession({ servers: server, space, auth: issuerAuth(), tls: false }, async (session) => {
601
+ // The row this connection's nonce names, read the way the client reads it.
602
+ const existing = await session.nc.request(acceptedReadGrant(space, acceptedToken), new Uint8Array(0), { timeout: 3000 });
603
+ const code = existing.headers?.code ?? 0;
604
+ const status = existing.headers?.get("Status") ?? "";
605
+ const missing = code === 404 || status.startsWith("404");
606
+ if (!missing && (code >= 400 || status !== ""))
607
+ throw new Error(`callout permissions: the accepted-row read for this connection failed (${code || status})`);
608
+ if (missing) {
609
+ const generation = mintGeneration();
610
+ const perms = mint({ generation, acceptedToken });
611
+ const ref = { space, owner, actor, uid, generation };
612
+ const prepared = await session.store.stage({
613
+ version: 1, ref, sources: [actorLedgerSource(space, owner, actor, uid)], permissions: importNativeSubjectPermissions(perms),
614
+ });
615
+ await session.store.release(prepared, async () => { });
616
+ await writeAcceptedRow(session.accepted, acceptedToken, ref);
617
+ return perms;
618
+ }
619
+ const row = JSON.parse(new TextDecoder().decode(existing.data));
620
+ const ref = row.ref;
621
+ if (row.version !== 1 || ref === undefined || ref.space !== space || ref.owner !== owner || ref.actor !== actor || ref.uid !== uid)
622
+ throw new Error("callout permissions: this connection's accepted row names another issued reference; reconnect under a new nonce");
623
+ const perms = mint({ generation: ref.generation, acceptedToken });
624
+ await session.store.confirm(ref, importNativeSubjectPermissions(perms));
625
+ return perms;
626
+ });
627
+ },
552
628
  verifySession: async (principal, claim) => {
553
629
  refuseIfFenced();
554
630
  return verifySessionRedemption({
@@ -619,6 +695,15 @@ export async function openAuthAuthorityPlane(opts) {
619
695
  await flight;
620
696
  return { retired: true, lifecycleUid };
621
697
  },
698
+ activateManagedLifecycle: async (args) => {
699
+ refuseIfFenced();
700
+ await activateLifecycleAtUid(registry, {
701
+ owner: assertDerivedOwnerToken(args.owner),
702
+ actor: assertValidOwnerToken(args.actor),
703
+ lifecycleUid: assertLifecycleToken(args.lifecycleUid),
704
+ managerInstance: `auth-service:${space}`,
705
+ });
706
+ },
622
707
  issueManagerServiceAuthority: async (holder) => {
623
708
  refuseIfFenced();
624
709
  const { owner } = holder;
@@ -987,7 +1072,7 @@ export async function openAuthAuthorityPlane(opts) {
987
1072
  return gate.observe();
988
1073
  },
989
1074
  issued: session.store,
990
- sourceIsLive: (source) => session.sourceIsLive(source),
1075
+ sourceIsLive: composedSourceIsLive(session),
991
1076
  admissions: await new Kvm(admitterNc).open(admissionBucket(space)),
992
1077
  });
993
1078
  }
@@ -1002,7 +1087,19 @@ export async function openAuthAuthorityPlane(opts) {
1002
1087
  requireManagerAuthorityHolder(holder, String(request?.instanceId), 'manager run attempt needs scope "supervise"; spawn/admin do not imply it');
1003
1088
  const req = parseRemoteRunAttemptRequest(request);
1004
1089
  const recordsKv = await openRecordsBucket(remoteIssuer.nc, space);
1005
- const grant = await authorizeRemoteRunAttempt({
1090
+ const authorize = (session) => authorizeRemoteRunAttempt({
1091
+ ...(session ? { issued: session.store, sourceIsLive: composedSourceIsLive(session) } : {}),
1092
+ isLiveManagedActor: (owner, actor, lifecycleUid) => {
1093
+ const row = findActorUnified(opts.dir, owner, actor);
1094
+ return row?.kind === "managed-agent" && row.lifecycleUid === lifecycleUid;
1095
+ },
1096
+ takeObserved: takeObservedRunRequest,
1097
+ registeredCommand: async (instanceId, registrationRevision, command) => {
1098
+ const registered = remoteManagerSurface(await registeredManagerCluster(owner, instanceId, { registrationRevision }))[command];
1099
+ if (registered === undefined)
1100
+ throw new EpEnvelopeError("failed-precondition", `the registered manager cluster declares no ${command}`);
1101
+ return registered;
1102
+ },
1006
1103
  request,
1007
1104
  owner,
1008
1105
  space,
@@ -1016,7 +1113,15 @@ export async function openAuthAuthorityPlane(opts) {
1016
1113
  readAdmission: (runId) => readRunAdmission(recordsJsm, space, "manager", runId),
1017
1114
  readRunStatus: async (runId) => (await readRunRecord(recordsKv, "manager", runId))?.status?.value,
1018
1115
  checkpointWaiting: async (token) => (await readCheckpointStatus(recordsKv, { endpoint: "manager", token }))?.value.state === "waiting",
1116
+ checkpointSettled: async (token) => {
1117
+ const status = (await readCheckpointStatus(recordsKv, { endpoint: "manager", token }))?.value;
1118
+ return status?.state === "resumed" && status.settledAnswerId !== undefined;
1119
+ },
1019
1120
  });
1121
+ // A served resume or answer resolves its caller's issuance, which needs one issuer window.
1122
+ const grant = req.attempt?.served !== undefined || req.operator?.served !== undefined
1123
+ ? await withIssuerSession({ servers: server, space, auth: issuerAuth(), tls: false }, (session) => authorize(session))
1124
+ : await authorize();
1020
1125
  const hostAuth = {
1021
1126
  space,
1022
1127
  operator: { seed: "", jwt: "" },
@@ -1369,7 +1474,7 @@ async function startAuthContext(o) {
1369
1474
  space,
1370
1475
  token: { key: issuer.localKeySet(), issuer: issuer.issuer },
1371
1476
  authorizeActor: plane.authorizeConnect,
1372
- permissionsFor: calloutPermissions(ledgerAclResolver(dir), plane.authorizeManagerCaller, plane.verifySession),
1477
+ permissionsFor: calloutPermissions(ledgerAclResolver(dir), plane.authorizeManagerCaller, plane.verifySession, plane.issueUserCaller),
1373
1478
  log: (l) => console.error(l),
1374
1479
  });
1375
1480
  // The subscription must be ON the broker before readiness is signaled — an `up` that recorded a
@@ -1458,6 +1563,9 @@ async function startAuthContext(o) {
1458
1563
  let state = "ready";
1459
1564
  let cause;
1460
1565
  let closePromise;
1566
+ // The readiness reader: one standing connection pinned to the assigned instance, replaced when
1567
+ // the assignment names another instance.
1568
+ let reader;
1461
1569
  const close = () => {
1462
1570
  if (closePromise !== undefined)
1463
1571
  return closePromise;
@@ -1465,6 +1573,7 @@ async function startAuthContext(o) {
1465
1573
  state = "draining";
1466
1574
  closePromise = (async () => {
1467
1575
  await Promise.all([closeServer(http), closeServer(publicHttp)]);
1576
+ await reader?.client.then((c) => c.close(), () => { });
1468
1577
  await plane.close().catch(() => { });
1469
1578
  await callNc.close().catch(() => { });
1470
1579
  })();
@@ -1498,20 +1607,48 @@ async function startAuthContext(o) {
1498
1607
  });
1499
1608
  return completeRemoteRetainedAgentValidation(retained, holder.owner, authority);
1500
1609
  };
1610
+ const refuseUnlessReady = () => {
1611
+ if (state !== "ready" || closePromise !== undefined)
1612
+ throw new EpEnvelopeError("unavailable", "auth-service context is not ready");
1613
+ };
1614
+ const platformDeps = platformControl === undefined ? undefined : {
1615
+ space,
1616
+ accountPublicKey: keys.dataAccount.pub,
1617
+ owner: platformControlOwner(ownerSecret, space, keys.dataAccount.pub),
1618
+ observeAssignment: (s, a) => platformControl.observeAssignment(s, a),
1619
+ observeManagerInstance: plane.observeManagerInstance,
1620
+ };
1621
+ const readStatus = (owner) => async (instanceId) => {
1622
+ refuseUnlessReady();
1623
+ if (reader?.instanceId !== instanceId) {
1624
+ void reader?.client.then((c) => c.close(), () => { });
1625
+ const caller = { owner, actor: `manager_ready_${instanceId}`, uid: mintLifecycleUid() };
1626
+ const client = openAuthorityClient({
1627
+ server,
1628
+ space,
1629
+ dataAccount: { pub: keys.dataAccount.pub, signingSeed: keys.dataAccount.signingSeed },
1630
+ label: `cotal:platform-readiness:${space}`,
1631
+ grants: (id) => platformReadinessGrants(space, id, caller, instanceId),
1632
+ log: (l) => console.error(l),
1633
+ });
1634
+ // A failed open is not cached: the next read opens again.
1635
+ client.catch(() => { if (reader?.client === client)
1636
+ reader = undefined; });
1637
+ reader = { instanceId, caller, client };
1638
+ }
1639
+ const { caller, client } = reader;
1640
+ const { nc: readerNc } = await client;
1641
+ return await invokeCommand(readerNc, space, await resolveService(readerNc, space, "manager", caller, { instanceId }), "status", undefined, {});
1642
+ };
1501
1643
  const service = {
1502
1644
  url,
1503
1645
  ...(publicUrl !== undefined ? { publicUrl } : {}),
1504
1646
  cap,
1505
- ...(platformControl !== undefined ? {
1647
+ ...(platformDeps !== undefined ? {
1506
1648
  platformControlAuthority: makePlatformControlAuthority({
1507
- space,
1508
- accountPublicKey: keys.dataAccount.pub,
1509
- owner: platformControlOwner(ownerSecret, space, keys.dataAccount.pub),
1510
- observeAssignment: (s, a) => platformControl.observeAssignment(s, a),
1511
- observeManagerInstance: plane.observeManagerInstance,
1649
+ ...platformDeps,
1512
1650
  dispatch: (holder, request) => {
1513
- if (state !== "ready" || closePromise !== undefined)
1514
- throw new EpEnvelopeError("unavailable", "auth-service context is not ready");
1651
+ refuseUnlessReady();
1515
1652
  switch (request.kind) {
1516
1653
  case "manager-service-authority": return plane.issueManagerServiceAuthority({ ...holder, request });
1517
1654
  case "manager-service-maintenance": return plane.maintainRemoteManager({ ...holder, request });
@@ -1523,6 +1660,15 @@ async function startAuthContext(o) {
1523
1660
  }
1524
1661
  },
1525
1662
  }),
1663
+ platformControlReadiness: makePlatformControlReadiness({ ...platformDeps, readStatus: readStatus(platformDeps.owner) }),
1664
+ observeManagerGate: async (instanceId) => {
1665
+ refuseUnlessReady();
1666
+ return (await plane.observeManagerInstance(instanceId)).gate;
1667
+ },
1668
+ activateManagedLifecycle: async (target) => {
1669
+ refuseUnlessReady();
1670
+ await plane.activateManagedLifecycle(target);
1671
+ },
1526
1672
  } : {}),
1527
1673
  readiness() {
1528
1674
  if (state === "unavailable")
@@ -1561,6 +1707,9 @@ export async function dispatchManagerAuthorityRequest(ctx, owner, body) {
1561
1707
  // The hosted runtime kinds read and drive host-owned intent state that stock does not hold.
1562
1708
  if (request.kind === "manager-managed-agent-runtime-create" || request.kind === "manager-managed-agent-runtime-status")
1563
1709
  throw new EpEnvelopeError("unimplemented", "managed agent runtime create and status must be handled by host platform interception");
1710
+ // A delegated user intent creates and consumes a record in the host's intent store, which stock does not hold.
1711
+ if (request.kind === "delegated-user-intent" || request.kind === "manager-delegated-user-intent-execution")
1712
+ throw new EpEnvelopeError("unimplemented", "delegated user intent admission and execution must be handled by host platform interception");
1564
1713
  if (request.kind === "manager-retained-agent-validation") {
1565
1714
  const retained = await ctx.validateRetainedAgent({
1566
1715
  owner,