@cotal-ai/auth 0.93.0 → 0.94.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/callout.d.ts.map +1 -1
  2. package/dist/callout.js +12 -3
  3. package/dist/callout.js.map +1 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/lifecycle-registry.d.ts +5 -0
  9. package/dist/lifecycle-registry.d.ts.map +1 -1
  10. package/dist/lifecycle-registry.js +8 -1
  11. package/dist/lifecycle-registry.js.map +1 -1
  12. package/dist/permissions.d.ts +9 -0
  13. package/dist/permissions.d.ts.map +1 -1
  14. package/dist/permissions.js +11 -2
  15. package/dist/permissions.js.map +1 -1
  16. package/dist/service-caller-ancestry.d.ts +19 -0
  17. package/dist/service-caller-ancestry.d.ts.map +1 -0
  18. package/dist/service-caller-ancestry.js +56 -0
  19. package/dist/service-caller-ancestry.js.map +1 -0
  20. package/dist/service-caller-effective.d.ts +12 -0
  21. package/dist/service-caller-effective.d.ts.map +1 -0
  22. package/dist/service-caller-effective.js +94 -0
  23. package/dist/service-caller-effective.js.map +1 -0
  24. package/dist/service-caller-payload-bootstrap.d.ts +30 -0
  25. package/dist/service-caller-payload-bootstrap.d.ts.map +1 -0
  26. package/dist/service-caller-payload-bootstrap.js +154 -0
  27. package/dist/service-caller-payload-bootstrap.js.map +1 -0
  28. package/dist/service-caller-payload.d.ts +50 -0
  29. package/dist/service-caller-payload.d.ts.map +1 -0
  30. package/dist/service-caller-payload.js +77 -0
  31. package/dist/service-caller-payload.js.map +1 -0
  32. package/dist/service-caller-policy.d.ts +71 -0
  33. package/dist/service-caller-policy.d.ts.map +1 -1
  34. package/dist/service-caller-policy.js +83 -20
  35. package/dist/service-caller-policy.js.map +1 -1
  36. package/dist/service-host-contract.d.ts +8 -2
  37. package/dist/service-host-contract.d.ts.map +1 -1
  38. package/dist/service-host-contract.js +67 -4
  39. package/dist/service-host-contract.js.map +1 -1
  40. package/dist/service-host-ops.d.ts +85 -7
  41. package/dist/service-host-ops.d.ts.map +1 -1
  42. package/dist/service-host-ops.js +804 -18
  43. package/dist/service-host-ops.js.map +1 -1
  44. package/dist/service-write.d.ts +3 -0
  45. package/dist/service-write.d.ts.map +1 -1
  46. package/dist/service-write.js +1 -1
  47. package/dist/service-write.js.map +1 -1
  48. package/dist/service.d.ts +8 -3
  49. package/dist/service.d.ts.map +1 -1
  50. package/dist/service.js +44 -2
  51. package/dist/service.js.map +1 -1
  52. package/package.json +7 -4
@@ -1,8 +1,9 @@
1
1
  /** Package-internal service host operations: register admission, decide (approve or refuse),
2
- * revoke, create-only name binding, the operation and proposal reads and one owner's assignment
3
- * state, over closed per-key records in the auth bucket. The index does not export it; only the
4
- * owner-proved public authority door calls register. It grants no serving, registration, caller
5
- * policy, restore, terminal retirement or KICK.
2
+ * revoke, create-only name binding, caller grants and caller revocation up to deny-new and its
3
+ * cutoff marker, the operation, proposal and caller-policy reads and one owner's assignment state,
4
+ * over closed per-key records in the auth bucket. The index does not export it; only the
5
+ * owner-proved public authority door calls register. It grants no serving, registration, issuance
6
+ * from caller policy, terminal caller revocation, restore, terminal retirement or KICK.
6
7
  *
7
8
  * Every write is one zero-history create or one exact-revision CAS on one key, issued through the
8
9
  * shared custody writer (service-write.ts) under the same plane-claim hold check and fatal latch as
@@ -13,17 +14,21 @@
13
14
  * what a later invocation reads. The invocation limit and the original deadline are limits, never
14
15
  * waits, fences or proof that an earlier write cannot still land. */
15
16
  import { createHash } from "node:crypto";
16
- import { EpEnvelopeError, GOVERN_HEAD, RECORD_KINDS, assertDerivedOwnerToken, assertLifecycleToken, canonicalJson, contractArtifactCanonicalBytes, contractDigest, contractStoreContext, endpointRegistrationBarrier, endpointToken, epgateKey, isContractDigest, mintLifecycleUid, parseEndpointCapabilities, parseNativeUidCustody, parseServiceSpec, parseServiceStatus, publishContractArtifact, recordAtomicKey, recordSpecKey, recordStatusKey, registerServiceInstance, serviceEndpointActor, serviceGateApprovalLive, serviceWitnessDigest, verifyClusterManifest, verifyClusterRoot, writeServiceStatus, } from "@cotal-ai/core";
17
+ import { EpEnvelopeError, GOVERN_HEAD, RECORD_KINDS, assertBoundedOwner, assertDerivedOwnerToken, assertLifecycleToken, canonicalJson, contractArtifactCanonicalBytes, contractDigest, contractStoreContext, endpointCapabilitiesWithin, endpointRegistrationBarrier, endpointToken, epgateKey, isContractDigest, mintLifecycleUid, parseEndpointCapabilities, parseNativeUidCustody, parseServiceSpec, parseServiceStatus, publishContractArtifact, recordAtomicKey, recordSpecKey, recordStatusKey, registerServiceInstance, serviceEndpointActor, serviceGateApprovalLive, serviceWitnessDigest, verifyClusterManifest, verifyClusterRoot, writeServiceStatus, } from "@cotal-ai/core";
17
18
  import { SERVICE_HOST_INVOCATION_MS, openAuthorityClient, serviceHostEffectGrants, serviceHostEffectLabel } from "./authority-client.js";
18
- import { findInteractiveActor } from "./ledger.js";
19
- import { openLifecycleRegistry, registryStores } from "./lifecycle-registry.js";
19
+ import { callerParentRefs } from "./service-caller-ancestry.js";
20
+ import { findActorUnified, findInteractiveActor } from "./ledger.js";
21
+ import { openLifecycleRegistry, registryMaxPayload, registryServerId, registryStores } from "./lifecycle-registry.js";
20
22
  import { activatedServiceGateRow, approvedServiceGateRow, assertApprovableServiceGate } from "./native-service-gate.js";
21
23
  import { nativeServiceNameAuthority, readNativeServiceName, serviceNameKey, serviceNameRowBytes } from "./native-service-name.js";
24
+ import { genuinePlaneHold, readPlaneClaim } from "./plane-claim.js";
25
+ import { SERVICE_CALLER_PAYLOAD_KEY, parseServiceCallerPayloadRecord, serviceCallerPayloadCheck, serviceCallerWriteFits, } from "./service-caller-payload.js";
26
+ import { callerAppendStep, callerBeginStep, callerCutoffStep, immutableGrantView, parseNativeServiceCallerPolicy, parseServiceCallerGrant, servicePolicyKey, } from "./service-caller-policy.js";
22
27
  import { encodeRoundTrip } from "./service-custody-shapes.js";
23
28
  import { cancelServiceCustody, runServiceBootstrap, sealServiceBootstrapIntent, sealServiceCancelIntent, serviceAssignmentGate, serviceCustodyKeys, serviceReservationKeys, } from "./service-custody.js";
24
29
  import { parseServiceHostRequest, parseServiceProposalClusters, parseServiceProposalCommands, } from "./service-host-contract.js";
25
30
  import { readRegistryServiceRoot } from "./service-root-reader.js";
26
- import { HOSTS, Held, bounded, context, endpoint, held, leader, message, openServiceCustodyHost, readyAfter, readyBefore, scopeCovers, write, } from "./service-write.js";
31
+ import { HOSTS, Held, S1_REQUEST_TIMEOUT_MS, bounded, context, endpoint, held, leader, message, openServiceCustodyHost, readyAfter, readyBefore, scopeCovers, write, } from "./service-write.js";
27
32
  /** Original authority limit of one approve, refuse or register, from its admission. */
28
33
  export const S2A_OPERATION_TTL_MS = 600_000;
29
34
  /** Per-invocation limit for reads, bookkeeping, settlement and native cancellation, and the
@@ -38,6 +43,9 @@ export const S2A_MAX_PROPOSAL_BYTES = 256 * 1024;
38
43
  export const S2A_MAX_REDECIDE = 4;
39
44
  /** Serving request intents one instance keeps (SPEC 13.1): never pruned or reused in this cut. */
40
45
  export const S3_MAX_INTENTS_PER_INSTANCE = 64;
46
+ /** Index entries one service instance may ever hold: every caller grant written for it, active,
47
+ * revoking or orphaned by a grant attempt refused after its index write. Nothing is pruned. */
48
+ export const S3_MAX_CALLER_GRANTS_PER_INSTANCE = 256;
41
49
  const ADMISSION_KEY = "svchost.admission";
42
50
  const CAPACITY_KEY = "svccap.space";
43
51
  const opKey = (operationId) => `svcop.${operationId}`;
