@githolon/dsl 0.4.0 → 0.5.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.
Files changed (32) hide show
  1. package/package.json +2 -1
  2. package/src/build_package.ts +4 -0
  3. package/src/capability_exports.ts +55 -0
  4. package/src/codegen_dart.ts +9 -0
  5. package/src/codegen_proof.ts +72 -11
  6. package/src/compile_package_main.ts +241 -13
  7. package/src/directive.ts +35 -10
  8. package/src/engine_entry.ts +5 -1
  9. package/src/framework/capability.ts +215 -0
  10. package/src/framework/impure_capability.ts +25 -3
  11. package/src/framework/workspaces.ts +129 -0
  12. package/src/index.ts +9 -0
  13. package/src/manifest.ts +103 -0
  14. package/src/read.ts +29 -0
  15. package/src/stable_ids.ts +226 -0
  16. package/src/stable_ids_types.ts +40 -0
  17. package/src/usd.ts +54 -0
  18. package/src/usd_layers.ts +65 -1
  19. package/src/wire_encode.ts +15 -0
  20. package/dart/.dart_tool/package_config.json +0 -328
  21. package/dart/.dart_tool/package_graph.json +0 -485
  22. package/dart/.dart_tool/pub/bin/test/test.dart-3.11.5.snapshot +0 -0
  23. package/dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjU= +0 -0
  24. package/dart/.dart_tool/version +0 -1
  25. package/dart/build/native_assets/macos/native_assets.json +0 -1
  26. package/dart/build/test_cache/build/89a6598c8854ed031dfc25d83c80860e.cache.dill.track.dill +0 -0
  27. package/dart/build/unit_test_assets/AssetManifest.bin +0 -0
  28. package/dart/build/unit_test_assets/FontManifest.json +0 -1
  29. package/dart/build/unit_test_assets/NOTICES.Z +0 -0
  30. package/dart/build/unit_test_assets/NativeAssetsManifest.json +0 -1
  31. package/dart/build/unit_test_assets/shaders/ink_sparkle.frag +0 -0
  32. package/dart/build/unit_test_assets/shaders/stretch_effect.frag +0 -0
package/src/directive.ts CHANGED
@@ -21,6 +21,7 @@ import type { PlannedOp } from "./ops.js";
21
21
  import type { Ports } from "./ctx.js";
22
22
  import type { CertifiedReadDecl } from "./certified_read.js";
23
23
  import type { RelationDecl } from "./relation.js";
24
+ import type { QueryDecl } from "./query.js";
24
25
 
25
26
  export type ReferentialMarker = "creates" | "mutates" | "ensures" | "archives";
26
27
 
