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