@githolon/dsl 0.103.18 → 0.104.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/dart/CHANGELOG.md +4 -0
- package/dart/lib/src/provider.dart +17 -8
- package/dart/lib/src/runner/node-entry.mjs +3 -2
- package/dart/lib/src/runner/offline-install.mjs +1 -1
- package/dart/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +2 -1
- package/src/aggregate.ts +43 -2
- package/src/capability_exports.ts +4 -1
- package/src/codegen_dart.ts +170 -2
- package/src/codegen_dart_client.ts +4 -1
- package/src/codegen_provider_dart.ts +81 -18
- package/src/compile_package_main.ts +4 -4
- package/src/framework/capability.ts +151 -42
- package/src/framework/impure_capability.ts +91 -36
- package/src/framework/owned_custody.ts +26 -2
- package/src/framework/sync_lifecycle.ts +23 -14
- package/src/fulfilment.ts +187 -0
- package/src/index.ts +8 -0
- package/src/law_graph_usda.ts +5 -0
- package/src/query/compile.ts +0 -0
|
@@ -27,15 +27,24 @@
|
|
|
27
27
|
* // capability "llm.complete" (the CapabilityBinding the executor must hold)
|
|
28
28
|
* ```
|
|
29
29
|
*
|
|
30
|
-
* THE RECEIPT
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* typed
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
30
|
+
* THE RECEIPT IS TYPED LAW. The complete Receipt's payload is the envelope
|
|
31
|
+
* `{requestId, completedAt}` MERGED with the declared receipt fields — typed USDA
|
|
32
|
+
* values, exactly as the obligation merges its request params. Each declared field
|
|
33
|
+
* becomes a first-class contract field in the emitted law (its own stable id and
|
|
34
|
+
* `nomos:type`), so the kernel can read it, the evolve gate governs it, and a
|
|
35
|
+
* wrong-typed or missing value is refused at the payload boundary.
|
|
36
|
+
*
|
|
37
|
+
* It previously travelled as a JSON object stringified into `resultRef` and re-parsed
|
|
38
|
+
* in the plan. That made the receipt half the one place a declared, typed shape
|
|
39
|
+
* degraded to an opaque string: invisible to the law, unindexable, and silently
|
|
40
|
+
* partial when a provider misspelled a key. `resultRef` now means only what its name
|
|
41
|
+
* says — the address of a produced ARTIFACT — and is required solely for a capability
|
|
42
|
+
* that declares no receipt fields, so a Receipt can never report nothing.
|
|
43
|
+
*
|
|
44
|
+
* Receipt fields are auto-`.optional()` on the AGGREGATE (absent while `requested` —
|
|
45
|
+
* the read-decode discipline) but required on the PAYLOAD unless the author declares
|
|
46
|
+
* them optional: a partial result is something you declare, not something you assume.
|
|
47
|
+
* Large bodies still belong in content-addressed `resultRef`/`resultEvidence` refs.
|
|
39
48
|
*
|
|
40
49
|
* Naming: the capability name uses the shipped binding grammar
|
|
41
50
|
* (`^[a-z0-9][a-z0-9._-]{0,63}$` — dots legal; marketplace interfaces are
|
|
@@ -55,10 +64,41 @@ import {
|
|
|
55
64
|
impureCapabilityInstanceId,
|
|
56
65
|
type ImpureCapability,
|
|
57
66
|
} from "./impure_capability.js";
|
|
67
|
+
import { key, relationRef, relationType, self, user } from "../relation_schema.js";
|
|
68
|
+
import { directive } from "../directive.js";
|
|
69
|
+
import { RelationTuple, removeTuple, writeTuple } from "./relations.js";
|
|
58
70
|
|
|
59
71
|
/** The shipped CapabilityBinding name grammar (worker + governance law agree). */
|
|
60
72
|
export const CAPABILITY_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
61
73
|
|
|
74
|
+
/**
|
|
75
|
+
* THE RECEIPT FENCE — who may close an obligation.
|
|
76
|
+
*
|
|
77
|
+
* A Receipt (`complete`/`fail`/`block`/`deadLetter`) is the moment an OUTSIDE effect becomes
|
|
78
|
+
* chain fact, so it is the one write that must prove it came from the provider the work was
|
|
79
|
+
* offered to. The proof is asymmetric and re-derived on every lane: the provider SIGNS its
|
|
80
|
+
* receipt with its DEVICE KEY, the kernel verifies that signature and resolves the author as
|
|
81
|
+
* `key:<hash>`, and this relation says whether that key may close obligations for THIS
|
|
82
|
+
* capability. No bearer token, nothing host-held, nothing that lands in committed state.
|
|
83
|
+
*
|
|
84
|
+
* The object is per-capability (`nomosCapability:<name>`), so a transcription provider cannot
|
|
85
|
+
* close a payments obligation. `user()` is allowed alongside `key()` for an operator closing a
|
|
86
|
+
* receipt by hand under an authenticated identity.
|
|
87
|
+
*/
|
|
88
|
+
export const CAPABILITY_PROVIDER_RELATION = relationRef("nomosCapabilityProvider");
|
|
89
|
+
|
|
90
|
+
/** The relation SCHEMA for the fence. Shipped automatically with every `capability()` (via the
|
|
91
|
+
* bundle's `relations` piece) because `.requires(...)` is INERT — it fails OPEN — in a law that
|
|
92
|
+
* carries no relation schema. A fence that silently enforces nothing is worse than none. */
|
|
93
|
+
export const capabilityProviderRelations = relationType("nomosCapability")
|
|
94
|
+
.asGlobal()
|
|
95
|
+
.relation(CAPABILITY_PROVIDER_RELATION, { rewrite: self(), allow: [user(), key()] });
|
|
96
|
+
|
|
97
|
+
/** The per-capability authz object a provider grant is written against. */
|
|
98
|
+
export function capabilityProviderObject(name: string): string {
|
|
99
|
+
return `nomosCapability:${name}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
62
102
|
/** "llm.complete" → "LlmComplete" — the deterministic id stem. */
|
|
63
103
|
export function capabilityPascal(name: string): string {
|
|
64
104
|
return name
|
|
@@ -81,7 +121,12 @@ type RequestShape<Req extends Record<string, Field>> = { [K in keyof Req]: Req[K
|
|
|
81
121
|
export interface CapabilityDecl<
|
|
82
122
|
Req extends Record<string, Field>,
|
|
83
123
|
Rec extends Record<string, Field>,
|
|
84
|
-
> extends ImpureCapability<
|
|
124
|
+
> extends ImpureCapability<
|
|
125
|
+
string,
|
|
126
|
+
RequestShape<Req>,
|
|
127
|
+
Req & Rec,
|
|
128
|
+
{ [K in keyof Rec]: Rec[K]["zod"] }
|
|
129
|
+
> {
|
|
85
130
|
/** The capability name — the `CapabilityBinding` slot the executor must hold. */
|
|
86
131
|
readonly name: string;
|
|
87
132
|
/**
|
|
@@ -111,8 +156,14 @@ export interface CapabilityDecl<
|
|
|
111
156
|
readonly tasks: QueryDecl;
|
|
112
157
|
/** The receipt fields' zod object — `{}` when the capability declares none. */
|
|
113
158
|
readonly receiptSchema: z.ZodObject<{ [K in keyof Rec]: Rec[K]["zod"] }>;
|
|
114
|
-
/**
|
|
115
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Validate typed receipt values against the declared receipt shape. A provider spreads the
|
|
161
|
+
* result into the `complete<Pascal>` payload alongside `{requestId, completedAt}`; there is
|
|
162
|
+
* no encode step, because receipt values travel as typed USDA exactly like request params.
|
|
163
|
+
*/
|
|
164
|
+
readonly parseResult: (values: { [K in keyof Rec]: z.infer<Rec[K]["zod"]> }) => {
|
|
165
|
+
[K in keyof Rec]: z.infer<Rec[K]["zod"]>;
|
|
166
|
+
};
|
|
116
167
|
}
|
|
117
168
|
|
|
118
169
|
/**
|
|
@@ -128,7 +179,22 @@ export function capability<
|
|
|
128
179
|
const Name extends string,
|
|
129
180
|
Req extends Record<string, Field>,
|
|
130
181
|
Rec extends Record<string, Field> = Record<never, Field>,
|
|
131
|
-
>(name: Name, spec: {
|
|
182
|
+
>(name: Name, spec: {
|
|
183
|
+
request: Req;
|
|
184
|
+
receipt?: Rec;
|
|
185
|
+
endpoint?: string;
|
|
186
|
+
declassify?: string;
|
|
187
|
+
/**
|
|
188
|
+
* THE ACKNOWLEDGED WAIVER — open this capability's Receipts to any authorised author.
|
|
189
|
+
*
|
|
190
|
+
* Receipts are fenced by default (see {@link CAPABILITY_PROVIDER_RELATION}): closing an
|
|
191
|
+
* obligation is how an outside effect becomes chain fact, so by default it takes a
|
|
192
|
+
* device-key-signed author holding provider authority for this capability. A genuinely
|
|
193
|
+
* public capability may waive that, but it says WHY in the law — the same shape as
|
|
194
|
+
* `declassify`, replayable and auditable, never a silent default.
|
|
195
|
+
*/
|
|
196
|
+
openReceipts?: string;
|
|
197
|
+
}): CapabilityDecl<Req, Rec> {
|
|
132
198
|
if (!CAPABILITY_NAME_RE.test(name)) {
|
|
133
199
|
throw new Error(
|
|
134
200
|
`capability('${name}'): the name must match ${CAPABILITY_NAME_RE} (lowercase, dot-namespaced — e.g. 'llm.complete'); it doubles as the CapabilityBinding slot the executor must hold`,
|
|
@@ -175,17 +241,6 @@ export function capability<
|
|
|
175
241
|
Object.entries(receipt).map(([k, f]) => [k, f.zod]),
|
|
176
242
|
) as { [K in keyof Rec]: Rec[K]["zod"] };
|
|
177
243
|
const receiptEntries = Object.entries(receipt) as [string, Field][];
|
|
178
|
-
const decodeReceipt = (resultRef: string): Record<string, unknown> => {
|
|
179
|
-
const decoded = JSON.parse(resultRef) as unknown;
|
|
180
|
-
if (decoded === null || typeof decoded !== "object" || Array.isArray(decoded)) {
|
|
181
|
-
throw new Error("a typed capability receipt must encode a JSON object");
|
|
182
|
-
}
|
|
183
|
-
const values = decoded as Record<string, unknown>;
|
|
184
|
-
for (const [k, f] of receiptEntries) {
|
|
185
|
-
if (values[k] !== undefined) f.zod.parse(values[k]);
|
|
186
|
-
}
|
|
187
|
-
return values;
|
|
188
|
-
};
|
|
189
244
|
|
|
190
245
|
const quartet = impureCapability({
|
|
191
246
|
aggregateId,
|
|
@@ -202,25 +257,17 @@ export function capability<
|
|
|
202
257
|
Object.keys(spec.request).map((k) =>
|
|
203
258
|
set(instance(taskAggregate, taskId), k as never, (p as Record<string, unknown>)[k] as never),
|
|
204
259
|
),
|
|
205
|
-
// The receipt
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
// Absent keys are skipped
|
|
209
|
-
// discipline), present keys must validate.
|
|
260
|
+
// The receipt half, symmetrical with the request half above: declared fields are merged
|
|
261
|
+
// onto the complete envelope as TYPED values and written straight through. The payload
|
|
262
|
+
// boundary has already validated them against the same zod the author declared, so the
|
|
263
|
+
// plan has nothing to decode. Absent keys are skipped — partial results stay legal.
|
|
210
264
|
...(receiptEntries.length > 0
|
|
211
265
|
? {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const ops: PlannedOp[] = [];
|
|
218
|
-
for (const [k, f] of receiptEntries) {
|
|
219
|
-
if (decoded[k] === undefined) continue;
|
|
220
|
-
ops.push(set(instance(taskAggregate, taskId), k as never, f.zod.parse(decoded[k]) as never));
|
|
221
|
-
}
|
|
222
|
-
return ops;
|
|
223
|
-
},
|
|
266
|
+
receiptSchema: receiptShape,
|
|
267
|
+
writeReceipt: (taskId: string, r: Record<string, unknown>): PlannedOp[] =>
|
|
268
|
+
receiptEntries
|
|
269
|
+
.filter(([k]) => r[k] !== undefined)
|
|
270
|
+
.map(([k]) => set(instance(taskAggregate, taskId), k as never, r[k] as never)),
|
|
224
271
|
}
|
|
225
272
|
: {}),
|
|
226
273
|
capability: name,
|
|
@@ -229,8 +276,70 @@ export function capability<
|
|
|
229
276
|
const camel = pascal[0]!.toLowerCase() + pascal.slice(1);
|
|
230
277
|
const tasks = query(`${camel}TasksByStatus`).key("status").returns(taskAggregate);
|
|
231
278
|
|
|
279
|
+
// THE GATED GRANT — never the raw tuple writer.
|
|
280
|
+
//
|
|
281
|
+
// `relateSubject`/`unrelateSubject` are UNGATED framework primitives; governance learned this
|
|
282
|
+
// the hard way (re-exporting them registered an ungated `workspace:self#admin@…` writer — a
|
|
283
|
+
// self-grant-admin hole). A law that exported them alongside this fence would let anyone
|
|
284
|
+
// self-grant provider authority and close their own obligations. So provider authority is
|
|
285
|
+
// seated ONLY through these admin-gated wrappers, scoped to THIS capability's object.
|
|
286
|
+
const grantPayload = z.object({
|
|
287
|
+
subject: z.string().min(1),
|
|
288
|
+
grantedBy: z.string().min(1),
|
|
289
|
+
grantedAt: z.string().min(1),
|
|
290
|
+
});
|
|
291
|
+
const grantProvider = directive(`grant${pascal}Provider`)
|
|
292
|
+
.ensures(RelationTuple)
|
|
293
|
+
.payload(grantPayload)
|
|
294
|
+
.plan((p) =>
|
|
295
|
+
writeTuple({
|
|
296
|
+
object: capabilityProviderObject(name),
|
|
297
|
+
relation: CAPABILITY_PROVIDER_RELATION,
|
|
298
|
+
subject: p.subject,
|
|
299
|
+
grantedBy: p.grantedBy,
|
|
300
|
+
grantedAt: p.grantedAt,
|
|
301
|
+
}),
|
|
302
|
+
)
|
|
303
|
+
.requires("admin");
|
|
304
|
+
const revokeProvider = directive(`revoke${pascal}Provider`)
|
|
305
|
+
.ensures(RelationTuple)
|
|
306
|
+
.payload(grantPayload.extend({ revokedBy: z.string().min(1), revokedAt: z.string().min(1) }))
|
|
307
|
+
.plan((p) =>
|
|
308
|
+
removeTuple({
|
|
309
|
+
object: capabilityProviderObject(name),
|
|
310
|
+
relation: CAPABILITY_PROVIDER_RELATION,
|
|
311
|
+
subject: p.subject,
|
|
312
|
+
grantedBy: p.grantedBy,
|
|
313
|
+
grantedAt: p.grantedAt,
|
|
314
|
+
revokedBy: p.revokedBy,
|
|
315
|
+
revokedAt: p.revokedAt,
|
|
316
|
+
}),
|
|
317
|
+
)
|
|
318
|
+
.requires("admin");
|
|
319
|
+
|
|
320
|
+
// THE FENCE, APPLIED. Every Receipt verb is gated on provider authority for THIS capability
|
|
321
|
+
// unless the law carries an explicit `openReceipts` waiver. The gate is the shipped native
|
|
322
|
+
// relation gate, so it is re-derived on author, edge admission AND verify_chain — a receipt
|
|
323
|
+
// forged on the open session lane never folds anywhere.
|
|
324
|
+
const fenced = <D extends { requires: (rel: string, opts?: { object?: string }) => unknown }>(d: D): D =>
|
|
325
|
+
spec.openReceipts !== undefined
|
|
326
|
+
? d
|
|
327
|
+
: (d.requires(CAPABILITY_PROVIDER_RELATION, {
|
|
328
|
+
object: capabilityProviderObject(name),
|
|
329
|
+
}) as D);
|
|
330
|
+
|
|
232
331
|
return {
|
|
233
332
|
...quartet,
|
|
333
|
+
complete: fenced(quartet.complete),
|
|
334
|
+
fail: fenced(quartet.fail),
|
|
335
|
+
block: fenced(quartet.block),
|
|
336
|
+
deadLetter: fenced(quartet.deadLetter),
|
|
337
|
+
// The relation SCHEMA rides with the bundle so the gate can never be inert (a law without
|
|
338
|
+
// a relation schema does not enforce `.requires` at all).
|
|
339
|
+
relations: capabilityProviderRelations,
|
|
340
|
+
grantProvider,
|
|
341
|
+
revokeProvider,
|
|
342
|
+
...(spec.openReceipts !== undefined ? { openReceipts: spec.openReceipts } : {}),
|
|
234
343
|
aggregate: taskAggregate,
|
|
235
344
|
instanceId: impureCapabilityInstanceId,
|
|
236
345
|
name,
|
|
@@ -238,7 +347,7 @@ export function capability<
|
|
|
238
347
|
...(spec.declassify !== undefined ? { declassify: spec.declassify } : {}),
|
|
239
348
|
requestSchema: z.object(requestShape),
|
|
240
349
|
receiptSchema: z.object(receiptShape),
|
|
241
|
-
|
|
350
|
+
parseResult: (values) => z.object(receiptShape).parse(values),
|
|
242
351
|
tasks,
|
|
243
352
|
// The one-export expansion tag (capability_exports.ts): every exports walk
|
|
244
353
|
// discovers this bundle's aggregate + quartet + scan query without re-exports.
|
|
@@ -102,12 +102,14 @@ export type ImpureCapabilityStatus = (typeof IMPURE_CAPABILITY_STATUSES)[number]
|
|
|
102
102
|
* - requestedBy Lww — who obligated the task (provenance). Immutable-after-create.
|
|
103
103
|
* - requestedAt Int — epoch-ms the obligation was authored. Immutable-after-create.
|
|
104
104
|
* - status Lww enum — `requested` → the reactor flips to a terminal status.
|
|
105
|
-
* - resultRef Lww string (OPTIONAL) — the content-addressed
|
|
106
|
-
* records on completion: a blob ref / download url.
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
105
|
+
* - resultRef Lww string (OPTIONAL) — the content-addressed ARTIFACT the provider
|
|
106
|
+
* records on completion: a blob ref / download url, and nothing else. It
|
|
107
|
+
* is honestly a STRING because an artifact address IS a string. A
|
|
108
|
+
* capability with something structured to report DECLARES receipt fields;
|
|
109
|
+
* those travel as typed USDA values merged onto the complete payload
|
|
110
|
+
* (symmetrically with request params) and fold into their own typed
|
|
111
|
+
* fields. `resultRef` is required only for a capability that declares no
|
|
112
|
+
* receipt fields, so a Receipt can never report nothing.
|
|
111
113
|
* Absent on a fresh request; absent on failure.
|
|
112
114
|
* - failureReason Lww (OPTIONAL) — set only when the reactor flips to `failed`.
|
|
113
115
|
* - completedAt Int (OPTIONAL) — epoch-ms the task reached its terminal state
|
|
@@ -193,14 +195,28 @@ const obligationEnvelopeSchema = z.object({
|
|
|
193
195
|
deadlineAt: z.number().int(),
|
|
194
196
|
});
|
|
195
197
|
|
|
196
|
-
/** The receipt-COMPLETE
|
|
197
|
-
const
|
|
198
|
+
/** The receipt-COMPLETE envelope every capability carries, whatever its result shape. */
|
|
199
|
+
const completeEnvelopeSchema = z.object({
|
|
198
200
|
requestId: z.string(),
|
|
199
|
-
// The produced artifact's content-addressed ref (blob ref / url) as a JSON string.
|
|
200
|
-
resultRef: z.string(),
|
|
201
201
|
completedAt: z.number().int(),
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* The receipt-COMPLETE payload for a capability declaring NO typed receipt fields: the
|
|
206
|
+
* envelope plus a required content-addressed ref to the produced ARTIFACT (blob ref / url).
|
|
207
|
+
* It is an artifact address, never an envelope for typed values — a capability that has
|
|
208
|
+
* something to say declares receipt fields and says it in typed USDA.
|
|
209
|
+
*/
|
|
210
|
+
const completeSchema = completeEnvelopeSchema.extend({
|
|
211
|
+
resultRef: z.string(),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/** The complete envelope for a capability whose result rides in DECLARED receipt fields:
|
|
215
|
+
* the artifact address becomes optional, because the typed fields carry the outcome. */
|
|
216
|
+
const completeArtifactOptionalSchema = completeEnvelopeSchema.extend({
|
|
217
|
+
resultRef: z.string().optional(),
|
|
218
|
+
});
|
|
219
|
+
|
|
204
220
|
/** The receipt-FAIL payload: the request id + the failure reason + when. */
|
|
205
221
|
const failSchema = z.object({
|
|
206
222
|
requestId: z.string(),
|
|
@@ -272,13 +288,15 @@ export function obligationOps(
|
|
|
272
288
|
*/
|
|
273
289
|
export function completeOps(
|
|
274
290
|
aggregateId: string,
|
|
275
|
-
p: z.infer<typeof
|
|
291
|
+
p: z.infer<typeof completeEnvelopeSchema> & { resultRef?: string },
|
|
276
292
|
resultOps: PlannedOp[] = [],
|
|
277
293
|
): PlannedOp[] {
|
|
278
294
|
const taskInstance = envelopeInstance(aggregateId, p.requestId);
|
|
279
295
|
return [
|
|
280
296
|
set(taskInstance, "status", "completed"),
|
|
281
|
-
|
|
297
|
+
// Present iff this capability produced an ARTIFACT. A typed-receipt capability carries
|
|
298
|
+
// its result in its declared fields instead, so it writes no artifact address at all.
|
|
299
|
+
...(p.resultRef !== undefined ? [set(taskInstance, "resultRef", p.resultRef)] : []),
|
|
282
300
|
set(taskInstance, "completedAt", p.completedAt),
|
|
283
301
|
// The FRAMEWORK Obligation↔Receipt CLOSURE link — this Receipt closes the Obligation whose
|
|
284
302
|
// id == requestId (the task instance the reactor mutates). Indexed by the read
|
|
@@ -349,16 +367,32 @@ export function deadLetterOps(
|
|
|
349
367
|
* • `complete` — `.mutates` directive (the reactor's success Receipt),
|
|
350
368
|
* • `fail` — `.mutates` directive (the reactor's failure Receipt),
|
|
351
369
|
* plus the deterministic `instanceId` rule. The `obligation` payload is the framework
|
|
352
|
-
* obligation-envelope MERGED with the tenant's `paramsSchema`; `complete
|
|
370
|
+
* obligation-envelope MERGED with the tenant's `paramsSchema`; the `complete` payload is
|
|
371
|
+
* SYMMETRICALLY the complete-envelope merged with the tenant's `receiptSchema` (typed USDA
|
|
372
|
+
* values, never an encoded blob); `fail`/`block`/`deadLetter` are fixed.
|
|
353
373
|
*/
|
|
374
|
+
// Infer from the EXTENDED schema, exactly as ObligationPayload does — a conditional or an
|
|
375
|
+
// intersection around this turns a `.optional()` receipt field into a REQUIRED key of type
|
|
376
|
+
// `T | undefined` under exactOptionalPropertyTypes, so a genuinely optional receipt field
|
|
377
|
+
// could not be omitted. The receipt-free case is then the same shape with no extra keys,
|
|
378
|
+
// where `resultRef` is separately required (see `completeSchema`).
|
|
379
|
+
export type CompletePayload<ReceiptShape extends z.ZodRawShape> = z.infer<
|
|
380
|
+
ReturnType<typeof completeArtifactOptionalSchema.extend<ReceiptShape>>
|
|
381
|
+
> &
|
|
382
|
+
// A capability declaring NO receipt fields must still report SOMETHING, so its artifact
|
|
383
|
+
// address stays required. Intersected only in that branch — where there are no receipt
|
|
384
|
+
// fields whose optionality it could flatten.
|
|
385
|
+
(keyof ReceiptShape extends never ? { resultRef: string } : unknown);
|
|
386
|
+
|
|
354
387
|
export interface ImpureCapability<
|
|
355
388
|
Id extends string,
|
|
356
389
|
ParamsShape extends z.ZodRawShape,
|
|
357
390
|
F extends Record<string, Field>,
|
|
391
|
+
ReceiptShape extends z.ZodRawShape = Record<never, never>,
|
|
358
392
|
> {
|
|
359
393
|
readonly aggregate: AggregateHandle<Id, typeof IMPURE_CAPABILITY_ENVELOPE & F>;
|
|
360
394
|
readonly obligation: RequirableDirective<ObligationPayload<ParamsShape>>;
|
|
361
|
-
readonly complete: RequirableDirective<
|
|
395
|
+
readonly complete: RequirableDirective<CompletePayload<ReceiptShape>>;
|
|
362
396
|
readonly fail: RequirableDirective<z.infer<typeof failSchema>>;
|
|
363
397
|
readonly block: RequirableDirective<z.infer<typeof blockSchema>>;
|
|
364
398
|
readonly deadLetter: RequirableDirective<z.infer<typeof deadLetterSchema>>;
|
|
@@ -429,6 +463,7 @@ export function impureCapability<
|
|
|
429
463
|
const FailId extends string,
|
|
430
464
|
ParamsShape extends z.ZodRawShape,
|
|
431
465
|
F extends Record<string, Field>,
|
|
466
|
+
ReceiptShape extends z.ZodRawShape = Record<never, never>,
|
|
432
467
|
>(spec: {
|
|
433
468
|
aggregateId: Id;
|
|
434
469
|
obligationDirectiveId: ObligationId;
|
|
@@ -446,29 +481,33 @@ export function impureCapability<
|
|
|
446
481
|
params: z.infer<z.ZodObject<ParamsShape>>,
|
|
447
482
|
) => PlannedOp[];
|
|
448
483
|
/**
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
* the
|
|
484
|
+
* The task's RECEIPT Zod SHAPE — merged onto the complete envelope schema exactly as
|
|
485
|
+
* `paramsSchema` is merged onto the obligation envelope. A declared receipt travels as
|
|
486
|
+
* TYPED USDA VALUES on the wire, symmetrically with the request half.
|
|
487
|
+
*
|
|
488
|
+
* It previously rode as a JSON object stringified into `resultRef` and re-parsed in the
|
|
489
|
+
* plan. That made the receipt half of a capability the one place in the framework where a
|
|
490
|
+
* declared, typed shape degraded to an opaque string — unqueryable until decoded, invisible
|
|
491
|
+
* to the value algebra, and (in the kernel's owner-recovery lane) silently empty on a
|
|
492
|
+
* malformed parse. Receipts are law, so they are typed like law.
|
|
457
493
|
*/
|
|
458
|
-
|
|
494
|
+
receiptSchema?: ReceiptShape;
|
|
459
495
|
/**
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
496
|
+
* Plan fragment writing the tenant's RECEIPT fields onto the task aggregate when the
|
|
497
|
+
* `complete` Receipt is authored (the lifecycle envelope + the `closesObligation` link is
|
|
498
|
+
* auto). The mirror of {@link writeParams}. Supplied iff `receiptSchema` is.
|
|
463
499
|
*/
|
|
464
|
-
|
|
500
|
+
writeReceipt?: (
|
|
501
|
+
taskInstanceId: string,
|
|
502
|
+
receipt: Record<string, unknown>,
|
|
503
|
+
) => PlannedOp[];
|
|
465
504
|
/**
|
|
466
505
|
* OPTIONAL: the capability-credential name the provider host must hold a
|
|
467
506
|
* law-state `CapabilityBinding` for (see {@link ImpureCapability.capability}).
|
|
468
507
|
* Omit-when-absent — declaring nothing changes nothing on the wire.
|
|
469
508
|
*/
|
|
470
509
|
capability?: string;
|
|
471
|
-
}): ImpureCapability<Id, ParamsShape, F> {
|
|
510
|
+
}): ImpureCapability<Id, ParamsShape, F, ReceiptShape> {
|
|
472
511
|
const taskAggregate = impureCapabilityAggregate(spec.aggregateId, spec.params);
|
|
473
512
|
const typedRequestId = { requestId: idOf(taskAggregate) };
|
|
474
513
|
const obligationPayloadBase = obligationEnvelopeSchema.extend(spec.paramsSchema);
|
|
@@ -476,9 +515,17 @@ export function impureCapability<
|
|
|
476
515
|
// replaces; its extra aggregate identity lives in Zod metadata for lint/codegen.
|
|
477
516
|
// Preserve the generic payload type while installing that richer runtime schema.
|
|
478
517
|
const obligationPayload = obligationPayloadBase.extend(typedRequestId) as typeof obligationPayloadBase;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
518
|
+
// THE RECEIPT IS TYPED LAW, SYMMETRICAL WITH THE REQUEST. A capability that declares
|
|
519
|
+
// receipt fields carries them as typed values merged onto the complete envelope, exactly
|
|
520
|
+
// as the obligation merges its params. `resultRef` then keeps only its honest meaning —
|
|
521
|
+
// a content-addressed ref to a produced ARTIFACT — and is required solely when there are
|
|
522
|
+
// no declared receipt fields to carry the result, so a receipt can never say nothing.
|
|
523
|
+
const typedCompleteBase = (spec.receiptSchema !== undefined
|
|
524
|
+
? completeArtifactOptionalSchema
|
|
525
|
+
: completeSchema
|
|
526
|
+
).extend(typedRequestId);
|
|
527
|
+
const typedCompleteSchema = spec.receiptSchema !== undefined
|
|
528
|
+
? typedCompleteBase.extend(spec.receiptSchema)
|
|
482
529
|
: typedCompleteBase;
|
|
483
530
|
const typedFailSchema = failSchema.extend(typedRequestId);
|
|
484
531
|
const typedBlockSchema = blockSchema.extend(typedRequestId);
|
|
@@ -503,15 +550,23 @@ export function impureCapability<
|
|
|
503
550
|
);
|
|
504
551
|
});
|
|
505
552
|
|
|
506
|
-
|
|
553
|
+
// The two branches above are distinct object schemas; the declared payload TYPE is the
|
|
554
|
+
// `CompletePayload<ReceiptShape>` conditional, so name it once here rather than leaking a
|
|
555
|
+
// union through the directive's public type.
|
|
556
|
+
const completePayload = typedCompleteSchema as unknown as z.ZodType<CompletePayload<ReceiptShape>>;
|
|
557
|
+
|
|
558
|
+
const complete: RequirableDirective<CompletePayload<ReceiptShape>> = directive(
|
|
559
|
+
spec.completeDirectiveId,
|
|
560
|
+
)
|
|
507
561
|
.mutates(taskAggregate)
|
|
508
|
-
.payload(
|
|
562
|
+
.payload(completePayload)
|
|
509
563
|
.plan((p) =>
|
|
510
564
|
completeOps(
|
|
511
565
|
spec.aggregateId,
|
|
512
|
-
p,
|
|
513
|
-
//
|
|
514
|
-
|
|
566
|
+
p as z.infer<typeof completeEnvelopeSchema> & { resultRef?: string },
|
|
567
|
+
// The tenant's TYPED receipt fields, read straight off the validated payload —
|
|
568
|
+
// no decode step, because there is no encoding to undo.
|
|
569
|
+
spec.writeReceipt?.(impureCapabilityInstanceId((p as { requestId: string }).requestId), p as Record<string, unknown>) ?? [],
|
|
515
570
|
),
|
|
516
571
|
);
|
|
517
572
|
|
|
@@ -65,8 +65,28 @@ const recoveryOrderParams = {
|
|
|
65
65
|
deviceRequest: t.string().merge(Lww),
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* THE RECOVERY RECEIPT'S OWN DECLARED FIELD. The re-wrapped owned custody the recovery
|
|
70
|
+
* principal produces is the RESULT of this capability, so it is a typed receipt field.
|
|
71
|
+
*
|
|
72
|
+
* It previously rode as a JSON object stringified into `resultRef`, which the kernel dug back
|
|
73
|
+
* out with `serde_json::from_str(...).unwrap_or_default()` — so a malformed Receipt silently
|
|
74
|
+
* read as an EMPTY custody string on the accept leg rather than refusing. Custody handoff is
|
|
75
|
+
* the last thing that should degrade quietly.
|
|
76
|
+
*/
|
|
77
|
+
const recoveryReceipt = {
|
|
78
|
+
ownedCustody: z.string().min(1),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Order fields ⊕ the receipt field, so the aggregate can hold what the Receipt writes. */
|
|
82
|
+
const recoveryOrderFields = {
|
|
83
|
+
...recoveryOrderParams,
|
|
84
|
+
// Absent until the recovery principal's Receipt lands (the read-decode discipline).
|
|
85
|
+
ownedCustody: t.string().merge(Lww).optional(),
|
|
86
|
+
};
|
|
87
|
+
|
|
68
88
|
/** Private framework Order -> Receipt journal. Generated business clients never expose this aggregate. */
|
|
69
|
-
export const OwnedRecoveryOrder = impureCapabilityAggregate("NomosOwnedRecoveryOrder",
|
|
89
|
+
export const OwnedRecoveryOrder = impureCapabilityAggregate("NomosOwnedRecoveryOrder", recoveryOrderFields);
|
|
70
90
|
const ownedRecoveryLifecycle = impureCapability({
|
|
71
91
|
aggregateId: OwnedRecoveryOrder.id,
|
|
72
92
|
obligationDirectiveId: OWNED_RECOVERY_REQUEST_DIRECTIVE,
|
|
@@ -74,7 +94,7 @@ const ownedRecoveryLifecycle = impureCapability({
|
|
|
74
94
|
failDirectiveId: OWNED_RECOVERY_FAIL_DIRECTIVE,
|
|
75
95
|
blockDirectiveId: OWNED_RECOVERY_BLOCK_DIRECTIVE,
|
|
76
96
|
deadLetterDirectiveId: OWNED_RECOVERY_DEAD_LETTER_DIRECTIVE,
|
|
77
|
-
params:
|
|
97
|
+
params: recoveryOrderFields,
|
|
78
98
|
paramsSchema: {
|
|
79
99
|
ownerPrincipal: principal,
|
|
80
100
|
recoveryPrincipal: principal,
|
|
@@ -85,6 +105,10 @@ const ownedRecoveryLifecycle = impureCapability({
|
|
|
85
105
|
set(instance(OwnedRecoveryOrder, id), "recoveryPrincipal", p.recoveryPrincipal),
|
|
86
106
|
set(instance(OwnedRecoveryOrder, id), "deviceRequest", p.deviceRequest),
|
|
87
107
|
],
|
|
108
|
+
receiptSchema: recoveryReceipt,
|
|
109
|
+
writeReceipt: (id, r) => [
|
|
110
|
+
set(instance(OwnedRecoveryOrder, id), "ownedCustody", r.ownedCustody as string),
|
|
111
|
+
],
|
|
88
112
|
});
|
|
89
113
|
|
|
90
114
|
export const ownedRecoveryOrdersByRecoveryPrincipal = query("nomosOwnedRecoveryOrdersByRecoveryPrincipal")
|
|
@@ -63,22 +63,30 @@ function optionalParamOps(
|
|
|
63
63
|
return ops;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The sync Receipt's own declared shape. These four were previously smuggled through
|
|
68
|
+
* `resultRef` as a JSON object and recovered with `typeof`/`Number.isInteger` guards — the
|
|
69
|
+
* receipt of the framework's OWN capability was untyped, so a provider that misspelled a key
|
|
70
|
+
* or sent a string count simply had that value dropped on the floor. They are ordinary typed
|
|
71
|
+
* receipt fields now: the payload boundary validates them and a wrong shape is refused.
|
|
72
|
+
*/
|
|
73
|
+
const syncReceipt = {
|
|
74
|
+
resultHead: z.string().optional(),
|
|
75
|
+
resultCursor: z.string().optional(),
|
|
76
|
+
appliedIntentCount: z.number().int().optional(),
|
|
77
|
+
pushedIntentCount: z.number().int().optional(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function syncResultOps(taskId: string, r: Record<string, unknown>): PlannedOp[] {
|
|
67
81
|
const sync = instance(WorkspaceSyncOperation, taskId);
|
|
68
82
|
const ops: PlannedOp[] = [];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
appliedIntentCount
|
|
73
|
-
pushedIntentCount?: unknown;
|
|
74
|
-
};
|
|
75
|
-
if (typeof result.head === "string") ops.push(set(sync, "resultHead", result.head));
|
|
76
|
-
if (typeof result.cursor === "string") ops.push(set(sync, "resultCursor", result.cursor));
|
|
77
|
-
if (Number.isInteger(result.appliedIntentCount)) {
|
|
78
|
-
ops.push(set(sync, "appliedIntentCount", result.appliedIntentCount as number));
|
|
83
|
+
if (r.resultHead !== undefined) ops.push(set(sync, "resultHead", r.resultHead as string));
|
|
84
|
+
if (r.resultCursor !== undefined) ops.push(set(sync, "resultCursor", r.resultCursor as string));
|
|
85
|
+
if (r.appliedIntentCount !== undefined) {
|
|
86
|
+
ops.push(set(sync, "appliedIntentCount", r.appliedIntentCount as number));
|
|
79
87
|
}
|
|
80
|
-
if (
|
|
81
|
-
ops.push(set(sync, "pushedIntentCount",
|
|
88
|
+
if (r.pushedIntentCount !== undefined) {
|
|
89
|
+
ops.push(set(sync, "pushedIntentCount", r.pushedIntentCount as number));
|
|
82
90
|
}
|
|
83
91
|
return ops;
|
|
84
92
|
}
|
|
@@ -110,7 +118,8 @@ const workspaceSyncTask = impureCapability({
|
|
|
110
118
|
...optionalParamOps(sync, p),
|
|
111
119
|
];
|
|
112
120
|
},
|
|
113
|
-
|
|
121
|
+
receiptSchema: syncReceipt,
|
|
122
|
+
writeReceipt: syncResultOps,
|
|
114
123
|
});
|
|
115
124
|
|
|
116
125
|
export const requestWorkspaceSync = workspaceSyncTask.obligation.requires("requestWorkspaceSync");
|