@githolon/dsl 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart/CHANGELOG.md +6 -0
- package/dart/lib/src/errors.dart +19 -0
- package/dart/lib/src/webview_bridge.dart +22 -0
- package/dart/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/src/codegen_dart_client.ts +42 -17
- package/src/codegen_proof.ts +142 -2
- package/src/codegen_ts.ts +44 -10
- package/src/compile_engine.ts +18 -1
- package/src/engine_entry.ts +16 -2
- package/src/framework/genesis.ts +9 -4
- package/src/framework/workspace_invariant.ts +28 -0
- package/src/framework/workspaces.ts +187 -28
- package/src/index.ts +1 -0
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.20.0
|
|
2
|
+
- Authority-aware: `NomosBridge.offerDirective(domain, directiveId, payload, {actor, domainHash})` — generated
|
|
3
|
+
clients route every offer through THE ONE encoder (`encodeIntentOfferBytes` in @githolon/client); no
|
|
4
|
+
per-client envelope encoding. New typed `MissingOfferAuthority` — a `.requires(...)` directive authored
|
|
5
|
+
with no actor throws BEFORE submit (an actorless gated offer cannot be generated). Lockstep jump to 0.20.0.
|
|
6
|
+
|
|
1
7
|
## 0.17.0
|
|
2
8
|
- Lockstep bump with the birth-primitive release (see `@githolon/dsl` 0.17.0): birth is one law primitive
|
|
3
9
|
(`birthChild`), the platform-creator tier collapsed onto `#creator`, `grantCreation` takes a user or key
|
package/dart/lib/src/errors.dart
CHANGED
|
@@ -77,3 +77,22 @@ class LawHashUnavailable extends NomosBridgeError {
|
|
|
77
77
|
LawHashUnavailable({required this.workspace, String? message})
|
|
78
78
|
: super(message ?? 'could not resolve the current law hash for "$workspace" — the cloud reported no installed law for the home');
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
/// AUTHORITY IS UNFORGEABLE-BY-CONSTRUCTION: a GATED directive (one carrying a `.requires(relation)` write
|
|
82
|
+
/// gate in its law) authored with NO `actor` cannot produce an offer — the generated client throws THIS
|
|
83
|
+
/// BEFORE any submit, so an actorless gated offer is impossible to GENERATE (never merely refused at the
|
|
84
|
+
/// kernel gate). REMEDY: construct the generated client with an `actor` (the verified principal — under a
|
|
85
|
+
/// home scope this is the subject; the bridge owns signing). Secrets NEVER appear here — only the identity.
|
|
86
|
+
class MissingOfferAuthority extends NomosBridgeError {
|
|
87
|
+
/// The domain of the gated directive.
|
|
88
|
+
final String domain;
|
|
89
|
+
|
|
90
|
+
/// The gated directive id that could not be offered without an authority.
|
|
91
|
+
final String directiveId;
|
|
92
|
+
|
|
93
|
+
/// The relation the law requires the author to hold (e.g. `admin`).
|
|
94
|
+
final String requires;
|
|
95
|
+
MissingOfferAuthority({required this.domain, required this.directiveId, required this.requires, String? message})
|
|
96
|
+
: super(message ??
|
|
97
|
+
'$domain/$directiveId requires the "$requires" relation — construct the client with an actor (the verified principal) to author it; a gated offer cannot be made without an authority');
|
|
98
|
+
}
|
|
@@ -176,6 +176,28 @@ class NomosBridge {
|
|
|
176
176
|
return r['head'].toString();
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
/// THE ONE ENCODER offer: hand the holon a PLANNED directive (domain/directiveId/payload) + the verified
|
|
180
|
+
/// [actor]; the runner's `offerDirective` op encodes the v2 envelope via the SINGLE codec (index.mjs
|
|
181
|
+
/// `encodeIntentOfferBytes`, with `security.actor` included iff [actor] is set). The generated Dart client
|
|
182
|
+
/// calls THIS — it never hand-builds offer bytes, so there is exactly ONE copy of the wire format. Returns
|
|
183
|
+
/// the new local head; durable on [sync]. ([offer] with opaque bytes stays for back-compat carriers.)
|
|
184
|
+
Future<String> offerDirective({
|
|
185
|
+
required String domain,
|
|
186
|
+
required String directiveId,
|
|
187
|
+
required Object? payload,
|
|
188
|
+
String? actor,
|
|
189
|
+
String? domainHash,
|
|
190
|
+
}) async {
|
|
191
|
+
final r = _asMap(await _call('offerDirective', {
|
|
192
|
+
'domain': domain,
|
|
193
|
+
'directiveId': directiveId,
|
|
194
|
+
'payload': payload,
|
|
195
|
+
if (actor != null) 'actor': actor,
|
|
196
|
+
if (domainHash != null) 'domainHash': domainHash,
|
|
197
|
+
}));
|
|
198
|
+
return r['head'].toString();
|
|
199
|
+
}
|
|
200
|
+
|
|
179
201
|
/// Mint an ed25519 DEVICE KEY in-kernel → `{secret, public}` (base64). The SECRET stays on this device
|
|
180
202
|
/// (persist it in secure storage); pass it to [connect] as `authorSecret` (or per-offer `signWith`) so the
|
|
181
203
|
/// holon SIGNS each intent. The PUBLIC half is what gets enrolled so the gate re-verifies the signature on
|
package/dart/pubspec.yaml
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
12
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
13
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
14
|
-
version: 0.
|
|
14
|
+
version: 0.20.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|
|
@@ -123,15 +123,39 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
123
123
|
const payloadClass = `${cap(d.id)}Payload`;
|
|
124
124
|
const stamps = autoStampFields((d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema);
|
|
125
125
|
const method = safeMethod(d.id);
|
|
126
|
+
// THE AUTHORITY GATE (codegen-time): a directive carrying `.requires(relation)` is GATED — its method
|
|
127
|
+
// throws MissingOfferAuthority when `actor` is null/empty, BEFORE any submit, so an actorless gated
|
|
128
|
+
// offer is impossible to GENERATE (the war: the gate is structural, not just kernel-side). Secrets never
|
|
129
|
+
// appear — only the actor identity (the bridge owns signing).
|
|
130
|
+
const gate = (d as { requiresRelationGate?: { relation: string } }).requiresRelationGate;
|
|
131
|
+
const gateGuard = gate !== undefined
|
|
132
|
+
? ` if (actor == null || actor!.trim().isEmpty) throw MissingOfferAuthority(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, requires: ${dartStr(gate.relation)});`
|
|
133
|
+
: "";
|
|
134
|
+
const gateDoc = gate !== undefined
|
|
135
|
+
? ` /// GATED: needs an \`actor\` (the \`${gate.relation}\` relation) — throws MissingOfferAuthority otherwise.`
|
|
136
|
+
: "";
|
|
126
137
|
if (stamps.length > 0) {
|
|
127
138
|
methods.push(
|
|
128
139
|
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
|
|
129
140
|
` /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at offer.`,
|
|
141
|
+
...(gateDoc ? [gateDoc] : []),
|
|
130
142
|
` Future<String> ${method}(${payloadClass} payload) {`,
|
|
143
|
+
...(gateGuard ? [gateGuard] : []),
|
|
131
144
|
` final data = payload.toPayloadJson();`,
|
|
132
145
|
` final now = DateTime.now().toUtc().toIso8601String();`,
|
|
133
146
|
...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
|
|
134
|
-
` return bridge.
|
|
147
|
+
` return bridge.offerDirective(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: data, actor: actor, domainHash: domainHash);`,
|
|
148
|
+
` }`,
|
|
149
|
+
``,
|
|
150
|
+
);
|
|
151
|
+
} else if (gateGuard) {
|
|
152
|
+
// A gated directive with no `…At` sugar still needs a block body for the throw guard.
|
|
153
|
+
methods.push(
|
|
154
|
+
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
|
|
155
|
+
gateDoc,
|
|
156
|
+
` Future<String> ${method}(${payloadClass} payload) {`,
|
|
157
|
+
gateGuard,
|
|
158
|
+
` return bridge.offerDirective(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: payload.toPayloadJson(), actor: actor, domainHash: domainHash);`,
|
|
135
159
|
` }`,
|
|
136
160
|
``,
|
|
137
161
|
);
|
|
@@ -139,7 +163,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
139
163
|
methods.push(
|
|
140
164
|
` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
|
|
141
165
|
` Future<String> ${method}(${payloadClass} payload) =>`,
|
|
142
|
-
` bridge.
|
|
166
|
+
` bridge.offerDirective(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: payload.toPayloadJson(), actor: actor, domainHash: domainHash);`,
|
|
143
167
|
``,
|
|
144
168
|
);
|
|
145
169
|
}
|
|
@@ -216,14 +240,15 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
216
240
|
`// final app = ${className}(bridge);`,
|
|
217
241
|
`// await app.${dirs[0] ? safeMethod(dirs[0].id) : "someDirective"}(/* typed payload */);`,
|
|
218
242
|
``,
|
|
219
|
-
`
|
|
220
|
-
|
|
243
|
+
// `dart:convert` is needed ONLY for the interface block's `jsonDecode` — the offer lane no longer encodes
|
|
244
|
+
// any envelope (it routes through `bridge.offerDirective` → THE ONE ENCODER). Omit it when no interface.
|
|
245
|
+
...(opts.interface !== undefined ? [`import 'dart:convert';`, ``] : []),
|
|
221
246
|
`import '${supportImport}';`,
|
|
222
247
|
`import '${mod.name}.dart';`,
|
|
223
248
|
`// Re-export the sibling types (read models + payloads + the bridge runtime) so ONE import of this`,
|
|
224
249
|
`// client file gives an app the whole typed surface.`,
|
|
225
250
|
`export '${mod.name}.dart';`,
|
|
226
|
-
`export '${supportImport}' show NomosBridge, BridgeTransport, NomosBridgeError, kWebviewBridgeVersion;`,
|
|
251
|
+
`export '${supportImport}' show NomosBridge, BridgeTransport, NomosBridgeError, MissingOfferAuthority, kWebviewBridgeVersion;`,
|
|
227
252
|
``,
|
|
228
253
|
`/// sha256 of the deployed \`.package.usda\` — the installed law this client offers under.`,
|
|
229
254
|
`const String ${hashConst} = ${dartStr(opts.domainHash)};`,
|
|
@@ -238,22 +263,22 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
238
263
|
``,
|
|
239
264
|
]
|
|
240
265
|
: []),
|
|
241
|
-
`String _encodeIntentOffer(String domain, String directiveId, Object? payload, String domainHash) =>`,
|
|
242
|
-
` base64Encode(utf8.encode(jsonEncode({`,
|
|
243
|
-
` 'v': 2,`,
|
|
244
|
-
` 'intent': {`,
|
|
245
|
-
` 'domain': domain,`,
|
|
246
|
-
` 'directiveId': directiveId,`,
|
|
247
|
-
` 'payload': payload,`,
|
|
248
|
-
` 'domainHash': domainHash,`,
|
|
249
|
-
` },`,
|
|
250
|
-
` })));`,
|
|
251
|
-
``,
|
|
252
266
|
`/// Typed async client for the \`${domain}\` domain, bound to a connected [NomosBridge].`,
|
|
267
|
+
`///`,
|
|
268
|
+
`/// Pass \`actor\` (the verified principal uid) to author AS that user — the bridge's \`offerDirective\` op`,
|
|
269
|
+
`/// carries it through THE ONE ENCODER (\`encodeIntentOfferBytes\` in @githolon/client) as \`security.actor\`,`,
|
|
270
|
+
`/// so the kernel seals it as the captured principal and the ReBAC \`.requires(...)\` gate judges the real`,
|
|
271
|
+
`/// author. A GATED directive (one whose law declares \`.requires(relation)\`) authored with no actor throws`,
|
|
272
|
+
`/// [MissingOfferAuthority] BEFORE any submit — an actorless gated offer can't be constructed. The bridge`,
|
|
273
|
+
`/// owns SIGNING (a device secret on \`connect\`); only the actor IDENTITY is passed here, never a secret.`,
|
|
253
274
|
`class ${className} {`,
|
|
254
275
|
` final NomosBridge bridge;`,
|
|
255
276
|
` final String domainHash;`,
|
|
256
|
-
|
|
277
|
+
``,
|
|
278
|
+
` /// The verified principal uid every offer is authored as (sealed as \`security.actor\` by the one encoder).`,
|
|
279
|
+
` /// Null ⇒ open (un-gated) authoring; a gated directive then throws [MissingOfferAuthority].`,
|
|
280
|
+
` final String? actor;`,
|
|
281
|
+
` ${className}(this.bridge, {this.domainHash = ${hashConst}, this.actor});`,
|
|
257
282
|
``,
|
|
258
283
|
...(opts.interface !== undefined
|
|
259
284
|
? [
|
package/src/codegen_proof.ts
CHANGED
|
@@ -183,6 +183,136 @@ function findConcurrencyLeg(
|
|
|
183
183
|
return undefined;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// ── authority-conformance discovery (the ONE-ENCODER + gate proof — actorless gated offer is impossible) ──
|
|
187
|
+
|
|
188
|
+
/** A discovered AUTHORITY-CONFORMANCE leg: a GATED directive (one carrying `.requires(relation)`) the proof
|
|
189
|
+
* can drive locally to prove (a) authored WITH an actor, the produced offer envelope carries `security.actor`
|
|
190
|
+
* (the one encoder threaded the actor through), and (b) authored WITHOUT an actor, the generated client
|
|
191
|
+
* THROWS MissingOfferAuthority before any submit (an actorless gated offer is impossible to GENERATE). */
|
|
192
|
+
export interface AuthorityConformanceLeg {
|
|
193
|
+
/** The gated directive's domain (the generated factory + its `domain` field). */
|
|
194
|
+
readonly domain: string;
|
|
195
|
+
/** The gated directive id (the generated client method). */
|
|
196
|
+
readonly directiveId: string;
|
|
197
|
+
/** The relation the law requires the author to hold (e.g. `admin`) — the MissingOfferAuthority `requires`. */
|
|
198
|
+
readonly relation: string;
|
|
199
|
+
/** The synthesized payload the proof drives the gated directive with (auto-stamped `…At` omitted). */
|
|
200
|
+
readonly payload: Record<string, unknown>;
|
|
201
|
+
/** Required `…At` fields the runtime stamps at dispatch (mirrors the create leg). */
|
|
202
|
+
readonly autoStamped: readonly string[];
|
|
203
|
+
/** When the gated directive is a `.creates` whose own id is kernel-minted, the field to mint at runtime. */
|
|
204
|
+
readonly mintField?: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Discover the AUTHORITY-CONFORMANCE leg: the first GATED directive (`.requires(relation)` ⇒
|
|
208
|
+
* `requiresRelationGate`) whose payload the synthesizer can sample. A birth / attestation-authorized
|
|
209
|
+
* directive is skipped (their authority is a carried cert / signed quote, not an `actor` block — a different
|
|
210
|
+
* lane). `undefined` ⇒ this package has no gated directive (the leg becomes a documented no-op). */
|
|
211
|
+
export function discoverAuthorityConformanceLeg(mod: DomainModule): AuthorityConformanceLeg | undefined {
|
|
212
|
+
const domain = mod.domain ?? mod.name;
|
|
213
|
+
const gated = (mod.directives as Directive<unknown>[]).find(
|
|
214
|
+
(d) =>
|
|
215
|
+
d?.requiresRelationGate !== undefined &&
|
|
216
|
+
typeof d.payloadSchema === "object" &&
|
|
217
|
+
d.birthsChild !== true &&
|
|
218
|
+
d.attestationAuthorizedGate === undefined,
|
|
219
|
+
);
|
|
220
|
+
if (gated === undefined) return undefined;
|
|
221
|
+
let synth: SynthesizedPayload;
|
|
222
|
+
try {
|
|
223
|
+
synth = synthesizePayload(gated.payloadSchema as z.ZodTypeAny, `${domain}/${gated.id}`);
|
|
224
|
+
} catch {
|
|
225
|
+
return undefined; // an exotic payload the synthesizer can't sample → no conformance leg (still gated at the kernel)
|
|
226
|
+
}
|
|
227
|
+
const agg = (mod.aggregates as AggregateHandle[]).find((a) => a.id === gated.aggregateId);
|
|
228
|
+
const mintField = agg !== undefined ? mintedCreateField(gated, agg) : undefined;
|
|
229
|
+
const payload: Record<string, unknown> = { ...synth.value };
|
|
230
|
+
if (mintField !== undefined) delete payload[mintField];
|
|
231
|
+
return {
|
|
232
|
+
domain,
|
|
233
|
+
directiveId: gated.id,
|
|
234
|
+
relation: gated.requiresRelationGate!.relation,
|
|
235
|
+
payload,
|
|
236
|
+
autoStamped: synth.autoStamped,
|
|
237
|
+
...(mintField !== undefined ? { mintField } : {}),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* THE AUTHORITY-CONFORMANCE LEG (the one-encoder + gate proof). On the real LOCAL holon, with fetch trapped:
|
|
243
|
+
* (a) bind the generated client WITH an actor + drive the gated directive. SPY on `holon.offerDirective` to
|
|
244
|
+
* capture the (domain, directiveId, payload, opts) the client passes, RE-ENCODE that through the v2
|
|
245
|
+
* envelope shape (the SAME `{ v:2, intent, security:{actor} }` the one encoder emits) and DECODE it back
|
|
246
|
+
* to assert `security.actor` IS the bound actor — the produced offer envelope carries the principal.
|
|
247
|
+
* (b) bind a SECOND client WITHOUT an actor + call the gated directive — assert it THROWS
|
|
248
|
+
* MissingOfferAuthority LOCALLY (before any offerDirective/submit fires; the spy stays untouched).
|
|
249
|
+
* Emits nothing when the package has no gated directive (a documented no-op note).
|
|
250
|
+
*/
|
|
251
|
+
function authorityConformanceLegLines(
|
|
252
|
+
leg: AuthorityConformanceLeg | undefined,
|
|
253
|
+
factory: string,
|
|
254
|
+
n: () => number,
|
|
255
|
+
): string[] {
|
|
256
|
+
if (leg === undefined) {
|
|
257
|
+
return [
|
|
258
|
+
`// ${n()}. authority-conformance leg: this law has no gated (\`.requires(...)\`) directive, so there is`,
|
|
259
|
+
`// nothing to prove about actorless-gated-offer refusal (an un-gated domain authors openly).`,
|
|
260
|
+
`console.log("– authority-conformance leg SKIPPED — no .requires(...) directive (add one to prove the one-encoder threads security.actor + an actorless gated offer can't be generated)");`,
|
|
261
|
+
``,
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
const payloadEntries = leg.mintField !== undefined
|
|
265
|
+
? `{ ...${JSON.stringify(leg.payload)}, ${JSON.stringify(leg.mintField)}: await holon.mint("conformance") }`
|
|
266
|
+
: JSON.stringify(leg.payload);
|
|
267
|
+
return [
|
|
268
|
+
`// ${n()}. AUTHORITY CONFORMANCE (THE ONE ENCODER + the gate): \`${leg.domain}/${leg.directiveId}\` is GATED`,
|
|
269
|
+
`// (\`.requires("${leg.relation}")\`). (a) WITH an actor the produced offer envelope must carry`,
|
|
270
|
+
`// security.actor; (b) WITHOUT one the generated client THROWS MissingOfferAuthority before any submit.`,
|
|
271
|
+
`{`,
|
|
272
|
+
` const realFetch2 = globalThis.fetch;`,
|
|
273
|
+
` globalThis.fetch = (() => { throw new Error("OFFLINE PROOF VIOLATED: authority-conformance touched the network"); }) as typeof fetch;`,
|
|
274
|
+
` try {`,
|
|
275
|
+
` // (a) SPY on offerDirective: capture what the generated client passes, re-encode the v2 envelope it`,
|
|
276
|
+
` // would produce (the one-encoder shape), decode it back, and assert security.actor IS the actor.`,
|
|
277
|
+
` const ACTOR = "user:proof-conformance";`,
|
|
278
|
+
` let captured: { domain: string; directiveId: string; payload: unknown; opts?: { domainHash?: string; actor?: string } } | undefined;`,
|
|
279
|
+
` const realOffer: typeof holon.offerDirective = holon.offerDirective.bind(holon);`,
|
|
280
|
+
` holon.offerDirective = (domain, directiveId, payload, opts) => {`,
|
|
281
|
+
` captured = { domain, directiveId, payload, opts };`,
|
|
282
|
+
` return realOffer(domain, directiveId, payload, opts);`,
|
|
283
|
+
` };`,
|
|
284
|
+
` try {`,
|
|
285
|
+
` await ${factory}(holon, { actor: ACTOR }).${leg.directiveId}(${payloadEntries});`,
|
|
286
|
+
` } finally {`,
|
|
287
|
+
` holon.offerDirective = realOffer;`,
|
|
288
|
+
` }`,
|
|
289
|
+
` if (!captured) fail("authority-conformance: the gated directive did not reach holon.offerDirective");`,
|
|
290
|
+
` const actorOnOffer = captured!.opts?.actor;`,
|
|
291
|
+
` if (actorOnOffer !== ACTOR) fail(\`authority-conformance: the offer did not carry the actor (got \${JSON.stringify(actorOnOffer)})\`);`,
|
|
292
|
+
` // Decode the bytes THE ONE ENCODER actually emits from these args (NOT a hand-rolled copy) — so this`,
|
|
293
|
+
` // leg catches encoder DRIFT (a dropped/renamed/whitespace-normalized security.actor), not merely that`,
|
|
294
|
+
` // the actor reached offerDirective. The v2 wire format lives in exactly one place: encodeIntentOfferBytes.`,
|
|
295
|
+
` const decoded = JSON.parse(new TextDecoder().decode(encodeIntentOfferBytes({ domain: captured!.domain, directiveId: captured!.directiveId, payload: captured!.payload, domainHash: captured!.opts?.domainHash, actor: actorOnOffer })));`,
|
|
296
|
+
` if (decoded?.security?.actor !== ACTOR) fail(\`authority-conformance: THE ONE ENCODER dropped security.actor (\${JSON.stringify(decoded?.security)})\`);`,
|
|
297
|
+
` ok(\`authority conformance (a) — \${${JSON.stringify(`${leg.domain}/${leg.directiveId}`)}} authored WITH an actor carries security.actor through THE ONE ENCODER\`);`,
|
|
298
|
+
``,
|
|
299
|
+
` // (b) the SAME directive with NO actor: the generated client refuses to BUILD the offer — it throws`,
|
|
300
|
+
` // MissingOfferAuthority locally, BEFORE any submit (an actorless gated offer is impossible to make).`,
|
|
301
|
+
` let threw: Error | undefined;`,
|
|
302
|
+
` try {`,
|
|
303
|
+
` await ${factory}(holon).${leg.directiveId}(${payloadEntries});`,
|
|
304
|
+
` } catch (e) { threw = e as Error; }`,
|
|
305
|
+
` if (threw === undefined) fail("authority-conformance: the gated directive with NO actor did NOT throw — an actorless gated offer must be impossible to generate");`,
|
|
306
|
+
` if (threw.name !== "MissingOfferAuthority") fail(\`authority-conformance: expected MissingOfferAuthority, got \${threw.name}: \${threw.message}\`);`,
|
|
307
|
+
` ok(\`authority conformance (b) — \${${JSON.stringify(`${leg.domain}/${leg.directiveId}`)}} with NO actor throws MissingOfferAuthority before any submit (gated offer impossible to generate)\`);`,
|
|
308
|
+
` } finally {`,
|
|
309
|
+
` globalThis.fetch = realFetch2;`,
|
|
310
|
+
` }`,
|
|
311
|
+
`}`,
|
|
312
|
+
``,
|
|
313
|
+
];
|
|
314
|
+
}
|
|
315
|
+
|
|
186
316
|
// ── child-birth discovery (the G3 `.births()` lane — prove a parent birth end-to-end into the child) ──────
|
|
187
317
|
|
|
188
318
|
/** ONE step of a discovered birth recipe's genesis chain (the subset codegen reads). */
|
|
@@ -733,7 +863,7 @@ function generateBirthOnlyProof(
|
|
|
733
863
|
`// Regenerated on every compile; do not edit.`,
|
|
734
864
|
`// @nomos-proof-legs ${legsMarker}`,
|
|
735
865
|
`import { readFileSync } from "node:fs";`,
|
|
736
|
-
`import { connect } from "@githolon/client";`,
|
|
866
|
+
`import { connect, encodeIntentOfferBytes } from "@githolon/client";`,
|
|
737
867
|
`import { ${factory}, ${hashConst} } from "./${opts.packageName}.client.ts";`,
|
|
738
868
|
`void connect; void ${factory};`,
|
|
739
869
|
``,
|
|
@@ -823,6 +953,11 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
823
953
|
|
|
824
954
|
const concurrency = findConcurrencyLeg(mod.directives as Directive<unknown>[], agg);
|
|
825
955
|
|
|
956
|
+
// THE AUTHORITY-CONFORMANCE LEG (the one-encoder + gate proof): the first gated (`.requires(...)`) directive
|
|
957
|
+
// in the primary domain, proven locally — actor threads through the one encoder; an actorless gated offer
|
|
958
|
+
// throws MissingOfferAuthority. `undefined` ⇒ a documented no-op (the domain authors openly).
|
|
959
|
+
const conformance = discoverAuthorityConformanceLeg(mod);
|
|
960
|
+
|
|
826
961
|
// Workspace-name slug: any package name → a safe, recognizable ws prefix.
|
|
827
962
|
const slug =
|
|
828
963
|
opts.packageName
|
|
@@ -865,7 +1000,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
865
1000
|
`// every call — start at docs/01-mental-model.md). Regenerated on every compile; do not edit.`,
|
|
866
1001
|
`// @nomos-proof-legs ${legsMarker}`,
|
|
867
1002
|
`import { readFileSync } from "node:fs";`,
|
|
868
|
-
`import { connect } from "@githolon/client";`,
|
|
1003
|
+
`import { connect, encodeIntentOfferBytes } from "@githolon/client";`,
|
|
869
1004
|
`import { ${factory}, ${hashConst} } from "./${opts.packageName}.client.ts";`,
|
|
870
1005
|
``,
|
|
871
1006
|
`const CLOUD = (process.env.NOMOS_CLOUD || "https://nomos.captainapp.co.uk").replace(/\\/+$/, "");`,
|
|
@@ -1009,6 +1144,11 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
1009
1144
|
);
|
|
1010
1145
|
}
|
|
1011
1146
|
|
|
1147
|
+
// THE AUTHORITY-CONFORMANCE LEG (the one-encoder + gate proof): runs on the LOCAL holon (fetch re-trapped),
|
|
1148
|
+
// proving the generated client threads security.actor through THE ONE ENCODER and that an actorless gated
|
|
1149
|
+
// offer is impossible to GENERATE (throws MissingOfferAuthority before any submit). No-op when un-gated.
|
|
1150
|
+
out.push(...authorityConformanceLegLines(conformance, factory, n));
|
|
1151
|
+
|
|
1012
1152
|
// THE CHILD-BIRTH LEG (G3, gap 2): when the primary domain has a `.births()` directive, prove the birth
|
|
1013
1153
|
// END-TO-END against the cloud — drive the parent birth offer, then read the BORN child workspace it spawns.
|
|
1014
1154
|
if (childBirth !== undefined) {
|
package/src/codegen_ts.ts
CHANGED
|
@@ -344,6 +344,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
344
344
|
``,
|
|
345
345
|
`/** The holon surface this client binds to (structurally satisfied by @githolon/client). */`,
|
|
346
346
|
`export interface NomosHolon {`,
|
|
347
|
+
` /** THE ONE ENCODER offer (the single v2-envelope codec lives in @githolon/client \`encodeIntentOfferBytes\`):`,
|
|
348
|
+
` * hand the holon a PLANNED directive + the verified \`actor\`; the holon encodes + offers. The generated`,
|
|
349
|
+
` * client calls THIS, never building offer bytes itself — so there is exactly ONE copy of the wire format. */`,
|
|
350
|
+
` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string }): Promise<string>;`,
|
|
351
|
+
` /** Opaque-bytes offer — retained for carriers that already hold an encoded envelope (back-compat). */`,
|
|
347
352
|
` offer(intentBytes: string | Uint8Array): Promise<string>;`,
|
|
348
353
|
` query(queryId: string, params?: Record<string, unknown>): Promise<unknown>;`,
|
|
349
354
|
` queryById(aggregateId: string): Promise<unknown>;`,
|
|
@@ -364,8 +369,16 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
364
369
|
` discardDeadLetter(id: string): Promise<{ ok: boolean; removed: number }>;`,
|
|
365
370
|
`}`,
|
|
366
371
|
``,
|
|
367
|
-
|
|
368
|
-
`
|
|
372
|
+
`/** AUTHORITY IS UNFORGEABLE-BY-CONSTRUCTION: a GATED directive (one carrying a \`.requires(relation)\` write`,
|
|
373
|
+
` * gate) authored with NO \`actor\` cannot produce an offer — it throws THIS before any submit, so an`,
|
|
374
|
+
` * actorless gated offer is impossible to GENERATE, never merely refused at the kernel gate. Bind the`,
|
|
375
|
+
` * client with \`{ actor }\` (the verified principal) to author a gated directive. */`,
|
|
376
|
+
`export class MissingOfferAuthority extends Error {`,
|
|
377
|
+
` constructor(readonly domain: string, readonly directiveId: string, readonly requires: string) {`,
|
|
378
|
+
` super(\`\${domain}/\${directiveId} requires the "\${requires}" relation — bind the client with { actor } (the verified principal) to author it; a gated offer cannot be made without an authority\`);`,
|
|
379
|
+
` this.name = "MissingOfferAuthority";`,
|
|
380
|
+
` }`,
|
|
381
|
+
`}`,
|
|
369
382
|
``,
|
|
370
383
|
);
|
|
371
384
|
|
|
@@ -632,6 +645,13 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
632
645
|
const stamps = autoStampFields(schema);
|
|
633
646
|
const mint = mintPlan?.get(d.id);
|
|
634
647
|
const placed = placementDirs.has(d.id) ? ["shard"] : [];
|
|
648
|
+
// THE AUTHORITY GATE (codegen-time): a directive carrying `.requires(relation)` is GATED — its
|
|
649
|
+
// generated method refuses to encode an offer without an `actor` (throws MissingOfferAuthority
|
|
650
|
+
// BEFORE any submit), so an actorless gated offer is impossible to GENERATE, not merely refused.
|
|
651
|
+
const gate = (d as { requiresRelationGate?: { relation: string } }).requiresRelationGate;
|
|
652
|
+
const gateGuard = gate !== undefined
|
|
653
|
+
? ` if (!actor || !actor.trim()) throw new MissingOfferAuthority(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${JSON.stringify(gate.relation)});\n`
|
|
654
|
+
: "";
|
|
635
655
|
out.push(
|
|
636
656
|
`/** \`${domain}/${d.id}\` payload (mirrors the engine's zod schema).${stamps.length ? ` Omitted ${stamps.map((s) => `\`${s}\``).join("/")} auto-stamp(s) ISO now() at offer.` : ""}${mint ? ` Omitted \`${mint.field}\` is KERNEL-MINTED at offer (returned on the result).` : ""}${placed.length ? " Omitted `shard` is picked LEAST-LOADED at offer (the routed holon)." : ""} */`,
|
|
637
657
|
`export type ${payloadName} = ${tsTypeOfPayload(schema, stamps, mint ? [mint.field] : [], placed)};`,
|
|
@@ -649,21 +669,35 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
649
669
|
: `{ ...payload, ${mint.field} }`;
|
|
650
670
|
methods.push(
|
|
651
671
|
` /** Author \`${domain}/${d.id}\`: the front-door MINTS \`${mint.field}\` (route-tagged) when omitted`,
|
|
652
|
-
` * — ids stay internal plumbing — and returns it with the new local head. */`,
|
|
672
|
+
` * — ids stay internal plumbing — and returns it with the new local head.${gate ? ` GATED: needs \`{ actor }\` (\`${gate.relation}\`).` : ""} */`,
|
|
653
673
|
` ${d.id}: async (payload: ${payloadName}): Promise<{ head: string; ${mint.field}: string }> => {`,
|
|
674
|
+
...(gateGuard ? [gateGuard.replace(/\n$/, "")] : []),
|
|
654
675
|
` const ${mint.field} = payload.${mint.field} ?? (await (holon as NomosRoutedHolon).mint(${JSON.stringify(mint.mintType)}${mintOpts}));`,
|
|
655
|
-
` const head = await holon.
|
|
676
|
+
` const head = await holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, { domainHash, actor });`,
|
|
656
677
|
` return { head, ${mint.field} };`,
|
|
657
678
|
` },`,
|
|
658
679
|
);
|
|
659
680
|
continue;
|
|
660
681
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
682
|
+
const offerPayload = stamps.length ? `stampNow(payload, ${JSON.stringify(stamps)})` : `payload`;
|
|
683
|
+
const offerCall = `holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor })`;
|
|
684
|
+
if (gateGuard) {
|
|
685
|
+
// GATED directive: a block body that throws MissingOfferAuthority when no actor is bound — an
|
|
686
|
+
// actorless gated offer can't be constructed (the war: the gate is structural, not just kernel-side).
|
|
687
|
+
methods.push(
|
|
688
|
+
` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
|
|
689
|
+
` * GATED: needs \`{ actor }\` (the \`${gate!.relation}\` relation) — throws MissingOfferAuthority otherwise. */`,
|
|
690
|
+
` ${d.id}: (payload: ${payloadName}): Promise<string> => {`,
|
|
691
|
+
gateGuard.replace(/\n$/, ""),
|
|
692
|
+
` return ${offerCall};`,
|
|
693
|
+
` },`,
|
|
694
|
+
);
|
|
695
|
+
} else {
|
|
696
|
+
methods.push(
|
|
697
|
+
` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head. */`,
|
|
698
|
+
` ${d.id}: (payload: ${payloadName}) => ${offerCall},`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
667
701
|
}
|
|
668
702
|
|
|
669
703
|
for (const q of (mod.queries ?? []) as QueryDecl[]) {
|
package/src/compile_engine.ts
CHANGED
|
@@ -444,6 +444,11 @@ export function makeEngineReport(
|
|
|
444
444
|
payload?: Record<string, unknown>;
|
|
445
445
|
};
|
|
446
446
|
priorState?: Record<string, unknown>;
|
|
447
|
+
capturedPorts?: {
|
|
448
|
+
principal?: string;
|
|
449
|
+
principal_verified?: boolean;
|
|
450
|
+
reads?: readonly { query_id?: string; queryId?: string; args?: Record<string, string>; result?: unknown }[];
|
|
451
|
+
};
|
|
447
452
|
}) => string {
|
|
448
453
|
return (job) => {
|
|
449
454
|
const intent = job.intent ?? {};
|
|
@@ -543,7 +548,19 @@ export function makeEngineReport(
|
|
|
543
548
|
);
|
|
544
549
|
}
|
|
545
550
|
const snapshots = (job.priorState ?? {}) as Record<string, Record<string, unknown>>;
|
|
546
|
-
|
|
551
|
+
// Mirror engine_entry.planWorkspaceInvariantAssert: surface intent + captured author identity +
|
|
552
|
+
// the committed captured reads (#58 — the captured-read invariant gate), normalising the kernel's
|
|
553
|
+
// snake_case `query_id` to the DSL `queryId` so the law body is wire-shape agnostic. A #266 body
|
|
554
|
+
// that ignores ctx is byte-identical.
|
|
555
|
+
const assertIntent = (intent.payload ?? {}) as Record<string, unknown>;
|
|
556
|
+
const principal = (job.capturedPorts?.principal ?? "") as string;
|
|
557
|
+
const principalVerified = job.capturedPorts?.principal_verified === true;
|
|
558
|
+
const reads = (job.capturedPorts?.reads ?? []).map((r) => ({
|
|
559
|
+
queryId: (r.queryId ?? r.query_id ?? "") as string,
|
|
560
|
+
args: (r.args ?? {}) as Record<string, string>,
|
|
561
|
+
result: r.result,
|
|
562
|
+
}));
|
|
563
|
+
const verdict = decl.assert(snapshots, { intent: assertIntent, principal, principalVerified, reads });
|
|
547
564
|
return JSON.stringify(verdict);
|
|
548
565
|
}
|
|
549
566
|
|
package/src/engine_entry.ts
CHANGED
|
@@ -606,7 +606,11 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
606
606
|
function planWorkspaceInvariantAssert(job: {
|
|
607
607
|
intent?: { workspaceInvariant?: { id?: string }; payload?: unknown };
|
|
608
608
|
priorState?: Record<string, unknown>;
|
|
609
|
-
capturedPorts?: {
|
|
609
|
+
capturedPorts?: {
|
|
610
|
+
principal?: string;
|
|
611
|
+
principal_verified?: boolean;
|
|
612
|
+
reads?: readonly { query_id?: string; queryId?: string; args?: Record<string, string>; result?: unknown }[];
|
|
613
|
+
};
|
|
610
614
|
}): string {
|
|
611
615
|
const id = job.intent?.workspaceInvariant?.id;
|
|
612
616
|
if (typeof id !== "string") {
|
|
@@ -623,7 +627,17 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
623
627
|
const principal = (job.capturedPorts?.principal ?? "") as string;
|
|
624
628
|
// VA-core (Phase 3): the kernel-DERIVED author-verified flag — threaded like `principal`.
|
|
625
629
|
const principalVerified = job.capturedPorts?.principal_verified === true;
|
|
626
|
-
|
|
630
|
+
// THE CAPTURED READS (#58 — the captured-read invariant gate): surface the committed
|
|
631
|
+
// `captured_ports.reads` the executor ships in the assert dispatch so a count/aggregate predicate
|
|
632
|
+
// (WorkspaceQuota) can judge over the row LIST a captured read returned. The wire field is the kernel's
|
|
633
|
+
// `CapturedRead` (snake_case `query_id`); normalize to the DSL `queryId` so the law body is wire-shape
|
|
634
|
+
// agnostic. Empty/absent ⇒ `[]` (every pre-#58 / #266 body ignores it — additive, byte-identical).
|
|
635
|
+
const reads = (job.capturedPorts?.reads ?? []).map((r) => ({
|
|
636
|
+
queryId: (r.queryId ?? r.query_id ?? "") as string,
|
|
637
|
+
args: (r.args ?? {}) as Record<string, string>,
|
|
638
|
+
result: r.result,
|
|
639
|
+
}));
|
|
640
|
+
return JSON.stringify(decl.assert(snapshots, { intent, principal, principalVerified, reads }));
|
|
627
641
|
}
|
|
628
642
|
|
|
629
643
|
function planDerive(job: {
|
package/src/framework/genesis.ts
CHANGED
|
@@ -81,10 +81,15 @@ export const seedGenesis = directive("seedGenesis")
|
|
|
81
81
|
.payload(
|
|
82
82
|
z.object({
|
|
83
83
|
principal: z.string().min(1),
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
84
|
+
// `seedGenesis` is GENERIC over the warrant: it records whatever self-id the genesis recipe hands it.
|
|
85
|
+
// EMPTY ⇒ the workspace stays UNWARRANTED (host-stamped authorship) — the LEGACY/compat lane: a
|
|
86
|
+
// pre-VA chain or a recipe that explicitly opted out (`requiresWarrant: false`) replays byte-identical.
|
|
87
|
+
// NON-EMPTY ⇒ WARRANTED: the self-id is the source binding author signatures cover, so the kernel
|
|
88
|
+
// enforces SIGNED control-plane authorship on every lane (author / receive / verify_chain).
|
|
89
|
+
// THE WARRANT-FLIP (THE GOVERNANCE DEFAULT, 2026-06-27): `buildGenesisChain` now seeds a NON-EMPTY
|
|
90
|
+
// self-id (the workspace name) by default for every governance/platform birth — control-plane authority
|
|
91
|
+
// is born warranted unless the recipe explicitly opts out. The choice lives in the recipe builder, not
|
|
92
|
+
// here; this directive just writes the warrant it is given. Era: the regime is per-workspace, at genesis.
|
|
88
93
|
workspaceId: z.string(),
|
|
89
94
|
bornAt: z.string().min(1),
|
|
90
95
|
}),
|
|
@@ -69,6 +69,22 @@ export function refAs(name: string, aggregateType: string, id: string): Invarian
|
|
|
69
69
|
return { name, aggregate: aggregateType, id };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* ONE captured write-path read the predicate may consume (#58 — the captured-read lane). The SAME
|
|
74
|
+
* `{queryId, args, result}` the directive's plan recorded onto `captured_ports.reads` (via `read(query,
|
|
75
|
+
* args)` + `.reads(theQuery)` on the directive), CAS-re-derived + byte-compared at the pre-apply position
|
|
76
|
+
* on every lane (author / edge admission / verify_chain). So a captured read the assert reads is a
|
|
77
|
+
* COMMITTED, peer-identical fact — the gate's count-style judgement input (e.g. the WorkspaceQuota
|
|
78
|
+
* invariant counts the workspaces a `workspacesByPrincipal` captured read returned). `result` is the
|
|
79
|
+
* canonical row array `[{id, data}]` `derive_captured_read` produces. The executor ships these into the
|
|
80
|
+
* assert dispatch on EVERY lane via the same `captured_ports` it hands the gate (no kernel/adapter change).
|
|
81
|
+
*/
|
|
82
|
+
export interface InvariantCapturedRead {
|
|
83
|
+
readonly queryId: string;
|
|
84
|
+
readonly args: Record<string, string>;
|
|
85
|
+
readonly result: unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
/** Derive the BOUNDED declared read-set from the intent payload. Pure — no IO, no live read.
|
|
73
89
|
* `ctx.principal` is the captured, authenticated AUTHOR identity (e.g. `key:<keyHash>` / a verified uid)
|
|
74
90
|
* — the same value on every replaying peer — so an invariant can gate by WHO authored (law-gated authz). */
|
|
@@ -101,6 +117,18 @@ export type WorkspaceInvariantAssert = (
|
|
|
101
117
|
principal?: string;
|
|
102
118
|
/** VA-core (Phase 3): the kernel-DERIVED author-verified flag — see {@link WorkspaceInvariantReads}. */
|
|
103
119
|
principalVerified?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* THE CAPTURED READS (#58 — the captured-read invariant gate). The committed `captured_ports.reads`
|
|
122
|
+
* the directive's plan recorded — present iff the directive DECLARES reads (`.reads(theQuery)`) and
|
|
123
|
+
* its plan called `read(query, args)`. Each is CAS-re-derived + byte-compared at the pre-apply
|
|
124
|
+
* position on every lane, so a count/aggregate judgement over them is a function of the COMMITTED,
|
|
125
|
+
* peer-identical pre-apply state — never a forgeable client claim. This is how a count-style gate
|
|
126
|
+
* (e.g. WorkspaceQuota: `liveWorkspaces < allowance`) judges O(#rows) WITHOUT a new kernel primitive:
|
|
127
|
+
* the `workspaceInvariant.reads` ref-set resolves single instances by id (it cannot ENUMERATE), so a
|
|
128
|
+
* cardinality predicate reads the row LIST off a captured read instead. Empty when the directive
|
|
129
|
+
* declares no reads (every pre-#58 / #266 body ignores it — additive, byte-identical).
|
|
130
|
+
*/
|
|
131
|
+
reads?: readonly InvariantCapturedRead[];
|
|
104
132
|
},
|
|
105
133
|
) => WorkspaceInvariantVerdict;
|
|
106
134
|
|
|
@@ -73,7 +73,8 @@ import { Lww } from "../drivers.js";
|
|
|
73
73
|
import { t } from "../fields.js";
|
|
74
74
|
import { set, withMarker } from "../ops.js";
|
|
75
75
|
import { query } from "../query.js";
|
|
76
|
-
import {
|
|
76
|
+
import { read } from "../read.js";
|
|
77
|
+
import { workspaceInvariant, refAs, type WorkspaceInvariantVerdict } from "./workspace_invariant.js";
|
|
77
78
|
import { writeTuple, removeTuple, RelationTuple, relationTupleInstanceId } from "./relations.js";
|
|
78
79
|
import { relationType, self, user, key } from "../relation_schema.js";
|
|
79
80
|
import { GENESIS_WARRANT_ID } from "./genesis.js";
|
|
@@ -458,13 +459,21 @@ export function buildGenesisChain(p: {
|
|
|
458
459
|
frameworkHash?: string;
|
|
459
460
|
lawHash?: string;
|
|
460
461
|
/**
|
|
461
|
-
* THE WARRANT-FLIP (VA-1, architecture/identity_keys.md).
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
462
|
+
* THE WARRANT-FLIP (VA-1, architecture/identity_keys.md). Controls whether `seedGenesis` records a REAL
|
|
463
|
+
* `workspaceId` (the workspace name as the unique self-id) so the chain is born WARRANTED — meaning
|
|
464
|
+
* `verify_captured_principal` enforces SIGNED control-plane authorship on every lane (author / edge
|
|
465
|
+
* receive / verify_chain). An empty `workspaceId` keeps the workspace UNWARRANTED (host-stamped authorship,
|
|
466
|
+
* the legacy lane), so a control-plane offer (grantAdmin etc.) is taken at its word — the forge that was
|
|
467
|
+
* open on unwarranted root/co2.
|
|
468
|
+
*
|
|
469
|
+
* THE GOVERNANCE DEFAULT (THE WARRANT-FLIP, 2026-06-27): `buildGenesisChain` only ever runs for a
|
|
470
|
+
* PLATFORM/GOVERNANCE birth (every call sits inside the `if (p.foundingAdminHash)` guard — a genesis that
|
|
471
|
+
* carries control-plane authority: grantAdmin/grantCreation). Control-plane authority is EXACTLY what the
|
|
472
|
+
* signed-author regime protects, so a governance birth is born WARRANTED by DEFAULT (`requiresWarrant`
|
|
473
|
+
* undefined/true ⇒ warranted). The ONLY escape is an explicit `requiresWarrant === false`, retained as an
|
|
474
|
+
* era-safe compat hatch (a pre-VA chain re-built byte-identical). NO flag-day: the era is per-workspace,
|
|
475
|
+
* decided at genesis — existing UNWARRANTED chains keep replaying; only newly-born/re-born governance
|
|
476
|
+
* chains arm the signature requirement.
|
|
468
477
|
*/
|
|
469
478
|
requiresWarrant?: boolean;
|
|
470
479
|
}): string {
|
|
@@ -495,15 +504,106 @@ export function buildGenesisChain(p: {
|
|
|
495
504
|
});
|
|
496
505
|
return JSON.stringify([
|
|
497
506
|
...installSteps,
|
|
498
|
-
// THE WARRANT-FLIP (
|
|
499
|
-
// signed authorship enforced on every lane
|
|
500
|
-
|
|
507
|
+
// THE WARRANT-FLIP (THE GOVERNANCE DEFAULT): a governance birth is born WARRANTED by default — a REAL
|
|
508
|
+
// self-id (the workspace name) so signed control-plane authorship is enforced on every lane. The ONLY
|
|
509
|
+
// way to stay on the legacy host-stamped lane ("" self-id, byte-identical to a pre-VA chain) is an
|
|
510
|
+
// EXPLICIT `requiresWarrant === false` (the era-safe compat hatch). `p.requiresWarrant !== false`
|
|
511
|
+
// ⇒ warranted whenever the flag is undefined OR true.
|
|
512
|
+
{ domain: "workspaces", directiveId: "seedGenesis", actor: founder, payload: { principal: founder, workspaceId: p.requiresWarrant !== false ? p.name : "", bornAt: p.bornAt } },
|
|
501
513
|
{ domain: "workspaces", directiveId: "grantAdmin", actor: founder, payload: { keyHash: p.foundingAdminHash, label: `founding admin (born for ${p.principal})`, grantedBy: p.principal, grantedAt: p.requestedAt } },
|
|
502
514
|
// No `role` — it was a vestigial node-class label (collapsed with `kind`); birthCloud ignores it now.
|
|
503
515
|
{ domain: "workspaces", directiveId: "birthCloud", actor: "", payload: { principal: p.principal, name: p.name, tier: p.tier, bornAt: p.requestedAt, bornUnder } },
|
|
504
516
|
]);
|
|
505
517
|
}
|
|
506
518
|
|
|
519
|
+
// ── THE QUOTA-GATE QUERIES (declared HERE — used by the WorkspaceQuota captured-read invariant) ────────
|
|
520
|
+
// The captured-read invariant lane (#58) needs these QueryDecls at the directive's CONSTRUCTION (the
|
|
521
|
+
// `.reads(...)` call below), which runs at module load BEFORE the read-section further down — so the
|
|
522
|
+
// three the WorkspaceQuota gate consults are declared up here (the rest stay in the read section). They
|
|
523
|
+
// are STILL auto-discovered by shape (export name = wire id), so the read manifest/openapi route them
|
|
524
|
+
// exactly as before; only their definition POSITION moved. The quota gate counts a principal's live
|
|
525
|
+
// (non-retired/-reclaimed) CloudWorkspaces against its allowance (a QuotaGrant else the CloudPolicy default).
|
|
526
|
+
|
|
527
|
+
/** One principal's workspaces — the WorkspaceQuota gate counts the live (non-retired/-reclaimed) ones. */
|
|
528
|
+
export const workspacesByPrincipal = query("workspacesByPrincipal")
|
|
529
|
+
.key("principal")
|
|
530
|
+
.returns(CloudWorkspace);
|
|
531
|
+
|
|
532
|
+
/** THE QUOTA LAW singleton (`scope: "cloud"`) — defaults for allowance + byte cap. */
|
|
533
|
+
export const cloudPolicy = query("cloudPolicy").key("scope").returns(CloudPolicy);
|
|
534
|
+
|
|
535
|
+
/** A principal's quota grants — the LATEST one is the allowance in force (overrides the CloudPolicy default). */
|
|
536
|
+
export const quotaByPrincipal = query("quotaByPrincipal")
|
|
537
|
+
.key("principal")
|
|
538
|
+
.returns(QuotaGrant);
|
|
539
|
+
|
|
540
|
+
/** THE DEFAULT WORKSPACE ALLOWANCE — the CLAUDE.md / CloudPolicy figure when no grant + no policy row. */
|
|
541
|
+
export const DEFAULT_MAX_WORKSPACES_PER_PRINCIPAL = 50;
|
|
542
|
+
|
|
543
|
+
/** A captured-read row as `derive_captured_read` shapes it: `{id, data:{field:value}}`. */
|
|
544
|
+
type QuotaRow = { id?: string; data?: Record<string, unknown> };
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* CAPTURE the three quota reads on the write path (the plan side of the WorkspaceQuota gate). Called by
|
|
548
|
+
* both `createWorkspace` and `birthChild` so the captured set is IDENTICAL + UNCONDITIONAL (a stable,
|
|
549
|
+
* byte-deterministic capture on every lane). It returns nothing — the values ride `captured_ports.reads`
|
|
550
|
+
* and the verdict is the invariant's ({@link quotaVerdict}); this just commits the premise.
|
|
551
|
+
*/
|
|
552
|
+
function countQuotaReads(principal: string): void {
|
|
553
|
+
read(workspacesByPrincipal, { principal });
|
|
554
|
+
read(quotaByPrincipal, { principal });
|
|
555
|
+
read(cloudPolicy, { scope: "cloud" });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* THE WORKSPACE-QUOTA VERDICT — the PURE predicate the {@link workspaceQuota} invariant runs over the
|
|
560
|
+
* COMMITTED captured reads (the same `{queryId,args,result}` the plan recorded, CAS-re-derived on every
|
|
561
|
+
* lane). Counts the principal's LIVE (non-retired/-reclaimed) CloudWorkspaces and compares against its
|
|
562
|
+
* allowance (the latest QuotaGrant.maxWorkspaces if any, else CloudPolicy.maxWorkspacesPerPrincipal, else
|
|
563
|
+
* the {@link DEFAULT_MAX_WORKSPACES_PER_PRINCIPAL} floor). The NEW workspace is NOT yet in the pre-apply
|
|
564
|
+
* state, so `liveCount < allowance` (strict) is the correct test: the principal may hold AT MOST
|
|
565
|
+
* `allowance`, so a fresh birth is admitted iff it already holds fewer. A principal-less call (should be
|
|
566
|
+
* impossible after S3) fails CLOSED. The verdict is a pure function of the captured reads ⇒ identical on
|
|
567
|
+
* author, edge admission, and verify_chain.
|
|
568
|
+
*/
|
|
569
|
+
function quotaVerdict(
|
|
570
|
+
reads: readonly { queryId: string; args: Record<string, string>; result: unknown }[],
|
|
571
|
+
principal: string,
|
|
572
|
+
): WorkspaceInvariantVerdict {
|
|
573
|
+
if (!principal) return { reject: "quota-missing-principal" };
|
|
574
|
+
const rowsOf = (queryId: string): QuotaRow[] => {
|
|
575
|
+
const hit = reads.find((r) => r.queryId === queryId && r.args.principal === principal);
|
|
576
|
+
return Array.isArray(hit?.result) ? (hit!.result as QuotaRow[]) : [];
|
|
577
|
+
};
|
|
578
|
+
// LIVE = not retired, not reclaimed (the WORKSPACE_STATUSES lifecycle). The captured read already
|
|
579
|
+
// excludes archived rows; status is a plain Lww string in `data`.
|
|
580
|
+
const liveCount = rowsOf(workspacesByPrincipal.id).filter((row) => {
|
|
581
|
+
const status = (row.data?.status ?? "") as string;
|
|
582
|
+
return status !== "retired" && status !== "reclaimed";
|
|
583
|
+
}).length;
|
|
584
|
+
// ALLOWANCE: the LATEST QuotaGrant for the principal (max grantedAt) wins; else the CloudPolicy default;
|
|
585
|
+
// else the hard floor. `derive_captured_read` decodes t.int() as a JSON number.
|
|
586
|
+
const grants = rowsOf(quotaByPrincipal.id);
|
|
587
|
+
let allowance: number | undefined;
|
|
588
|
+
let latestGrantedAt = -Infinity;
|
|
589
|
+
for (const g of grants) {
|
|
590
|
+
const at = Number(g.data?.grantedAt ?? -Infinity);
|
|
591
|
+
const max = Number(g.data?.maxWorkspaces ?? NaN);
|
|
592
|
+
if (Number.isFinite(at) && at > latestGrantedAt && Number.isFinite(max)) {
|
|
593
|
+
latestGrantedAt = at;
|
|
594
|
+
allowance = max;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (allowance === undefined) {
|
|
598
|
+
const policyHit = reads.find((r) => r.queryId === cloudPolicy.id && r.args.scope === "cloud");
|
|
599
|
+
const policyRows = Array.isArray(policyHit?.result) ? (policyHit!.result as QuotaRow[]) : [];
|
|
600
|
+
const max = Number(policyRows[0]?.data?.maxWorkspacesPerPrincipal ?? NaN);
|
|
601
|
+
if (Number.isFinite(max)) allowance = max;
|
|
602
|
+
}
|
|
603
|
+
if (allowance === undefined) allowance = DEFAULT_MAX_WORKSPACES_PER_PRINCIPAL;
|
|
604
|
+
return liveCount < allowance ? { accept: true } : { reject: "workspace-quota-exceeded" };
|
|
605
|
+
}
|
|
606
|
+
|
|
507
607
|
/**
|
|
508
608
|
* createWorkspace — record the lawful REQUEST for a workspace. Nomos MINTS the
|
|
509
609
|
* `CloudWorkspace` id (the payload carries none); the record is born
|
|
@@ -523,17 +623,30 @@ export const createWorkspace = directive("createWorkspace")
|
|
|
523
623
|
tier: z.enum(WORKSPACE_TIERS).default("edge"),
|
|
524
624
|
/** Parent CloudWorkspace aggregate id; `""` (default) = root. */
|
|
525
625
|
parent: z.string().default(""),
|
|
526
|
-
/** The auth-provider principal uid the request claimed
|
|
527
|
-
|
|
626
|
+
/** The auth-provider principal uid the request claimed. REQUIRED (S3 — no anonymous nodes):
|
|
627
|
+
* every birth bottoms out in a principal, so the kernel REFUSES an empty-principal birth at
|
|
628
|
+
* the typed payload gate (the host `if(!principal) 401` was the only enforcer; now it is LAW).
|
|
629
|
+
* Era-safe: pre-change chains only ever recorded born-with-principal records (the host gate
|
|
630
|
+
* already blocked anonymous births), and replay resolves each intent under the law that sealed
|
|
631
|
+
* it (per-position era), so old intents never re-validate against this schema. */
|
|
632
|
+
principal: z.string().min(1),
|
|
528
633
|
requestedBy: z.string().min(1),
|
|
529
634
|
requestedAt: epochMs,
|
|
530
635
|
}),
|
|
531
636
|
)
|
|
532
637
|
.plan((p) => {
|
|
638
|
+
// THE WORKSPACE-QUOTA CAPTURED READS (#58 / S2): consult the principal's live-workspace count + its
|
|
639
|
+
// allowance on the write path. Each read is CAS-re-derived + byte-compared at the pre-apply position
|
|
640
|
+
// on every lane, so the WorkspaceQuota invariant judges over the COMMITTED, peer-identical state (never
|
|
641
|
+
// a forged client claim). The plan only CAPTURES them (the verdict is the invariant's job); the values
|
|
642
|
+
// ride `captured_ports.reads`. The captured set must be STABLE across author + replay — always read all
|
|
643
|
+
// three (an unconditional capture is byte-deterministic; a data-dependent one would not be).
|
|
644
|
+
countQuotaReads(p.principal);
|
|
533
645
|
// No genesis ⇒ a plain lineage record (no foundingAdminHash) — never drained; materialized by boot-on-touch.
|
|
534
646
|
planBirthRecord(p);
|
|
535
647
|
return [];
|
|
536
648
|
})
|
|
649
|
+
.reads(workspacesByPrincipal, quotaByPrincipal, cloudPolicy) // arm the captured-read invariant gate (S2)
|
|
537
650
|
.requires("creator"); // posture-gated creation: only a #creator holder may request a workspace
|
|
538
651
|
|
|
539
652
|
/**
|
|
@@ -733,8 +846,13 @@ export const birthChild = directive("birthChild")
|
|
|
733
846
|
z
|
|
734
847
|
.object({
|
|
735
848
|
name: z.string().min(1),
|
|
736
|
-
/** The auth-provider principal uid the request claimed
|
|
737
|
-
|
|
849
|
+
/** The auth-provider principal uid the request claimed. REQUIRED (S3 — no anonymous nodes):
|
|
850
|
+
* THE WORLD RESTS ON A BIRTH UID, so a birth with no principal is refused at the typed
|
|
851
|
+
* payload gate (the host `if(!principal) 401` was the only enforcer; now it is LAW, judged
|
|
852
|
+
* on every lane — author / edge admission / verify_chain). Era-safe: pre-change chains only
|
|
853
|
+
* ever recorded born-with-principal records, and replay resolves each intent under the law
|
|
854
|
+
* that sealed it (per-position era), so old intents never re-validate against this schema. */
|
|
855
|
+
principal: z.string().min(1),
|
|
738
856
|
/** Parent CloudWorkspace aggregate id; `""` (default) = root. */
|
|
739
857
|
parent: z.string().default(""),
|
|
740
858
|
/** Hosting tier; defaults to the edge DO (a genesis/platform birth typically passes `heavy`). */
|
|
@@ -769,11 +887,18 @@ export const birthChild = directive("birthChild")
|
|
|
769
887
|
}),
|
|
770
888
|
)
|
|
771
889
|
.plan((p) => {
|
|
890
|
+
// THE WORKSPACE-QUOTA CAPTURED READS (#58 / S2) — same unconditional capture as createWorkspace, so the
|
|
891
|
+
// WorkspaceQuota invariant counts this principal's live workspaces against its allowance on every lane.
|
|
892
|
+
countQuotaReads(p.principal);
|
|
772
893
|
const rec = planBirthRecord(p, p.foundingAdminHash ? { foundingAdminHash: p.foundingAdminHash, pool: p.pool } : undefined);
|
|
773
894
|
// LAW AUTHORS THE GENESIS RECIPE (P1): a genesis birth carries its child's [installDomain×2 →]
|
|
774
895
|
// seedGenesis→grantAdmin→birthCloud chain. (refine guarantees principal + bornAt here.)
|
|
775
896
|
if (p.foundingAdminHash) {
|
|
776
|
-
|
|
897
|
+
// THE GOVERNANCE DEFAULT (THE WARRANT-FLIP): a governance birth is warranted unless the request
|
|
898
|
+
// EXPLICITLY opts out (`requiresWarrant === false`). Thread the flag through ONLY when the request set
|
|
899
|
+
// it — undefined falls to buildGenesisChain's warranted default; an explicit `false` rides the compat
|
|
900
|
+
// hatch (an era-safe legacy re-build).
|
|
901
|
+
const genesisChain = buildGenesisChain({ name: p.name, principal: p.principal, parent: p.parent, tier: p.tier, foundingAdminHash: p.foundingAdminHash, bornAt: p.bornAt as string, requestedAt: p.requestedAt, ...(p.frameworkHash ? { frameworkHash: p.frameworkHash } : {}), ...(p.lawHash ? { lawHash: p.lawHash } : {}), ...(p.requiresWarrant !== undefined ? { requiresWarrant: p.requiresWarrant } : {}) });
|
|
777
902
|
// THE GENESIS RECIPE IS ALSO A LINEAGE/AUDIT FACT on the parent's chain (the parent records what it
|
|
778
903
|
// ORDERED — law naming law, legitimate). The kernel does NOT read this field to detect the birth.
|
|
779
904
|
rec.set("genesisChain", genesisChain);
|
|
@@ -786,6 +911,7 @@ export const birthChild = directive("birthChild")
|
|
|
786
911
|
}
|
|
787
912
|
return [];
|
|
788
913
|
})
|
|
914
|
+
.reads(workspacesByPrincipal, quotaByPrincipal, cloudPolicy) // arm the WorkspaceQuota captured-read gate (S2)
|
|
789
915
|
.births() // BIRTH IS A DECLARED KERNEL PRIMITIVE (G3): emits the `nomosBirthGate` era key; the kernel folds the carried recipe
|
|
790
916
|
.requires("creator"); // THE ONE birth authority — a #creator may birth a leaf OR a platform (tier collapsed)
|
|
791
917
|
|
|
@@ -1248,18 +1374,9 @@ export const workspacesByParent = query("workspacesByParent")
|
|
|
1248
1374
|
.key("parent")
|
|
1249
1375
|
.returns(CloudWorkspace);
|
|
1250
1376
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
.returns(CloudWorkspace);
|
|
1255
|
-
|
|
1256
|
-
/** THE QUOTA LAW singleton (`scope: "cloud"`) — defaults for allowance + byte cap. */
|
|
1257
|
-
export const cloudPolicy = query("cloudPolicy").key("scope").returns(CloudPolicy);
|
|
1258
|
-
|
|
1259
|
-
/** A principal's quota grants — the LATEST one is the allowance in force. */
|
|
1260
|
-
export const quotaByPrincipal = query("quotaByPrincipal")
|
|
1261
|
-
.key("principal")
|
|
1262
|
-
.returns(QuotaGrant);
|
|
1377
|
+
// workspacesByPrincipal / cloudPolicy / quotaByPrincipal — DECLARED ABOVE (before createWorkspace), so the
|
|
1378
|
+
// WorkspaceQuota captured-read invariant can reference them at directive construction. (Auto-discovered by
|
|
1379
|
+
// shape regardless of position; they route into the read manifest/openapi exactly as the others here do.)
|
|
1263
1380
|
|
|
1264
1381
|
/** "May this principal create platforms?" — invite-only; active grant = yes. */
|
|
1265
1382
|
export const platformGrantByPrincipal = query("platformGrantByPrincipal")
|
|
@@ -1416,3 +1533,45 @@ export const reclaimRequiresRetired = workspaceInvariant("ReclaimRequiresRetired
|
|
|
1416
1533
|
const rec = ws as { status?: string } | undefined;
|
|
1417
1534
|
return rec?.status === "retired" ? { accept: true } : { reject: "reclaim-requires-retired" };
|
|
1418
1535
|
});
|
|
1536
|
+
|
|
1537
|
+
// ── THE WORKSPACE-QUOTA GATE (S2 — THE QUOTA LAW, ENFORCED) ────────────────────────────────────────────
|
|
1538
|
+
// The CloudPolicy/QuotaGrant law-state used to be enforced by NOBODY — `createWorkspace`/`birthChild` were
|
|
1539
|
+
// gated only by `.requires("creator")`, so a single #creator could mint unbounded workspaces. NOW the
|
|
1540
|
+
// COUNT quota is REAL law at the gate: each birth directive CAPTURES (via the #58 captured-read lane,
|
|
1541
|
+
// `countQuotaReads`) the principal's live-workspace list + its allowance, and THIS invariant counts them
|
|
1542
|
+
// and refuses once the principal already holds its allowance. It rides the #41 captured-read invariant
|
|
1543
|
+
// gate — O(#workspaces-for-the-principal), no new kernel primitive: the standard `workspaceInvariant.reads`
|
|
1544
|
+
// ref-set resolves single instances by id (it CANNOT enumerate), so the cardinality predicate reads the row
|
|
1545
|
+
// LIST off the captured reads in `ctx.reads` instead (the snapshot ref-set is empty — nothing to resolve by
|
|
1546
|
+
// id). The captured reads are CAS-re-derived + byte-compared at the pre-apply position on every lane
|
|
1547
|
+
// (author / edge admission / verify_chain), so the count the gate judges is the COMMITTED, peer-identical
|
|
1548
|
+
// pre-apply count — a forged/stale client claim is a typed read-conflict refusal BEFORE this even runs.
|
|
1549
|
+
//
|
|
1550
|
+
// HONEST SCOPE: this is the COUNT quota only (live workspaces per principal). The BYTE cap
|
|
1551
|
+
// (`workspaceByteCap` / cumulative pushed bytes) is a CUSTODY fact, NOT chain state — it is metered by the
|
|
1552
|
+
// transport rate-limit lane, never by a kernel cap, and this invariant does NOT pretend to enforce it.
|
|
1553
|
+
//
|
|
1554
|
+
// ERA-SAFE: the era is the bundle. Both the captured-read lane (`nomosReadGate`, emitted iff a directive
|
|
1555
|
+
// declares `.reads(...)`) and the invariant (`nomosInvariantGate`, emitted iff the package declares ≥1
|
|
1556
|
+
// invariant — which governance already does) are PER-INSTALL era keys; every chain sealed under pre-S2 law
|
|
1557
|
+
// carries NEITHER for these directives, so it replays BYTE-GREEN (no captured reads to re-derive, no
|
|
1558
|
+
// WorkspaceQuota invariant to fire). The verdict is a pure function of the captured reads ⇒ identical on
|
|
1559
|
+
// every replaying peer.
|
|
1560
|
+
|
|
1561
|
+
/** createWorkspace: the principal must hold FEWER than its allowance of live workspaces (count quota). */
|
|
1562
|
+
export const workspaceQuotaCreate = workspaceInvariant("WorkspaceQuota")
|
|
1563
|
+
.on("createWorkspace")
|
|
1564
|
+
// The snapshot ref-set is EMPTY — the count comes from the captured reads (ctx.reads), which the standard
|
|
1565
|
+
// ref-resolution cannot produce (it resolves single instances by id, never an enumeration).
|
|
1566
|
+
.reads(() => [])
|
|
1567
|
+
.assert((_snapshots, ctx) =>
|
|
1568
|
+
quotaVerdict(ctx?.reads ?? [], String((ctx?.intent ?? {}).principal ?? "")),
|
|
1569
|
+
);
|
|
1570
|
+
|
|
1571
|
+
/** birthChild: the SAME count quota (a leaf or platform birth draws on the principal's allowance). */
|
|
1572
|
+
export const workspaceQuotaBirth = workspaceInvariant("WorkspaceQuotaBirth")
|
|
1573
|
+
.on("birthChild")
|
|
1574
|
+
.reads(() => [])
|
|
1575
|
+
.assert((_snapshots, ctx) =>
|
|
1576
|
+
quotaVerdict(ctx?.reads ?? [], String((ctx?.intent ?? {}).principal ?? "")),
|
|
1577
|
+
);
|
package/src/index.ts
CHANGED
|
@@ -386,6 +386,7 @@ export {
|
|
|
386
386
|
type WorkspaceInvariantReads,
|
|
387
387
|
type WorkspaceInvariantAssert,
|
|
388
388
|
type InvariantRef,
|
|
389
|
+
type InvariantCapturedRead,
|
|
389
390
|
} from "./framework/workspace_invariant.js";
|
|
390
391
|
// NATIVE ZANZIBAR RELATION SCHEMA (P1) — the userset-rewrite law as pure data.
|
|
391
392
|
export {
|