@githolon/dsl 0.55.1 → 0.57.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/lib/nomos_headless.dart +16 -1
- package/dart/lib/src/test_harness.dart +184 -6
- package/dart/lib/src/webview_bridge.dart +255 -29
- package/dart/pubspec.yaml +1 -1
- package/dart/test/cluster_attested_test.dart +185 -0
- package/dart/test/co2_acceptance_test.dart +0 -1
- package/dart/test/vo_read_typing_test.dart +0 -1
- package/dart/test/webview_bridge_test.dart +13 -0
- package/package.json +1 -1
- package/src/attested.ts +9 -0
- package/src/attested_core.ts +199 -2
- package/src/build_package.ts +17 -1
- package/src/codegen_dart.ts +7 -0
- package/src/codegen_proof.ts +31 -1
- package/src/directive.ts +55 -11
- package/src/index.ts +6 -0
- package/src/law_check.ts +107 -0
- package/src/read.ts +5 -2
- package/src/shape_compat.ts +10 -6
- package/src/usd.ts +145 -26
- package/src/usd_layers.ts +16 -4
|
@@ -239,6 +239,18 @@ Future<Object?> _invoke(
|
|
|
239
239
|
return b.sync(admit: a['admit'] as bool);
|
|
240
240
|
case 'lawStatus':
|
|
241
241
|
return b.lawStatus(a['hash'] as String);
|
|
242
|
+
case 'offerDirective':
|
|
243
|
+
return b.offerDirective(
|
|
244
|
+
domain: a['domain'] as String,
|
|
245
|
+
directiveId: a['directiveId'] as String,
|
|
246
|
+
payload: a['payload'],
|
|
247
|
+
actor: a['actor'] as String?,
|
|
248
|
+
domainHash: a['domainHash'] as String?,
|
|
249
|
+
attestations: (a['attestations'] as List?)
|
|
250
|
+
?.map((e) => (e as Map).cast<String, Object?>())
|
|
251
|
+
.toList(),
|
|
252
|
+
force: a['force'] as bool?,
|
|
253
|
+
);
|
|
242
254
|
default:
|
|
243
255
|
fail('corpus op not wired into the Dart bridge: $op');
|
|
244
256
|
}
|
|
@@ -248,6 +260,7 @@ Future<Object?> _invoke(
|
|
|
248
260
|
Object? _expectedTyped(String op, Map<String, Object?> result) {
|
|
249
261
|
switch (op) {
|
|
250
262
|
case 'offer':
|
|
263
|
+
case 'offerDirective':
|
|
251
264
|
return result['head'];
|
|
252
265
|
case 'query':
|
|
253
266
|
case 'queryById':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/src/attested.ts
CHANGED
|
@@ -32,12 +32,21 @@ export {
|
|
|
32
32
|
PARENT_TRUST_SENTINEL,
|
|
33
33
|
foreignQuery,
|
|
34
34
|
isTrustSourceDecl,
|
|
35
|
+
trustPolicy,
|
|
36
|
+
isTrustPolicyDecl,
|
|
37
|
+
fromPayload,
|
|
38
|
+
isFromPayloadRef,
|
|
35
39
|
NomosSourceFrontier,
|
|
36
40
|
sourceFrontierId,
|
|
37
41
|
SOURCE_FRONTIER_PREFIX,
|
|
38
42
|
__trustFor,
|
|
43
|
+
__policyFor,
|
|
39
44
|
type TrustSourceDecl,
|
|
40
45
|
type TrustSourceOptions,
|
|
46
|
+
type TrustPolicyDecl,
|
|
47
|
+
type TrustPolicyOptions,
|
|
48
|
+
type FromPayloadRef,
|
|
49
|
+
type ForeignReadEdge,
|
|
41
50
|
type ForeignQueryRef,
|
|
42
51
|
} from "./attested_core.js";
|
|
43
52
|
|
package/src/attested_core.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* module-init cycle. The sanctioning ensure directive + the merge-seam injector live in
|
|
11
11
|
* `attested.ts`. Full doctrine: `architecture/attested_reads_design.md`.
|
|
12
12
|
*/
|
|
13
|
-
import { aggregate } from "./aggregate.js";
|
|
14
|
-
import { t } from "./fields.js";
|
|
13
|
+
import { aggregate, type AggregateHandle } from "./aggregate.js";
|
|
14
|
+
import { t, type Field } from "./fields.js";
|
|
15
15
|
import { Lww } from "./drivers.js";
|
|
16
16
|
|
|
17
17
|
/** A lightweight reference to a query declared in the SOURCE workspace's law — the reader
|
|
@@ -125,6 +125,203 @@ export function isTrustSourceDecl(v: unknown): v is TrustSourceDecl {
|
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// ── DYNAMIC-SOURCE trust: a policy over a LAW-STATE allow-list (dynamic_source_attested_reads.md) ──
|
|
129
|
+
// The static `trustSource("ws", …)` edge names a FIXED workspace; a `trustPolicy` instead names a
|
|
130
|
+
// reader-side aggregate whose folded rows ARE the acceptable-source set — so the routed workspace is
|
|
131
|
+
// chosen at RUNTIME (the payload names it) while the trust judgment (genesis + accepted keys) stays a
|
|
132
|
+
// deterministic fold of the reader's own gated-enrolment writes. The gate resolves `att.source_genesis`
|
|
133
|
+
// against the allow-list rows; the NAME is routing, the genesis + keys are identity.
|
|
134
|
+
|
|
135
|
+
/** A marker naming a PAYLOAD FIELD that carries the routed source-workspace name at runtime — the
|
|
136
|
+
* first argument of the dynamic `.readsFrom(fromPayload("field"), query, { policy })` overload. */
|
|
137
|
+
export interface FromPayloadRef {
|
|
138
|
+
readonly payloadField: string;
|
|
139
|
+
/** brand — distinguishes the dynamic (payload-routed) lane from a static workspace-name string. */
|
|
140
|
+
readonly __nomosFromPayload: true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Name the PAYLOAD FIELD whose value routes a dynamic attested read (see {@link FromPayloadRef}). */
|
|
144
|
+
export function fromPayload(field: string): FromPayloadRef {
|
|
145
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
146
|
+
throw new Error(`fromPayload(): the payload field name must be a non-empty string`);
|
|
147
|
+
}
|
|
148
|
+
return { payloadField: field, __nomosFromPayload: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Shape predicate — a {@link FromPayloadRef} vs a static workspace-name string. Library-internal. */
|
|
152
|
+
export function isFromPayloadRef(v: unknown): v is FromPayloadRef {
|
|
153
|
+
return (
|
|
154
|
+
v !== null &&
|
|
155
|
+
typeof v === "object" &&
|
|
156
|
+
(v as { __nomosFromPayload?: unknown }).__nomosFromPayload === true &&
|
|
157
|
+
typeof (v as { payloadField?: unknown }).payloadField === "string"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The §4.4 freshness + allow-list vocabulary of one declared trust POLICY. */
|
|
162
|
+
export interface TrustPolicyOptions {
|
|
163
|
+
/** The reader-side aggregate whose folded rows are the acceptable-source set (genesis + keyHashes
|
|
164
|
+
* per row). Enrolling a row is a GATED WRITE (owner authority); revoking = archiving the row. */
|
|
165
|
+
readonly allowList: AggregateHandle;
|
|
166
|
+
/** The allow-list field (a `t.string()`) holding a row's source GENESIS oid. Inferred when the
|
|
167
|
+
* aggregate has exactly one string field whose name contains "genesis"; else required. */
|
|
168
|
+
readonly genesisField?: string;
|
|
169
|
+
/** The allow-list field (a `t.set(t.string())`) holding a row's accepted attestor keyHashes.
|
|
170
|
+
* Inferred when the aggregate has exactly one set-of-string field; else required. */
|
|
171
|
+
readonly keysField?: string;
|
|
172
|
+
/** §4.4a — wall-clock freshness bound in ms (judged against the intent's frozen HLC). */
|
|
173
|
+
readonly maxAgeMs?: number;
|
|
174
|
+
/** §4.4b — the epoch anchor: `"birth"` (the reader's own birth-cert epoch) or an integer floor. */
|
|
175
|
+
readonly minSourceEpoch?: "birth" | number;
|
|
176
|
+
/** §4.4c — maintain + enforce the monotone `SourceFrontier` (no regression per source genesis). */
|
|
177
|
+
readonly monotone?: boolean;
|
|
178
|
+
/** §5.3 — what a folded `SourceDishonor` fact does at verify: `"refuse"` or `"record"`. */
|
|
179
|
+
readonly onDishonor?: "refuse" | "record";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** One declared trust POLICY — an exported {@link trustPolicy} value, auto-discovered by shape.
|
|
183
|
+
* Lowered into the `nomosAttestedReadGate` era key's `policies` map. */
|
|
184
|
+
export interface TrustPolicyDecl {
|
|
185
|
+
readonly policyId: string;
|
|
186
|
+
/** The resolved allow-list aggregate TYPE id. */
|
|
187
|
+
readonly allowListType: string;
|
|
188
|
+
/** The resolved genesis field LABEL. */
|
|
189
|
+
readonly genesisField: string;
|
|
190
|
+
/** The resolved keyHashes field LABEL. */
|
|
191
|
+
readonly keysField: string;
|
|
192
|
+
readonly maxAgeMs?: number;
|
|
193
|
+
readonly minSourceEpoch?: "birth" | number;
|
|
194
|
+
readonly monotone?: boolean;
|
|
195
|
+
readonly onDishonor?: "refuse" | "record";
|
|
196
|
+
readonly __nomosTrustPolicy: true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The runtime policy registry — the dynamic read lane resolves a policy's monotone posture through
|
|
200
|
+
// the directive's readsFrom (which registers the queryId); this map lets the compile discovery + the
|
|
201
|
+
// validators resolve a decl by id. Keyed by policyId; later declaration wins.
|
|
202
|
+
const policyRegistry = new Map<string, TrustPolicyDecl>();
|
|
203
|
+
|
|
204
|
+
/** The registered trust policy for an id (`undefined` = undeclared). Library-internal. */
|
|
205
|
+
export function __policyFor(policyId: string): TrustPolicyDecl | undefined {
|
|
206
|
+
return policyRegistry.get(policyId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* DECLARE a dynamic trust POLICY (dynamic_source_attested_reads.md): the acceptable-source set is
|
|
211
|
+
* LAW-STATE — the folded rows of `allowList`, each a `{ <genesisField>: string, <keysField>:
|
|
212
|
+
* set-of-string }`. A directive routes a runtime-named source through this policy with
|
|
213
|
+
* `.readsFrom(fromPayload("wsField"), theQuery, { policy: thisDecl })`. Auto-discovered by shape and
|
|
214
|
+
* lowered into the `nomosAttestedReadGate` era key's `policies` map. The field names are inferred by
|
|
215
|
+
* shape (see {@link TrustPolicyOptions}) or given explicitly; a bad allow-list is a NAMED compile error.
|
|
216
|
+
*/
|
|
217
|
+
export function trustPolicy(policyId: string, opts: TrustPolicyOptions): TrustPolicyDecl {
|
|
218
|
+
const al = opts.allowList as AggregateHandle | undefined;
|
|
219
|
+
if (
|
|
220
|
+
al === null ||
|
|
221
|
+
typeof al !== "object" ||
|
|
222
|
+
(al as { __isAggregateHandle?: unknown }).__isAggregateHandle !== true
|
|
223
|
+
) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`trustPolicy('${policyId}'): 'allowList' must be an aggregate handle — pass the aggregate(...) ` +
|
|
226
|
+
`whose rows hold the trusted sources ({ <genesis> string, <keyHashes> set-of-string })`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const fields = al.fields as Record<string, Field>;
|
|
230
|
+
const stringFields = Object.keys(fields).filter((n) => (fields[n] as Field).kind === "string");
|
|
231
|
+
const setFields = Object.keys(fields).filter((n) => (fields[n] as Field).kind === "set");
|
|
232
|
+
|
|
233
|
+
let genesisField: string;
|
|
234
|
+
if (opts.genesisField !== undefined) {
|
|
235
|
+
if (!(opts.genesisField in fields)) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`trustPolicy('${policyId}'): genesisField '${opts.genesisField}' is not a field of aggregate '${al.id}'`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if ((fields[opts.genesisField] as Field).kind !== "string") {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`trustPolicy('${policyId}'): genesisField '${opts.genesisField}' must be a t.string() field (the source genesis oid)`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
genesisField = opts.genesisField;
|
|
246
|
+
} else {
|
|
247
|
+
const cands = stringFields.filter((n) => n.toLowerCase().includes("genesis"));
|
|
248
|
+
if (cands.length !== 1) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`trustPolicy('${policyId}'): cannot infer genesisField on aggregate '${al.id}' — declare exactly ` +
|
|
251
|
+
`ONE t.string() field whose name contains "genesis", or pass genesisField: "<name>" explicitly`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
genesisField = cands[0]!;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let keysField: string;
|
|
258
|
+
if (opts.keysField !== undefined) {
|
|
259
|
+
if (!(opts.keysField in fields)) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
`trustPolicy('${policyId}'): keysField '${opts.keysField}' is not a field of aggregate '${al.id}'`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if ((fields[opts.keysField] as Field).kind !== "set") {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`trustPolicy('${policyId}'): keysField '${opts.keysField}' must be a t.set(t.string()) field (the accepted attestor keyHashes)`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
keysField = opts.keysField;
|
|
270
|
+
} else {
|
|
271
|
+
if (setFields.length !== 1) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`trustPolicy('${policyId}'): cannot infer keysField on aggregate '${al.id}' — declare exactly ` +
|
|
274
|
+
`ONE t.set(t.string()) field (the accepted attestor keyHashes), or pass keysField: "<name>" explicitly`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
keysField = setFields[0]!;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const decl: TrustPolicyDecl = {
|
|
281
|
+
policyId,
|
|
282
|
+
allowListType: al.id,
|
|
283
|
+
genesisField,
|
|
284
|
+
keysField,
|
|
285
|
+
...(opts.maxAgeMs !== undefined ? { maxAgeMs: opts.maxAgeMs } : {}),
|
|
286
|
+
...(opts.minSourceEpoch !== undefined ? { minSourceEpoch: opts.minSourceEpoch } : {}),
|
|
287
|
+
...(opts.monotone !== undefined ? { monotone: opts.monotone } : {}),
|
|
288
|
+
...(opts.onDishonor !== undefined ? { onDishonor: opts.onDishonor } : {}),
|
|
289
|
+
__nomosTrustPolicy: true,
|
|
290
|
+
};
|
|
291
|
+
policyRegistry.set(policyId, decl);
|
|
292
|
+
return decl;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Shape predicate for compile discovery (the `isTrustSourceDecl` pattern). */
|
|
296
|
+
export function isTrustPolicyDecl(v: unknown): v is TrustPolicyDecl {
|
|
297
|
+
return (
|
|
298
|
+
v !== null &&
|
|
299
|
+
typeof v === "object" &&
|
|
300
|
+
(v as { __nomosTrustPolicy?: unknown }).__nomosTrustPolicy === true &&
|
|
301
|
+
typeof (v as { policyId?: unknown }).policyId === "string"
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** One declared foreign-read edge on a directive — a STATIC workspace edge or a dynamic policy edge.
|
|
306
|
+
* The static edge emits `{queryId, from}`; the policy edge emits `{queryId, policy}` (the routing
|
|
307
|
+
* `fromPayload` field is DSL-internal — the gate judges by genesis, the name is transport). */
|
|
308
|
+
export type ForeignReadEdge =
|
|
309
|
+
| { readonly queryId: string; readonly from: string }
|
|
310
|
+
| { readonly queryId: string; readonly policy: string; readonly fromPayload: string };
|
|
311
|
+
|
|
312
|
+
// A dynamic (policy-routed) read carries no declared `from` string, so the read lane cannot resolve
|
|
313
|
+
// its monotone posture through `__trustFor(name)`. The directive's readsFrom registers the queryId of
|
|
314
|
+
// every MONOTONE policy edge here; the foreign read lane consults it to record the SourceFrontier.
|
|
315
|
+
const monotoneForeignQueries = new Set<string>();
|
|
316
|
+
/** Register a foreign query id whose governing policy is monotone. Library-internal. */
|
|
317
|
+
export function __registerMonotoneForeignQuery(queryId: string): void {
|
|
318
|
+
monotoneForeignQueries.add(queryId);
|
|
319
|
+
}
|
|
320
|
+
/** Is a foreign query governed by a monotone policy edge? Library-internal. */
|
|
321
|
+
export function __isMonotoneForeignQuery(queryId: string): boolean {
|
|
322
|
+
return monotoneForeignQueries.has(queryId);
|
|
323
|
+
}
|
|
324
|
+
|
|
128
325
|
// ── the SourceFrontier (§4.4c) — the reader-side monotone record of the source's position ──────
|
|
129
326
|
|
|
130
327
|
/** The deterministic instance-id prefix — `source-frontier:<source_genesis>` (keyed by the source's
|
package/src/build_package.ts
CHANGED
|
@@ -61,7 +61,13 @@ import {
|
|
|
61
61
|
} from "./birth_meter.js";
|
|
62
62
|
import { withBirthMeterExports } from "./birth_meter_ensure.js";
|
|
63
63
|
import { withClearDirectives } from "./clear_directive.js";
|
|
64
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
withAttestedReadExports,
|
|
66
|
+
isTrustSourceDecl,
|
|
67
|
+
isTrustPolicyDecl,
|
|
68
|
+
type TrustSourceDecl,
|
|
69
|
+
type TrustPolicyDecl,
|
|
70
|
+
} from "./attested.js";
|
|
65
71
|
import type { RelationTypeDecl } from "./relation_schema.js";
|
|
66
72
|
import { domainHash, emitManifestBytes } from "./manifest.js";
|
|
67
73
|
import { collectValueObjectDescriptors } from "./value_object.js";
|
|
@@ -336,6 +342,15 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
336
342
|
const trustSources = [...trustByWs.values()].sort((a, b) =>
|
|
337
343
|
a.workspace < b.workspace ? -1 : a.workspace > b.workspace ? 1 : 0,
|
|
338
344
|
);
|
|
345
|
+
// Dynamic-source trust POLICIES (dynamic_source_attested_reads.md) — discovered by shape like
|
|
346
|
+
// trustSource. Later-wins by policy id; sorted for byte-determinism.
|
|
347
|
+
const policyById = new Map<string, TrustPolicyDecl>();
|
|
348
|
+
for (const v of Object.values(merged)) {
|
|
349
|
+
if (isTrustPolicyDecl(v)) policyById.set(v.policyId, v);
|
|
350
|
+
}
|
|
351
|
+
const trustPolicies = [...policyById.values()].sort((a, b) =>
|
|
352
|
+
a.policyId < b.policyId ? -1 : a.policyId > b.policyId ? 1 : 0,
|
|
353
|
+
);
|
|
339
354
|
const pabMarker =
|
|
340
355
|
parentAttestedBirthMarker(merged) ??
|
|
341
356
|
spec.modules.map((m) => parentAttestedBirthMarker(m)).find((x) => x !== null) ??
|
|
@@ -385,6 +400,7 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
385
400
|
// Omit-when-empty — the nomosAttestedReadGate era rule: a domain with no declared trust
|
|
386
401
|
// edge is byte-identical in the canonical manifest/USD.
|
|
387
402
|
...(trustSources.length > 0 ? { trustSources } : {}),
|
|
403
|
+
...(trustPolicies.length > 0 ? { trustPolicies } : {}),
|
|
388
404
|
...(dartImports.length > 0 ? { dartImports } : {}),
|
|
389
405
|
...(spec.dartRefImports !== undefined && spec.dartRefImports.length > 0
|
|
390
406
|
? { dartRefImports: [...spec.dartRefImports] }
|
package/src/codegen_dart.ts
CHANGED
|
@@ -3488,6 +3488,13 @@ export interface DomainModule {
|
|
|
3488
3488
|
* ADDITIVE + OPTIONAL: a domain with no trust edge omits this and is byte-identical (era rule).
|
|
3489
3489
|
*/
|
|
3490
3490
|
trustSources?: import("./attested.js").TrustSourceDecl[];
|
|
3491
|
+
/**
|
|
3492
|
+
* The domain's DYNAMIC-SOURCE TRUST POLICIES (dynamic_source_attested_reads.md): the exported
|
|
3493
|
+
* `trustPolicy(id, { allowList, … })` declarations, discovered by shape. Lowered — together with
|
|
3494
|
+
* directives' `.readsFrom(fromPayload(…), query, { policy })` edges — into the `nomosAttestedReadGate`
|
|
3495
|
+
* era key's `policies` map. ADDITIVE + OPTIONAL: a policy-free domain omits this (era rule).
|
|
3496
|
+
*/
|
|
3497
|
+
trustPolicies?: import("./attested.js").TrustPolicyDecl[];
|
|
3491
3498
|
/**
|
|
3492
3499
|
* The domain's NATIVE ZANZIBAR RELATION SCHEMA (`relation_schema.ts` — the userset-rewrite law,
|
|
3493
3500
|
* P1). Pure-data per-type relation rewrites lowered to the `nomosRelations` USD era key (NO JS on
|
package/src/codegen_proof.ts
CHANGED
|
@@ -1884,7 +1884,20 @@ export interface ProofLegs {
|
|
|
1884
1884
|
function isStandaloneCreate(d: Directive<unknown> | undefined): boolean {
|
|
1885
1885
|
return d?.marker === "creates" && typeof d.payloadSchema === "object"
|
|
1886
1886
|
&& d.birthsChild !== true && d.attestationAuthorizedGate === undefined
|
|
1887
|
-
&& d.requiresRelationGate === undefined
|
|
1887
|
+
&& d.requiresRelationGate === undefined
|
|
1888
|
+
// NOT a foreign-read directive (`.readsFrom(...)` — a static attested edge or a dynamic
|
|
1889
|
+
// policy edge): its plan reads a SIGNED cross-workspace attestation the single-workspace
|
|
1890
|
+
// offline proof plane cannot mint (that needs a second SOURCE workspace + its attestor key).
|
|
1891
|
+
// Dispatched offline it would throw at the un-served read; proven instead by the two-workspace
|
|
1892
|
+
// smoke (bench/scale/{attested_reads.test,dynamic_attested_reads.smoke}.mjs). Named below.
|
|
1893
|
+
&& (((d as { declaredForeignReads?: readonly unknown[] }).declaredForeignReads?.length ?? 0) === 0);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/** Does this module carry ≥1 foreign-read directive (a static or dynamic-policy `.readsFrom`)? */
|
|
1897
|
+
function hasForeignReadDirective(mod: DomainModule): boolean {
|
|
1898
|
+
return (mod.directives as Directive<unknown>[]).some(
|
|
1899
|
+
(d) => ((d as { declaredForeignReads?: readonly unknown[] }).declaredForeignReads?.length ?? 0) > 0,
|
|
1900
|
+
);
|
|
1888
1901
|
}
|
|
1889
1902
|
|
|
1890
1903
|
/** Does `v` carry an RFC 7946 geometry object at `segs` (walked into the composite)? */
|
|
@@ -2772,6 +2785,23 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
2772
2785
|
);
|
|
2773
2786
|
}
|
|
2774
2787
|
|
|
2788
|
+
// THE ATTESTED / DYNAMIC-POLICY FOREIGN-READ LEG — named, honestly SKIPPED here. A `.readsFrom(...)`
|
|
2789
|
+
// directive's plan reads a SIGNED cross-workspace attestation; proving it needs a SECOND source
|
|
2790
|
+
// workspace, its read-attestor key, and a served+signed envelope — none of which the single-workspace,
|
|
2791
|
+
// client-driven offline proof plane can mint (the client has no `attestedRead` serving op). The full
|
|
2792
|
+
// offline two-workspace proof (birth a scratch source → seed a row → enrol its genesis → attested read
|
|
2793
|
+
// through the policy edge → admitted, plus the wrong-genesis/unenrolled negative + revocation +
|
|
2794
|
+
// verify_chain) lives in the bench harness against the real wasm.
|
|
2795
|
+
if (hasForeignReadDirective(mod)) {
|
|
2796
|
+
out.push(
|
|
2797
|
+
`// ${n()}. attested foreign-read leg: a .readsFrom(...) directive reads a SIGNED cross-workspace`,
|
|
2798
|
+
`// attestation — proving it needs a second SOURCE workspace + its attestor key, which this`,
|
|
2799
|
+
`// single-workspace client proof cannot mint. Proven in the two-workspace bench smoke.`,
|
|
2800
|
+
`console.log("– attested foreign-read leg SKIPPED — a .readsFrom(...) attested/policy read is proven in the two-workspace bench harness (bench/scale/{attested_reads.test,dynamic_attested_reads.smoke}.mjs), not this single-workspace offline proof");`,
|
|
2801
|
+
``,
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2775
2805
|
// THE AUTHORITY-CONFORMANCE LEG (the one-encoder + gate proof): runs on the LOCAL holon (fetch re-trapped),
|
|
2776
2806
|
// proving the generated client threads security.actor through THE ONE ENCODER and that an actorless gated
|
|
2777
2807
|
// offer is impossible to GENERATE (throws MissingOfferAuthority before any submit). No-op when un-gated.
|
package/src/directive.ts
CHANGED
|
@@ -24,6 +24,14 @@ import type { CertifiedReadDecl } from "./certified_read.js";
|
|
|
24
24
|
import type { RelationDecl } from "./relation.js";
|
|
25
25
|
import type { QueryDecl } from "./query.js";
|
|
26
26
|
import { BIRTH_METER_QUERIES } from "./birth_meter.js";
|
|
27
|
+
import {
|
|
28
|
+
isFromPayloadRef,
|
|
29
|
+
isTrustPolicyDecl,
|
|
30
|
+
__registerMonotoneForeignQuery,
|
|
31
|
+
type FromPayloadRef,
|
|
32
|
+
type TrustPolicyDecl,
|
|
33
|
+
type ForeignReadEdge,
|
|
34
|
+
} from "./attested_core.js";
|
|
27
35
|
|
|
28
36
|
export type ReferentialMarker = "creates" | "mutates" | "ensures" | "archives";
|
|
29
37
|
|
|
@@ -183,7 +191,7 @@ export interface Directive<P = unknown> {
|
|
|
183
191
|
* era key. OPTIONAL + ADDITIVE; OMITTED when empty (a foreign-read-free directive is
|
|
184
192
|
* byte-identical — the era rule).
|
|
185
193
|
*/
|
|
186
|
-
readonly declaredForeignReads?:
|
|
194
|
+
readonly declaredForeignReads?: ForeignReadEdge[];
|
|
187
195
|
/**
|
|
188
196
|
* The directive's DECLARED emit boundary: the event types its `plan` may EMIT,
|
|
189
197
|
* each with an optional `max` bound. Part of the domain identity. Defaults to `{}`
|
|
@@ -331,6 +339,18 @@ export interface RequirableDirective<P> extends Directive<P> {
|
|
|
331
339
|
* times; deduped by `(queryId, from)`. Purely additive (the era rule).
|
|
332
340
|
*/
|
|
333
341
|
readsFrom(sourceWorkspace: string, query: { id: string }): RequirableDirective<P>;
|
|
342
|
+
/**
|
|
343
|
+
* Declare ONE DYNAMIC (payload-routed) foreign read (dynamic_source_attested_reads.md): the plan may
|
|
344
|
+
* `read(theQuery, args, { from: payload.<field> })` — the routed workspace is chosen at runtime by the
|
|
345
|
+
* named payload field, while the trust judgment (genesis + accepted attestor keys) is a fold of the
|
|
346
|
+
* reader's own {@link TrustPolicyDecl} allow-list. Lowered into the era key as a `{queryId, policy}`
|
|
347
|
+
* edge (the routing field is DSL-internal — the gate judges by genesis, the name is transport).
|
|
348
|
+
*/
|
|
349
|
+
readsFrom(
|
|
350
|
+
source: FromPayloadRef,
|
|
351
|
+
query: { id: string },
|
|
352
|
+
opts: { policy: TrustPolicyDecl },
|
|
353
|
+
): RequirableDirective<P>;
|
|
334
354
|
/**
|
|
335
355
|
* Declare ONE entry of the directive's EMIT boundary: an `eventType` its `plan` may
|
|
336
356
|
* emit, with an optional `max` count bound. Callable MULTIPLE times to accumulate
|
|
@@ -671,17 +691,41 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
|
|
|
671
691
|
const mergedQueries = [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
672
692
|
return makeRequirable({ ...base, declaredReads: merged, declaredQueryReads: mergedQueries });
|
|
673
693
|
},
|
|
674
|
-
readsFrom(
|
|
675
|
-
|
|
676
|
-
|
|
694
|
+
readsFrom(
|
|
695
|
+
source: string | FromPayloadRef,
|
|
696
|
+
query: { id: string },
|
|
697
|
+
opts?: { policy: TrustPolicyDecl },
|
|
698
|
+
): RequirableDirective<P> {
|
|
699
|
+
// ATTESTED CROSS-WORKSPACE READ (slice B + dynamic policies): accumulate a STATIC (queryId, from)
|
|
700
|
+
// edge OR a DYNAMIC (queryId, policy, fromPayload) edge, dedup by canonical key, sort (only the
|
|
701
|
+
// SET is the contract — byte-determinism for the era key).
|
|
702
|
+
let entry: ForeignReadEdge;
|
|
703
|
+
if (isFromPayloadRef(source)) {
|
|
704
|
+
const policy = opts?.policy;
|
|
705
|
+
if (policy === undefined) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
`readsFrom(fromPayload('${source.payloadField}'), '${query.id}'): a payload-routed foreign ` +
|
|
708
|
+
`read REQUIRES a trust policy — pass { policy: <trustPolicy(...) decl> } (the acceptable ` +
|
|
709
|
+
`sources are LAW-STATE, so the runtime-named workspace is judged against a folded allow-list).`,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
if (!isTrustPolicyDecl(policy)) {
|
|
713
|
+
throw new Error(
|
|
714
|
+
`readsFrom(fromPayload('${source.payloadField}'), '${query.id}'): { policy } must be a ` +
|
|
715
|
+
`trustPolicy(...) declaration.`,
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
entry = { queryId: query.id, policy: policy.policyId, fromPayload: source.payloadField };
|
|
719
|
+
if (policy.monotone === true) __registerMonotoneForeignQuery(query.id);
|
|
720
|
+
} else {
|
|
721
|
+
entry = { queryId: query.id, from: source };
|
|
722
|
+
}
|
|
723
|
+
const canon = (e: ForeignReadEdge): string =>
|
|
724
|
+
"from" in e ? `0${e.from}${e.queryId}` : `1${e.policy}${e.fromPayload}${e.queryId}`;
|
|
677
725
|
const prior = base.declaredForeignReads ?? [];
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
byKey.set(key(entry), entry);
|
|
682
|
-
const merged = [...byKey.values()].sort((a, b) =>
|
|
683
|
-
a.from !== b.from ? (a.from < b.from ? -1 : 1) : a.queryId < b.queryId ? -1 : a.queryId > b.queryId ? 1 : 0,
|
|
684
|
-
);
|
|
726
|
+
const byKey = new Map<string, ForeignReadEdge>(prior.map((e) => [canon(e), e]));
|
|
727
|
+
byKey.set(canon(entry), entry);
|
|
728
|
+
const merged = [...byKey.values()].sort((a, b) => (canon(a) < canon(b) ? -1 : canon(a) > canon(b) ? 1 : 0));
|
|
685
729
|
return makeRequirable({ ...base, declaredForeignReads: merged });
|
|
686
730
|
},
|
|
687
731
|
emits(eventType: string, opts?: { max?: number }): RequirableDirective<P> {
|
package/src/index.ts
CHANGED
|
@@ -148,12 +148,18 @@ export {
|
|
|
148
148
|
trustParent,
|
|
149
149
|
PARENT_TRUST_SENTINEL,
|
|
150
150
|
foreignQuery,
|
|
151
|
+
trustPolicy,
|
|
152
|
+
fromPayload,
|
|
151
153
|
NomosSourceFrontier,
|
|
152
154
|
sourceFrontierId,
|
|
153
155
|
SOURCE_FRONTIER_PREFIX,
|
|
154
156
|
nomosSourceFrontierEnsure,
|
|
155
157
|
type TrustSourceDecl,
|
|
156
158
|
type TrustSourceOptions,
|
|
159
|
+
type TrustPolicyDecl,
|
|
160
|
+
type TrustPolicyOptions,
|
|
161
|
+
type FromPayloadRef,
|
|
162
|
+
type ForeignReadEdge,
|
|
157
163
|
type ForeignQueryRef,
|
|
158
164
|
} from "./attested.js";
|
|
159
165
|
// ATTESTATION RECEIPTS + EQUIVOCATION ACCOUNTABILITY (slice B stage 3 — §5): the source-side
|
package/src/law_check.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { finishSum } from "./sum.js";
|
|
|
44
44
|
import { finishExtremum } from "./extremum.js";
|
|
45
45
|
import { finishExists } from "./exists.js";
|
|
46
46
|
import { clearDirectiveId } from "./clear_directive.js";
|
|
47
|
+
import type { ForeignReadEdge } from "./attested_core.js";
|
|
47
48
|
|
|
48
49
|
/** One author-time finding — the gate refusal pre-fired at compile, or a courtesy note. */
|
|
49
50
|
export interface Finding {
|
|
@@ -994,6 +995,111 @@ function topLevelPortReadFindings(mod: DomainModule, portReadingDomains: Readonl
|
|
|
994
995
|
];
|
|
995
996
|
}
|
|
996
997
|
|
|
998
|
+
// ── ATTESTED-READ twins (dynamic_source_attested_reads.md §"proof + check") ──────────────────────
|
|
999
|
+
// (a) a foreign read whose (directive, query) has no declared trust edge → REFUSE naming the fix;
|
|
1000
|
+
// (b) a trustPolicy whose allow-list aggregate has no directive writing it → WARN (unpopulatable);
|
|
1001
|
+
// (c) a query declared BOTH as a local .reads() premise AND a foreign policy read → REFUSE (laundering).
|
|
1002
|
+
|
|
1003
|
+
/** The FOREIGN query ids a directive's plan actually reads (`read(q, …, { from })`) on the one probe
|
|
1004
|
+
* sample — `undefined` when the plan can't be probed (unsynthesizable payload / it threw). A clean re-run
|
|
1005
|
+
* under a read port that answers both lanes; every side-effect sink is drained (never leaks). */
|
|
1006
|
+
function foreignReadsPerformed(dir: Directive<unknown>, where: string): Set<string> | undefined {
|
|
1007
|
+
let synth: SynthesizedPayload;
|
|
1008
|
+
try {
|
|
1009
|
+
synth = synthesizePayload(dir.payloadSchema as z.ZodTypeAny, where);
|
|
1010
|
+
} catch {
|
|
1011
|
+
return undefined;
|
|
1012
|
+
}
|
|
1013
|
+
const payload: Record<string, unknown> = { ...synth.value };
|
|
1014
|
+
for (const f of synth.autoStamped) payload[f] = "2026-01-01T00:00:00Z";
|
|
1015
|
+
const performed = new Set<string>();
|
|
1016
|
+
let ok = true;
|
|
1017
|
+
withBirthProbeGlobals(() => {
|
|
1018
|
+
const g = globalThis as unknown as { nomos: Record<string, unknown> };
|
|
1019
|
+
const prevRead = g.nomos.read;
|
|
1020
|
+
const stubRows = [{ id: "probe", data: {} }];
|
|
1021
|
+
g.nomos.read = (_q: string, _a: unknown, o?: { from?: string }) =>
|
|
1022
|
+
o?.from
|
|
1023
|
+
? {
|
|
1024
|
+
rows: stubRows,
|
|
1025
|
+
attestation: { v: 1, source_genesis: "probe-genesis", source_head: "h", source_epoch: 0, attested_at_ms: 0 },
|
|
1026
|
+
}
|
|
1027
|
+
: stubRows;
|
|
1028
|
+
__resetAuthoring();
|
|
1029
|
+
__resetReads();
|
|
1030
|
+
try {
|
|
1031
|
+
dir.plan(payload as never, deterministicPorts({ physical: 1, replica: 1, principal: "principal:probe" }));
|
|
1032
|
+
for (const r of __drainReads()) {
|
|
1033
|
+
if ((r as { attestation?: unknown }).attestation !== undefined) performed.add(r.queryId);
|
|
1034
|
+
}
|
|
1035
|
+
} catch {
|
|
1036
|
+
ok = false;
|
|
1037
|
+
} finally {
|
|
1038
|
+
try { __drainAuthoring(); } catch { /* drained */ }
|
|
1039
|
+
try { __drainReads(); } catch { /* drained */ }
|
|
1040
|
+
g.nomos.read = prevRead;
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
return ok ? performed : undefined;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function attestedReadFindings(mod: DomainModule): Finding[] {
|
|
1047
|
+
const domain = mod.domain ?? mod.name;
|
|
1048
|
+
const findings: Finding[] = [];
|
|
1049
|
+
const directives = mod.directives as Directive<unknown>[];
|
|
1050
|
+
|
|
1051
|
+
// (b) unpopulatable trust set — a policy allow-list no directive writes.
|
|
1052
|
+
const writtenTypes = new Set(directives.map((d) => (d as { aggregateId?: string }).aggregateId).filter(Boolean) as string[]);
|
|
1053
|
+
for (const pol of mod.trustPolicies ?? []) {
|
|
1054
|
+
if (!writtenTypes.has(pol.allowListType)) {
|
|
1055
|
+
findings.push({
|
|
1056
|
+
domain,
|
|
1057
|
+
level: "warn",
|
|
1058
|
+
what: `trustPolicy '${pol.policyId}' trusts sources enrolled in aggregate '${pol.allowListType}', but NO directive in this domain writes that aggregate — the trust set is unpopulatable (no gated enrolment lane), so every dynamic read under this policy will refuse (untrusted source)`,
|
|
1059
|
+
fix: `add a gated enrolment directive, e.g. directive("enroll…").creates(${pol.allowListType}).requires("<ownerCap>") whose payload carries { <genesis> string, <keyHashes> string[] }`,
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
for (const dir of directives) {
|
|
1065
|
+
const edges = (dir as { declaredForeignReads?: ForeignReadEdge[] }).declaredForeignReads ?? [];
|
|
1066
|
+
const declaredQueryIds = new Set(edges.map((e) => e.queryId));
|
|
1067
|
+
const localQueryIds = new Set(
|
|
1068
|
+
((dir as { declaredQueryReads?: { id: string }[] }).declaredQueryReads ?? []).map((q) => q.id),
|
|
1069
|
+
);
|
|
1070
|
+
|
|
1071
|
+
// (c) laundering — a query cannot be both a local .reads() premise AND a foreign read.
|
|
1072
|
+
for (const qid of declaredQueryIds) {
|
|
1073
|
+
if (localQueryIds.has(qid)) {
|
|
1074
|
+
findings.push({
|
|
1075
|
+
domain,
|
|
1076
|
+
directiveId: dir.id,
|
|
1077
|
+
level: "refuse",
|
|
1078
|
+
what: `query '${qid}' is declared BOTH as a local .reads() premise AND a foreign read on directive '${dir.id}' — a query cannot be both local (same-workspace CAS) and foreign (a signed cross-workspace attestation)`,
|
|
1079
|
+
fix: `drop one: keep .reads(${qid}) for a same-workspace captured read, OR keep the foreign .readsFrom(...) edge and remove the local .reads(${qid})`,
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// (a) an undeclared foreign read — the plan routes read(q, …, { from }) with no trust edge for q.
|
|
1085
|
+
const performed = foreignReadsPerformed(dir, `${domain}/${dir.id}`);
|
|
1086
|
+
if (performed !== undefined) {
|
|
1087
|
+
for (const qid of performed) {
|
|
1088
|
+
if (!declaredQueryIds.has(qid)) {
|
|
1089
|
+
findings.push({
|
|
1090
|
+
domain,
|
|
1091
|
+
directiveId: dir.id,
|
|
1092
|
+
level: "refuse",
|
|
1093
|
+
what: `directive '${dir.id}' reads a foreign workspace — read('${qid}', …, { from }) — but declares no trust edge for '${qid}', so the gate refuses it on every lane`,
|
|
1094
|
+
fix: `declare the edge: .readsFrom(fromPayload("<wsField>"), <${qid} query>, { policy: <trustPolicy(...) decl> }) for a payload-routed source, or .readsFrom("<ws>", <query>) + trustSource("<ws>", …) for a fixed one`,
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return findings;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
997
1103
|
/**
|
|
998
1104
|
* Run every author-time twin over the composed domain modules and return the findings, ordered
|
|
999
1105
|
* refuse → warn → note (stable within a level, by domain/directive appearance). This is the ONE
|
|
@@ -1044,6 +1150,7 @@ export function checkDomainModules(
|
|
|
1044
1150
|
findings.push(...dottedQueryKeyFindings(mod, aggById));
|
|
1045
1151
|
findings.push(...nullBanFindings(mod, aggById));
|
|
1046
1152
|
findings.push(...nullClearAuthorityFindings(mod, aggById));
|
|
1153
|
+
findings.push(...attestedReadFindings(mod));
|
|
1047
1154
|
}
|
|
1048
1155
|
|
|
1049
1156
|
// The BIRTH-RECIPE twin (unify): fold the scattered `.births()` recipe diagnostics into the
|
package/src/read.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { QueryDecl } from "./query.js";
|
|
21
21
|
import type { WireRead, WireReadAttestation } from "./wire.js";
|
|
22
|
-
import { NomosSourceFrontier, sourceFrontierId, __trustFor, __ensure } from "./attested_core.js";
|
|
22
|
+
import { NomosSourceFrontier, sourceFrontierId, __trustFor, __isMonotoneForeignQuery, __ensure } from "./attested_core.js";
|
|
23
23
|
|
|
24
24
|
// ── the per-dispatch read footprint — library state drained by `executeDirectiveToIntent` (mirrors the authoring sink) ──
|
|
25
25
|
let footprint: WireRead[] = [];
|
|
@@ -101,7 +101,10 @@ export function read<T = unknown>(
|
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
footprint.push({ queryId: query.id, args, result: answer.rows, attestation: att });
|
|
104
|
-
|
|
104
|
+
// `from` is a STATIC declared workspace for a trustSource edge, or a RUNTIME payload value for a
|
|
105
|
+
// dynamic policy edge — so the monotone posture comes either from the static edge (`__trustFor`) or
|
|
106
|
+
// from the policy the directive registered for this query id. Either way the frontier is genesis-keyed.
|
|
107
|
+
if (__trustFor(from)?.monotone === true || __isMonotoneForeignQuery(query.id)) {
|
|
105
108
|
const frontier = __ensure(NomosSourceFrontier, sourceFrontierId(att.source_genesis)) as {
|
|
106
109
|
set(f: string, v: unknown): unknown;
|
|
107
110
|
};
|