@githolon/dsl 0.14.0 → 0.16.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/example/nomos_webview_transport.dart +2 -2
- package/dart/lib/nomos_dsl.dart +4 -4
- package/dart/lib/src/{dispatch.dart → offer.dart} +7 -7
- package/dart/lib/src/provider.dart +30 -30
- package/dart/lib/src/subscriptions.dart +3 -3
- package/dart/lib/src/webview_bridge.dart +56 -21
- package/dart/test/webview_bridge_test.dart +35 -34
- package/package.json +5 -3
- package/src/aggregate.ts +9 -1
- package/src/authoring.ts +10 -1
- package/src/build_package.ts +1 -1
- package/src/capability_exports.ts +2 -2
- package/src/codegen_dart.ts +34 -24
- package/src/codegen_dart_client.ts +74 -12
- package/src/codegen_provider_dart.ts +67 -61
- package/src/codegen_ts.ts +45 -17
- package/src/compile_package_main.ts +103 -36
- package/src/ctx.ts +13 -0
- package/src/engine_entry.ts +22 -5
- package/src/fields.ts +44 -0
- package/src/flow_gate.ts +208 -0
- package/src/framework/capability.ts +29 -11
- package/src/framework/encryption_scope.ts +9 -1
- package/src/framework/genesis.ts +5 -1
- package/src/framework/impure_capability.ts +73 -67
- package/src/framework/relations.ts +23 -4
- package/src/framework/signing_identity.ts +1 -1
- package/src/framework/sync_lifecycle.ts +3 -3
- package/src/framework/workspace_invariant.ts +29 -8
- package/src/framework/workspaces.ts +7 -0
- package/src/index.ts +25 -1
- package/src/manifest.ts +11 -17
- package/src/ops.ts +42 -1
- package/src/principal_evidence.ts +112 -0
- package/src/relation_sugar.ts +10 -4
- package/src/shape_compat.ts +208 -0
- package/src/usd.ts +287 -0
- package/src/usd_layers.ts +213 -26
- package/src/wire.ts +8 -0
- package/src/wire_encode.ts +75 -2
- package/src/workspace_sharding.ts +197 -24
- package/src/workspace_type.ts +1 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* framework/impure_capability.ts — the FRAMEWORK
|
|
9
|
+
* framework/impure_capability.ts — the FRAMEWORK Obligation→Receipt mechanism (tenant-agnostic).
|
|
10
10
|
*
|
|
11
11
|
* ── Why this module exists (the framework↔tenant line) ───────────────────────
|
|
12
12
|
* A *provider-side intent* in Nomos 1 was an ephemeral job: dispatch → the
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Nomos 2's law is "**every authored intent folds SOME aggregate**" (contract §1) —
|
|
19
19
|
* there is no ephemeral job. The faithful shape of an async impure capability is therefore
|
|
20
|
-
* an **
|
|
20
|
+
* an **Obligation → Receipt** pair of ordinary `WireIntent`s with a first-class lifecycle law:
|
|
21
21
|
*
|
|
22
|
-
* • **
|
|
23
|
-
* `
|
|
24
|
-
* `.creates` a task aggregate keyed by a KERNEL-MINTED
|
|
25
|
-
* (`<
|
|
22
|
+
* • **Obligation** — a normal intent whose role is the request: a peer dispatches
|
|
23
|
+
* `oblige…(params, status:'requested')`,
|
|
24
|
+
* `.creates` a task aggregate keyed by a KERNEL-MINTED obligation id
|
|
25
|
+
* (`<ObligationType>_<uuidv7>`, via the marker-driven front-door; idempotent on
|
|
26
26
|
* replay by capture).
|
|
27
27
|
* • **(impure work)** — a capability provider watches `status:'requested'` rows, performs
|
|
28
28
|
* the side-effect (rendering / upload / multi-target provider work — a host/report
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
* Some domains also expose explicit blocked/DLQ receipts using the
|
|
34
34
|
* same envelope fields below.
|
|
35
35
|
*
|
|
36
|
-
*
|
|
36
|
+
* Obligation and Receipt are roles/facets of `Intent`, not special admission objects, job-table rows,
|
|
37
37
|
* or a privileged mutation channel.
|
|
38
38
|
*
|
|
39
39
|
* **Reporting upward = the authoring peer reactively WATCHES the task aggregate.** The receipt
|
|
40
40
|
* arrives as an ordinary projection update (no polling, no fire-and-forget push, no
|
|
41
41
|
* response-mutation). This is exactly the proven `ExportJobAggregate` shape
|
|
42
42
|
* (the tenant domains package `thing_structures`) generalised into a reusable framework pattern so a
|
|
43
|
-
* domain dev does NOT hand-roll the {aggregate +
|
|
43
|
+
* domain dev does NOT hand-roll the {aggregate + obligation + complete + fail} quartet each
|
|
44
44
|
* time — they DECLARE the task's params and get the quartet for free, like `relations.ts`
|
|
45
45
|
* gives grant/revoke for free.
|
|
46
46
|
*
|
|
@@ -77,7 +77,7 @@ import type { Ports } from "../ctx.js";
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* The impure-capability lifecycle vocabulary — framework-fixed. A task is `requested` when
|
|
80
|
-
* the peer
|
|
80
|
+
* the peer obligates it; the provider authors the ordinary Receipt intent that flips it
|
|
81
81
|
* to `completed` (with a `resultRef`) or `failed` (with a `failureReason`). One-way after a
|
|
82
82
|
* terminal state: the lifecycle law is terminal-once at the gate/envelope, not a separate
|
|
83
83
|
* privileged mutation channel.
|
|
@@ -92,14 +92,14 @@ export const IMPURE_CAPABILITY_STATUSES = [
|
|
|
92
92
|
export type ImpureCapabilityStatus = (typeof IMPURE_CAPABILITY_STATUSES)[number];
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* The framework envelope fields EVERY impure-capability aggregate carries — the
|
|
95
|
+
* The framework envelope fields EVERY impure-capability aggregate carries — the Obligation→Receipt
|
|
96
96
|
* lifecycle, independent of the tenant's request params:
|
|
97
97
|
*
|
|
98
98
|
* - requestId Lww — the task's own id (== the aggregate instance id; a
|
|
99
|
-
* KERNEL-MINTED `<
|
|
99
|
+
* KERNEL-MINTED `<ObligationType>_<uuidv7>`). Immutable-after-create (the
|
|
100
100
|
* create-only gap; modelled Lww).
|
|
101
|
-
* - requestedBy Lww — who
|
|
102
|
-
* - requestedAt Int — epoch-ms the
|
|
101
|
+
* - requestedBy Lww — who obligated the task (provenance). Immutable-after-create.
|
|
102
|
+
* - requestedAt Int — epoch-ms the obligation was authored. Immutable-after-create.
|
|
103
103
|
* - status Lww enum — `requested` → the reactor flips to a terminal status.
|
|
104
104
|
* - resultRef Lww json (OPTIONAL) — the content-addressed RESULT the reactor
|
|
105
105
|
* records on completion (a blob ref / download url JSON leaf). Absent
|
|
@@ -111,10 +111,10 @@ export type ImpureCapabilityStatus = (typeof IMPURE_CAPABILITY_STATUSES)[number]
|
|
|
111
111
|
* external condition.
|
|
112
112
|
* - blockedAt Int (OPTIONAL) — epoch-ms the task was parked as blocked.
|
|
113
113
|
* - nextAction Lww (OPTIONAL) — human/host-action hint, e.g. refreshAuth.
|
|
114
|
-
* - deadLetterReason Lww (OPTIONAL) — why the
|
|
115
|
-
* - deadLetteredAt Int (OPTIONAL) — epoch-ms the
|
|
114
|
+
* - deadLetterReason Lww (OPTIONAL) — why the obligation was moved to the DLQ.
|
|
115
|
+
* - deadLetteredAt Int (OPTIONAL) — epoch-ms the obligation entered the DLQ.
|
|
116
116
|
*
|
|
117
|
-
* The OPTIONALs are `.optional()` so a freshly-
|
|
117
|
+
* The OPTIONALs are `.optional()` so a freshly-obligated task (which folds none of them)
|
|
118
118
|
* decodes on the generated read type — the same read-decode discipline the rest of the
|
|
119
119
|
* framework uses (identity NOTE 1a / gap 1).
|
|
120
120
|
*/
|
|
@@ -122,6 +122,8 @@ export const IMPURE_CAPABILITY_ENVELOPE = {
|
|
|
122
122
|
requestId: t.string().merge(Lww),
|
|
123
123
|
requestedBy: t.string().merge(Lww),
|
|
124
124
|
requestedAt: t.int().merge(Lww),
|
|
125
|
+
scheduledAt: t.int().merge(Lww),
|
|
126
|
+
deadlineAt: t.int().merge(Lww),
|
|
125
127
|
status: t.enum(IMPURE_CAPABILITY_STATUSES).merge(Lww),
|
|
126
128
|
resultRef: t.json().merge(Lww).optional(),
|
|
127
129
|
failureReason: t.string().merge(Lww).optional(),
|
|
@@ -131,16 +133,16 @@ export const IMPURE_CAPABILITY_ENVELOPE = {
|
|
|
131
133
|
nextAction: t.string().merge(Lww).optional(),
|
|
132
134
|
deadLetterReason: t.string().merge(Lww).optional(),
|
|
133
135
|
deadLetteredAt: t.int().merge(Lww).optional(),
|
|
134
|
-
// The FRAMEWORK
|
|
136
|
+
// The FRAMEWORK Obligation↔Receipt CLOSURE link (OPTIONAL): the id of the Obligation this
|
|
135
137
|
// RECEIPT closes (== the task's own requestId — the reactor mutates the task instance).
|
|
136
138
|
// GENERIC, written by every impureCapability Receipt (complete OR fail), so the read
|
|
137
|
-
// projection answers "is
|
|
138
|
-
// off the `(
|
|
139
|
-
// `requested` (a fresh
|
|
139
|
+
// projection answers "is Obligation O closed, by which Receipt, with what result" in O(1)
|
|
140
|
+
// off the `(closesObligation, obligation_id)` index — never a ledger scan. Absent while
|
|
141
|
+
// `requested` (a fresh obligation has no closing receipt). The kernel supplies its
|
|
140
142
|
// framework-fixed Lww driver (`kernel::manifest_view::IMPURE_CAPABILITY_RECEIPT_FIELDS`)
|
|
141
|
-
// and the gate enforces the lifecycle around it (`nomos_executor::
|
|
142
|
-
// admit step 5.5: OrphanReceipt /
|
|
143
|
-
|
|
143
|
+
// and the gate enforces the lifecycle around it (`nomos_executor::check_obligation_receipt` —
|
|
144
|
+
// admit step 5.5: OrphanReceipt / ReceiptOnTerminalObligation, terminal-once as law).
|
|
145
|
+
closesObligation: t.string().merge(Lww).optional(),
|
|
144
146
|
} as const;
|
|
145
147
|
|
|
146
148
|
/**
|
|
@@ -163,9 +165,9 @@ export function impureCapabilityAggregate<
|
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
/**
|
|
166
|
-
* The deterministic instance-id rule: a impure capability is keyed by its OWN
|
|
167
|
-
* That id is KERNEL-MINTED (`<
|
|
168
|
-
* captured + committed — so the `
|
|
168
|
+
* The deterministic instance-id rule: a impure capability is keyed by its OWN obligation id.
|
|
169
|
+
* That id is KERNEL-MINTED (`<ObligationType>_<uuidv7>`, via the marker-driven front-door),
|
|
170
|
+
* captured + committed — so the `obligation` create is idempotent on replay by capture, and the
|
|
169
171
|
* `complete`/`fail` receipt addresses the SAME instance by reading the committed minted id.
|
|
170
172
|
* Trivial today (identity), but centralised here so the keying rule has ONE home (the
|
|
171
173
|
* symmetry with `relations.ts`'s `relationTupleInstanceId`).
|
|
@@ -174,11 +176,13 @@ export function impureCapabilityInstanceId(requestId: string): string {
|
|
|
174
176
|
return requestId;
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
/** The Zod schema for the framework envelope fields the
|
|
178
|
-
const
|
|
179
|
+
/** The Zod schema for the framework envelope fields the Obligation payload always carries. */
|
|
180
|
+
const obligationEnvelopeSchema = z.object({
|
|
179
181
|
requestId: z.string(),
|
|
180
182
|
requestedBy: z.string(),
|
|
181
183
|
requestedAt: z.number().int(),
|
|
184
|
+
scheduledAt: z.number().int(),
|
|
185
|
+
deadlineAt: z.number().int(),
|
|
182
186
|
});
|
|
183
187
|
|
|
184
188
|
/** The receipt-COMPLETE payload: the request id + the content-addressed result + when. */
|
|
@@ -204,7 +208,7 @@ const blockSchema = z.object({
|
|
|
204
208
|
nextAction: z.string(),
|
|
205
209
|
});
|
|
206
210
|
|
|
207
|
-
/** The receipt-DLQ payload: terminal dead-letter, requiring a fresh
|
|
211
|
+
/** The receipt-DLQ payload: terminal dead-letter, requiring a fresh obligation to recover. */
|
|
208
212
|
const deadLetterSchema = z.object({
|
|
209
213
|
requestId: z.string(),
|
|
210
214
|
deadLetterReason: z.string(),
|
|
@@ -232,13 +236,13 @@ function envelopeInstance(aggregateId: string, requestId: string) {
|
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
/**
|
|
235
|
-
*
|
|
239
|
+
* obligationOps — the Obligation half. Seeds the envelope provenance + timing + `status:'requested'`,
|
|
236
240
|
* PLUS the tenant's request params (the `paramOps` the caller's `.plan` supplies for
|
|
237
241
|
* its own fields). The task is bound to its requestId instance (#105).
|
|
238
242
|
*/
|
|
239
|
-
export function
|
|
243
|
+
export function obligationOps(
|
|
240
244
|
aggregateId: string,
|
|
241
|
-
p: z.infer<typeof
|
|
245
|
+
p: z.infer<typeof obligationEnvelopeSchema>,
|
|
242
246
|
paramOps: PlannedOp[] = [],
|
|
243
247
|
): PlannedOp[] {
|
|
244
248
|
const taskInstance = envelopeInstance(aggregateId, p.requestId);
|
|
@@ -246,6 +250,8 @@ export function orderOps(
|
|
|
246
250
|
set(taskInstance, "requestId", p.requestId),
|
|
247
251
|
set(taskInstance, "requestedBy", p.requestedBy),
|
|
248
252
|
set(taskInstance, "requestedAt", p.requestedAt),
|
|
253
|
+
set(taskInstance, "scheduledAt", p.scheduledAt),
|
|
254
|
+
set(taskInstance, "deadlineAt", p.deadlineAt),
|
|
249
255
|
set(taskInstance, "status", "requested"),
|
|
250
256
|
...paramOps,
|
|
251
257
|
];
|
|
@@ -266,10 +272,10 @@ export function completeOps(
|
|
|
266
272
|
set(taskInstance, "status", "completed"),
|
|
267
273
|
set(taskInstance, "resultRef", p.resultRef),
|
|
268
274
|
set(taskInstance, "completedAt", p.completedAt),
|
|
269
|
-
// The FRAMEWORK
|
|
275
|
+
// The FRAMEWORK Obligation↔Receipt CLOSURE link — this Receipt closes the Obligation whose
|
|
270
276
|
// id == requestId (the task instance the reactor mutates). Indexed by the read
|
|
271
277
|
// projection for the O(1) closure read. Wire-identical to the Rust reactor's op.
|
|
272
|
-
set(taskInstance, "
|
|
278
|
+
set(taskInstance, "closesObligation", impureCapabilityInstanceId(p.requestId)),
|
|
273
279
|
// The TENANT RESULT FIELDS the Receipt writes into the task aggregate (e.g.
|
|
274
280
|
// `transcription = <text>`). The envelope is auto; these are the tenant's own
|
|
275
281
|
// structured result. Framework writes them verbatim; the tenant owns their meaning.
|
|
@@ -293,8 +299,8 @@ export function failOps(
|
|
|
293
299
|
set(taskInstance, "failureReason", p.failureReason),
|
|
294
300
|
set(taskInstance, "completedAt", p.failedAt),
|
|
295
301
|
// CLOSURE link — a FAILED task is still CLOSED by its receipt (the closure read
|
|
296
|
-
// reports it CLOSED with no result). Same `
|
|
297
|
-
set(taskInstance, "
|
|
302
|
+
// reports it CLOSED with no result). Same `closesObligation == requestId` as `completeOps`.
|
|
303
|
+
set(taskInstance, "closesObligation", impureCapabilityInstanceId(p.requestId)),
|
|
298
304
|
];
|
|
299
305
|
}
|
|
300
306
|
|
|
@@ -309,11 +315,11 @@ export function blockOps(
|
|
|
309
315
|
set(taskInstance, "blockedReason", p.blockedReason),
|
|
310
316
|
set(taskInstance, "blockedAt", p.blockedAt),
|
|
311
317
|
set(taskInstance, "nextAction", p.nextAction),
|
|
312
|
-
set(taskInstance, "
|
|
318
|
+
set(taskInstance, "closesObligation", impureCapabilityInstanceId(p.requestId)),
|
|
313
319
|
];
|
|
314
320
|
}
|
|
315
321
|
|
|
316
|
-
/** deadLetterOps — terminal DLQ receipt. Recovery is a fresh explicit
|
|
322
|
+
/** deadLetterOps — terminal DLQ receipt. Recovery is a fresh explicit Obligation. */
|
|
317
323
|
export function deadLetterOps(
|
|
318
324
|
aggregateId: string,
|
|
319
325
|
p: z.infer<typeof deadLetterSchema>,
|
|
@@ -323,7 +329,7 @@ export function deadLetterOps(
|
|
|
323
329
|
set(taskInstance, "status", "deadLettered"),
|
|
324
330
|
set(taskInstance, "deadLetterReason", p.deadLetterReason),
|
|
325
331
|
set(taskInstance, "deadLetteredAt", p.deadLetteredAt),
|
|
326
|
-
set(taskInstance, "
|
|
332
|
+
set(taskInstance, "closesObligation", impureCapabilityInstanceId(p.requestId)),
|
|
327
333
|
];
|
|
328
334
|
}
|
|
329
335
|
|
|
@@ -331,11 +337,11 @@ export function deadLetterOps(
|
|
|
331
337
|
* The generated directive QUARTET for a impure capability. A domain dev calls
|
|
332
338
|
* {@link impureCapability} ONCE and gets:
|
|
333
339
|
* • `aggregate` — the task aggregate handle (envelope ⊕ params),
|
|
334
|
-
* • `
|
|
340
|
+
* • `obligation` — `.creates` directive (the authored Obligation),
|
|
335
341
|
* • `complete` — `.mutates` directive (the reactor's success Receipt),
|
|
336
342
|
* • `fail` — `.mutates` directive (the reactor's failure Receipt),
|
|
337
|
-
* plus the deterministic `instanceId` rule. The `
|
|
338
|
-
*
|
|
343
|
+
* plus the deterministic `instanceId` rule. The `obligation` payload is the framework
|
|
344
|
+
* obligation-envelope MERGED with the tenant's `paramsSchema`; `complete`/`fail` are fixed.
|
|
339
345
|
*/
|
|
340
346
|
export interface ImpureCapability<
|
|
341
347
|
Id extends string,
|
|
@@ -343,7 +349,7 @@ export interface ImpureCapability<
|
|
|
343
349
|
F extends Record<string, Field>,
|
|
344
350
|
> {
|
|
345
351
|
readonly aggregate: AggregateHandle<Id, typeof IMPURE_CAPABILITY_ENVELOPE & F>;
|
|
346
|
-
readonly
|
|
352
|
+
readonly obligation: RequirableDirective<ObligationPayload<ParamsShape>>;
|
|
347
353
|
readonly complete: RequirableDirective<z.infer<typeof completeSchema>>;
|
|
348
354
|
readonly fail: RequirableDirective<z.infer<typeof failSchema>>;
|
|
349
355
|
readonly block: RequirableDirective<z.infer<typeof blockSchema>>;
|
|
@@ -364,17 +370,17 @@ export interface ImpureCapability<
|
|
|
364
370
|
}
|
|
365
371
|
|
|
366
372
|
/**
|
|
367
|
-
* The inferred
|
|
368
|
-
* tenant's param shape — the EXACT inference `
|
|
369
|
-
* produces (so the declared `ImpureCapability.
|
|
373
|
+
* The inferred Obligation payload type = the framework envelope schema EXTENDED with the
|
|
374
|
+
* tenant's param shape — the EXACT inference `obligationEnvelopeSchema.extend(paramsSchema)`
|
|
375
|
+
* produces (so the declared `ImpureCapability.obligation` directive type and the value the factory
|
|
370
376
|
* builds are the same type, with no brittle hand-written intersection).
|
|
371
377
|
*/
|
|
372
|
-
export type
|
|
373
|
-
ReturnType<typeof
|
|
378
|
+
export type ObligationPayload<ParamsShape extends z.ZodRawShape> = z.infer<
|
|
379
|
+
ReturnType<typeof obligationEnvelopeSchema.extend<ParamsShape>>
|
|
374
380
|
>;
|
|
375
381
|
|
|
376
382
|
/**
|
|
377
|
-
* impureCapability — the framework FACTORY. Declares an
|
|
383
|
+
* impureCapability — the framework FACTORY. Declares an Obligation→Receipt task in ONE call:
|
|
378
384
|
*
|
|
379
385
|
* ```ts
|
|
380
386
|
* // Build the aggregate handle FIRST so `writeParams` references it without a
|
|
@@ -384,7 +390,7 @@ export type OrderPayload<ParamsShape extends z.ZodRawShape> = z.infer<
|
|
|
384
390
|
*
|
|
385
391
|
* export const exportJob = impureCapability({
|
|
386
392
|
* aggregateId: "ExportJobAggregate",
|
|
387
|
-
*
|
|
393
|
+
* obligationDirectiveId: "createExportJob",
|
|
388
394
|
* completeDirectiveId: "completeExportJob",
|
|
389
395
|
* failDirectiveId: "failExportJob",
|
|
390
396
|
* blockDirectiveId: "blockExportJob",
|
|
@@ -400,33 +406,33 @@ export type OrderPayload<ParamsShape extends z.ZodRawShape> = z.infer<
|
|
|
400
406
|
*
|
|
401
407
|
* The tenant declares the wire ids (so they're stable, minify-safe DATA — the same
|
|
402
408
|
* declared-once rule as `aggregate()`/`directive()`), the param FIELDS (for the
|
|
403
|
-
* aggregate shape + codegen) and the param SCHEMA (for the
|
|
409
|
+
* aggregate shape + codegen) and the param SCHEMA (for the obligation payload + the
|
|
404
410
|
* `writeParams` plan fragment). The framework wires the lifecycle: status defaulting,
|
|
405
411
|
* the requestId keying, and the complete/fail receipt directives.
|
|
406
412
|
*
|
|
407
413
|
* `writeParams(taskInstanceId, params)` returns the `PlannedOp[]` for the tenant's OWN
|
|
408
|
-
* fields (the framework already writes the envelope) — the param half of the
|
|
414
|
+
* fields (the framework already writes the envelope) — the param half of the obligation
|
|
409
415
|
* plan, kept type-checked against the tenant's param fields via the `set()` helper.
|
|
410
416
|
*/
|
|
411
417
|
export function impureCapability<
|
|
412
418
|
const Id extends string,
|
|
413
|
-
const
|
|
419
|
+
const ObligationId extends string,
|
|
414
420
|
const CompleteId extends string,
|
|
415
421
|
const FailId extends string,
|
|
416
422
|
ParamsShape extends z.ZodRawShape,
|
|
417
423
|
F extends Record<string, Field>,
|
|
418
424
|
>(spec: {
|
|
419
425
|
aggregateId: Id;
|
|
420
|
-
|
|
426
|
+
obligationDirectiveId: ObligationId;
|
|
421
427
|
completeDirectiveId: CompleteId;
|
|
422
428
|
failDirectiveId: FailId;
|
|
423
429
|
blockDirectiveId: string;
|
|
424
430
|
deadLetterDirectiveId: string;
|
|
425
431
|
/** The task's request-param FIELDS (merged onto the envelope for the aggregate shape). */
|
|
426
432
|
params: F;
|
|
427
|
-
/** The task's request-param Zod SHAPE (merged onto the
|
|
433
|
+
/** The task's request-param Zod SHAPE (merged onto the obligation envelope schema). */
|
|
428
434
|
paramsSchema: ParamsShape;
|
|
429
|
-
/** Plan fragment writing the tenant's param fields for the
|
|
435
|
+
/** Plan fragment writing the tenant's param fields for the obligation (envelope is auto). */
|
|
430
436
|
writeParams: (
|
|
431
437
|
taskInstanceId: string,
|
|
432
438
|
params: z.infer<z.ZodObject<ParamsShape>>,
|
|
@@ -434,7 +440,7 @@ export function impureCapability<
|
|
|
434
440
|
/**
|
|
435
441
|
* OPTIONAL plan fragment writing the tenant's STRUCTURED RESULT fields onto the task
|
|
436
442
|
* aggregate when the `complete` Receipt is authored (the lifecycle envelope + the
|
|
437
|
-
* `
|
|
443
|
+
* `closesObligation` link is auto). The receipt's `resultRef` carries the content-addressed
|
|
438
444
|
* Evidence ref (the authoritative artifact); a tenant that ALSO wants the result in a
|
|
439
445
|
* typed, queryable field (e.g. a voice-note's `transcription`) supplies it here. The
|
|
440
446
|
* `resultRef` string is passed through so the tenant can derive the field value from the
|
|
@@ -451,22 +457,22 @@ export function impureCapability<
|
|
|
451
457
|
}): ImpureCapability<Id, ParamsShape, F> {
|
|
452
458
|
const taskAggregate = impureCapabilityAggregate(spec.aggregateId, spec.params);
|
|
453
459
|
|
|
454
|
-
// The
|
|
455
|
-
const
|
|
460
|
+
// The obligation payload = the framework provenance/timing envelope ⊕ the tenant's params.
|
|
461
|
+
const obligationPayload = obligationEnvelopeSchema.extend(spec.paramsSchema);
|
|
456
462
|
|
|
457
|
-
const
|
|
458
|
-
spec.
|
|
463
|
+
const obligation: RequirableDirective<ObligationPayload<ParamsShape>> = directive(
|
|
464
|
+
spec.obligationDirectiveId,
|
|
459
465
|
)
|
|
460
466
|
.creates(taskAggregate)
|
|
461
|
-
.payload(
|
|
467
|
+
.payload(obligationPayload)
|
|
462
468
|
.plan((p) => {
|
|
463
469
|
// The validated payload carries BOTH the envelope (required string/int) and the
|
|
464
|
-
// tenant params. `
|
|
470
|
+
// tenant params. `obligationOps` writes the envelope; the tenant's `writeParams`
|
|
465
471
|
// writes its own fields. Extract the envelope explicitly (it is required on the
|
|
466
472
|
// schema, so these are present non-undefined values).
|
|
467
|
-
const envelope =
|
|
473
|
+
const envelope = obligationEnvelopeSchema.parse(p);
|
|
468
474
|
const params = p as z.infer<z.ZodObject<ParamsShape>>;
|
|
469
|
-
return
|
|
475
|
+
return obligationOps(
|
|
470
476
|
spec.aggregateId,
|
|
471
477
|
envelope,
|
|
472
478
|
spec.writeParams(impureCapabilityInstanceId(envelope.requestId), params),
|
|
@@ -502,7 +508,7 @@ export function impureCapability<
|
|
|
502
508
|
|
|
503
509
|
return {
|
|
504
510
|
aggregate: taskAggregate,
|
|
505
|
-
|
|
511
|
+
obligation,
|
|
506
512
|
complete,
|
|
507
513
|
fail,
|
|
508
514
|
block,
|
|
@@ -516,7 +522,7 @@ export function impureCapability<
|
|
|
516
522
|
|
|
517
523
|
// ─────────────────────────── THE CAPABILITY PROVIDER ─────────────────────────
|
|
518
524
|
//
|
|
519
|
-
// The
|
|
525
|
+
// The Obligation half (`obligation`) is an ordinary Intent authored by the peer through the one write
|
|
520
526
|
// path. The Receipt half (`complete`/`fail`) is an ordinary Intent authored by a capability
|
|
521
527
|
// PROVIDER — itself a Nomos author (`admission-hosting.md`: hosted/provider-originated
|
|
522
528
|
// intents author *through* a policy unit too"). The provider is the runtime ROLE; this
|
|
@@ -46,6 +46,7 @@ import { Lww, MapOf } from "../drivers.js";
|
|
|
46
46
|
import { directive } from "../directive.js";
|
|
47
47
|
import { set, setEntry, withMarker, type PlannedOp } from "../ops.js";
|
|
48
48
|
import { query } from "../query.js";
|
|
49
|
+
import { endorsementOf, type AttributionPrincipal, type EndorsedAuthority } from "../principal_evidence.js";
|
|
49
50
|
|
|
50
51
|
/** Tuple subject lifecycle — the same vocabulary the kernel read drops a non-active leaf by. */
|
|
51
52
|
export const RELATION_STATUSES = ["active", "revoked"] as const;
|
|
@@ -126,8 +127,15 @@ function compact(obj: Record<string, unknown>): RelationSubjectLeaf {
|
|
|
126
127
|
interface WriteTupleInput {
|
|
127
128
|
object: string;
|
|
128
129
|
relation: string;
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* THE AUTHORITY SINK (the endorse model — security_model.md): who is granted the relation. A plain
|
|
132
|
+
* subject string (`user:<id>` from the payload) OR `ctx.principal().asAuthoritySubject(reason)` — an
|
|
133
|
+
* un-endorsed identity (`asAttribution()`) cannot be a subject (it won't typecheck), so identity
|
|
134
|
+
* never SILENTLY decides a grant.
|
|
135
|
+
*/
|
|
136
|
+
subject: string | EndorsedAuthority<string>;
|
|
137
|
+
/** ATTRIBUTION (free): who recorded the grant. A plain string OR `ctx.principal().asAttribution()`. */
|
|
138
|
+
grantedBy: string | AttributionPrincipal;
|
|
131
139
|
grantedAt: string;
|
|
132
140
|
}
|
|
133
141
|
|
|
@@ -141,19 +149,30 @@ function lowerTuple(
|
|
|
141
149
|
},
|
|
142
150
|
): PlannedOp[] {
|
|
143
151
|
const rt = instance(RelationTuple, relationTupleInstanceId(p.object, p.relation));
|
|
152
|
+
// MATERIALISE PrincipalEvidence subjects/grantedBy → the raw principal (the endorse model). The
|
|
153
|
+
// subject's endorsement is recorded on the setEntry op so the discovery harvest lifts it into
|
|
154
|
+
// nomosFlowGate.endorse; grantedBy is free attribution (materialised, not gated).
|
|
155
|
+
//
|
|
156
|
+
// The captured authoring principal IS already a wire-form subject (`user:<uid>` / `key:<hash>`, the
|
|
157
|
+
// host-stamped actor — the SAME form the write gate parses via `Subject::parse`), so an endorsed
|
|
158
|
+
// authority subject lowers to that leaf verbatim and the gate's `engine.check` matches it.
|
|
159
|
+
const subjectE = endorsementOf(p.subject);
|
|
160
|
+
const subjectRaw = subjectE ? subjectE.principal : (p.subject as string);
|
|
161
|
+
const grantedByRaw = endorsementOf(p.grantedBy)?.principal ?? (p.grantedBy as string);
|
|
144
162
|
const leaf = compact({
|
|
145
163
|
status: p.status,
|
|
146
|
-
grantedBy:
|
|
164
|
+
grantedBy: grantedByRaw,
|
|
147
165
|
grantedAt: p.grantedAt,
|
|
148
166
|
revokedBy: p.revokedBy,
|
|
149
167
|
revokedAt: p.revokedAt,
|
|
150
168
|
});
|
|
169
|
+
const subjectEntry = setEntry(rt, "subjects", subjectRaw, JSON.stringify(leaf));
|
|
151
170
|
return [
|
|
152
171
|
// upsert the aggregate head (first relate creates it); without this the referential
|
|
153
172
|
// gate would reject a setEntry on an absent id.
|
|
154
173
|
withMarker(set(rt, "object", p.object), "ensures"),
|
|
155
174
|
set(rt, "relation", p.relation),
|
|
156
|
-
|
|
175
|
+
subjectE ? { ...subjectEntry, endorsement: { facet: subjectE.facet, reason: subjectE.reason } } : subjectEntry,
|
|
157
176
|
];
|
|
158
177
|
}
|
|
159
178
|
|
|
@@ -67,7 +67,7 @@ const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256
|
|
|
67
67
|
* bootstrap chicken-and-egg. The forge surface a bare `relateSubject` would open is closed by
|
|
68
68
|
* DOMAIN PLACEMENT: this directive is composed into the CONTROL-PLANE governance law (domain
|
|
69
69
|
* `workspaces`/`nomos`), which the open session lane structurally refuses
|
|
70
|
-
* (`cloud/
|
|
70
|
+
* (`cloud/cloudflare-shell/src/engine.mjs` — only the birth door + the secret-gated `/author` + admin
|
|
71
71
|
* lanes author it), so no tenant can forge a signer for another principal.
|
|
72
72
|
*
|
|
73
73
|
* THE ORDER IS THE CONTRACT (verified, zero gate change): `enrollSigner` (E, unsigned/pre-warrant) →
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Nomos sync lifecycle.
|
|
11
11
|
*
|
|
12
12
|
* Sync is a control-plane impure capability: Nomos records the desired workspace
|
|
13
|
-
* reconciliation as an ordinary
|
|
13
|
+
* reconciliation as an ordinary Obligation, a host/provider performs transport using
|
|
14
14
|
* explicit capabilities, and the outcome comes back as an ordinary Receipt. The
|
|
15
15
|
* remote/JWT/network is never ambient domain state; the ledger carries request,
|
|
16
16
|
* status, cursors, heads, and errors.
|
|
@@ -85,7 +85,7 @@ function syncResultOps(taskId: string, resultRef: string): PlannedOp[] {
|
|
|
85
85
|
|
|
86
86
|
const workspaceSyncTask = impureCapability({
|
|
87
87
|
aggregateId: "WorkspaceSyncOperation",
|
|
88
|
-
|
|
88
|
+
obligationDirectiveId: "requestWorkspaceSync",
|
|
89
89
|
completeDirectiveId: "completeWorkspaceSync",
|
|
90
90
|
failDirectiveId: "failWorkspaceSync",
|
|
91
91
|
blockDirectiveId: "blockWorkspaceSync",
|
|
@@ -113,7 +113,7 @@ const workspaceSyncTask = impureCapability({
|
|
|
113
113
|
completeResult: syncResultOps,
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
export const requestWorkspaceSync = workspaceSyncTask.
|
|
116
|
+
export const requestWorkspaceSync = workspaceSyncTask.obligation.requires("requestWorkspaceSync");
|
|
117
117
|
export const completeWorkspaceSync = workspaceSyncTask.complete.requires("completeWorkspaceSync");
|
|
118
118
|
export const failWorkspaceSync = workspaceSyncTask.fail.requires("failWorkspaceSync");
|
|
119
119
|
export const blockWorkspaceSync = workspaceSyncTask.block.requires("blockWorkspaceSync");
|
|
@@ -99,6 +99,14 @@ export interface WorkspaceInvariantDecl {
|
|
|
99
99
|
readonly reads: WorkspaceInvariantReads;
|
|
100
100
|
/** The executable predicate — ships in the engine bundle, NOT the manifest. */
|
|
101
101
|
readonly assert: WorkspaceInvariantAssert;
|
|
102
|
+
/**
|
|
103
|
+
* THE DECLASSIFY WAIVER (nomosFlowGate rule 4 — architecture/security_model.md): present iff
|
|
104
|
+
* the author acknowledged that this invariant's refusal MAY correlate with a secret it reads
|
|
105
|
+
* (a `requires`/`owned` aggregate). The reason is hashed into the `nomosFlowGate` era key (so
|
|
106
|
+
* it is replay-verified + the entire audit surface). Absent ⇒ the flow gate refuses to compile
|
|
107
|
+
* a secret-correlated refusal.
|
|
108
|
+
*/
|
|
109
|
+
readonly declassify?: string;
|
|
102
110
|
/** Brand so a plain object is never mistaken for a declaration. */
|
|
103
111
|
readonly __isWorkspaceInvariant: true;
|
|
104
112
|
}
|
|
@@ -112,22 +120,35 @@ export function workspaceInvariant(id: string): {
|
|
|
112
120
|
on(directiveId: string): {
|
|
113
121
|
reads(reads: WorkspaceInvariantReads): {
|
|
114
122
|
assert(assert: WorkspaceInvariantAssert): WorkspaceInvariantDecl;
|
|
123
|
+
/** Acknowledge an intended secret-correlated refusal (nomosFlowGate rule 4). The reason
|
|
124
|
+
* rides the `nomosFlowGate` era key; then chain `.assert(...)` as usual. */
|
|
125
|
+
declassify(reason: string): { assert(assert: WorkspaceInvariantAssert): WorkspaceInvariantDecl };
|
|
115
126
|
};
|
|
116
127
|
};
|
|
117
128
|
} {
|
|
129
|
+
const finish = (directiveId: string, reads: WorkspaceInvariantReads, declassify: string | undefined) => ({
|
|
130
|
+
assert(assert: WorkspaceInvariantAssert): WorkspaceInvariantDecl {
|
|
131
|
+
return {
|
|
132
|
+
id,
|
|
133
|
+
on: directiveId,
|
|
134
|
+
reads,
|
|
135
|
+
assert,
|
|
136
|
+
...(declassify !== undefined ? { declassify } : {}),
|
|
137
|
+
__isWorkspaceInvariant: true as const,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
});
|
|
118
141
|
return {
|
|
119
142
|
on(directiveId: string) {
|
|
120
143
|
return {
|
|
121
144
|
reads(reads: WorkspaceInvariantReads) {
|
|
122
145
|
return {
|
|
123
|
-
assert
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
__isWorkspaceInvariant: true,
|
|
130
|
-
};
|
|
146
|
+
assert: finish(directiveId, reads, undefined).assert,
|
|
147
|
+
declassify(reason: string) {
|
|
148
|
+
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
149
|
+
throw new Error(`workspaceInvariant('${id}').declassify(reason) needs a non-empty human reason — it is the audit record for an intended secret-correlated refusal.`);
|
|
150
|
+
}
|
|
151
|
+
return finish(directiveId, reads, reason);
|
|
131
152
|
},
|
|
132
153
|
};
|
|
133
154
|
},
|
|
@@ -75,6 +75,13 @@ import { query } from "../query.js";
|
|
|
75
75
|
import { workspaceInvariant, refAs } from "./workspace_invariant.js";
|
|
76
76
|
import { writeTuple, removeTuple, RelationTuple } from "./relations.js";
|
|
77
77
|
import { relationType, self, user, key } from "../relation_schema.js";
|
|
78
|
+
// THE GENESIS BOOTSTRAP (platform-birth genesis bootstrap, Approach A): governance carries the
|
|
79
|
+
// warrant-flip primitives so the birth door can lawfully bootstrap a virgin platform's founding admin —
|
|
80
|
+
// E `enrollSigner` (the founding signer tuple) → W `seedGenesis` (the GenesisWarrant) → A signed
|
|
81
|
+
// `grantAdmin` (admitted by the kernel genesis clause). Re-exported so the package's shape-discovery picks
|
|
82
|
+
// them up as part of the `workspaces` domain. (architecture/platform_birth_genesis_bootstrap.md)
|
|
83
|
+
export { GenesisWarrant, seedGenesis } from "./genesis.js";
|
|
84
|
+
export { identityRelations, enrollSigner } from "./signing_identity.js";
|
|
78
85
|
|
|
79
86
|
// ── WORKSPACE SELF-IDENTITY (the law reads its OWN install-context) ──────────────────────────────────
|
|
80
87
|
// The governance bundle runs on root AND every born platform. A workspace knows its ROLE via this
|
package/src/index.ts
CHANGED
|
@@ -32,7 +32,21 @@ export {
|
|
|
32
32
|
type FieldKind,
|
|
33
33
|
type EvidenceRefValue,
|
|
34
34
|
evidenceRefSchema,
|
|
35
|
+
// FIELD FACETS (the endorse model — security_model.md): authority/standing/attribution gate
|
|
36
|
+
// set() acceptance of PrincipalEvidence; secret/public are the confidentiality lattice.
|
|
37
|
+
authorityField,
|
|
38
|
+
standingField,
|
|
39
|
+
attributionField,
|
|
40
|
+
secretField,
|
|
41
|
+
publicField,
|
|
35
42
|
} from "./fields.js";
|
|
43
|
+
export {
|
|
44
|
+
type PrincipalEvidence,
|
|
45
|
+
type AttributionPrincipal,
|
|
46
|
+
type EndorsedAuthority,
|
|
47
|
+
type EndorsedStanding,
|
|
48
|
+
type FieldFacet,
|
|
49
|
+
} from "./principal_evidence.js";
|
|
36
50
|
export {
|
|
37
51
|
Lww,
|
|
38
52
|
LwwCausal,
|
|
@@ -278,7 +292,7 @@ export {
|
|
|
278
292
|
impureCapabilityAggregate,
|
|
279
293
|
impureCapabilityInstanceId,
|
|
280
294
|
impureCapability,
|
|
281
|
-
|
|
295
|
+
obligationOps,
|
|
282
296
|
completeOps,
|
|
283
297
|
failOps,
|
|
284
298
|
blockOps,
|
|
@@ -410,6 +424,16 @@ export {
|
|
|
410
424
|
type InvariantVerdict,
|
|
411
425
|
} from "./relation.js";
|
|
412
426
|
export { generateDartDomain, type DomainModule } from "./codegen_dart.js";
|
|
427
|
+
// SHAPE COMPATIBILITY (#72) — the structural contract a client negotiates with a workspace
|
|
428
|
+
// by, instead of the law hash. Pure + deterministic; the editor/CI mirror imports these.
|
|
429
|
+
export {
|
|
430
|
+
domainInterface,
|
|
431
|
+
compatible,
|
|
432
|
+
type DomainInterface,
|
|
433
|
+
type CompatVerdict,
|
|
434
|
+
type DirectivePayloadShape,
|
|
435
|
+
type AggregateReadShape,
|
|
436
|
+
} from "./shape_compat.js";
|
|
413
437
|
// The OPTIONAL Graphviz `.dot` ER-diagram emitter (off-by-default presentation projection,
|
|
414
438
|
// turned on by the `--emit-dot` compiler flag). Pure `DomainModule -> string`, deterministic;
|
|
415
439
|
// NOT part of the domain identity. Barrel-safe (no node:* imports), so the engine bundle can
|