@@ -70,6 +71,18 @@ export interface Directive<P = unknown> {
70
71
  * (read ⊆ declared) is a SEPARATE later step; here it only DECLARES.
71
72
  */
72
73
  readonly declaredReads: string[];
74
+ /**
75
+ * The directive's DECLARED CAPTURED-READ queries (#58 — the captured-read lane):
76
+ * the O(1) DSL-defined queries its `plan` may `read()` on the write path. Each is a
77
+ * declared, indexed {@link QueryDecl}; the gate serves `nomos.read` LIVE from the
78
+ * pre-apply committed state at author, replays the captured result at verify, and
79
+ * RE-DERIVES it at the pre-apply position for the read-conflict (CAS-as-law) check.
80
+ * Same-workspace only, size-bounded. Defaults to `[]` (no captured reads — the
81
+ * engine then provides no `nomos.read`, exactly the pre-#58 posture). OPTIONAL +
82
+ * ADDITIVE (a hand-built directive object without it is a read-free directive);
83
+ * OMITTED from the canonical manifest when empty (hash-stable for read-free directives).
84
+ */
85
+ readonly declaredQueryReads?: QueryDecl[];
73
86
  /**
74
87
  * The directive's DECLARED emit boundary: the event types its `plan` may EMIT,
75
88
  * each with an optional `max` bound. Part of the domain identity. Defaults to `{}`
@@ -130,13 +143,18 @@ export interface RequirableDirective<P> extends Directive<P> {
130
143
  */
131
144
  requires(capability?: string, scopeFrom?: ScopeFrom<P>): RequirableDirective<P>;
132
145
  /**
133
- * Declare the directive's READ boundary: the ref types its `plan` may read. Callable
134
- * MULTIPLE times to accumulate; results are deduped + sorted on the directive's
135
- * `declaredReads`. Purely additive — a directive that never calls this declares no
136
- * reads (`[]`) and is byte-identical in the canonical manifest to before this existed.
137
- * Returns a NEW requirable directive (non-destructive).
146
+ * Declare the directive's READ boundary. TWO argument kinds, one declaration site:
147
+ * * a STRING declares a ref TYPE its `plan` may read (the original boundary —
148
+ * deduped + sorted on `declaredReads`);
149
+ * * a {@link QueryDecl} declares a CAPTURED-READ query (#58): an O(1) DSL query
150
+ * its `plan` may `read()` on the write path — served live at author from the
151
+ * pre-apply state, captured onto the intent, replayed + CAS-re-derived at every
152
+ * later gate (deduped by query id on `declaredQueryReads`).
153
+ * Callable MULTIPLE times to accumulate. Purely additive — a directive that never
154
+ * calls this declares no reads and is byte-identical in the canonical manifest to
155
+ * before this existed. Returns a NEW requirable directive (non-destructive).
138
156
  */
139
- reads(...refTypes: string[]): RequirableDirective<P>;
157
+ reads(...reads: (string | QueryDecl)[]): RequirableDirective<P>;
140
158
  /**
141
159
  * Declare ONE entry of the directive's EMIT boundary: an `eventType` its `plan` may
142
160
  * emit, with an optional `max` count bound. Callable MULTIPLE times to accumulate
@@ -239,6 +257,7 @@ class DirectivePlanStep<Id extends string, P> {
239
257
  payloadSchema: this.payloadSchema,
240
258
  plan: fn,
241
259
  declaredReads: [],
260
+ declaredQueryReads: [],
242
261
  declaredEmits: {},
243
262
  declaredCertifiedReads: {},
244
263
  declaredRelations: [],
@@ -263,11 +282,17 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
263
282
  ...(scopeFrom !== undefined ? { scopeFrom } : {}),
264
283
  });
265
284
  },
266
- reads(...refTypes: string[]): RequirableDirective<P> {
267
- // Accumulate onto any prior reads, dedup, sort order is incidental to the
268
- // declared boundary; only the SET of read ref types is the contract.
285
+ reads(...reads: (string | QueryDecl)[]): RequirableDirective<P> {
286
+ // Split by argument kind: strings are the ref-type boundary; QueryDecls are the
287
+ // captured-read lane (#58). Both accumulate, dedup (ref types by value, queries
288
+ // by id — later decl wins), sort — only the SET is the contract.
289
+ const refTypes = reads.filter((r): r is string => typeof r === "string");
290
+ const queryDecls = reads.filter((r): r is QueryDecl => typeof r !== "string");
269
291
  const merged = [...new Set([...base.declaredReads, ...refTypes])].sort();
270
- return makeRequirable({ ...base, declaredReads: merged });
292
+ const byId = new Map<string, QueryDecl>((base.declaredQueryReads ?? []).map((q) => [q.id, q]));
293
+ for (const q of queryDecls) byId.set(q.id, q);
294
+ const mergedQueries = [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
295
+ return makeRequirable({ ...base, declaredReads: merged, declaredQueryReads: mergedQueries });
271
296
  },
272
297
  emits(eventType: string, opts?: { max?: number }): RequirableDirective<P> {
273
298
  // Accumulate one event type onto any prior emits; a `max` bound is recorded only
@@ -52,6 +52,7 @@
52
52
  * tenant package for the long-form rationale of each branch.
53
53
  */
54
54
  import { executeDirectiveToIntent } from "./wire_encode.js";
55
+ import { expandCapabilityExports } from "./capability_exports.js";
55
56
  import type { Directive } from "./directive.js";
56
57
  import type { AggregateHandle, AggregateInvariantFn, AggregateInvariantVerdict } from "./aggregate.js";
57
58
  import type { Ports } from "./ctx.js";
@@ -200,7 +201,10 @@ function combinedsOf(mod: DomainModuleExports): CombinedDecl[] {
200
201
  function mergeModules(mods: readonly DomainModuleExports[]): DomainModuleExports {
201
202
  let merged: DomainModuleExports = {};
202
203
  for (const m of mods) merged = { ...merged, ...m };
203
- return merged;
204
+ // `capability()` bundles expand into discoverable entries here — the engine
205
+ // plane's ONE merge point, so the registry sees the task aggregate + quartet
206
+ // without per-piece re-exports (capability_exports.ts).
207
+ return expandCapabilityExports(merged);
204
208
  }
205
209
 
206
210
  /**
@@ -0,0 +1,215 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded
4
+ // QuickJS engine} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
5
+ // wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
6
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
7
+
8
+ /**
9
+ * framework/capability.ts — `capability()`: THE MARKETPLACE DECLARATION
10
+ * (`architecture/capability_marketplace.md` §4, ruling M2 — the framework-hidden
11
+ * lane). ONE call declares a typed Order→Receipt capability interface; the dev
12
+ * sees a typed order call and a typed task row, and the framework supplies
13
+ * everything else through the SHIPPED `impureCapability()` quartet — so the gate
14
+ * law (`check_order_receipt`), the kernel recognizer, codegen, and the stable-id
15
+ * lockfile all see a perfectly ordinary impure capability. Hidden means *not
16
+ * hand-rolled*, never *not auditable*: the Order and Receipt facts ride the
17
+ * ledger verbatim.
18
+ *
19
+ * ```ts
20
+ * export const llm = capability("llm.complete", {
21
+ * request: { prompt: t.string(), model: t.string(), maxTokens: t.int() },
22
+ * receipt: { tokensIn: t.int(), tokensOut: t.int() },
23
+ * });
24
+ * // derived, deterministically from the name:
25
+ * // aggregate LlmCompleteTask order orderLlmComplete
26
+ * // complete completeLlmComplete fail/block/deadLetter likewise
27
+ * // capability "llm.complete" (the CapabilityBinding the executor must hold)
28
+ * ```
29
+ *
30
+ * THE RECEIPT-FIELD MECHANISM (stated honestly — the sheet's §4 comment): the
31
+ * complete Receipt's payload is the framework-FIXED `{requestId, resultRef,
32
+ * completedAt}`; typed receipt VALUES ride INSIDE `resultRef` as a JSON object,
33
+ * and the generated `completeResult` ops decode them into the declared typed
34
+ * fields (per-field zod-validated IN the plan — a malformed executor result is a
35
+ * typed plan-halt refusal, never a silent partial fold). Receipt fields are
36
+ * auto-`.optional()` on the aggregate (absent while `requested` — the
37
+ * read-decode discipline). Large bodies belong in content-addressed
38
+ * `resultRef`/`resultEvidence` refs, not inline values.
39
+ *
40
+ * Naming: the capability name uses the shipped binding grammar
41
+ * (`^[a-z0-9][a-z0-9._-]{0,63}$` — dots legal; marketplace interfaces are
42
+ * dot-namespaced by convention) and is carried as the quartet's `capability`
43
+ * declaration — the `CapabilityBinding` the fulfilling executor must hold
44
+ * (`architecture/capability_credentials.md`).
45
+ */
46
+ import { z } from "zod";
47
+ import { instance } from "../aggregate.js";
48
+ import type { Field } from "../fields.js";
49
+ import { set, type PlannedOp } from "../ops.js";
50
+ import { query, type QueryDecl } from "../query.js";
51
+ import {
52
+ IMPURE_CAPABILITY_ENVELOPE,
53
+ impureCapability,
54
+ impureCapabilityAggregate,
55
+ impureCapabilityInstanceId,
56
+ type ImpureCapability,
57
+ } from "./impure_capability.js";
58
+
59
+ /** The shipped CapabilityBinding name grammar (worker + governance law agree). */
60
+ export const CAPABILITY_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
61
+
62
+ /** "llm.complete" → "LlmComplete" — the deterministic id stem. */
63
+ export function capabilityPascal(name: string): string {
64
+ return name
65
+ .split(/[._-]+/)
66
+ .filter(Boolean)
67
+ .map((part) => part[0]!.toUpperCase() + part.slice(1))
68
+ .join("");
69
+ }
70
+
71
+ /** The zod RAW SHAPE derived from request fields (each Field carries its own zod). */
72
+ type RequestShape<Req extends Record<string, Field>> = { [K in keyof Req]: Req[K]["zod"] };
73
+
74
+ /**
75
+ * What `capability()` returns: the full shipped quartet (so every existing
76
+ * consumer — codegen, lowering, the reactor helpers — works unchanged) plus the
77
+ * marketplace surface: the capability NAME and the request/receipt zod schemas
78
+ * (the executor side validates provider results against `receiptSchema` BEFORE
79
+ * building `resultRef`, so a refusal happens host-side first, gate-side always).
80
+ */
81
+ export interface CapabilityDecl<
82
+ Req extends Record<string, Field>,
83
+ Rec extends Record<string, Field>,
84
+ > extends ImpureCapability<string, RequestShape<Req>, Req & Rec> {
85
+ /** The capability name — the `CapabilityBinding` slot the executor must hold. */
86
+ readonly name: string;
87
+ /** The request fields' zod object (what `order<Pascal>`'s params validate as). */
88
+ readonly requestSchema: z.ZodObject<RequestShape<Req>>;
89
+ /**
90
+ * THE EXECUTOR'S SCAN LANE — a declared, indexed query over the task
91
+ * aggregate keyed on `status` (`<camelPascal>TasksByStatus`). The executor
92
+ * polls `{status: "requested"}` through the ordinary RPC query lane — no
93
+ * special scan op, no wasm arm: the projection index IS the work queue.
94
+ */
95
+ readonly tasks: QueryDecl;
96
+ /** The receipt fields' zod object — `{}` when the capability declares none. */
97
+ readonly receiptSchema: z.ZodObject<{ [K in keyof Rec]: Rec[K]["zod"] }>;
98
+ /** Build the `resultRef` JSON for a complete Receipt from typed receipt values. */
99
+ readonly encodeResult: (values: { [K in keyof Rec]: z.infer<Rec[K]["zod"]> }) => string;
100
+ }
101
+
102
+ /**
103
+ * Declare a typed marketplace capability. Sugar over {@link impureCapability} —
104
+ * nothing new on the wire; the one piece of REAL work it adds is the
105
+ * resultRef-decoding `completeResult` (the receipt-field mechanism above).
106
+ *
107
+ * Refusals are COMPILE-TIME loud (thrown at module load, so `nomos-compile`
108
+ * names them): a malformed name, a request/receipt field shadowing the reserved
109
+ * envelope, or a field declared on both sides.
110
+ */
111
+ export function capability<
112
+ const Name extends string,
113
+ Req extends Record<string, Field>,
114
+ Rec extends Record<string, Field> = Record<never, Field>,
115
+ >(name: Name, spec: { request: Req; receipt?: Rec }): CapabilityDecl<Req, Rec> {
116
+ if (!CAPABILITY_NAME_RE.test(name)) {
117
+ throw new Error(
118
+ `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`,
119
+ );
120
+ }
121
+ const reserved = new Set(Object.keys(IMPURE_CAPABILITY_ENVELOPE));
122
+ const receipt = (spec.receipt ?? {}) as Rec;
123
+ for (const [side, fields] of [
124
+ ["request", spec.request],
125
+ ["receipt", receipt],
126
+ ] as const) {
127
+ for (const k of Object.keys(fields)) {
128
+ if (reserved.has(k)) {
129
+ throw new Error(
130
+ `capability('${name}'): ${side} field '${k}' shadows the reserved Order→Receipt envelope — rename it (the envelope is framework law: ${[...reserved].join(", ")})`,
131
+ );
132
+ }
133
+ }
134
+ }
135
+ for (const k of Object.keys(receipt)) {
136
+ if (k in spec.request) {
137
+ throw new Error(
138
+ `capability('${name}'): '${k}' is declared on BOTH request and receipt — a field has one writer (the order writes requests, the complete Receipt writes receipts); split or rename it`,
139
+ );
140
+ }
141
+ }
142
+
143
+ const pascal = capabilityPascal(name);
144
+ const aggregateId = `${pascal}Task`;
145
+
146
+ // Receipt fields fold only when the complete Receipt lands — auto-`.optional()`
147
+ // so a freshly-ordered task decodes on the generated read type (the same
148
+ // read-decode discipline the envelope's own optionals follow).
149
+ const receiptFields = Object.fromEntries(
150
+ Object.entries(receipt).map(([k, f]) => [k, f.optional()]),
151
+ ) as Rec;
152
+ const params = { ...spec.request, ...receiptFields } as Req & Rec;
153
+ const taskAggregate = impureCapabilityAggregate(aggregateId, params);
154
+
155
+ const requestShape = Object.fromEntries(
156
+ Object.entries(spec.request).map(([k, f]) => [k, f.zod]),
157
+ ) as RequestShape<Req>;
158
+ const receiptShape = Object.fromEntries(
159
+ Object.entries(receipt).map(([k, f]) => [k, f.zod]),
160
+ ) as { [K in keyof Rec]: Rec[K]["zod"] };
161
+ const receiptEntries = Object.entries(receipt) as [string, Field][];
162
+
163
+ const quartet = impureCapability({
164
+ aggregateId,
165
+ orderDirectiveId: `order${pascal}`,
166
+ completeDirectiveId: `complete${pascal}`,
167
+ failDirectiveId: `fail${pascal}`,
168
+ blockDirectiveId: `block${pascal}`,
169
+ deadLetterDirectiveId: `deadLetter${pascal}`,
170
+ params,
171
+ paramsSchema: requestShape,
172
+ // The loop is dynamic (declared field names), so the per-field `set()` typing
173
+ // is asserted — the PUBLIC payload types stay fully inferred from the zod shape.
174
+ writeParams: (taskId, p) =>
175
+ Object.keys(spec.request).map((k) =>
176
+ set(instance(taskAggregate, taskId), k as never, (p as Record<string, unknown>)[k] as never),
177
+ ),
178
+ // The receipt-field mechanism: typed values ride INSIDE resultRef as JSON;
179
+ // decode + per-field zod-validate IN the plan (pure function of payload —
180
+ // deterministic; a malformed executor result is a typed plan-halt refusal).
181
+ // Absent keys are skipped (partial results are legal — read-decode
182
+ // discipline), present keys must validate.
183
+ ...(receiptEntries.length > 0
184
+ ? {
185
+ completeResult: (taskId: string, resultRef: string): PlannedOp[] => {
186
+ const decoded = JSON.parse(resultRef) as Record<string, unknown>;
187
+ const ops: PlannedOp[] = [];
188
+ for (const [k, f] of receiptEntries) {
189
+ if (decoded[k] === undefined) continue;
190
+ ops.push(set(instance(taskAggregate, taskId), k as never, f.zod.parse(decoded[k]) as never));
191
+ }
192
+ return ops;
193
+ },
194
+ }
195
+ : {}),
196
+ capability: name,
197
+ });
198
+
199
+ const camel = pascal[0]!.toLowerCase() + pascal.slice(1);
200
+ const tasks = query(`${camel}TasksByStatus`).key("status").returns(taskAggregate);
201
+
202
+ return {
203
+ ...quartet,
204
+ aggregate: taskAggregate,
205
+ instanceId: impureCapabilityInstanceId,
206
+ name,
207
+ requestSchema: z.object(requestShape),
208
+ receiptSchema: z.object(receiptShape),
209
+ encodeResult: (values) => JSON.stringify(z.object(receiptShape).parse(values)),
210
+ tasks,
211
+ // The one-export expansion tag (capability_exports.ts): every exports walk
212
+ // discovers this bundle's aggregate + quartet + scan query without re-exports.
213
+ __isCapabilityDecl: true,
214
+ } as CapabilityDecl<Req, Rec>;
215
+ }
@@ -136,9 +136,10 @@ export const IMPURE_CAPABILITY_ENVELOPE = {
136
136
  // GENERIC, written by every impureCapability Receipt (complete OR fail), so the read
137
137
  // projection answers "is Order O closed, by which Receipt, with what result" in O(1)
138
138
  // off the `(closesOrder, order_id)` index — never a ledger scan. Absent while
139
- // `requested` (a fresh order has no closing receipt). Wire-identical to the Rust
140
- // reactor's `closesOrder` op (`git-holon::reactor::receipt_intent`,
141
- // `kernel::manifest_view::IMPURE_CAPABILITY_RECEIPT_FIELDS`).
139
+ // `requested` (a fresh order has no closing receipt). The kernel supplies its
140
+ // framework-fixed Lww driver (`kernel::manifest_view::IMPURE_CAPABILITY_RECEIPT_FIELDS`)
141
+ // and the gate enforces the lifecycle around it (`nomos_executor::check_order_receipt`
142
+ // admit step 5.5: OrphanReceipt / ReceiptOnTerminalOrder, terminal-once as law).
142
143
  closesOrder: t.string().merge(Lww).optional(),
143
144
  } as const;
144
145
 
@@ -348,6 +349,18 @@ export interface ImpureCapability<
348
349
  readonly block: RequirableDirective<z.infer<typeof blockSchema>>;
349
350
  readonly deadLetter: RequirableDirective<z.infer<typeof deadLetterSchema>>;
350
351
  readonly instanceId: (requestId: string) => string;
352
+ /**
353
+ * The CAPABILITY CREDENTIAL this task's provider must hold to fulfil Orders
354
+ * (`architecture/capability_credentials.md`): the law name of a
355
+ * `CapabilityBinding` — e.g. `"openai"`, `"cf-analytics"` — bound per
356
+ * workspace via the bind lane (`githolon capability bind <ws> <name>`). The
357
+ * provider HOST resolves `(workspace, capability)` → the credential VALUE
358
+ * from its own store, fail-closed against the law fact (active, unexpired);
359
+ * the value never rides an intent and never appears here. DECLARATION
360
+ * METADATA only — absent (the default) changes nothing on the wire, so
361
+ * credential-free domains stay byte-identical.
362
+ */
363
+ readonly capability?: string;
351
364
  }
352
365
 
353
366
  /**
@@ -429,6 +442,12 @@ export function impureCapability<
429
442
  * the Rust reactor's `result_fields` (`HandlerSuccess::with_result_field`).
430
443
  */
431
444
  completeResult?: (taskInstanceId: string, resultRef: string) => PlannedOp[];
445
+ /**
446
+ * OPTIONAL: the capability-credential name the provider host must hold a
447
+ * law-state `CapabilityBinding` for (see {@link ImpureCapability.capability}).
448
+ * Omit-when-absent — declaring nothing changes nothing on the wire.
449
+ */
450
+ capability?: string;
432
451
  }): ImpureCapability<Id, ParamsShape, F> {
433
452
  const taskAggregate = impureCapabilityAggregate(spec.aggregateId, spec.params);
434
453
 
@@ -489,6 +508,9 @@ export function impureCapability<
489
508
  block,
490
509
  deadLetter,
491
510
  instanceId: impureCapabilityInstanceId,
511
+ // Omit-when-absent (hash discipline): a capability-free declaration carries
512
+ // NO key at all, so its bundled source — and the package hash — is unchanged.
513
+ ...(spec.capability !== undefined ? { capability: spec.capability } : {}),
492
514
  };
