@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.
- package/package.json +2 -1
- package/src/build_package.ts +4 -0
- package/src/capability_exports.ts +55 -0
- package/src/codegen_dart.ts +9 -0
- package/src/codegen_proof.ts +72 -11
- package/src/compile_package_main.ts +241 -13
- package/src/directive.ts +35 -10
- package/src/engine_entry.ts +5 -1
- package/src/framework/capability.ts +215 -0
- package/src/framework/impure_capability.ts +25 -3
- package/src/framework/workspaces.ts +129 -0
- package/src/index.ts +9 -0
- package/src/manifest.ts +103 -0
- package/src/read.ts +29 -0
- package/src/stable_ids.ts +226 -0
- package/src/stable_ids_types.ts +40 -0
- package/src/usd.ts +54 -0
- package/src/usd_layers.ts +65 -1
- package/src/wire_encode.ts +15 -0
- package/dart/.dart_tool/package_config.json +0 -328
- package/dart/.dart_tool/package_graph.json +0 -485
- package/dart/.dart_tool/pub/bin/test/test.dart-3.11.5.snapshot +0 -0
- package/dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjU= +0 -0
- package/dart/.dart_tool/version +0 -1
- package/dart/build/native_assets/macos/native_assets.json +0 -1
- package/dart/build/test_cache/build/89a6598c8854ed031dfc25d83c80860e.cache.dill.track.dill +0 -0
- package/dart/build/unit_test_assets/AssetManifest.bin +0 -0
- package/dart/build/unit_test_assets/FontManifest.json +0 -1
- package/dart/build/unit_test_assets/NOTICES.Z +0 -0
- package/dart/build/unit_test_assets/NativeAssetsManifest.json +0 -1
- package/dart/build/unit_test_assets/shaders/ink_sparkle.frag +0 -0
- 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
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
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(...
|
|
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(...
|
|
267
|
-
//
|
|
268
|
-
//
|
|
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
|
-
|
|
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
|
package/src/engine_entry.ts
CHANGED
|
@@ -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
|
-
|
|
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).
|
|
140
|
-
//
|
|
141
|
-
// `
|
|
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,
|