@@ -45,6 +53,8 @@ const proposalKey = (proposalId) => `svcprop.${proposalId}`;
45
53
  const assignmentKey = (endpoint, instanceId) => `svcasg.${endpointToken(endpoint)}.${assertLifecycleToken(instanceId, "instanceId")}`;
46
54
  const intentKey = (operationId) => `svcint.${hexN(operationId, 32)}`;
47
55
  const intentIndexKey = (endpoint, instanceId) => `svcintx.${endpointToken(endpoint)}.${assertLifecycleToken(instanceId, "instanceId")}`;
56
+ const callerIndexKey = (endpoint, instanceId) => `svcpolx.${endpointToken(endpoint)}.${assertLifecycleToken(instanceId, "instanceId")}`;
57
+ const cutoffKey = (operationId) => `svccut.${hexN(operationId, 32)}`;
48
58
  const enc = new TextEncoder();
49
59
  const seconds = (ms) => Math.floor(ms / 1000);
50
60
  // ---- identities ---------------------------------------------------------------------------------
@@ -53,6 +63,8 @@ const hex32 = (tag, space, value) => createHash("sha256").update(`${tag}\0${spac
53
63
  export const serviceHostOperationId = (space, requestId) => hex32("cotal-service-host-op", space, requestId);
54
64
  /** The native cancel identity an expiry settlement derives from recorded facts only. */
55
65
  const expiryOperationId = (space, approveOperationId) => hex32("cotal-service-host-expiry", space, approveOperationId);
66
+ /** A caller grant's id, from its operation: one grant per bound grant request, never reused. */
67
+ const callerGrantId = (space, operationId) => hex32("cotal-service-caller-grant", space, operationId);
56
68
  const expiryRequestDigest = (operationId, assignmentDigest) => contractDigest({ v: 1, kind: "service-approval-expiry", operationId, assignmentDigest });
57
69
  const requestDigestOf = (r) => contractDigest({ v: r.v, kind: r.kind, requestId: r.requestId, operation: r.operation, input: r.input, granter: r.granter });
58
70
  const proposalIdOf = (proposalDigest, registerRequestId) => contractDigest({ v: 1, kind: "service-proposal-id", proposalDigest, registerRequestId }).slice("sha256:".length);
@@ -92,6 +104,20 @@ const endpointName = (raw) => { if (typeof raw !== "string")
92
104
  bad("expected an endpoint"); grammar(() => endpointToken(raw)); return raw; };
93
105
  const token = (raw, what) => { if (typeof raw !== "string")
94
106
  bad(`expected ${what}`); return grammar(() => assertLifecycleToken(raw, what)); };
107
+ /** A caller-ceiling entry an approval may carry: pinned to the proposal's own instance with
108
+ * `instanceOnly`, on a declared command, with exactly the target presence and mode that command
109
+ * admits. `any` and `ledger` need an operator target policy, which stock does not wire. Containment
110
+ * then compares instance rails directly; no class entry is re-read as an instance one. */
111
+ const ceilingEntryAdmitted = (c, p) => {
112
+ if (c.endpoint !== p.endpoint || c.instanceId !== p.instanceId || c.instanceOnly !== true)
113
+ return false;
114
+ const k = p.commands.find((x) => x.name === c.command);
115
+ if (k === undefined)
116
+ return false;
117
+ if (!k.targeted)
118
+ return c.target === undefined;
119
+ return c.target !== undefined && c.target.mode !== "any" && c.target.mode !== "ledger" && k.modes.includes(c.target.mode);
120
+ };
95
121
  const CAUSES = ["owner-request", "membership-removal", "operator-request", "restore-quarantine", "approval-expired"];
96
122
  const REFUSALS = ["conflict", "expired", "revoked", "superseded", "custody-refused", "admission-closed",
97
123
  "resource-exhausted", "selector-pending", "selector-bound", "name-owned", "restore", "owner", "operator"];
@@ -132,6 +158,20 @@ function cancelOf(raw) {
132
158
  const o = closed(raw, ["operationId", "requestDigest", "cause"]);
133
159
  return { operationId: token(o.operationId, "operationId"), requestDigest: digest(o.requestDigest), cause: pick(o.cause, CAUSES) };
134
160
  }
161
+ function principalOf(raw) {
162
+ const o = closed(raw, ["owner", "actor", "lifecycleUid"]);
163
+ if (typeof o.actor !== "string")
164
+ bad("expected an actor");
165
+ return { owner: owner(o.owner), actor: grammar(() => assertBoundedOwner(o.actor, "actor")), lifecycleUid: token(o.lifecycleUid, "lifecycleUid") };
166
+ }
167
+ /** A grant body: the stored grant syntax with the status left out. */
168
+ function grantBody(raw) {
169
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || Object.hasOwn(raw, "status"))
170
+ bad("expected a grant body");
171
+ const { status: _status, ...body } = grammar(() => parseServiceCallerGrant({ ...raw, status: { state: "active" } }));
172
+ return body;
173
+ }
174
+ const withStatus = (body, status) => ({ ...body, status });
135
175
  function plan(raw) {
136
176
  const o = raw;
137
177
  switch (o?.kind) {
@@ -149,6 +189,27 @@ function plan(raw) {
149
189
  case "revoke":
150
190
  closed(raw, ["kind", "endpoint", "instanceId", "expectedAssignmentDigest", "cause"]);
151
191
  return { kind: "revoke", endpoint: endpointName(o.endpoint), instanceId: token(o.instanceId, "instanceId"), expectedAssignmentDigest: digest(o.expectedAssignmentDigest), cause: pick(o.cause, CAUSES) };
192
+ case "caller-grant": {
193
+ closed(raw, ["kind", "grantee", "assignmentDigest", "approvalDigest", "nameAuthorityRevision", "expectedPolicyRevision", "request", "grant"]);
194
+ const p = { kind: "caller-grant", grantee: principalOf(o.grantee), assignmentDigest: digest(o.assignmentDigest), approvalDigest: digest(o.approvalDigest),
195
+ nameAuthorityRevision: ms(o.nameAuthorityRevision), expectedPolicyRevision: ms(o.expectedPolicyRevision),
196
+ request: structuredClone(o.request), grant: grantBody(o.grant) };
197
+ const src = p.grant.source;
198
+ if (src.kind !== "service-owner" || src.approvalDigest !== p.approvalDigest || src.nameAuthorityRevision !== p.nameAuthorityRevision
199
+ || p.grant.endpoint !== p.request?.service?.endpoint || p.grant.instanceId !== p.request?.service?.instanceId || p.approvalDigest !== p.request?.service?.approvalDigest
200
+ || canonicalJson(p.grantee) !== canonicalJson(p.request?.grantee) || p.expectedPolicyRevision !== p.request?.expectedPolicyRevision)
201
+ bad("the caller grant plan does not match its request");
202
+ return p;
203
+ }
204
+ case "caller-revoke": {
205
+ closed(raw, ["kind", "grantee", "grantId", "expectedPolicyRevision", "request", "grant"]);
206
+ const p = { kind: "caller-revoke", grantee: principalOf(o.grantee), grantId: hexN(o.grantId, 32), expectedPolicyRevision: ms(o.expectedPolicyRevision),
207
+ request: structuredClone(o.request), grant: grantBody(o.grant) };
208
+ if (p.grant.grantId !== p.grantId || p.grantId !== p.request?.grantId || canonicalJson(p.grantee) !== canonicalJson(p.request?.grantee)
209
+ || p.expectedPolicyRevision !== p.request?.expectedPolicyRevision)
210
+ bad("the caller revoke plan does not match its request");
211
+ return p;
212
+ }
152
213
  default: return bad("unknown plan");
153
214
  }
154
215
  }
@@ -172,6 +233,10 @@ function approvalResult(raw) {
172
233
  approvedGateRevision: o.approvedGateRevision === null ? null : ms(o.approvedGateRevision) };
173
234
  }
174
235
  function result(raw, operation, decision) {
236
+ if (operation === "grant") {
237
+ const o = closed(raw, ["grant", "revision"]);
238
+ return { grant: grammar(() => parseServiceCallerGrant(o.grant)), revision: ms(o.revision) };
239
+ }
175
240
  if (operation === "register") {
176
241
  const o = closed(raw, ["proposalId", "revision", "expiresAtMs"]);
177
242
  return { proposalId: hexN(o.proposalId, 64), revision: ms(o.revision), expiresAtMs: ms(o.expiresAtMs) };
@@ -203,20 +268,32 @@ const terminalDecision = (operation, outcome) => operation !== "decide" ? null :
203
268
  export function parseServiceHostOperationRecord(raw) {
204
269
  const o = raw;
205
270
  const common = (x) => ({ operationId: token(x.operationId, "operationId"), requestId: hexN(x.requestId, 32), requestDigest: digest(x.requestDigest),
206
- operation: pick(x.operation, ["register", "decide", "revoke"]), account: account(x.account), granter: granter(x.granter), admittedAtMs: ms(x.admittedAtMs), projection: projection(x.projection) });
271
+ operation: pick(x.operation, ["register", "decide", "revoke", "grant"]), account: account(x.account), granter: granter(x.granter), admittedAtMs: ms(x.admittedAtMs), projection: projection(x.projection) });
207
272
  if (o?.kind === "service-host-operation") {
208
273
  closed(raw, ["v", "kind", "operationId", "requestId", "requestDigest", "operation", "account", "granter", "admittedAtMs", "notAfterMs", "admissionEpoch", "projection", "plan", "progress", "debt"]);
209
274
  if (o.v !== 1)
210
275
  bad("unknown version");
211
276
  const c = common(o), p = plan(o.plan);
212
- if ((c.operation === "register") !== (p.kind === "register") || (c.operation === "revoke") !== (p.kind === "revoke"))
277
+ if ((c.operation === "register") !== (p.kind === "register") || (c.operation === "revoke") !== (p.kind === "revoke" || p.kind === "caller-revoke")
278
+ || (c.operation === "grant") !== (p.kind === "caller-grant"))
213
279
  bad("plan does not match its operation");
280
+ // A caller plan carries its request's exact input; the record binds only if that input reproduces the digest.
281
+ if (p.kind === "caller-grant" || p.kind === "caller-revoke") {
282
+ const request = { v: 1, kind: "service-endpoint-host", requestId: c.requestId, operation: p.kind, input: p.request, granter: c.granter };
283
+ grammar(() => parseServiceHostRequest(request));
284
+ if (requestDigestOf(request) !== c.requestDigest)
285
+ bad("the stored request does not reproduce the request digest");
286
+ }
214
287
  const g = o.progress;
215
288
  let progress;
216
289
  if (g?.state === "admitted") {
217
290
  closed(g, ["state"]);
218
291
  progress = { state: "admitted" };
219
292
  }
293
+ else if (g?.state === "issued") {
294
+ closed(g, ["state", "step", "key", "atRevision"]);
295
+ progress = { state: "issued", step: word(g.step), key: word(g.key, 512), atRevision: ms(g.atRevision) };
296
+ }
220
297
  else if (g?.state === "custody") {
221
298
  closed(g, ["state", "step"]);
222
299
  progress = { state: "custody", step: word(g.step) };
@@ -330,6 +407,46 @@ export function parseServiceAdmissionRecord(raw) {
330
407
  bad("admission epochs start at 1");
331
408
  return { v: 1, kind: "service-host-admission", state: pick(o.state, ["open", "quarantined"]), epoch, restoreId: o.restoreId === null ? null : hexN(o.restoreId, 32) };
332
409
  }
410
+ export function parseServiceCallerIndexRecord(raw) {
411
+ const o = closed(raw, ["v", "kind", "entries"]);
412
+ if (o.v !== 1 || o.kind !== "service-caller-grant-index")
413
+ bad("unknown version or kind");
414
+ if (!Array.isArray(o.entries) || o.entries.length > S3_MAX_CALLER_GRANTS_PER_INSTANCE)
415
+ bad("invalid caller grant index");
416
+ const entries = o.entries.map((x) => { const e = closed(x, ["granteeUid", "grantId"]); return { granteeUid: token(e.granteeUid, "granteeUid"), grantId: hexN(e.grantId, 32) }; });
417
+ if (entries.some((e, i) => i > 0 && !(byEntry(entries[i - 1], e) < 0)))
418
+ bad("caller grant index entries must be unique and sorted");
419
+ return { v: 1, kind: "service-caller-grant-index", entries };
420
+ }
421
+ const byEntry = (a, b) => a.granteeUid < b.granteeUid ? -1 : a.granteeUid > b.granteeUid ? 1 : a.grantId < b.grantId ? -1 : a.grantId > b.grantId ? 1 : 0;
422
+ export function parseServiceCutoffMarkerRecord(raw) {
423
+ const o = closed(raw, ["v", "kind", "accountPublicKey", "space", "operationId", "target", "authorityDigest"]);
424
+ if (o.v !== 1 || o.kind !== "service-cutoff-marker")
425
+ bad("unknown version or kind");
426
+ const t = closed(o.target, ["kind", "grantee", "grantId", "endpoint", "instanceId"]);
427
+ pick(t.kind, ["caller-grant"]);
428
+ const a = account({ accountPublicKey: o.accountPublicKey, space: o.space });
429
+ return { v: 1, kind: "service-cutoff-marker", ...a, operationId: hexN(o.operationId, 32),
430
+ target: { kind: "caller-grant", grantee: principalOf(t.grantee), grantId: hexN(t.grantId, 32), endpoint: endpointName(t.endpoint), instanceId: token(t.instanceId, "instanceId") },
431
+ authorityDigest: digest(o.authorityDigest) };
432
+ }
433
+ /** ceil(T) in epoch seconds for the server's raw stored-message time, or undefined unless it is
434
+ * exactly `YYYY-MM-DDTHH:MM:SS[.d{1,9}]Z` naming a real calendar second. The whole second is
435
+ * formatted back and compared, so a normalized date or a leap second never passes, and any
436
+ * non-zero fractional digit rounds up. No double or Date carries the fraction. */
437
+ export function serviceMarkerCeilSeconds(raw) {
438
+ if (typeof raw !== "string")
439
+ return undefined;
440
+ const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?Z$/.exec(raw);
441
+ if (!m)
442
+ return undefined;
443
+ const at = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6]));
444
+ if (!Number.isSafeInteger(at) || at < 0 || new Date(at).toISOString().slice(0, 19) !== raw.slice(0, 19))
445
+ return undefined;
446
+ return at / 1000 + (m[7] !== undefined && /[1-9]/.test(m[7]) ? 1 : 0);
447
+ }
448
+ /** The canonical cutoff deadline after the marker's time: ceil(T) + 60 + 2*5 + 1 seconds. */
449
+ export const SERVICE_CUTOFF_MARGIN_S = 71;
333
450
  /** Leader-read one S2-A key. A deletion marker or spent tombstone holds under a key-specific
334
451
  * reason and is never recreated; anything its closed parser refuses holds as malformed. */
335
452
  async function read(ctx, key, what, parse) {
@@ -487,6 +604,18 @@ export function serviceHostScopeKeys(account, t) {
487
604
  bad("a name binding scope names its endpoint");
488
605
  auth.push(serviceNameKey(t.endpoint));
489
606
  break;
607
+ // A caller operation writes its op, the grantee's policy row and the payload precondition's
608
+ // withdrawal, plus the instance index for a grant or the cutoff marker for a revoke. No custody key.
609
+ case "caller-grant":
610
+ if (t.endpoint === null || t.instanceId === null || t.granteeUid === undefined)
611
+ bad("a caller grant scope names its selector and grantee");
612
+ auth.push(opKeyOf(), servicePolicyKey(t.granteeUid), callerIndexKey(t.endpoint, t.instanceId), SERVICE_CALLER_PAYLOAD_KEY);
613
+ break;
614
+ case "caller-revoke":
615
+ if (t.granteeUid === undefined || t.cutoffOperationId === undefined)
616
+ bad("a caller revoke scope names its grantee and cutoff operation");
617
+ auth.push(opKeyOf(), servicePolicyKey(t.granteeUid), cutoffKey(t.cutoffOperationId), SERVICE_CALLER_PAYLOAD_KEY);
618
+ break;
490
619
  case "serve": {
491
620
  // A serving request writes its intent and its instance's index, then only its own effect's keys.
492
621
  if (t.endpoint === null || t.instanceId === null || t.serve === undefined)
@@ -599,11 +728,10 @@ export async function openServiceHostOperations(opts) {
599
728
  /** One invocation: an effect connection for the discovered scope and its original deadline, one
600
729
  * custody host over it sharing the process hold and writing only the scope's keys, and the
601
730
  * engine latch. The connection's loss trips the host. Every awaited call returns before close. */
602
- async function invoke(target, run) {
731
+ async function invoke(target, run, deadlineMs = Date.now() + S2A_INVOCATION_MS) {
603
732
  if (tripped !== undefined)
604
733
  throw new Held("readiness", "open", `the service host is fenced: ${tripped}`);
605
734
  const keys = serviceHostScopeKeys({ accountPublicKey, space }, target), reads = serviceHostScopeReads(target);
606
- const deadlineMs = Date.now() + S2A_INVOCATION_MS;
607
735
  const scope = Object.freeze({ ...target, keys, reads, deadlineMs });
608
736
  let effect;
609
737
  try {
@@ -1017,7 +1145,7 @@ export async function openServiceHostOperations(opts) {
1017
1145
  if (name.revision !== req.input.expectedNameRevision)
1018
1146
  return out(denied("conflict"));
1019
1147
  const ceiling = parseEndpointCapabilities(req.input.callerCeiling);
1020
- if (ceiling.some((c) => c.endpoint !== p.endpoint || !p.commands.some((k) => k.name === c.command) || (c.instanceId !== undefined && c.instanceId !== p.instanceId)))
1148
+ if (ceiling.some((c) => !ceilingEntryAdmitted(c, p)))
1021
1149
  return out(denied("bad-request"));
1022
1150
  const reservation = { v: 1, kind: "native-service-uid", native: { accountPublicKey, space, lifecycleUid },
1023
1151
  assignment: { owner: p.owner, actor: serviceEndpointActor({ accountPublicKey, space, owner: p.owner, endpoint: p.endpoint, instanceId: p.instanceId }),
@@ -1467,6 +1595,657 @@ export async function openServiceHostOperations(opts) {
1467
1595
  }
1468
1596
  }
1469
1597
  }
1598
+ // ---- caller policy: grant, revoke and the policy reads ------------------------------------------
1599
+ const readPolicy = (ctx, uid) => read(ctx, servicePolicyKey(uid), "policy", parseNativeServiceCallerPolicy);
1600
+ const readCallerIndex = (ctx, e, i) => read(ctx, callerIndexKey(e, i), "caller-index", parseServiceCallerIndexRecord);
1601
+ const readPayload = (ctx) => read(ctx, SERVICE_CALLER_PAYLOAD_KEY, "payload", parseServiceCallerPayloadRecord);
1602
+ /** Whether a row at a grantee's key is bound to that grantee in this account and space. */
1603
+ const boundTo = (row, g) => row.accountPublicKey === accountPublicKey && row.space === space && canonicalJson(row.grantee) === canonicalJson(g);
1604
+ const emptyPolicy = (g) => ({ v: 1, accountPublicKey, space, grantee: g, grants: [] });
1605
+ /** This connection's live limits: its server's `max_payload` and the auth stream's `max_msg_size`. */
1606
+ async function payloadLimits(ctx) {
1607
+ const maxPayload = registryMaxPayload(ctx.h.registry);
1608
+ const r = await bounded(ctx, () => ctx.jsm.streams.info(`KV_${ctx.bucket.auth}`));
1609
+ const size = r.ok ? r.value.config.max_msg_size : undefined;
1610
+ return { maxPayload: Number.isSafeInteger(maxPayload) ? maxPayload : undefined, authMaxMsgSize: Number.isSafeInteger(size) ? size : undefined };
1611
+ }
1612
+ /** Withdraw an asserted payload precondition after a failed live check or a write that would not
1613
+ * fit; only an operator's assertion records it again. A withdrawal whose own write does not land
1614
+ * is made again by the next writer that sees the same failure. */
1615
+ async function withdrawPayload(ctx, rec, reason, operationId) {
1616
+ if (rec.state !== "value" || rec.row.state !== "asserted")
1617
+ return;
1618
+ const step = ctx.step;
1619
+ try {
1620
+ await put(ctx, SERVICE_CALLER_PAYLOAD_KEY, { v: 1, kind: "service-caller-payload", state: "withdrawn", reason, operationId, atMs: Date.now() }, parseServiceCallerPayloadRecord, rec);
1621
+ }
1622
+ catch (e) {
1623
+ if (!(e instanceof Held))
1624
+ throw e;
1625
+ }
1626
+ finally {
1627
+ ctx.step = step;
1628
+ }
1629
+ }
1630
+ /** Whether the payload precondition holds now, from its durable record and this connection's live limits. */
1631
+ async function payloadHolds(ctx, operationId) {
1632
+ const rec = await readPayload(ctx), check = serviceCallerPayloadCheck(rec.state === "value" ? rec.row : undefined, await payloadLimits(ctx));
1633
+ if (check.holds && rec.state === "value" && rec.row.state === "asserted" && rec.row.broker) {
1634
+ const broker = rec.row.broker, proof = genuinePlaneHold(opts.hold);
1635
+ if (!proof || broker.serverId !== registryServerId(ctx.h.registry))
1636
+ return false;
1637
+ const observed = await bounded(ctx, () => readPlaneClaim(registryStores(ctx.h.registry).authKv, space));
1638
+ if (!observed.ok || !observed.value || observed.value.state !== "held" || observed.value.claimId !== broker.claimId || observed.value.generation !== broker.claimGeneration)
1639
+ return false;
1640
+ const held = await bounded(ctx, () => proof.assertHeld("after"));
1641
+ if (!held.ok)
1642
+ return false;
1643
+ }
1644
+ if (!check.holds && (check.why === "server-limit" || check.why === "stream-limit"))
1645
+ await withdrawPayload(ctx, rec, "live-check", operationId);
1646
+ return check.holds;
1647
+ }
1648
+ /** Before a policy row write: a row that would not fit the live limits is never issued. The step
1649
+ * holds `payload-precondition`, a known non-issue, never an unknown outcome. */
1650
+ async function assertWriteFits(ctx, row, operationId) {
1651
+ if (serviceCallerWriteFits(encodeRoundTrip(row, parseNativeServiceCallerPolicy).length, await payloadLimits(ctx)))
1652
+ return;
1653
+ await withdrawPayload(ctx, await readPayload(ctx), "payload-precondition", operationId);
1654
+ held("payload-precondition", ctx.step, "the policy row write would not fit the live payload limits; nothing was issued");
1655
+ }
1656
+ /** Containment within the approval's ceiling image for grantee G. The image keeps only entries on
1657
+ * this endpoint pinned to this instance with `instanceOnly`, and drops `any` and `ledger` entries
1658
+ * (no operator target policy is wired) and `owner` or `child` entries for another owner, which can
1659
+ * never cover G. Mode filtering of stored entries waits on grant-time command declarations. A
1660
+ * lowering throw counts as not contained. */
1661
+ const withinCeiling = (ceiling, capabilities, g, sel) => {
1662
+ const image = ceiling.filter((c) => {
1663
+ const t = c.target;
1664
+ return c.endpoint === sel.endpoint && c.instanceId === sel.instanceId && c.instanceOnly === true && t?.mode !== "any" && t?.mode !== "ledger"
1665
+ && !((t?.mode === "owner" || t?.mode === "child") && t.tOwner !== g.owner);
1666
+ });
1667
+ const caller = { owner: g.owner, actor: g.actor, uid: g.lifecycleUid };
1668
+ try {
1669
+ return endpointCapabilitiesWithin({ space, child: { caller, capabilities }, parent: { caller, capabilities: image } });
1670
+ }
1671
+ catch {
1672
+ return false;
1673
+ }
1674
+ };
1675
+ const grantedArm = (r) => { const g = r; return { state: "granted", grant: structuredClone(g.grant), revision: g.revision }; };
1676
+ /** caller-grant (G1-G6): an interactive owner grants a current exact-lifecycle grantee rights on
1677
+ * its own approved instance, within the approval's ceiling and covering managed ancestry. A grant
1678
+ * records policy only; the separately composed callout establishes effective authority. */
1679
+ async function callerGrant(req, authorized) {
1680
+ // Authorization precedes every request-key read, and its refusal is one answer whether or not the request is bound.
1681
+ if (!authorized)
1682
+ return unread(req.requestId, "permission-denied", "authorize");
1683
+ const operationId = serviceHostOperationId(space, req.requestId), requestDigest = requestDigestOf(req);
1684
+ const granterOwner = req.granter.kind === "owner" ? req.granter.owner : badRequest("a caller grant requires an owner decision");
1685
+ const { grantee, service, expectedPolicyRevision } = req.input;
1686
+ // The scope comes from the input alone, so a grant has no discovery read.
1687
+ const scope = { operation: "caller-grant", operationId, proposalId: null, owner: granterOwner, endpoint: service.endpoint,
1688
+ instanceId: service.instanceId, lifecycleUid: null, granteeUid: grantee.lifecycleUid };
1689
+ try {
1690
+ return await invoke(scope, async (inv) => {
1691
+ inv.operationId = operationId;
1692
+ try {
1693
+ const ctx = readCtx(inv, "G1-binding");
1694
+ for (let n = 0;; n++) {
1695
+ if (n >= S2A_MAX_REDECIDE)
1696
+ held("contention", ctx.step);
1697
+ const op = await readOp(ctx, operationId);
1698
+ if (op.state === "value")
1699
+ return op.row.requestDigest === requestDigest ? await advanceGrant(inv, op.row) : denied("conflict");
1700
+ inv.bound = false;
1701
+ const admitted = await admitGrant(inv, ctx);
1702
+ if (admitted.kind !== "service-host-operation")
1703
+ return admitted.out;
1704
+ // The create may be issued from here, so a held step names the operation.
1705
+ inv.bound = true;
1706
+ if ((await put(ctx, opKey(operationId), admitted, parseServiceHostOperationRecord)) !== "moved")
1707
+ return await advanceGrant(inv, admitted);
1708
+ }
1709
+ }
1710
+ catch (err) {
1711
+ if (err instanceof Held)
1712
+ return heldArm(inv, err);
1713
+ throw err;
1714
+ }
1715
+ });
1716
+ }
1717
+ catch (err) {
1718
+ if (err instanceof Held)
1719
+ return unread(req.requestId, err.reason, err.step);
1720
+ throw err;
1721
+ }
1722
+ /** G2: the payload precondition, then each eligibility check from one leader read, point in
1723
+ * time. Every refusal is a zero-write denial; the only write before binding is the precondition's
1724
+ * withdrawal after a failed live check. */
1725
+ async function admitGrant(inv, ctx) {
1726
+ const out = (o) => ({ kind: "out", out: o });
1727
+ ctx.step = "G2-precondition";
1728
+ if (!(await payloadHolds(ctx, null)))
1729
+ return out(denied("unsupported"));
1730
+ ctx.step = "G2-eligibility";
1731
+ const adm = await readAdmission(ctx);
1732
+ if (adm.state === "absent" || adm.row.state !== "open")
1733
+ return out(denied("admission-closed"));
1734
+ let row;
1735
+ try {
1736
+ row = findActorUnified(opts.ledgerDir, grantee.owner, grantee.actor);
1737
+ }
1738
+ catch {
1739
+ return out(denied("not-found"));
1740
+ }
1741
+ if (row === undefined)
1742
+ return out(denied("not-found"));
1743
+ if (row.kind === "interactive" && row.parent !== undefined)
1744
+ return out(denied("unsupported"));
1745
+ if (row.kind === "managed-agent" && row.parent === undefined)
1746
+ return out(denied("unsupported"));
1747
+ if (row.lifecycleUid !== grantee.lifecycleUid)
1748
+ return out(denied("not-found"));
1749
+ const asg = await readAssignment(ctx, service.endpoint, service.instanceId);
1750
+ if (asg.state === "absent" || asg.row.owner !== granterOwner || asg.row.state !== "bound" || asg.row.cancel !== null)
1751
+ return out(denied("not-found"));
1752
+ const a = asg.row;
1753
+ const gate = await endpoint(ctx, serviceAssignmentGate(inv.host, a.reservation));
1754
+ if (gate.kind !== "approved" || gate.row.serviceAuthority.approvalDigest !== service.approvalDigest)
1755
+ return out(denied("conflict"));
1756
+ const now = Date.now();
1757
+ if (!serviceGateApprovalLive(gate.row.serviceAuthority, seconds(now)))
1758
+ return out(denied("expired"));
1759
+ const name = await nameOf(ctx, service.endpoint);
1760
+ if (name === undefined || name.owner !== granterOwner || name.revision !== gate.row.nameAuthorityRevision)
1761
+ return out(denied("conflict"));
1762
+ const capabilities = parseEndpointCapabilities(req.input.capabilities);
1763
+ if (capabilities.length === 0 || capabilities.some((c) => c.endpoint !== service.endpoint || c.instanceId !== service.instanceId || c.instanceOnly !== true))
1764
+ return out(denied("bad-request"));
1765
+ // A journal row has no instance segment, so it would reach the command's journal on every instance.
1766
+ if (capabilities.some((c) => c.journal === true))
1767
+ return out(denied("unsupported"));
1768
+ if (capabilities.some((c) => c.target?.mode === "any" || c.target?.mode === "ledger"))
1769
+ return out(denied("permission-denied"));
1770
+ if (!withinCeiling(gate.row.serviceAuthority.callerCeiling, capabilities, grantee, service))
1771
+ return out(denied("permission-denied"));
1772
+ let parentRefs;
1773
+ try {
1774
+ parentRefs = await callerParentRefs({ space, ledgerDir: opts.ledgerDir, grantee, capabilities, service,
1775
+ readPolicy: async (p) => {
1776
+ const read = await readPolicy(ctx, p.lifecycleUid);
1777
+ if (read.state === "absent")
1778
+ return undefined;
1779
+ if (!boundTo(read.row, p))
1780
+ throw new EpEnvelopeError("permission-denied", "parent policy has another exact principal");
1781
+ return read.row;
1782
+ } });
1783
+ }
1784
+ catch (e) {
1785
+ if (e instanceof EpEnvelopeError)
1786
+ return out(denied("permission-denied"));
1787
+ throw e;
1788
+ }
1789
+ const policy = await readPolicy(ctx, grantee.lifecycleUid);
1790
+ if (policy.state === "value" && !boundTo(policy.row, grantee))
1791
+ return out(denied("conflict"));
1792
+ if ((policy.state === "value" ? policy.revision : 0) !== expectedPolicyRevision)
1793
+ return out(denied("conflict"));
1794
+ const body = { grantId: callerGrantId(space, operationId), endpoint: service.endpoint, instanceId: service.instanceId,
1795
+ serviceApprovalDigest: service.approvalDigest, capabilities,
1796
+ source: { kind: "service-owner", owner: granterOwner, approvalDigest: service.approvalDigest, nameAuthorityRevision: gate.row.nameAuthorityRevision }, parentRefs };
1797
+ const step = callerAppendStep(policy.state === "value" ? { row: policy.row, revision: policy.revision } : undefined, emptyPolicy(grantee), expectedPolicyRevision, withStatus(body, { state: "active" }));
1798
+ if (step.kind === "headroom")
1799
+ return out(denied("resource-exhausted"));
1800
+ if (step.kind !== "write")
1801
+ return out(denied("conflict"));
1802
+ const index = await readCallerIndex(ctx, service.endpoint, service.instanceId);
1803
+ if (index.state === "value" && index.row.entries.length >= S3_MAX_CALLER_GRANTS_PER_INSTANCE)
1804
+ return out(denied("resource-exhausted"));
1805
+ ctx.step = "G3-binding";
1806
+ return { v: 1, kind: "service-host-operation", operationId, requestId: req.requestId, requestDigest, operation: "grant", account: { accountPublicKey, space },
1807
+ granter: req.granter, admittedAtMs: now, notAfterMs: now + S2A_OPERATION_TTL_MS, admissionEpoch: adm.row.epoch,
1808
+ projection: { subjectOwner: granterOwner, endpoint: service.endpoint, instanceId: service.instanceId, proposalId: a.proposalId },
1809
+ plan: { kind: "caller-grant", grantee, assignmentDigest: a.assignmentDigest, approvalDigest: service.approvalDigest,
1810
+ nameAuthorityRevision: gate.row.nameAuthorityRevision, expectedPolicyRevision, request: req.input, grant: body },
1811
+ progress: { state: "admitted" }, debt: [] };
1812
+ }
1813
+ /** G4-G6 from the stored plan. A positive write is recorded `issued` first; past `notAfterMs`
1814
+ * a replay makes no positive write and settles from one read of the policy row. */
1815
+ async function advanceGrant(inv, first) {
1816
+ const ctx = readCtx(inv, "G-replay");
1817
+ const stored = outcomeOf(first);
1818
+ if (stored !== undefined)
1819
+ return stored.state === "complete" ? grantedArm(stored.result) : finalArm(operationId, stored, debtOf(first));
1820
+ let op = first;
1821
+ const p = op.plan, uid = p.grantee.lifecycleUid, candidate = withStatus(p.grant, { state: "active" });
1822
+ const view = immutableGrantView(candidate), indexKey = callerIndexKey(p.grant.endpoint, p.grant.instanceId), policyKey = servicePolicyKey(uid);
1823
+ const mine = (e) => e.granteeUid === uid && e.grantId === p.grant.grantId;
1824
+ const finish = async (f) => {
1825
+ const s = await settle(ctx, operationId, f, []), o = outcomeOf(s.op);
1826
+ return o.state === "complete" ? grantedArm(o.result) : finalArm(operationId, o, debtOf(s.op));
1827
+ };
1828
+ const refuse = (reason) => finish({ state: "refused", reason });
1829
+ const adopt = (grant, revision) => finish({ state: "complete", result: { grant, revision } });
1830
+ /** Move the live progress from what this invocation last saw; another invocation's record holds. */
1831
+ const progressTo = async (next) => {
1832
+ const seen = canonicalJson(op.progress), want = canonicalJson(next);
1833
+ const r = await updateOp(ctx, operationId, (cur) => {
1834
+ const now = canonicalJson(cur.progress);
1835
+ if (isFinal(cur.progress) || now === want)
1836
+ return undefined;
1837
+ if (now !== seen)
1838
+ held("contention", ctx.step, "another invocation advanced this operation");
1839
+ return { ...cur, progress: next };
1840
+ });
1841
+ if (r.op.kind !== "service-host-operation" || isFinal(r.op.progress))
1842
+ held("contention", ctx.step, "the operation settled under another invocation");
1843
+ op = r.op;
1844
+ };
1845
+ /** Record the write as issued, never with too little of the authority window left to start it:
1846
+ * an open record is settled only by a later read, so one without its write holds until the key moves. */
1847
+ const issue = async (step, key, atRevision) => {
1848
+ if (Date.now() + 2 * S1_REQUEST_TIMEOUT_MS >= Math.min(inv.endMs, op.notAfterMs))
1849
+ held("deadline", step, "too little of the authority window remains to issue the write");
1850
+ await progressTo({ state: "issued", step, key, atRevision });
1851
+ };
1852
+ /** The grant in the row from one read: present with this plan's immutable view, or absent. Anything else holds. */
1853
+ const lookup = async () => {
1854
+ const pol = await readPolicy(ctx, uid);
1855
+ if (pol.state === "value" && !boundTo(pol.row, p.grantee))
1856
+ held("witness-mismatch", ctx.step, "the policy row at the grantee key is bound to another principal");
1857
+ const grant = pol.state === "value" ? pol.row.grants.find((x) => x.grantId === p.grant.grantId) : undefined;
1858
+ if (grant !== undefined && immutableGrantView(grant) !== view)
1859
+ held("witness-mismatch", ctx.step, "the stored grant differs from the operation's witness");
1860
+ return { pol, grant, revision: pol.state === "value" ? pol.revision : 0 };
1861
+ };
1862
+ // An open issued write: one read of its own key settles it, or its outcome stays unknown.
1863
+ if (op.progress.state === "issued") {
1864
+ const iss = op.progress;
1865
+ ctx.step = iss.step;
1866
+ if (iss.key === policyKey) {
1867
+ const found = await lookup();
1868
+ if (found.grant !== undefined)
1869
+ return adopt(found.grant, found.revision);
1870
+ if (found.revision <= iss.atRevision)
1871
+ held("outcome-unknown", ctx.step, "the issued append's outcome stays unknown until its key moves");
1872
+ await progressTo({ state: "custody", step: "G5-not-landed" });
1873
+ }
1874
+ else {
1875
+ const idx = await readCallerIndex(ctx, p.grant.endpoint, p.grant.instanceId);
1876
+ const landed = idx.state === "value" && idx.row.entries.some(mine);
1877
+ if (!landed && (idx.state === "value" ? idx.revision : 0) <= iss.atRevision)
1878
+ held("outcome-unknown", ctx.step, "the issued index write's outcome stays unknown until its key moves");
1879
+ await progressTo({ state: "custody", step: landed ? "G4-indexed" : "G4-not-landed" });
1880
+ }
1881
+ }
1882
+ // Past the operation's bound: no positive write. Adopt a landed grant, else the attempt expired.
1883
+ if (Date.now() >= op.notAfterMs) {
1884
+ ctx.step = "G-expired";
1885
+ const found = await lookup();
1886
+ return found.grant !== undefined ? adopt(found.grant, found.revision) : refuse("expired");
1887
+ }
1888
+ // A bound grant whose precondition stopped holding holds before G4 or G5; nothing is issued.
1889
+ ctx.step = "G4-precondition";
1890
+ if (!(await payloadHolds(ctx, operationId)))
1891
+ held("payload-precondition", ctx.step, "the payload precondition does not hold; nothing was issued");
1892
+ for (let n = 0;; n++) {
1893
+ if (n >= S2A_MAX_REDECIDE)
1894
+ held("contention", "G4-index");
1895
+ ctx.step = "G4-index";
1896
+ const idx = await readCallerIndex(ctx, p.grant.endpoint, p.grant.instanceId);
1897
+ if (idx.state === "value" && idx.row.entries.some(mine)) {
1898
+ if (op.progress.state === "issued")
1899
+ await progressTo({ state: "custody", step: "G4-indexed" });
1900
+ break;
1901
+ }
1902
+ if (idx.state === "value" && idx.row.entries.length >= S3_MAX_CALLER_GRANTS_PER_INSTANCE)
1903
+ return refuse("resource-exhausted");
1904
+ const entries = [...(idx.state === "value" ? idx.row.entries : []), { granteeUid: uid, grantId: p.grant.grantId }].sort(byEntry);
1905
+ await issue("G4-index", indexKey, idx.state === "value" ? idx.revision : 0);
1906
+ const wrote = await put(authorityCtx(inv, op.notAfterMs, "G4-index"), indexKey, { v: 1, kind: "service-caller-grant-index", entries }, parseServiceCallerIndexRecord, idx.state === "value" ? idx : undefined);
1907
+ if (wrote !== "moved") {
1908
+ await progressTo({ state: "custody", step: "G4-indexed" });
1909
+ break;
1910
+ }
1911
+ // A lost write: the next read of the same key settles it before anything is issued again.
1912
+ }
1913
+ // G5: the grant may already be in the row; otherwise re-check the approval's authority, then append.
1914
+ ctx.step = "G5-recheck";
1915
+ const found = await lookup();
1916
+ if (found.grant !== undefined)
1917
+ return adopt(found.grant, found.revision);
1918
+ const asg = await readAssignment(ctx, p.grant.endpoint, p.grant.instanceId);
1919
+ if (asg.state === "absent" || asg.row.assignmentDigest !== p.assignmentDigest || asg.row.owner !== op.projection.subjectOwner)
1920
+ return refuse("conflict");
1921
+ if (asg.row.state === "fenced" || asg.row.cancel !== null)
1922
+ return refuse("revoked");
1923
+ const gate = await endpoint(ctx, serviceAssignmentGate(inv.host, asg.row.reservation));
1924
+ if (gate.kind === "revoked")
1925
+ return refuse("revoked");
1926
+ if (gate.kind !== "approved" || gate.row.serviceAuthority.approvalDigest !== p.approvalDigest || gate.row.nameAuthorityRevision !== p.nameAuthorityRevision)
1927
+ return refuse("conflict");
1928
+ if (!serviceGateApprovalLive(gate.row.serviceAuthority, seconds(Date.now())))
1929
+ return refuse("expired");
1930
+ const name = await nameOf(ctx, p.grant.endpoint);
1931
+ if (name === undefined || name.owner !== op.projection.subjectOwner || name.revision !== p.nameAuthorityRevision)
1932
+ return refuse("conflict");
1933
+ try {
1934
+ const refs = await callerParentRefs({ space, ledgerDir: opts.ledgerDir, grantee: p.grantee, capabilities: p.grant.capabilities, expectedRefs: p.grant.parentRefs,
1935
+ service: { endpoint: p.grant.endpoint, instanceId: p.grant.instanceId, approvalDigest: p.approvalDigest },
1936
+ readPolicy: async (parent) => {
1937
+ const read = await readPolicy(ctx, parent.lifecycleUid);
1938
+ if (read.state === "absent")
1939
+ return undefined;
1940
+ if (!boundTo(read.row, parent))
1941
+ throw new EpEnvelopeError("permission-denied", "parent policy binding changed");
1942
+ return read.row;
1943
+ } });
1944
+ if (canonicalJson(refs) !== canonicalJson(p.grant.parentRefs))
1945
+ return refuse("revoked");
1946
+ }
1947
+ catch (e) {
1948
+ if (e instanceof EpEnvelopeError)
1949
+ return refuse("revoked");
1950
+ throw e;
1951
+ }
1952
+ const step = callerAppendStep(found.pol.state === "value" ? { row: found.pol.row, revision: found.pol.revision } : undefined, emptyPolicy(p.grantee), p.expectedPolicyRevision, candidate);
1953
+ if (step.kind === "moved")
1954
+ return refuse("conflict");
1955
+ if (step.kind === "headroom")
1956
+ return refuse("resource-exhausted");
1957
+ if (step.kind !== "write")
1958
+ held("witness-mismatch", ctx.step, "the stored grant differs from the operation's witness");
1959
+ ctx.step = "G5-precondition";
1960
+ if (!(await payloadHolds(ctx, operationId)))
1961
+ held("payload-precondition", ctx.step, "the payload precondition does not hold; nothing was issued");
1962
+ await issue("G5-append", policyKey, p.expectedPolicyRevision);
1963
+ const wrote = await put(authorityCtx(inv, op.notAfterMs, "G5-append"), policyKey, step.row, parseNativeServiceCallerPolicy, found.pol.state === "value" ? found.pol : undefined);
1964
+ if (wrote !== "moved")
1965
+ return adopt(candidate, wrote);
1966
+ ctx.step = "G5-append";
1967
+ const after = await lookup();
1968
+ if (after.grant !== undefined)
1969
+ return adopt(after.grant, after.revision);
1970
+ if (after.revision > p.expectedPolicyRevision)
1971
+ return refuse("conflict");
1972
+ return held("outcome-unknown", ctx.step, "the issued append's outcome stays unknown until its key moves");
1973
+ }
1974
+ }
1975
+ /** caller-revoke (K0-K3): deny-new, the op-bound cutoff marker and its fixed deadline. It answers
1976
+ * `pending` and never completes here: a terminal caller revocation needs the post-cutoff receipt
1977
+ * inventory and absence proof this host does not build. */
1978
+ async function callerRevoke(req, authorized) {
1979
+ if (!authorized)
1980
+ return unread(req.requestId, "permission-denied", "authorize");
1981
+ const operationId = serviceHostOperationId(space, req.requestId), requestDigest = requestDigestOf(req);
1982
+ const { grantee, grantId, expectedPolicyRevision } = req.input;
1983
+ const scope = { operation: "caller-revoke", operationId, proposalId: null, owner: null, endpoint: null, instanceId: null,
1984
+ lifecycleUid: null, granteeUid: grantee.lifecycleUid, cutoffOperationId: operationId };
1985
+ let found;
1986
+ // Discovery, read only: the cutoff operation is the one the grant's revoking status names, else this request's own.
1987
+ try {
1988
+ const dctx = discoveryCtx("discover");
1989
+ found = await readOp(dctx, operationId);
1990
+ const pol = await readPolicy(dctx, grantee.lifecycleUid);
1991
+ const g = pol.state === "value" ? pol.row.grants.find((x) => x.grantId === grantId) : undefined;
1992
+ if (g !== undefined && g.status.state !== "active")
1993
+ scope.cutoffOperationId = g.status.operationId;
1994
+ }
1995
+ catch (err) {
1996
+ if (err instanceof Held)
1997
+ return discoveryHeld(operationId, requestDigest, found, err);
1998
+ throw err;
1999
+ }
2000
+ const deadlineMs = Date.now() + S2A_INVOCATION_MS;
2001
+ try {
2002
+ const first = await invoke(scope, (inv) => attempt(inv, scope.cutoffOperationId), deadlineMs);
2003
+ if (!("rescope" in first))
2004
+ return first;
2005
+ // Another operation landed deny-new first. Every write this invocation issued had a known
2006
+ // outcome and its effect closed; reopen once, under the same deadline, for that operation's marker.
2007
+ if (Date.now() >= deadlineMs)
2008
+ return { state: "held", operationId, reason: "reconcile-pending", phase: "K1-rescope" };
2009
+ const second = await invoke({ ...scope, cutoffOperationId: first.rescope }, (inv) => attempt(inv, first.rescope), deadlineMs);
2010
+ return "rescope" in second ? { state: "held", operationId, reason: "scope-mismatch", phase: "K1-rescope" } : second;
2011
+ }
2012
+ catch (err) {
2013
+ if (err instanceof Held)
2014
+ return discoveryHeld(operationId, requestDigest, found, err);
2015
+ throw err;
2016
+ }
2017
+ async function attempt(inv, cutoff) {
2018
+ inv.operationId = operationId;
2019
+ try {
2020
+ const ctx = readCtx(inv, "K0-binding");
2021
+ for (let n = 0;; n++) {
2022
+ if (n >= S2A_MAX_REDECIDE)
2023
+ held("contention", ctx.step);
2024
+ const op = await readOp(ctx, operationId);
2025
+ if (op.state === "value" && op.row.requestDigest === requestDigest) {
2026
+ // The digest covers the granter, so a replay is by the recorded granter: an operator, or an
2027
+ // owner who is the witness's source owner or grantee owner.
2028
+ if (req.granter.kind === "owner" && op.row.kind === "service-host-operation" && op.row.plan.kind === "caller-revoke") {
2029
+ const w = op.row.plan;
2030
+ if (!((w.grant.source.kind === "service-owner" && w.grant.source.owner === req.granter.owner) || w.grantee.owner === req.granter.owner))
2031
+ return { state: "held", operationId, reason: "permission-denied", phase: "authorize" };
2032
+ }
2033
+ return await advanceCallerRevoke(inv, op.row, cutoff);
2034
+ }
2035
+ if (op.state === "value")
2036
+ return denied("conflict");
2037
+ inv.bound = false;
2038
+ ctx.step = "K0-admit";
2039
+ const pol = await readPolicy(ctx, grantee.lifecycleUid);
2040
+ const g = pol.state === "value" && boundTo(pol.row, grantee) ? pol.row.grants.find((x) => x.grantId === grantId) : undefined;
2041
+ if (g === undefined)
2042
+ return denied("not-found");
2043
+ // A delegated source is never written here, and only a source or grantee owner may revoke as an owner.
2044
+ if (g.source.kind !== "service-owner")
2045
+ return denied("unsupported");
2046
+ if (req.granter.kind === "owner" && req.granter.owner !== g.source.owner && req.granter.owner !== grantee.owner)
2047
+ return denied("not-found");
2048
+ if (g.status.state === "revoked")
2049
+ return denied("conflict");
2050
+ if (g.status.state === "active" && pol.revision !== expectedPolicyRevision)
2051
+ return denied("conflict");
2052
+ const adm = await readAdmission(ctx), now = Date.now();
2053
+ const { status: _status, ...body } = g;
2054
+ ctx.step = "K0-binding";
2055
+ const binding = { v: 1, kind: "service-host-operation", operationId, requestId: req.requestId, requestDigest, operation: "revoke",
2056
+ account: { accountPublicKey, space }, granter: req.granter, admittedAtMs: now, notAfterMs: now + S2A_OPERATION_TTL_MS,
2057
+ admissionEpoch: adm.state === "value" ? adm.row.epoch : 0,
2058
+ projection: { subjectOwner: g.source.owner, endpoint: g.endpoint, instanceId: g.instanceId, proposalId: null },
2059
+ plan: { kind: "caller-revoke", grantee, grantId, expectedPolicyRevision, request: req.input, grant: body }, progress: { state: "admitted" }, debt: [] };
2060
+ inv.bound = true;
2061
+ if ((await put(ctx, opKey(operationId), binding, parseServiceHostOperationRecord)) !== "moved")
2062
+ return await advanceCallerRevoke(inv, binding, cutoff);
2063
+ }
2064
+ }
2065
+ catch (err) {
2066
+ if (err instanceof Held)
2067
+ return heldArm(inv, err);
2068
+ throw err;
2069
+ }
2070
+ }
2071
+ /** K1-K3 from the stored witness. Each step narrows authority only, so none is bounded by the
2072
+ * operation's `notAfterMs`; each still ends at its invocation's deadline. */
2073
+ async function advanceCallerRevoke(inv, first, cutoff) {
2074
+ const ctx = readCtx(inv, "K-replay");
2075
+ const stored = outcomeOf(first);
2076
+ if (stored !== undefined)
2077
+ return stored.state === "complete" ? held("operation-malformed", ctx.step, "a caller revoke never completes") : finalArm(operationId, stored, debtOf(first));
2078
+ const p = first.plan, policyKey = servicePolicyKey(p.grantee.lifecycleUid);
2079
+ const witness = immutableGrantView(withStatus(p.grant, { state: "active" }));
2080
+ /** The grant from one read; anything but the witness's immutable view holds and writes nothing. */
2081
+ const lookup = async () => {
2082
+ const pol = await readPolicy(ctx, p.grantee.lifecycleUid);
2083
+ const grant = pol.state === "value" && boundTo(pol.row, p.grantee) ? pol.row.grants.find((x) => x.grantId === p.grantId) : undefined;
2084
+ if (grant === undefined || immutableGrantView(grant) !== witness)
2085
+ return held("witness-mismatch", ctx.step, "the grant no longer matches the operation's witness");
2086
+ return { pol: pol, grant };
2087
+ };
2088
+ // K1: deny-new under this operation, or the operation that already holds the grant's intent.
2089
+ let holder;
2090
+ for (let n = 0; holder === undefined; n++) {
2091
+ if (n >= S2A_MAX_REDECIDE)
2092
+ held("contention", "K1-deny-new");
2093
+ ctx.step = "K1-deny-new";
2094
+ const { pol, grant } = await lookup();
2095
+ if (grant.status.state === "revoked")
2096
+ held("terminal-unsupported", ctx.step, "a terminal caller revocation is outside this host");
2097
+ const step = callerBeginStep(pol.row, p.grantId, operationId);
2098
+ if (step.kind === "held") {
2099
+ holder = step.operationId;
2100
+ break;
2101
+ }
2102
+ if (step.kind !== "write")
2103
+ held("witness-mismatch", ctx.step, "the grant no longer matches the operation's witness");
2104
+ await assertWriteFits(ctx, step.row, operationId);
2105
+ if ((await put(ctx, policyKey, step.row, parseNativeServiceCallerPolicy, pol)) !== "moved")
2106
+ holder = operationId;
2107
+ }
2108
+ if (holder !== cutoff)
2109
+ return { rescope: holder };
2110
+ // K2: the holder's marker, created once from the witness; identical bytes are adopted.
2111
+ ctx.step = "K2-marker";
2112
+ const markerKey = cutoffKey(holder);
2113
+ const marker = { v: 1, kind: "service-cutoff-marker", accountPublicKey, space, operationId: holder,
2114
+ target: { kind: "caller-grant", grantee: p.grantee, grantId: p.grantId, endpoint: p.grant.endpoint, instanceId: p.grant.instanceId },
2115
+ authorityDigest: contractDigest({ v: 1, kind: "service-caller-grant-authority", accountPublicKey, space, grantee: p.grantee, grant: p.grant }) };
2116
+ let m = await leader(ctx, "auth", markerKey);
2117
+ if (m.state === "absent") {
2118
+ await put(ctx, markerKey, marker, parseServiceCutoffMarkerRecord);
2119
+ m = await leader(ctx, "auth", markerKey);
2120
+ }
2121
+ if (m.state === "marker")
2122
+ held("marker-tombstone", ctx.step, `${markerKey} carries a ${m.op} marker`);
2123
+ let same = false;
2124
+ if (m.state === "value") {
2125
+ try {
2126
+ same = contractDigest(parseServiceCutoffMarkerRecord(m.value)) === contractDigest(marker);
2127
+ }
2128
+ catch {
2129
+ same = false;
2130
+ }
2131
+ }
2132
+ if (m.state !== "value" || !same)
2133
+ return held("marker-conflict", ctx.step, "the cutoff marker key holds another value; it is never overwritten");
2134
+ const at = serviceMarkerCeilSeconds(m.timestamp);
2135
+ if (at === undefined)
2136
+ return held("marker-time-unknown", ctx.step, "the marker's stored time is not a readable server time; now is never substituted");
2137
+ const cut = { operationId: holder, markerKey, markerRevision: m.revision, notBefore: at + SERVICE_CUTOFF_MARGIN_S };
2138
+ // K3: the deadline, recorded once and never reset.
2139
+ for (let n = 0;; n++) {
2140
+ if (n >= S2A_MAX_REDECIDE)
2141
+ held("contention", "K3-deadline");
2142
+ ctx.step = "K3-deadline";
2143
+ const { pol } = await lookup();
2144
+ const step = callerCutoffStep(pol.row, p.grantId, cut);
2145
+ if (step.kind === "same")
2146
+ break;
2147
+ if (step.kind === "conflict")
2148
+ held("marker-conflict", ctx.step, step.detail);
2149
+ if (step.kind !== "write")
2150
+ held("witness-mismatch", ctx.step, "the grant no longer matches the operation's witness");
2151
+ await assertWriteFits(ctx, step.row, operationId);
2152
+ if ((await put(ctx, policyKey, step.row, parseNativeServiceCallerPolicy, pol)) !== "moved")
2153
+ break;
2154
+ }
2155
+ await updateOp(ctx, operationId, (cur) => (cur.progress.state === "custody" && cur.progress.step === "cutoff-recorded" ? undefined
2156
+ : isFinal(cur.progress) ? undefined : { ...cur, progress: { state: "custody", step: "cutoff-recorded" } })).catch((err) => { if (!(err instanceof Held))
2157
+ throw err; });
2158
+ // The phase is advisory, in the gate's whole-second convention; a terminal proof compares broker time.
2159
+ return { state: "pending", operationId, phase: seconds(Date.now()) < cut.notBefore ? "cutoff-wait" : "awaiting-terminal-proof", notBefore: cut.notBefore, denyNew: "landed" };
2160
+ }
2161
+ }
2162
+ /** A view of one row; a filtered view carries only that service's grants, and the revision is always the whole row's. */
2163
+ const policyView = (row, service) => ({
2164
+ grantee: { ...row.grantee }, service: service === null ? null : { endpoint: service.endpoint, instanceId: service.instanceId },
2165
+ grants: structuredClone(service === null ? row.grants : row.grants.filter((g) => g.endpoint === service.endpoint && g.instanceId === service.instanceId))
2166
+ });
2167
+ /** caller-policy-read: an operator reads any grantee; an owner reads its own grantees in full or
2168
+ * filtered, and another owner's grantee only filtered to a service the owner holds. */
2169
+ async function callerPolicyRead(req) {
2170
+ try {
2171
+ const ctx = discoveryCtx("caller-policy-read");
2172
+ const { grantee, service } = req.input, g = req.granter;
2173
+ if (g.kind === "owner" && grantee.owner !== g.owner) {
2174
+ if (service === null)
2175
+ return { state: "absent" };
2176
+ const asg = await readAssignment(ctx, service.endpoint, service.instanceId);
2177
+ if (asg.state === "absent" || asg.row.owner !== g.owner)
2178
+ return { state: "absent" };
2179
+ }
2180
+ const pol = await readPolicy(ctx, grantee.lifecycleUid);
2181
+ if (pol.state === "absent")
2182
+ return { state: "absent" };
2183
+ if (!boundTo(pol.row, grantee))
2184
+ return denied("conflict");
2185
+ return { state: "present", record: { value: policyView(pol.row, service), revision: pol.revision } };
2186
+ }
2187
+ catch (err) {
2188
+ if (err instanceof Held)
2189
+ return denied("unavailable");
2190
+ throw err;
2191
+ }
2192
+ }
2193
+ const listCursor = (service, ownerFilter, after) => Buffer.from(canonicalJson({ v: 1, op: "caller-policy-list", accountPublicKey, space, service, owner: ownerFilter, after }), "utf8").toString("base64url");
2194
+ /** caller-policy-list: one service's grantees, from its instance index, in uid order. Space-wide
2195
+ * enumeration is unsupported. An owner lists only a service it holds; an operator any. Orphan
2196
+ * entries are skipped, and an unreadable row fails the whole page. */
2197
+ async function callerPolicyList(req) {
2198
+ const { service, owner: ownerFilter, cursor, limit } = req.input;
2199
+ if (service === null)
2200
+ return denied("unsupported");
2201
+ const sel = { endpoint: service.endpoint, instanceId: service.instanceId };
2202
+ let after = null;
2203
+ if (cursor !== null) {
2204
+ try {
2205
+ after = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"))?.after;
2206
+ }
2207
+ catch {
2208
+ return denied("bad-request");
2209
+ }
2210
+ if (typeof after !== "string" || listCursor(sel, ownerFilter, after) !== cursor)
2211
+ return denied("bad-request");
2212
+ }
2213
+ try {
2214
+ const ctx = discoveryCtx("caller-policy-list");
2215
+ const empty = { state: "page", records: [], nextCursor: null };
2216
+ if (req.granter.kind === "owner") {
2217
+ const asg = await readAssignment(ctx, sel.endpoint, sel.instanceId);
2218
+ if (asg.state === "absent" || asg.row.owner !== req.granter.owner)
2219
+ return empty;
2220
+ }
2221
+ const idx = await readCallerIndex(ctx, sel.endpoint, sel.instanceId);
2222
+ const uids = [...new Set((idx.state === "value" ? idx.row.entries : []).map((e) => e.granteeUid))].filter((u) => after === null || u > after);
2223
+ const records = [];
2224
+ let last, more = false;
2225
+ for (const uid of uids) {
2226
+ const pol = await readPolicy(ctx, uid);
2227
+ if (pol.state === "absent")
2228
+ continue;
2229
+ if (pol.row.grantee.lifecycleUid !== uid || pol.row.accountPublicKey !== accountPublicKey || pol.row.space !== space)
2230
+ return denied("unavailable");
2231
+ const value = policyView(pol.row, sel);
2232
+ if (value.grants.length === 0 || (ownerFilter !== null && pol.row.grantee.owner !== ownerFilter))
2233
+ continue;
2234
+ if (records.length === limit) {
2235
+ more = true;
2236
+ break;
2237
+ }
2238
+ records.push({ value, revision: pol.revision });
2239
+ last = uid;
2240
+ }
2241
+ return { state: "page", records, nextCursor: more && last !== undefined ? listCursor(sel, ownerFilter, last) : null };
2242
+ }
2243
+ catch (err) {
2244
+ if (err instanceof Held)
2245
+ return denied("unavailable");
2246
+ throw err;
2247
+ }
2248
+ }
1470
2249
  async function operationRead(req) {
1471
2250
  try {
1472
2251
  const ctx = discoveryCtx("operation-read");
@@ -1480,11 +2259,14 @@ export async function openServiceHostOperations(opts) {
1480
2259
  if (o.kind === "service-host-operation-terminal")
1481
2260
  return { state: "present", record: { value: { operationId: o.operationId, requestDigest: o.requestDigest, operation: o.operation, projection, outcome: finalView(o.outcome) }, revision: op.revision } };
1482
2261
  const p = o.plan, assignmentDigest = p.kind === "approve" ? serviceAssignmentGate(discovery.host, p.reservation).binding.assignmentDigest : p.kind === "revoke" ? p.expectedAssignmentDigest : null;
2262
+ const target = p.kind === "caller-grant" || p.kind === "caller-revoke" ? { kind: "caller", grantee: { ...p.grantee } }
2263
+ : { kind: "service", endpoint: o.projection.endpoint, instanceId: o.projection.instanceId, assignmentDigest };
1483
2264
  const g2 = o.progress;
1484
- const progress = g2.state === "admitted" ? { state: "pending", phase: "admitted" } : g2.state === "custody" ? { state: "pending", phase: g2.step }
1485
- : g2.state === "held" ? { state: "held", reason: g2.reason, phase: g2.phase } : finalView(g2);
2265
+ const progress = g2.state === "admitted" ? { state: "pending", phase: "admitted" }
2266
+ : g2.state === "custody" || g2.state === "issued" ? { state: "pending", phase: g2.step }
2267
+ : g2.state === "held" ? { state: "held", reason: g2.reason, phase: g2.phase } : finalView(g2);
1486
2268
  return { state: "present", record: { value: { operationId: o.operationId, requestDigest: o.requestDigest, operation: o.operation,
1487
- target: { kind: "service", endpoint: o.projection.endpoint, instanceId: o.projection.instanceId, assignmentDigest }, projection, progress, debt: o.debt.map((d) => ({ ...d })) }, revision: op.revision } };
2269
+ target, projection, progress, debt: o.debt.map((d) => ({ ...d })) }, revision: op.revision } };
1488
2270
  }
1489
2271
  catch (err) {
1490
2272
  if (err instanceof Held)
@@ -2141,9 +2923,9 @@ export async function openServiceHostOperations(opts) {
2141
2923
  const out = (r) => ({ ...env, ...r });
2142
2924
  // The latch comes before the ledger and every broker read. A request that could bind holds under its
2143
2925
  // operation coordinate, with no deny-new claim because no gate was read; a read never binds.
2926
+ const binds = req.operation === "decide" || req.operation === "revoke" || req.operation === "name-bind" || req.operation === "caller-grant" || req.operation === "caller-revoke";
2144
2927
  if (tripped !== undefined || closed)
2145
- return out(req.operation === "decide" || req.operation === "revoke" || req.operation === "name-bind"
2146
- ? unread(req.requestId, "readiness", "open", req.operation === "revoke" ? req.input.expectedAssignmentDigest : undefined) : denied("unavailable"));
2928
+ return out(binds ? unread(req.requestId, "readiness", "open", req.operation === "revoke" ? req.input.expectedAssignmentDigest : undefined) : denied("unavailable"));
2147
2929
  const authorized = req.granter.kind === "operator" || interactive(req.granter.owner);
2148
2930
  switch (req.operation) {
2149
2931
  case "decide": return out(await decide(req, authorized));
@@ -2151,6 +2933,10 @@ export async function openServiceHostOperations(opts) {
2151
2933
  case "name-bind": return out(await nameBind(req));
2152
2934
  case "operation-read": return out(authorized ? await operationRead(req) : denied("permission-denied"));
2153
2935
  case "proposal-read": return out(authorized ? await proposalRead(req) : denied("permission-denied"));
2936
+ case "caller-grant": return out(await callerGrant(req, authorized));
2937
+ case "caller-revoke": return out(await callerRevoke(req, authorized));
2938
+ case "caller-policy-read": return out(authorized ? await callerPolicyRead(req) : denied("permission-denied"));
2939
+ case "caller-policy-list": return out(authorized ? await callerPolicyList(req) : denied("permission-denied"));
2154
2940
  default: return out(denied("unsupported"));
2155
2941
  }
2156
2942
  }