@cotal-ai/auth 0.65.0 → 0.67.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.js +1 -1
- package/dist/auth-admin.js.map +1 -1
- package/dist/credential-ledger.js +3 -3
- package/dist/credential-ledger.js.map +1 -1
- package/dist/idp.d.ts +3 -1
- package/dist/idp.d.ts.map +1 -1
- package/dist/idp.js +3 -2
- package/dist/idp.js.map +1 -1
- package/dist/issuer.d.ts +4 -2
- package/dist/issuer.d.ts.map +1 -1
- package/dist/issuer.js +11 -6
- package/dist/issuer.js.map +1 -1
- package/dist/ledger.d.ts +3 -0
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +7 -0
- package/dist/ledger.js.map +1 -1
- package/dist/managed-agent-enrollment.d.ts +2 -7
- package/dist/managed-agent-enrollment.d.ts.map +1 -1
- package/dist/managed-agent-enrollment.js.map +1 -1
- package/dist/manager-admin-authorization.d.ts +2 -6
- package/dist/manager-admin-authorization.d.ts.map +1 -1
- package/dist/manager-admin-authorization.js +2 -17
- package/dist/manager-admin-authorization.js.map +1 -1
- package/dist/manager-authority.d.ts +4 -15
- package/dist/manager-authority.d.ts.map +1 -1
- package/dist/manager-authority.js +22 -33
- package/dist/manager-authority.js.map +1 -1
- package/dist/manager-goal-index.d.ts +2 -6
- package/dist/manager-goal-index.d.ts.map +1 -1
- package/dist/manager-goal-index.js +2 -17
- package/dist/manager-goal-index.js.map +1 -1
- package/dist/manager-maintenance.d.ts +2 -6
- package/dist/manager-maintenance.d.ts.map +1 -1
- package/dist/manager-maintenance.js +2 -18
- package/dist/manager-maintenance.js.map +1 -1
- package/dist/permissions.js +2 -0
- package/dist/permissions.js.map +1 -1
- package/dist/platform-control.d.ts +2 -5
- package/dist/platform-control.d.ts.map +1 -1
- package/dist/platform-control.js.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +13 -5
- package/dist/provider.js.map +1 -1
- package/dist/retained-manager-validation.d.ts +2 -6
- package/dist/retained-manager-validation.d.ts.map +1 -1
- package/dist/retained-manager-validation.js +2 -18
- package/dist/retained-manager-validation.js.map +1 -1
- package/dist/retirement-barrier.js +4 -4
- package/dist/retirement-barrier.js.map +1 -1
- package/dist/service.d.ts +24 -8
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +183 -40
- package/dist/service.js.map +1 -1
- package/dist/token.d.ts +15 -2
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +31 -5
- package/dist/token.js.map +1 -1
- package/package.json +3 -3
package/dist/service.js
CHANGED
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
* the public face is the credential itself: the human arm presents an EdDSA IdP JWT verified
|
|
33
33
|
* against the pinned JWKS/issuer/audience; the agent arm presents an actorToken whose sha256 must
|
|
34
34
|
* match a FRESH ledger row. Origin rejection, JSON-only bodies, the 64 KB bound, and no-CORS-ever
|
|
35
|
-
* hold verbatim. Public views are channel-writer/channel-purger
|
|
36
|
-
* (instance-bound, with no added command scope). Other views stay
|
|
37
|
-
* arm may request only manager-caller on either face;
|
|
35
|
+
* hold verbatim. Public views are channel-writer/channel-purger and the one-object transfer-writer
|
|
36
|
+
* (admin-gated) and manager-caller (instance-bound, with no added command scope). Other views stay
|
|
37
|
+
* loopback-only. The agent-secret arm may request only manager-caller on either face;
|
|
38
38
|
* failures are bucketed per peer (`--exchange-trusted-proxy` opts into the last X-Forwarded-For
|
|
39
39
|
* hop as the peer key; otherwise the socket remote address) in a bounded LRU, under a global
|
|
40
40
|
* concurrent-admission cap and a hard request deadline — all of it pools SEPARATE from the
|
|
@@ -59,14 +59,14 @@ 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 { contractDigest, contractRefToHex, contractStoreContext, endpointToken, 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,
|
|
62
|
+
import { CotalEndpoint, deprovisionAgent, isConcreteChannel, provisionAgentDurables, contractDigest, contractRefToHex, contractStoreContext, endpointToken, fetchContractArtifact, verifyClusterManifest, verifyClusterRoot, admissionBucket, admissionMediatorGrants, assertDerivedOwnerToken, assertLifecycleToken, assertValidOwnerToken, authorizeTrustedServeSnapshot, commitSiblingIssuance, credsClaims, EpEnvelopeError, ensureAuthorityStores, epAuthBucket, isReachable, jwtFromCreds, managedRetirementOpId, mintCreds, mintPublicUserJwt, newIdentity, 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, invokeCommand, resolveService, contractArtifactCanonicalBytes, DEV_OWNER, endpointRegistrationBarrier, mintLifecycleUid, principalKey, provisionEndpointGateOpen, publishContractArtifact, registerServiceInstance, registerServingInstance } 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
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
|
-
import { PUBLIC_EXCHANGE_VIEWS } from "./token.js";
|
|
69
|
+
import { PUBLIC_EXCHANGE_VIEWS, assertTransferWriterClaim } 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";
|
|
@@ -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, findActorUnified, ledgerAclResolver, ledgerActorSourceIsLive, ledgerAuthorizeAgentExchange, ledgerAuthorizeConnect, ledgerAuthorizeGrant, } from "./ledger.js";
|
|
96
|
+
import { AGENT_BEARER_TTL_SEC, assertWithinSpawnerGrant, findInteractiveActor, findManagedActor, findActorUnified, grantManagedActor, ledgerAclResolver, ledgerActorSourceIsLive, ledgerAuthorizeAgentExchange, ledgerAuthorizeConnect, ledgerAuthorizeGrant, revokeManagedActorAt, } 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. */
|
|
@@ -123,6 +123,19 @@ const PUBLIC_MAX_IN_FLIGHT = 64; // global concurrent-admission cap on the publi
|
|
|
123
123
|
const PUBLIC_DEADLINE_MS = 10_000; // hard wall-clock deadline per public request
|
|
124
124
|
const HOST_FENCE_POLL_MS = 1_000; // how often awaitHostFence re-reads the host gate
|
|
125
125
|
const OBSERVED_RUN_REQUEST_WINDOW_MS = 5 * 60_000; // how long an observed served resume or answer can still be forwarded
|
|
126
|
+
/** Enrollment and retirement preparation of one managed agent run one at a time, so neither acts on
|
|
127
|
+
* a grant the other is still writing or releasing. The chain lives at module level, keyed by the
|
|
128
|
+
* ledger directory, so two plane instances over one ledger share it. */
|
|
129
|
+
const MANAGED_ALIAS_CHAINS = new Map();
|
|
130
|
+
const serializedForAlias = (dir, owner, actor, fn) => {
|
|
131
|
+
const key = `${resolve(dir)}:${principalKey(owner, actor).key}`;
|
|
132
|
+
const run = (MANAGED_ALIAS_CHAINS.get(key) ?? Promise.resolve()).then(fn, fn);
|
|
133
|
+
const tail = run.then(() => undefined, () => undefined);
|
|
134
|
+
MANAGED_ALIAS_CHAINS.set(key, tail);
|
|
135
|
+
void tail.then(() => { if (MANAGED_ALIAS_CHAINS.get(key) === tail)
|
|
136
|
+
MANAGED_ALIAS_CHAINS.delete(key); });
|
|
137
|
+
return run;
|
|
138
|
+
};
|
|
126
139
|
/** Package-internal policy step for the hosted retirement requester. The HTTP production door calls
|
|
127
140
|
* this after deriving the owner and manager actors; broker-free smokes exercise the same decision. */
|
|
128
141
|
export function authorizeRemoteManagerRetirement(args) {
|
|
@@ -152,9 +165,10 @@ function contractArtifactReader(values) {
|
|
|
152
165
|
* registration that advertises them. The issuance gate is provisioned open once, on first sight
|
|
153
166
|
* (§13.1); on a restart it already exists and `registerServiceInstance` freezes and re-registers
|
|
154
167
|
* it, which advances the process epoch and fences the predecessor. A first registration stays at
|
|
155
|
-
* epoch 0.
|
|
168
|
+
* epoch 0. `register` is {@link registerServiceInstance}, or {@link registerServingInstance} for an
|
|
169
|
+
* instance that serves. It returns what `register` committed, and refuses when its confirming read
|
|
156
170
|
* finds that a later registration of the same instance superseded it. */
|
|
157
|
-
async function registerSelfAuthorizedInstance(nc, args) {
|
|
171
|
+
async function registerSelfAuthorizedInstance(nc, args, register) {
|
|
158
172
|
const { space, endpoint, instanceId } = args;
|
|
159
173
|
const authKv = await new Kvm(nc).open(epAuthBucket(space));
|
|
160
174
|
const recordsKv = await new Kvm(nc).open(recordsBucket(space));
|
|
@@ -165,10 +179,11 @@ async function registerSelfAuthorizedInstance(nc, args) {
|
|
|
165
179
|
await provisionEndpointGateOpen(authKv, { endpoint, instanceId, principal: args.principal });
|
|
166
180
|
const barrier = endpointRegistrationBarrier(authKv, space, { endpoint, instanceId, opId: mintLifecycleUid() });
|
|
167
181
|
const spec = { endpoint, owner: DEV_OWNER, clusterDigests: [args.clusterDigest], protocol: { v: 1 } };
|
|
168
|
-
const
|
|
182
|
+
const registered = await register(recordsKv, {
|
|
169
183
|
space, spec, instanceId, registrant: { owner: DEV_OWNER }, authority: selfNameAuthority(endpoint), barrier,
|
|
170
184
|
readClusterArtifact: contractArtifactReader(args.artifacts),
|
|
171
185
|
});
|
|
186
|
+
const { registrationRevision, processEpoch } = registered;
|
|
172
187
|
// A second start of the same instance can complete between this reopen and this read. Its epoch
|
|
173
188
|
// is not this start's to claim, and this start's epoch is already fenced. One that completes after
|
|
174
189
|
// the read leaves this start returning its own epoch, which the gate fences as it fences any
|
|
@@ -176,7 +191,7 @@ async function registerSelfAuthorizedInstance(nc, args) {
|
|
|
176
191
|
const observed = await serveIssuanceGateKv(authKv, space, { endpoint, instanceId }).observe();
|
|
177
192
|
if (observed?.state !== "open" || observed.processEpoch !== processEpoch || observed.registrationRevision !== registrationRevision)
|
|
178
193
|
throw new EpEnvelopeError("conflict", `the issuance gate of ${endpoint}/${instanceId} is no longer open at this registration (process epoch ${processEpoch}, revision ${registrationRevision}); a later barrier fenced this start (SPEC 13.1)`);
|
|
179
|
-
return
|
|
194
|
+
return registered;
|
|
180
195
|
}
|
|
181
196
|
/**
|
|
182
197
|
* Open the authority plane — the PRODUCTION connect/exchange composition (exported so the live
|
|
@@ -229,26 +244,11 @@ export async function openAuthAuthorityPlane(opts) {
|
|
|
229
244
|
log,
|
|
230
245
|
});
|
|
231
246
|
try {
|
|
232
|
-
const {
|
|
247
|
+
const { grant } = await registerSelfAuthorizedInstance(regClient.nc, {
|
|
233
248
|
space, endpoint: AUTH_SERVICE_ENDPOINT, instanceId: iid,
|
|
234
249
|
principal: principalKey(DEV_OWNER, authIdentity.serveIdentity.id).key,
|
|
235
250
|
clusterDigest: artifacts.closureDigest, artifacts: values,
|
|
236
|
-
});
|
|
237
|
-
const readProcessEpoch = async () => {
|
|
238
|
-
const g = await serveIssuanceGateKv(authKv, space, { endpoint: AUTH_SERVICE_ENDPOINT, instanceId: iid }).observe();
|
|
239
|
-
if (g === null)
|
|
240
|
-
throw new Error(`no issuance gate for ${AUTH_SERVICE_ENDPOINT}/${iid}`);
|
|
241
|
-
return g.processEpoch;
|
|
242
|
-
};
|
|
243
|
-
const grant = await authorizeServeGrant(recordsKv, {
|
|
244
|
-
space, endpoint: AUTH_SERVICE_ENDPOINT, instanceId: iid, epoch: processEpoch, holder: { owner: DEV_OWNER },
|
|
245
|
-
authority: selfNameAuthority(AUTH_SERVICE_ENDPOINT), readClusterArtifact: contractArtifactReader(values), readProcessEpoch,
|
|
246
|
-
});
|
|
247
|
-
await writeServiceStatus(recordsKv, {
|
|
248
|
-
endpoint: AUTH_SERVICE_ENDPOINT, instanceId: iid, epoch: processEpoch,
|
|
249
|
-
status: { state: SERVICE_READY, epoch: processEpoch, observedSpecRevision: registrationRevision },
|
|
250
|
-
readProcessEpoch,
|
|
251
|
-
});
|
|
251
|
+
}, registerServingInstance);
|
|
252
252
|
authServeInstanceId = iid;
|
|
253
253
|
authServeGrant = grant;
|
|
254
254
|
}
|
|
@@ -590,6 +590,14 @@ export async function openAuthAuthorityPlane(opts) {
|
|
|
590
590
|
observedRunRequests.delete(subject);
|
|
591
591
|
return observed !== undefined && observed.expires > Date.now() ? observed.request : undefined;
|
|
592
592
|
};
|
|
593
|
+
const observeManagerGate = async (instanceId) => serveIssuanceGateKv(await new Kvm(remoteIssuer.nc).open(epAuthBucket(space)), space, { endpoint: "manager", instanceId }).observe();
|
|
594
|
+
// A managed lifecycle's broker footprint, released under a deprovisioner pinned to that uid, so
|
|
595
|
+
// no call here can reach a same-name successor's durables.
|
|
596
|
+
const deprovisionManagedFootprint = async (owner, actor, lifecycleUid, memberChannels) => {
|
|
597
|
+
const targetId = principalKey(owner, actor).key;
|
|
598
|
+
const creds = await mintCreds(issuerAuth(), newIdentity(), "deprovisioner", { deprovisionTarget: { principal: targetId, lifecycleUid, memberChannels } });
|
|
599
|
+
await deprovisionAgent({ servers: server, space, targetId, lifecycleUid, memberChannels, creds });
|
|
600
|
+
};
|
|
593
601
|
return {
|
|
594
602
|
authorizeConnect: async (t) => {
|
|
595
603
|
refuseIfFenced();
|
|
@@ -798,6 +806,13 @@ export async function openAuthAuthorityPlane(opts) {
|
|
|
798
806
|
});
|
|
799
807
|
return { credentials };
|
|
800
808
|
}
|
|
809
|
+
if (r.operation === "transferReader") {
|
|
810
|
+
const expectedProof = remoteManagerRegistrationProof(owner, r);
|
|
811
|
+
if (r.registrationProof !== expectedProof)
|
|
812
|
+
throw new EpEnvelopeError("permission-denied", "manager-service transfer reader proof does not match this owner/lifecycle");
|
|
813
|
+
credentials.transferReader = await mintPublicUserJwt({ space, account: { pub: dataAccount.pub, signingSeed: dataAccount.signingSeed } }, r.transferReader.id, "transfer-reader", { principal: { owner, actor: actors.serve }, lifecycleUid: r.managerLifecycleUid, transferReader: { instanceId: r.instanceId } });
|
|
814
|
+
return { credentials };
|
|
815
|
+
}
|
|
801
816
|
if (r.operation === "retire") {
|
|
802
817
|
const expectedProof = remoteManagerRegistrationProof(owner, r);
|
|
803
818
|
if (r.registrationProof !== expectedProof)
|
|
@@ -881,7 +896,8 @@ export async function openAuthAuthorityPlane(opts) {
|
|
|
881
896
|
credentials.runMediator = await mintPublicUserJwt(auth, r.run.mediatorId, "run-mediator", {
|
|
882
897
|
principal: { owner, actor: caller.actor },
|
|
883
898
|
lifecycleUid: r.managerLifecycleUid,
|
|
884
|
-
|
|
899
|
+
// Renewed with the placement the attempt was issued with (authorizeRemoteRunAttempt).
|
|
900
|
+
runMediator: { ...binding, placement: { instanceId: r.instanceId } },
|
|
885
901
|
expiresInSeconds: standingTtl,
|
|
886
902
|
});
|
|
887
903
|
return { credentials };
|
|
@@ -1017,6 +1033,121 @@ export async function openAuthAuthorityPlane(opts) {
|
|
|
1017
1033
|
},
|
|
1018
1034
|
});
|
|
1019
1035
|
},
|
|
1036
|
+
enrollManagedAgent: async ({ owner, scope, request, sentinelCreds, agentBearerExchangeUrl }) => {
|
|
1037
|
+
refuseIfFenced();
|
|
1038
|
+
const r = await authorizeRemoteManagedAgentEnrollment({ owner, scope, proofSecret: dataAccount.signingSeed, space, request, observeManagerGate });
|
|
1039
|
+
const t = r.target;
|
|
1040
|
+
const subscribe = t.subscribe ?? [];
|
|
1041
|
+
const { lifecycleUid, row } = await serializedForAlias(opts.dir, owner, t.actor, async () => {
|
|
1042
|
+
// The token digest names one enrollment. A held row with this request's digest is that
|
|
1043
|
+
// enrollment retried, so it answers the same uid; any other held row is a lifecycle that
|
|
1044
|
+
// still runs, and taking its grant would strand it.
|
|
1045
|
+
const held = findManagedActor(opts.dir, owner, t.actor);
|
|
1046
|
+
if (held !== undefined && held.tokenHash !== t.tokenHash)
|
|
1047
|
+
throw new EpEnvelopeError("conflict", `managed agent "${t.actor}" is enrolled at lifecycle ${held.lifecycleUid}; prepare that lifecycle's retirement before enrolling a successor`);
|
|
1048
|
+
// A fresh uid is never one a retirement tombstoned, and only the host can make that promise.
|
|
1049
|
+
const lifecycleUid = held?.lifecycleUid ?? mintLifecycleUid();
|
|
1050
|
+
// The grant first: its envelope walk against the supervising actor refuses an over-wide
|
|
1051
|
+
// request before any durable exists. A retry walks the held grant again: its spawner chain
|
|
1052
|
+
// may have narrowed since the first answer, and provisioning would restore what it took away.
|
|
1053
|
+
if (held !== undefined)
|
|
1054
|
+
assertWithinSpawnerGrant(opts.dir, held, "spawn");
|
|
1055
|
+
const row = held ?? grantManagedActor(opts.dir, {
|
|
1056
|
+
owner,
|
|
1057
|
+
actor: t.actor,
|
|
1058
|
+
scope: t.capabilities ?? [],
|
|
1059
|
+
allowSubscribe: t.allowSubscribe ?? [],
|
|
1060
|
+
allowPublish: t.allowPublish ?? [],
|
|
1061
|
+
...(t.role !== undefined ? { role: t.role } : {}),
|
|
1062
|
+
...(t.label !== undefined ? { label: t.label } : {}),
|
|
1063
|
+
parent: principalKey(owner, r.actor).key,
|
|
1064
|
+
lifecycleUid,
|
|
1065
|
+
tokenHash: t.tokenHash,
|
|
1066
|
+
});
|
|
1067
|
+
try {
|
|
1068
|
+
const identity = newIdentity();
|
|
1069
|
+
const provisioner = new CotalEndpoint({
|
|
1070
|
+
space,
|
|
1071
|
+
servers: server,
|
|
1072
|
+
channels: [],
|
|
1073
|
+
creds: await mintCreds(issuerAuth(), identity, "provisioner"),
|
|
1074
|
+
card: { id: identity.id, name: "provisioner", role: "provisioner", kind: "endpoint" },
|
|
1075
|
+
registerPresence: false,
|
|
1076
|
+
watchPresence: false,
|
|
1077
|
+
watchChannels: false,
|
|
1078
|
+
consume: false,
|
|
1079
|
+
});
|
|
1080
|
+
await provisioner.start();
|
|
1081
|
+
try {
|
|
1082
|
+
await provisionAgentDurables(provisioner, { owner, actor: t.actor, lifecycleUid }, { subscribe, allowSubscribe: row.allowSubscribe, role: row.role });
|
|
1083
|
+
}
|
|
1084
|
+
finally {
|
|
1085
|
+
await provisioner.stop();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
catch (e) {
|
|
1089
|
+
// A retried enrollment's first answer may already have reached the participant, so its
|
|
1090
|
+
// grant stays. A grant this call wrote gave the participant no uid, so it can never be
|
|
1091
|
+
// retired: it and any durable it got must go now.
|
|
1092
|
+
if (held === undefined) {
|
|
1093
|
+
revokeManagedActorAt(opts.dir, owner, t.actor, lifecycleUid);
|
|
1094
|
+
await deprovisionManagedFootprint(owner, t.actor, lifecycleUid, []).catch((err) => opts.log(`auth-service: refused enrollment of ${owner}/${t.actor} left durables at ${lifecycleUid}: ${err instanceof Error ? err.message : String(err)}`));
|
|
1095
|
+
}
|
|
1096
|
+
throw e;
|
|
1097
|
+
}
|
|
1098
|
+
return { lifecycleUid, row };
|
|
1099
|
+
});
|
|
1100
|
+
return {
|
|
1101
|
+
v: 1,
|
|
1102
|
+
kind: "manager-managed-agent-enrollment",
|
|
1103
|
+
space,
|
|
1104
|
+
owner,
|
|
1105
|
+
actor: r.actor,
|
|
1106
|
+
instanceId: r.instanceId,
|
|
1107
|
+
managerLifecycleUid: r.managerLifecycleUid,
|
|
1108
|
+
requestId: r.requestId,
|
|
1109
|
+
registrationProof: r.registrationProof,
|
|
1110
|
+
serveEpoch: r.serveEpoch,
|
|
1111
|
+
material: {
|
|
1112
|
+
owner,
|
|
1113
|
+
actor: t.actor,
|
|
1114
|
+
lifecycleUid,
|
|
1115
|
+
sentinelCreds,
|
|
1116
|
+
subscribe,
|
|
1117
|
+
allowSubscribe: row.allowSubscribe,
|
|
1118
|
+
allowPublish: row.allowPublish,
|
|
1119
|
+
agentBearerExchangeUrl,
|
|
1120
|
+
},
|
|
1121
|
+
};
|
|
1122
|
+
},
|
|
1123
|
+
prepareManagedAgentRetirement: async ({ owner, scope, request }) => {
|
|
1124
|
+
refuseIfFenced();
|
|
1125
|
+
const r = await authorizeRemoteManagedAgentPrepareRetirement({ owner, scope, proofSecret: dataAccount.signingSeed, space, request, observeManagerGate });
|
|
1126
|
+
const { actor, lifecycleUid } = r.target;
|
|
1127
|
+
await serializedForAlias(opts.dir, owner, actor, async () => {
|
|
1128
|
+
// Only a grant still at the target uid is this lifecycle's. A row at another uid belongs to a
|
|
1129
|
+
// successor, and an absent row means an earlier attempt already released both halves.
|
|
1130
|
+
const row = findManagedActor(opts.dir, owner, actor);
|
|
1131
|
+
if (row?.lifecycleUid === lifecycleUid) {
|
|
1132
|
+
// Footprint before grant: while the row stands, a retry still knows which channels to purge.
|
|
1133
|
+
await deprovisionManagedFootprint(owner, actor, lifecycleUid, row.allowSubscribe.filter(isConcreteChannel));
|
|
1134
|
+
revokeManagedActorAt(opts.dir, owner, actor, lifecycleUid);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
return {
|
|
1138
|
+
v: 1,
|
|
1139
|
+
kind: "manager-managed-agent-prepare-retirement",
|
|
1140
|
+
space,
|
|
1141
|
+
owner,
|
|
1142
|
+
actor: r.actor,
|
|
1143
|
+
instanceId: r.instanceId,
|
|
1144
|
+
managerLifecycleUid: r.managerLifecycleUid,
|
|
1145
|
+
requestId: r.requestId,
|
|
1146
|
+
target: r.target,
|
|
1147
|
+
opId: r.opId,
|
|
1148
|
+
prepared: true,
|
|
1149
|
+
};
|
|
1150
|
+
},
|
|
1020
1151
|
verifyManagedAgentRuntime: async ({ owner, request }) => {
|
|
1021
1152
|
refuseIfFenced();
|
|
1022
1153
|
const args = {
|
|
@@ -1217,7 +1348,7 @@ export async function openAuthAuthorityPlane(opts) {
|
|
|
1217
1348
|
// The host is minted no serve credential, so the gate binds a principal nothing holds.
|
|
1218
1349
|
principal: principalKey(DEV_OWNER, `host_serve_${instanceId}`).key,
|
|
1219
1350
|
clusterDigest: host.clusterDigest, artifacts: host.artifacts,
|
|
1220
|
-
})).processEpoch;
|
|
1351
|
+
}, registerServiceInstance)).processEpoch;
|
|
1221
1352
|
}
|
|
1222
1353
|
finally {
|
|
1223
1354
|
await regClient.close();
|
|
@@ -1273,7 +1404,7 @@ async function resumeOpenOperations(reg, evictPrincipal, retirement, log) {
|
|
|
1273
1404
|
// Owed by the gate: a freeze by THIS op is the barrier's live claim (the bar of every
|
|
1274
1405
|
// barrier). A takeover completed under this predicate reopens the gate, so its completed
|
|
1275
1406
|
// intent stays skipped exactly as before.
|
|
1276
|
-
let owed = gate !== undefined && gate.row.state === "frozen" && gate.row.op
|
|
1407
|
+
let owed = gate !== undefined && gate.row.state === "frozen" && gate.row.op.opId === it.opId;
|
|
1277
1408
|
// #878: a retirement whose gate terminal landed by THIS op but whose head terminal did not.
|
|
1278
1409
|
// The head decides the completed cell: `retired` at this uid means the barrier's last step ran
|
|
1279
1410
|
// (skip), and a head at ANOTHER uid means a successor already replaced the retired predecessor
|
|
@@ -1281,7 +1412,7 @@ async function resumeOpenOperations(reg, evictPrincipal, retirement, log) {
|
|
|
1281
1412
|
// the wedged crash window, and `active` at this uid is a backward head (impossible; a head
|
|
1282
1413
|
// never un-retires) — both resume, so the barrier either finishes the tail or refuses loud; an
|
|
1283
1414
|
// absent head is corruption the barrier refuses loud (a head is never deleted, SPEC 13.12).
|
|
1284
|
-
if (!owed && it.kind === "retirement" && gate !== undefined && gate.row.state === "retired" && gate.row.op
|
|
1415
|
+
if (!owed && it.kind === "retirement" && gate !== undefined && gate.row.state === "retired" && gate.row.op.opId === it.opId) {
|
|
1285
1416
|
const head = await readLifecycleHeadForOperation(reg, it.owner, it.actor);
|
|
1286
1417
|
owed = head === undefined
|
|
1287
1418
|
|| (head.mapping.lifecycleUid === it.lifecycleUid && head.mapping.state !== "retired");
|
|
@@ -1536,6 +1667,9 @@ async function startAuthContext(o) {
|
|
|
1536
1667
|
mintConnectCredential: plane.mintConnectCredential,
|
|
1537
1668
|
});
|
|
1538
1669
|
const cap = randomBytes(32).toString("hex"); // per-start exchange capability (rotates with the daemon)
|
|
1670
|
+
// Set once the public face binds. An enrolled child's bearer exchanges there without the
|
|
1671
|
+
// loopback capability, so enrollment needs that face.
|
|
1672
|
+
let publicUrl;
|
|
1539
1673
|
const failures = []; // rolling-window timestamps of REFUSED exchanges
|
|
1540
1674
|
const badCaps = []; // rolling-window timestamps of invalid-capability attempts
|
|
1541
1675
|
const ctx = {
|
|
@@ -1549,6 +1683,12 @@ async function startAuthContext(o) {
|
|
|
1549
1683
|
verifyManagedAgentEnrollment: plane.verifyManagedAgentEnrollment,
|
|
1550
1684
|
verifyManagedAgentPrepareRetirement: plane.verifyManagedAgentPrepareRetirement,
|
|
1551
1685
|
verifyManagedAgentRuntime: plane.verifyManagedAgentRuntime,
|
|
1686
|
+
enrollManagedAgent: (args) => {
|
|
1687
|
+
if (publicUrl === undefined)
|
|
1688
|
+
throw new EpEnvelopeError("failed-precondition", "managed agent enrollment needs the public exchange face, where the enrolled agent's bearer exchanges - start the auth service with --exchange-public-port");
|
|
1689
|
+
return plane.enrollManagedAgent({ ...args, sentinelCreds: callout.sentinelCreds, agentBearerExchangeUrl: publicUrl });
|
|
1690
|
+
},
|
|
1691
|
+
prepareManagedAgentRetirement: plane.prepareManagedAgentRetirement,
|
|
1552
1692
|
scanManagerGoalIndex: plane.scanManagerGoalIndex,
|
|
1553
1693
|
authorizeManagerAdmin: plane.authorizeManagerAdmin,
|
|
1554
1694
|
admitManagerRun: plane.admitManagerRun,
|
|
@@ -1576,7 +1716,6 @@ async function startAuthContext(o) {
|
|
|
1576
1716
|
const url = `http://127.0.0.1:${boundPort}`;
|
|
1577
1717
|
// The optional PUBLIC face: its own server, its own closed route table, its own budgets — also
|
|
1578
1718
|
// loopback-bound (the operator's reverse proxy terminates TLS and forwards here).
|
|
1579
|
-
let publicUrl;
|
|
1580
1719
|
if (o.publicFace !== undefined) {
|
|
1581
1720
|
const face = o.publicFace;
|
|
1582
1721
|
// The discovery bundle is GENERATED from the daemon's own recorded config — the pinned IdP,
|
|
@@ -1766,13 +1905,12 @@ export async function dispatchManagerAuthorityRequest(ctx, owner, body) {
|
|
|
1766
1905
|
if (typeof request.actor !== "string")
|
|
1767
1906
|
throw new Error("manager-service authority request requires an actor");
|
|
1768
1907
|
const row = ledgerAuthorizeGrant(ctx.dir)(owner, request.actor);
|
|
1769
|
-
// #1972:
|
|
1770
|
-
//
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
throw new EpEnvelopeError("unimplemented", "managed agent enrollment and retirement preparation must be handled by host platform interception");
|
|
1908
|
+
// #1972: this service owns the ledger and the space's provisioning authority, so it is the host
|
|
1909
|
+
// that enrolls a remote manager's managed agent and prepares its terminal release.
|
|
1910
|
+
if (request.kind === "manager-managed-agent-enrollment")
|
|
1911
|
+
return ctx.enrollManagedAgent({ owner, scope: row.scope ?? [], request: body.request });
|
|
1912
|
+
if (request.kind === "manager-managed-agent-prepare-retirement")
|
|
1913
|
+
return ctx.prepareManagedAgentRetirement({ owner, scope: row.scope ?? [], request: body.request });
|
|
1776
1914
|
// The hosted runtime kinds read and drive host-owned intent state that stock does not hold.
|
|
1777
1915
|
if (request.kind === "manager-managed-agent-runtime-create" || request.kind === "manager-managed-agent-runtime-status")
|
|
1778
1916
|
throw new EpEnvelopeError("unimplemented", "managed agent runtime create and status must be handled by host platform interception");
|
|
@@ -2125,9 +2263,11 @@ async function handleExchange(req, res, ctx, policy) {
|
|
|
2125
2263
|
const peer = policy.peerKey(req);
|
|
2126
2264
|
const peerThrottled = policy.throttled(ctx, peer);
|
|
2127
2265
|
const body = await readJsonBody(req);
|
|
2128
|
-
const { idpToken, actor, actorToken, owner, ttlSec, view, managerInstanceId, sessionGrant } = body;
|
|
2266
|
+
const { idpToken, actor, actorToken, owner, ttlSec, view, managerInstanceId, sessionGrant, transferWriter } = body;
|
|
2129
2267
|
if ((view === "session-caller") !== (sessionGrant !== undefined))
|
|
2130
2268
|
return send(res, 400, { error: 'view "session-caller" and sessionGrant come together or not at all' });
|
|
2269
|
+
if ((view === "transfer-writer") !== (transferWriter !== undefined))
|
|
2270
|
+
return send(res, 400, { error: 'view "transfer-writer" and transferWriter come together or not at all' });
|
|
2131
2271
|
if (ttlSec !== undefined && typeof ttlSec !== "number")
|
|
2132
2272
|
return send(res, 400, { error: "ttlSec must be a number" });
|
|
2133
2273
|
if (view !== undefined && typeof view !== "string")
|
|
@@ -2210,12 +2350,15 @@ async function handleExchange(req, res, ctx, policy) {
|
|
|
2210
2350
|
// principal holds THAT redeemed session. The bridge's `verifySession` hook runs the decision on
|
|
2211
2351
|
// the principal it derived, before signing; the callout re-runs it at the mint.
|
|
2212
2352
|
const session = view === "session-caller" ? grantCoordinates(sessionGrant) : undefined;
|
|
2353
|
+
if (transferWriter !== undefined)
|
|
2354
|
+
assertTransferWriterClaim(transferWriter);
|
|
2213
2355
|
const r = await ctx.bridge.exchange(idpToken, {
|
|
2214
2356
|
actor,
|
|
2215
2357
|
ttlSec,
|
|
2216
2358
|
view: view,
|
|
2217
2359
|
managerInstanceId: selectedManagerInstanceId,
|
|
2218
2360
|
...(session ? { verifySession: (p) => ctx.verifySession(p, session) } : {}),
|
|
2361
|
+
...(transferWriter !== undefined ? { transferWriter } : {}),
|
|
2219
2362
|
});
|
|
2220
2363
|
return send(res, 200, r);
|
|
2221
2364
|
}
|