493
515
  }
494
516
 
@@ -1,3 +1,7 @@
1
+ // governance-law revision: capability bindings 2026-06-12 (facts about authority on the
2
+ // ledger, authority itself host-side — architecture/capability_credentials.md). Carries
3
+ // the founding-key-rotation re-entry forward (genesis re-seeds idempotently by hash).
4
+ export const GOVERNANCE_REVISION = "2026-06-12-capability-bindings"; // hash-bearing: re-enters root genesis idempotently
1
5
  // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
6
  // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
7
  // wasm32-wasip1 artifact {kernel · projection · embedded
@@ -203,6 +207,15 @@ export const CloudPolicy = aggregate("CloudPolicy", {
203
207
  maxWorkspacesPerPrincipal: t.int().merge(Lww),
204
208
  /** Cumulative pushed-bytes cap per workspace ledger (bytes). */
205
209
  workspaceByteCap: t.int().merge(Lww),
210
+ /**
211
+ * The workspace-CREATION posture dial (cloud_sovereignty.md S6) —
212
+ * `"open"` (anyone creates), `"invite"` (an active CreationGrant key
213
+ * required), or `"closed"` (admins only). OPTIONAL: absent keeps the
214
+ * implicit posture the cloud has always had (zero active CreationGrants ⇒
215
+ * open, any ⇒ invite-governed) — pre-dial chains and policies behave
216
+ * exactly as before.
217
+ */
218
+ creationPosture: t.string().merge(Lww).optional(),
206
219
  /** Who last set the policy (attribution). */
207
220
  setBy: t.string().merge(Lww),
208
221
  /** Epoch-ms the policy was last set (caller-stamped). */
@@ -245,6 +258,47 @@ export const PlatformGrant = aggregate("PlatformGrant", {
245
258
  status: t.enum(GRANT_STATUSES).merge(Lww),
246
259
  });
247
260
 
261
+ /**
262
+ * CapabilityBinding — A FACT ABOUT AUTHORITY, never authority itself (the
263
+ * doctrine: `architecture/capability_credentials.md`). A workspace's impure
264
+ * capabilities (analytics tokens, provider API keys, upload credentials) need
265
+ * credentials — and credentials are constitutionally incompatible with a
266
+ * replicated chain (every peer would hold them forever; git never forgets;
267
+ * revocation cannot recall bytes). So the LEDGER records only the binding fact:
268
+ * WHICH workspace bound WHICH capability under WHICH credential digest
269
+ * (`keyHash` — the AdminGrant discipline, again), with what scope/expiry, by
270
+ * whom, at which `epoch`. The credential VALUE lives at exactly ONE
271
+ * credential-holding host (the workspace DO's storage, keyed
272
+ * `(workspace, capability)`), is never passed host-to-host, and is unusable
273
+ * the moment the law flips this record — the executing host checks the law
274
+ * fail-closed before every use.
275
+ *
276
+ * EPOCHS ARE PROVENANCE: a rotation revokes the active record and creates the
277
+ * next one (`epoch + 1`, fresh `keyHash`) — so every Receipt fact a provider
278
+ * stamps can name the binding epoch that produced it, and "which credential
279
+ * produced which facts" is answerable forever from law-state alone.
280
+ */
281
+ export const CapabilityBinding = aggregate("CapabilityBinding", {
282
+ /** The bound workspace's CloudWorkspace AGGREGATE id (root-custody lineage). */
283
+ workspaceId: t.string().merge(Lww),
284
+ /** The capability's law name (e.g. "cf-analytics", "openai", "gcs-upload"). */
285
+ capability: t.string().merge(Lww),
286
+ /** sha256 hex of the credential VALUE — the value itself NEVER lands here. */
287
+ keyHash: t.string().merge(Lww),
288
+ /** Monotone per (workspace, capability): 1, 2, … — rotation provenance. */
289
+ epoch: t.int().merge(Lww),
290
+ /** OPTIONAL narrowing label ("read-only", an account tag, …); absent = whole capability. */
291
+ scope: t.string().merge(Lww).optional(),
292
+ /** OPTIONAL epoch-ms expiry; absent = until revoked. Expired = fail-closed at the host. */
293
+ expiresAt: t.int().merge(Lww).optional(),
294
+ /** Who bound it (attribution, on the ledger forever). */
295
+ boundBy: t.string().merge(Lww),
296
+ /** Epoch-ms the binding was authored (caller-stamped). */
297
+ boundAt: t.int().merge(Lww),
298
+ /** `active` | `revoked` — the law's kill switch for the credential's USE. */
299
+ status: t.enum(GRANT_STATUSES).merge(Lww),
300
+ });
301
+
248
302
  // --- workspace directives ---------------------------------------------------------
249
303
 
250
304
  /**
@@ -607,6 +661,66 @@ export const releaseHostname = directive("releaseHostname")
607
661
  return [set(c, "status", "released" as const)];
608
662
  });
609
663
 
664
+ /**
665
+ * bindCapability — record the FACT that a workspace holds a capability
666
+ * credential (worker-authored after the value is stored host-side; the value
667
+ * itself takes the other road — DO storage — and NEVER this one). The payload's
668
+ * only credential-shaped field is `keyHash`, regex-pinned to sha256 hex: a raw
669
+ * secret value structurally cannot enter this directive. `scope`/`expiresAt`
670
+ * are omit-when-absent — a scope-free, expiry-free binding writes neither field.
671
+ */
672
+ export const bindCapability = directive("bindCapability")
673
+ .creates(CapabilityBinding)
674
+ .payload(
675
+ z.object({
676
+ workspaceId: z.string().min(1),
677
+ /** Lowercase capability name — a law identifier, not free text. */
678
+ capability: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/, "expected a lowercase capability name ([a-z0-9._-], max 64)"),
679
+ keyHash: sha256Hex,
680
+ epoch: z.number().int().positive(),
681
+ scope: z.string().min(1).optional(),
682
+ expiresAt: epochMs.optional(),
683
+ boundBy: z.string().min(1),
684
+ boundAt: epochMs,
685
+ }),
686
+ )
687
+ .plan((p) => {
688
+ const b = create(CapabilityBinding)
689
+ .set("workspaceId", p.workspaceId)
690
+ .set("capability", p.capability)
691
+ .set("keyHash", p.keyHash)
692
+ .set("epoch", p.epoch)
693
+ .set("boundBy", p.boundBy)
694
+ .set("boundAt", p.boundAt)
695
+ .set("status", "active" as const);
696
+ // Omit-when-absent: optional facts fold only when the payload carries them.
697
+ if (p.scope !== undefined) b.set("scope", p.scope);
698
+ if (p.expiresAt !== undefined) b.set("expiresAt", p.expiresAt);
699
+ return [];
700
+ });
701
+
702
+ /**
703
+ * revokeCapability — flip a CapabilityBinding to `revoked`. The credential
704
+ * stops being USABLE at fold time (the executing host checks this record,
705
+ * fail-closed, before resolving the value); the record (who bound it, when,
706
+ * who killed it — the revoker rides the intent) stays on the ledger — audit is
707
+ * structural. Rotation = revoke + bind at `epoch + 1`; unbind = revoke + the
708
+ * host deletes the stored value.
709
+ */
710
+ export const revokeCapability = directive("revokeCapability")
711
+ .mutates(CapabilityBinding)
712
+ .payload(
713
+ z.object({
714
+ bindingId: z.string().min(1),
715
+ revokedBy: z.string().min(1),
716
+ revokedAt: epochMs,
717
+ }),
718
+ )
719
+ .plan((p) => {
720
+ const b = instance(CapabilityBinding, p.bindingId);
721
+ return [set(b, "status", "revoked" as const)];
722
+ });
723
+
610
724
  /**
611
725
  * setCloudPolicy — set (or amend) THE QUOTA LAW. An Ensure on the law-state
612
726
  * singleton: the first author births it, later authors amend it in place —
@@ -618,6 +732,10 @@ export const setCloudPolicy = directive("setCloudPolicy")
618
732
  z.object({
619
733
  maxWorkspacesPerPrincipal: z.number().int().positive(),
620
734
  workspaceByteCap: z.number().int().positive(),
735
+ // The creation-posture dial (cloud_sovereignty.md S6). Optional:
736
+ // omitted leaves the field untouched (a partial amend never silently
737
+ // resets the door).
738
+ creationPosture: z.enum(["open", "invite", "closed"]).optional(),
621
739
  setBy: z.string().min(1),
622
740
  setAt: epochMs,
623
741
  }),
@@ -628,6 +746,7 @@ export const setCloudPolicy = directive("setCloudPolicy")
628
746
  withMarker(set(pol, "scope", "cloud"), "ensures"),
629
747
  set(pol, "maxWorkspacesPerPrincipal", p.maxWorkspacesPerPrincipal),
630
748
  set(pol, "workspaceByteCap", p.workspaceByteCap),
749
+ ...(p.creationPosture !== undefined ? [set(pol, "creationPosture", p.creationPosture)] : []),
631
750
  set(pol, "setBy", p.setBy),
632
751
  set(pol, "setAt", p.setAt),
633
752
  ];
@@ -815,3 +934,13 @@ export const hostnameClaimByHostname = query("hostnameClaimByHostname")
815
934
  export const hostnameClaimsByWorkspace = query("hostnameClaimsByWorkspace")
816
935
  .key("workspaceId")
817
936
  .returns(HostnameClaim);
937
+
938
+ /** A workspace's capability bindings — the executor's fail-closed law probe AND the audit list. */
939
+ export const capabilityBindingsByWorkspace = query("capabilityBindingsByWorkspace")
940
+ .key("workspaceId")
941
+ .returns(CapabilityBinding);
942
+
943
+ /** Provenance: which binding (workspace, capability, epoch) a credential digest belongs to. */
944
+ export const capabilityBindingByKeyHash = query("capabilityBindingByKeyHash")
945
+ .key("keyHash")
946
+ .returns(CapabilityBinding);
package/src/index.ts CHANGED
@@ -66,6 +66,9 @@ export { create, existing, type AggregateRef } from "./authoring.js";
66
66
  // THE CAPTURED READ — a plan reads O(1) DSL queries via `read(query, args)`; the result is committed as
67
67
  // the intent's read footprint (deterministic replay + stale-premise detection). The dev calls typed DSL
68
68
  // queries; the library routes them here. The host capability is `nomos.read` (twin of `nomos.rng`).
69
+ // NOT YET DEPLOYABLE (#57): the sealed wasm engine does not provide `nomos.read` yet, so
70
+ // `nomos-compile` REFUSES law that references read() (a named compile error, never a runtime halt).
71
+ // The captured-reads wave (task #58) wires the capability in and lifts the refusal.
69
72
  export { read } from "./read.js";
70
73
  export {
71
74
  query,
@@ -216,6 +219,12 @@ export {
216
219
  type TaskOutcome,
217
220
  type TaskReceipt,
218
221
  } from "./framework/impure_capability.js";
222
+ export {
223
+ capability,
224
+ capabilityPascal,
225
+ CAPABILITY_NAME_RE,
226
+ type CapabilityDecl,
227
+ } from "./framework/capability.js";
219
228
  export {
220
229
  DOMAIN_INSTALLATION_PHASES,
221
230
  PolicyBundle,