@githolon/dsl 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/aggregate.ts +19 -16
- package/src/build_package.ts +7 -0
- package/src/codegen_dart.ts +9 -0
- package/src/codegen_ts.ts +12 -10
- package/src/compile_package_main.ts +10 -0
- package/src/directive.ts +15 -18
- package/src/engine_entry.ts +8 -2
- package/src/framework/encryption_identity.ts +3 -3
- package/src/framework/genesis.ts +88 -0
- package/src/framework/impure_capability.ts +3 -3
- package/src/framework/relations.ts +28 -4
- package/src/framework/signing_identity.ts +97 -0
- package/src/framework/workspace_invariant.ts +5 -3
- package/src/framework/workspaces.ts +158 -103
- package/src/index.ts +72 -18
- package/src/manifest.ts +0 -6
- package/src/predicate.ts +4 -3
- package/src/relation_schema.ts +298 -0
- package/src/relation_sugar.ts +324 -0
- package/src/usd.ts +142 -7
- package/src/usd_layers.ts +53 -0
- package/src/framework/rbac.ts +0 -418
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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/aggregate.ts
CHANGED
|
@@ -26,25 +26,28 @@ export type AggregateInvariantVerdict = { accept: true } | { reject: string };
|
|
|
26
26
|
export type AggregateInvariantFn = (snapshot: Record<string, unknown>) => AggregateInvariantVerdict;
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* READ VISIBILITY (
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
29
|
+
* READ VISIBILITY (the native-relations read gate) — the DECLARED posture of an
|
|
30
|
+
* aggregate's data on the read side. `public` (the DEFAULT, and the only value
|
|
31
|
+
* before this existed) means any peer may read the type; `requires(cap)` makes the
|
|
32
|
+
* type PRIVATE: a read that touches it (a `query` returning it, a `count`/`sum`
|
|
33
|
+
* over it, a by-id read of one of its instances) is served ONLY to a principal that
|
|
34
|
+
* holds the read RELATION `cap` on the workspace-scope object — decided by the ONE
|
|
35
|
+
* native Zanzibar `RewriteEngine.check` over THIS workspace's own folded relation
|
|
36
|
+
* tuples (`nomos_identity::relations`, the SAME evaluator the write gate uses).
|
|
36
37
|
*
|
|
37
38
|
* THE LAW DECIDES, NOT THE HOST: the visibility rides IN the law package (the
|
|
38
|
-
* `nomosReadVisibility` USD era key —
|
|
39
|
-
* an all-public domain hashes byte-identically to before
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* `nomosReadVisibility` USD era key — type → required cap — OMITTED when every
|
|
40
|
+
* aggregate is public, so an all-public domain hashes byte-identically to before
|
|
41
|
+
* this existed), and the compiler ALSO synthesizes a `workspace#<cap>` relation into
|
|
42
|
+
* `nomosRelations` so the cap is a checkable native relation. The githolon enforces
|
|
43
|
+
* it at the one wasm read boundary; the host only STAMPS the presented principal (an
|
|
44
|
+
* impure identity read) and relays the githolon's verdict.
|
|
42
45
|
*
|
|
43
|
-
* `cap` is
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* secret-gated author lane).
|
|
47
|
-
*
|
|
46
|
+
* `cap` is the read RELATION NAME (the same vocabulary as a directive's required
|
|
47
|
+
* relation): the workspace's owner authors a `relateSubject` tuple
|
|
48
|
+
* `workspace:self#<cap>@user:<principal>` to the reader (e.g. at birth, over the
|
|
49
|
+
* secret-gated author lane). Holding the relation (an active, non-revoked tuple) =
|
|
50
|
+
* may read.
|
|
48
51
|
*/
|
|
49
52
|
export type ReadVisibility = { readonly requires: string };
|
|
50
53
|
|
package/src/build_package.ts
CHANGED
|
@@ -52,6 +52,7 @@ import type { CombinedDecl } from "./combined.js";
|
|
|
52
52
|
import type { ImpureCapabilityDecl } from "./codegen_provider_dart.js";
|
|
53
53
|
import type { WorkspaceTypeDecl } from "./workspace_type.js";
|
|
54
54
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
55
|
+
import type { RelationTypeDecl } from "./relation_schema.js";
|
|
55
56
|
import { domainHash, emitManifestBytes } from "./manifest.js";
|
|
56
57
|
import { emitUsd } from "./usd.js";
|
|
57
58
|
import { expandCapabilityExports } from "./capability_exports.js";
|
|
@@ -121,6 +122,8 @@ export interface DomainModuleSpec {
|
|
|
121
122
|
readonly workspaceTypes?: readonly WorkspaceTypeDecl[];
|
|
122
123
|
/** Declared workspace invariants (#266) — presence is hashed; bodies ship in the lump. */
|
|
123
124
|
readonly workspaceInvariants?: readonly WorkspaceInvariantDecl[];
|
|
125
|
+
/** Native Zanzibar relation schema (P1) — pure-data rewrites → the `nomosRelations` era key. */
|
|
126
|
+
readonly relationTypes?: readonly RelationTypeDecl[];
|
|
124
127
|
readonly dartImports?: readonly DartImport[];
|
|
125
128
|
readonly dartRefImports?: DomainModule["dartRefImports"];
|
|
126
129
|
readonly permissionVocabulary?: PermissionVocabulary;
|
|
@@ -176,6 +179,7 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
176
179
|
const maxes = [...(spec.maxes ?? [])];
|
|
177
180
|
const workspaceTypes = [...(spec.workspaceTypes ?? [])];
|
|
178
181
|
const workspaceInvariants = [...(spec.workspaceInvariants ?? [])];
|
|
182
|
+
const relationTypes = [...(spec.relationTypes ?? [])];
|
|
179
183
|
const dartImports = [...(spec.dartImports ?? [])];
|
|
180
184
|
|
|
181
185
|
return {
|
|
@@ -200,6 +204,9 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
200
204
|
// Omit-when-empty — presence-only hashed law (#266): an invariant-free domain's
|
|
201
205
|
// canonical manifest must stay byte-identical (adding/removing an invariant moves it).
|
|
202
206
|
...(workspaceInvariants.length > 0 ? { workspaceInvariants } : {}),
|
|
207
|
+
// Omit-when-empty — the relation schema is hashed law (P1); a relation-free domain's
|
|
208
|
+
// canonical manifest/USD stays byte-identical (the nomosRelations era rule).
|
|
209
|
+
...(relationTypes.length > 0 ? { relationTypes } : {}),
|
|
203
210
|
...(dartImports.length > 0 ? { dartImports } : {}),
|
|
204
211
|
...(spec.dartRefImports !== undefined && spec.dartRefImports.length > 0
|
|
205
212
|
? { dartRefImports: [...spec.dartRefImports] }
|
package/src/codegen_dart.ts
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { z } from "zod";
|
|
32
32
|
import type { AggregateHandle } from "./aggregate.js";
|
|
33
33
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
34
|
+
import type { RelationTypeDecl } from "./relation_schema.js";
|
|
34
35
|
import type { Field, FieldKind } from "./fields.js";
|
|
35
36
|
import type { Directive } from "./directive.js";
|
|
36
37
|
import { executeDirectiveToIntent } from "./wire_encode.js";
|
|
@@ -2427,6 +2428,14 @@ export interface DomainModule {
|
|
|
2427
2428
|
* aggregate `hasInvariant` / a directive `plan`). See `docs/workspace_invariant.md`.
|
|
2428
2429
|
*/
|
|
2429
2430
|
workspaceInvariants?: WorkspaceInvariantDecl[];
|
|
2431
|
+
/**
|
|
2432
|
+
* The domain's NATIVE ZANZIBAR RELATION SCHEMA (`relation_schema.ts` — the userset-rewrite law,
|
|
2433
|
+
* P1). Pure-data per-type relation rewrites lowered to the `nomosRelations` USD era key (NO JS on
|
|
2434
|
+
* the schema path). ADDITIVE + OPTIONAL: a domain that declares none omits this entirely and is
|
|
2435
|
+
* byte-identical in the canonical manifest to before the key existed (era rule). PRESENCE + the
|
|
2436
|
+
* AST is hashed; the recursive Check/Expand EVALUATOR is the Rust engine substrate, never authored.
|
|
2437
|
+
*/
|
|
2438
|
+
relationTypes?: RelationTypeDecl[];
|
|
2430
2439
|
/**
|
|
2431
2440
|
* The domain's FIRST-CLASS WORKSPACE TYPES (`workspace_type.ts`; sharding §10
|
|
2432
2441
|
* RATIFIED, slice 1) — the law-declared workspace taxonomy
|
package/src/codegen_ts.ts
CHANGED
|
@@ -451,8 +451,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
451
451
|
` authToken?: string;`,
|
|
452
452
|
` /** Transitional bare principal uid, sent as \`x-nomos-principal\` (no token). */`,
|
|
453
453
|
` principal?: string;`,
|
|
454
|
-
` /** Bearer credential when the lane is gated (
|
|
454
|
+
` /** Bearer credential when the lane is gated (an invite/creation key). */`,
|
|
455
455
|
` bearer?: string;`,
|
|
456
|
+
` /** Platform births: the founding admin key YOU choose (sent as \`x-nomos-platform-admin-key\`). The`,
|
|
457
|
+
` * host never mints or returns one — keep this; it is the AdminGrant of the platform's governance. */`,
|
|
458
|
+
` platformAdminKey?: string;`,
|
|
456
459
|
`}`,
|
|
457
460
|
``,
|
|
458
461
|
`/** A birth lane's response — \`workspace\` is the born holon's addressable name. */`,
|
|
@@ -462,8 +465,6 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
462
465
|
` workspace?: string;`,
|
|
463
466
|
` /** Returned ONCE at birth — the workspace's deploy credential. SAVE IT. */`,
|
|
464
467
|
` workspaceSecret?: string;`,
|
|
465
|
-
` /** Platform births only — the platform's own governance credential, ONCE. */`,
|
|
466
|
-
` platformAdminKey?: string;`,
|
|
467
468
|
` error?: string;`,
|
|
468
469
|
` [k: string]: unknown;`,
|
|
469
470
|
`}`,
|
|
@@ -473,6 +474,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
473
474
|
` if (auth.authToken !== undefined) headers["x-nomos-auth"] = auth.authToken;`,
|
|
474
475
|
` if (auth.principal !== undefined) headers["x-nomos-principal"] = auth.principal;`,
|
|
475
476
|
` if (auth.bearer !== undefined) headers["authorization"] = \`Bearer \${auth.bearer}\`;`,
|
|
477
|
+
` if (auth.platformAdminKey !== undefined) headers["x-nomos-platform-admin-key"] = auth.platformAdminKey;`,
|
|
476
478
|
` const res = await fetch(\`\${cloud.replace(/\\/$/, "")}\${path}\`, { method: "POST", headers });`,
|
|
477
479
|
` return (await res.json()) as NomosBirthResult;`,
|
|
478
480
|
`};`,
|
|
@@ -507,11 +509,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
507
509
|
// A TOP-LEVEL type: births directly under the cloud.
|
|
508
510
|
emit(
|
|
509
511
|
fnName(t.id),
|
|
510
|
-
`Birth a '${t.id}' workspace${isPlatform ? " (a platform-kind coordinator: hosts its own subtree)" : ""} — derived from the taxonomy over the existing ${isPlatform ? "platform" : "workspace"} lane.`,
|
|
511
|
-
isPlatform ? [`name: string`] : [`name: string
|
|
512
|
+
`Birth a '${t.id}' workspace${isPlatform ? " (a platform-kind coordinator: hosts its own subtree; PRESENT your chosen platformAdminKey)" : ""} — derived from the taxonomy over the existing ${isPlatform ? "platform" : "workspace"} lane. Placement is automatic (measured shape) — no tier to pick.`,
|
|
513
|
+
isPlatform ? [`name: string`, `platformAdminKey: string`] : [`name: string`],
|
|
512
514
|
isPlatform
|
|
513
515
|
? `\`/v1/platforms/\${encodeURIComponent(opts.name)}\``
|
|
514
|
-
: `\`/v1/workspaces/\${encodeURIComponent(opts.name)}
|
|
516
|
+
: `\`/v1/workspaces/\${encodeURIComponent(opts.name)}\``,
|
|
515
517
|
);
|
|
516
518
|
}
|
|
517
519
|
for (const childId of t.childIds) {
|
|
@@ -521,13 +523,13 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
521
523
|
const childIsPlatform = child.childIds.length > 0;
|
|
522
524
|
emit(
|
|
523
525
|
fnName(child.id),
|
|
524
|
-
`Birth a '${child.id}' under a '${t.id}' — the taxonomy's hasMany edge over the existing platform lane (lineage + pool enforced by the parent's law).`,
|
|
526
|
+
`Birth a '${child.id}' under a '${t.id}' — the taxonomy's hasMany edge over the existing platform lane (lineage + pool enforced by the parent's law).${childIsPlatform ? " PRESENT your chosen platformAdminKey." : ""} Placement is automatic (measured shape).`,
|
|
525
527
|
childIsPlatform
|
|
526
|
-
? [`${parentParam}: string`, `name: string`]
|
|
527
|
-
: [`${parentParam}: string`, `name: string
|
|
528
|
+
? [`${parentParam}: string`, `name: string`, `platformAdminKey: string`]
|
|
529
|
+
: [`${parentParam}: string`, `name: string`],
|
|
528
530
|
childIsPlatform
|
|
529
531
|
? `\`/v1/platforms/\${encodeURIComponent(opts.${parentParam})}/platforms/\${encodeURIComponent(opts.name)}\``
|
|
530
|
-
: `\`/v1/platforms/\${encodeURIComponent(opts.${parentParam})}/workspaces/\${encodeURIComponent(opts.name)}
|
|
532
|
+
: `\`/v1/platforms/\${encodeURIComponent(opts.${parentParam})}/workspaces/\${encodeURIComponent(opts.name)}\``,
|
|
531
533
|
);
|
|
532
534
|
}
|
|
533
535
|
}
|
|
@@ -88,6 +88,7 @@ import { canonicalTaxonomyFragment, validateWorkspaceTaxonomyAndRoutes } from ".
|
|
|
88
88
|
import { shardingLawModule, type ShardingLawSpec } from "./workspace_sharding.js";
|
|
89
89
|
import type { AnyExtremum } from "./extremum.js";
|
|
90
90
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
91
|
+
import { type RelationTypeDecl, isRelationTypeDecl, validateRelationType } from "./relation_schema.js";
|
|
91
92
|
import {
|
|
92
93
|
emitLayeredDomain,
|
|
93
94
|
flattenLayeredUsd,
|
|
@@ -126,6 +127,8 @@ interface DomainConfig {
|
|
|
126
127
|
readonly workspaceTypes?: readonly string[];
|
|
127
128
|
/** Export-name escape hatch for workspace invariants (auto-discovered by shape otherwise). */
|
|
128
129
|
readonly workspaceInvariants?: readonly string[];
|
|
130
|
+
/** Export-name escape hatch for relation types (auto-discovered by brand otherwise). */
|
|
131
|
+
readonly relationTypes?: readonly string[];
|
|
129
132
|
readonly impureCapabilities?: readonly string[];
|
|
130
133
|
readonly extraAggregates?: readonly string[];
|
|
131
134
|
readonly readModelAggregates?: readonly string[];
|
|
@@ -691,6 +694,12 @@ async function main(): Promise<void> {
|
|
|
691
694
|
...discover<WorkspaceInvariantDecl>(merged, isWorkspaceInvariantDecl),
|
|
692
695
|
...resolveNamed<WorkspaceInvariantDecl>(merged, d.workspaceInvariants, "workspaceInvariants"),
|
|
693
696
|
];
|
|
697
|
+
// Native Zanzibar relation schema (P1) — auto-discovered by brand, like workspaceInvariants.
|
|
698
|
+
const relationTypes = [
|
|
699
|
+
...discover<RelationTypeDecl>(merged, isRelationTypeDecl),
|
|
700
|
+
...resolveNamed<RelationTypeDecl>(merged, d.relationTypes, "relationTypes"),
|
|
701
|
+
];
|
|
702
|
+
for (const rt of relationTypes) validateRelationType(rt); // DX fast-fail (Rust resolve is authoritative)
|
|
694
703
|
// Sums are AUTO-DISCOVERED like queries/counts (#57 — an exported SumDecl used to be
|
|
695
704
|
// SILENTLY DROPPED unless config-named; the config lane stays as the escape hatch).
|
|
696
705
|
// Reference-dedupe: a sum that is BOTH exported and config-named is the same object.
|
|
@@ -758,6 +767,7 @@ async function main(): Promise<void> {
|
|
|
758
767
|
...(maxes.length > 0 ? { maxes } : {}),
|
|
759
768
|
...(workspaceTypes.length > 0 ? { workspaceTypes } : {}),
|
|
760
769
|
...(workspaceInvariants.length > 0 ? { workspaceInvariants } : {}),
|
|
770
|
+
...(relationTypes.length > 0 ? { relationTypes } : {}),
|
|
761
771
|
...(spatials.length > 0 ? { spatials } : {}),
|
|
762
772
|
...(d.dartImports !== undefined && d.dartImports.length > 0
|
|
763
773
|
? { dartImports: d.dartImports }
|
package/src/directive.ts
CHANGED
|
@@ -32,7 +32,6 @@ export type ReferentialMarker = "creates" | "mutates" | "ensures" | "archives";
|
|
|
32
32
|
* replay, and `admit`'s authz gate folds the permission projection strictly-before
|
|
33
33
|
* the intent's HLC against exactly this scope.
|
|
34
34
|
*/
|
|
35
|
-
export type ScopeFrom<P> = (payload: P) => string[];
|
|
36
35
|
|
|
37
36
|
export interface Directive<P = unknown> {
|
|
38
37
|
readonly id: string;
|
|
@@ -50,20 +49,15 @@ export interface Directive<P = unknown> {
|
|
|
50
49
|
*/
|
|
51
50
|
readonly description?: string;
|
|
52
51
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
52
|
+
* NATIVE ZANZIBAR write-authz (P4): the RELATION the captured author must HOLD to author this
|
|
53
|
+
* directive, and the OBJECT it is checked on. The ONE gate runs `engine.check(schema, object,
|
|
54
|
+
* relation, principal)` over the pre-apply folded tuple store — refusing an author who does not
|
|
55
|
+
* hold it (the side-door fix made real). `object` is SELF-RELATIVE by default (`"workspace:self"`,
|
|
56
|
+
* the governance convention); a payload-derived per-record object is the follow-on. DISTINCT from
|
|
57
|
+
* the legacy `requiresCapability` (RBAC) and `declaredRelations` (cross-workspace) axes. Emitted
|
|
58
|
+
* into the `nomosRelationGate` USD era key; enforced IFF the law also declares a relation schema.
|
|
59
59
|
*/
|
|
60
|
-
readonly
|
|
61
|
-
/**
|
|
62
|
-
* Pure payload→scope derivation feeding `admit`'s `target` (see {@link ScopeFrom}).
|
|
63
|
-
* `undefined` ⇒ root scope. Authz checks the actor holds `requiresCapability` at
|
|
64
|
-
* a scope COVERING this target.
|
|
65
|
-
*/
|
|
66
|
-
readonly scopeFrom?: ScopeFrom<P>;
|
|
60
|
+
readonly requiresRelationGate?: { readonly relation: string; readonly object: string };
|
|
67
61
|
/**
|
|
68
62
|
* The directive's DECLARED read boundary: the ref types its `plan` may READ (the
|
|
69
63
|
* "IR for law boundaries" decision — the boundary is part of the domain identity).
|
|
@@ -141,7 +135,7 @@ export interface RequirableDirective<P> extends Directive<P> {
|
|
|
141
135
|
* to the directive id) at the scope `scopeFrom(payload)` derives (defaults to root).
|
|
142
136
|
* Returns a new `Directive` carrying `requiresCapability` + `scopeFrom`.
|
|
143
137
|
*/
|
|
144
|
-
requires(
|
|
138
|
+
requires(relation: string, opts?: { object?: string }): RequirableDirective<P>;
|
|
145
139
|
/**
|
|
146
140
|
* Declare the directive's READ boundary. TWO argument kinds, one declaration site:
|
|
147
141
|
* * a STRING declares a ref TYPE its `plan` may read (the original boundary —
|
|
@@ -275,11 +269,14 @@ class DirectivePlanStep<Id extends string, P> {
|
|
|
275
269
|
function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
|
|
276
270
|
return {
|
|
277
271
|
...base,
|
|
278
|
-
requires(
|
|
272
|
+
requires(relation: string, opts?: { object?: string }): RequirableDirective<P> {
|
|
273
|
+
// THE ONE write-authz declaration (full-native Zanzibar, no mixed systems): the captured author
|
|
274
|
+
// must HOLD `relation` on `object` (self-relative `"workspace:self"` by default) for the one gate
|
|
275
|
+
// to admit. Lowers to the `nomosRelationGate` era key; `admit` runs `engine.check`. (Replaces the
|
|
276
|
+
// deleted RBAC capability axis — a capability WAS just a relation.)
|
|
279
277
|
return makeRequirable({
|
|
280
278
|
...base,
|
|
281
|
-
|
|
282
|
-
...(scopeFrom !== undefined ? { scopeFrom } : {}),
|
|
279
|
+
requiresRelationGate: { relation, object: opts?.object ?? "workspace:self" },
|
|
283
280
|
});
|
|
284
281
|
},
|
|
285
282
|
reads(...reads: (string | QueryDecl)[]): RequirableDirective<P> {
|
package/src/engine_entry.ts
CHANGED
|
@@ -536,6 +536,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
536
536
|
/** `{workspaceInvariantReads:{id}, payload}` → the bounded ref-set `[{name,aggregate,id},…]`. */
|
|
537
537
|
function planWorkspaceInvariantReads(job: {
|
|
538
538
|
intent?: { workspaceInvariantReads?: { id?: string }; payload?: unknown };
|
|
539
|
+
capturedPorts?: { principal?: string };
|
|
539
540
|
}): string {
|
|
540
541
|
const id = job.intent?.workspaceInvariantReads?.id;
|
|
541
542
|
if (typeof id !== "string") {
|
|
@@ -548,13 +549,17 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
548
549
|
throw new Error(`engine workspaceInvariantReads: no workspace invariant registered for "${id}"`);
|
|
549
550
|
}
|
|
550
551
|
const intent = (job.intent?.payload ?? {}) as Record<string, unknown>;
|
|
551
|
-
|
|
552
|
+
// LAW-GATED AUTHZ: the captured author identity (a top-level sibling of `intent`, set by the wasm
|
|
553
|
+
// author leg + replayed verbatim) — the governance invariant reads it to gate by the AUTHOR.
|
|
554
|
+
const principal = (job.capturedPorts?.principal ?? "") as string;
|
|
555
|
+
return JSON.stringify(decl.reads({ intent, principal }));
|
|
552
556
|
}
|
|
553
557
|
|
|
554
558
|
/** `{workspaceInvariant:{id,on}, payload?}` + priorState (named snapshots) → {accept}|{reject}. */
|
|
555
559
|
function planWorkspaceInvariantAssert(job: {
|
|
556
560
|
intent?: { workspaceInvariant?: { id?: string }; payload?: unknown };
|
|
557
561
|
priorState?: Record<string, unknown>;
|
|
562
|
+
capturedPorts?: { principal?: string };
|
|
558
563
|
}): string {
|
|
559
564
|
const id = job.intent?.workspaceInvariant?.id;
|
|
560
565
|
if (typeof id !== "string") {
|
|
@@ -568,7 +573,8 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
568
573
|
}
|
|
569
574
|
const snapshots = (job.priorState ?? {}) as Record<string, Record<string, unknown>>;
|
|
570
575
|
const intent = (job.intent?.payload ?? {}) as Record<string, unknown>;
|
|
571
|
-
|
|
576
|
+
const principal = (job.capturedPorts?.principal ?? "") as string;
|
|
577
|
+
return JSON.stringify(decl.assert(snapshots, { intent, principal }));
|
|
572
578
|
}
|
|
573
579
|
|
|
574
580
|
function planDerive(job: {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* framework/encryption_identity.ts — the E2E ENCRYPTION KEY REGISTRY (slice 1 of the
|
|
8
8
|
* `architecture/e2e_encryption.md` arc). Tenant-agnostic framework MECHANISM, the
|
|
9
|
-
* encryption sibling of `framework/
|
|
9
|
+
* encryption sibling of the native authority registry (`framework/relations.ts`).
|
|
10
10
|
*
|
|
11
11
|
* THE ROLE: the key-handoff lane (slice 4) wraps a scope's epoch secret TO a grant
|
|
12
12
|
* recipient's X25519 PUBLIC key. To do that it must look up "principal → encryption
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* / ~/.holon, never on any chain), and `grant`/the fan-out gate resolve it by principal.
|
|
16
16
|
*
|
|
17
17
|
* PRINCIPAL-KEYED, NOT device-keyed — deliberately. The read-visibility grant
|
|
18
|
-
* (`
|
|
18
|
+
* (a native `relateSubject` tuple) targets a `principal` (the auth-provider uid), and the launch platforms use
|
|
19
19
|
* bare auth-provider principals with no full Actor/DeviceKey model. So the encryption key
|
|
20
20
|
* is keyed by the SAME principal the grant targets — one decoupled registry a tenant
|
|
21
21
|
* composes WITHOUT the full `identity.ts` domain. (Multi-device per principal, and the
|
|
@@ -42,7 +42,7 @@ const X25519_PUB_B64_RE = /^[A-Za-z0-9+/]{42,44}={0,2}$/;
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* The per-principal encryption-key record. Multi-instance, keyed
|
|
45
|
-
* `enc-key-<principal>` (mirrors
|
|
45
|
+
* `enc-key-<principal>` (mirrors the native `reltuple-<object>-<relation>` keying). `encPublicKey`
|
|
46
46
|
* is the published X25519 public key; a re-publish (Lww) is a client-driven key rotation
|
|
47
47
|
* (which the owner should follow with a scope epoch rotation so new wraps target it).
|
|
48
48
|
*/
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
|
|
4
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* framework/genesis.ts — THE GENESIS WARRANT (the bootstrap base case;
|
|
8
|
+
* `architecture/identity_keys.md` step 0, `home_holons_and_cross_custody.md` §2).
|
|
9
|
+
*
|
|
10
|
+
* THE BOOTSTRAP PARADOX: the relation gate refuses a founding control-plane grant
|
|
11
|
+
* on a VIRGIN chain (an empty `#admin`/`#creator`/… roster) — there is no admin yet
|
|
12
|
+
* to gate the first admin. Resolved LAWFULLY (not by a host bypass): at birth the
|
|
13
|
+
* host verifies the `x-nomos-auth` token ONCE and records the verified principal as
|
|
14
|
+
* an in-history fact — this `GenesisWarrant`. The gate's GENESIS CLAUSE
|
|
15
|
+
* (`executor::genesis_admits`) then admits a founding grant on a virgin object IFF
|
|
16
|
+
* the warrant binds the chain to the captured principal. Strictly additive (only an
|
|
17
|
+
* otherwise-`Unauthorized` virgin grant), so every sealed chain replays unchanged.
|
|
18
|
+
*
|
|
19
|
+
* CONTROL-PLANE: `seedGenesis` is authored by the BIRTH DOOR (the host, after the
|
|
20
|
+
* one-time token verification), NOT by the open session lane — the session lane
|
|
21
|
+
* structurally refuses control-plane intents, so a tenant cannot forge a warrant for
|
|
22
|
+
* a principal it does not own. The warrant is the host's recorded verification, not
|
|
23
|
+
* a host authority decision: every replaying peer re-reads the chain fact.
|
|
24
|
+
*
|
|
25
|
+
* SINGLETON per chain: one founding principal per home/workspace, so the warrant is
|
|
26
|
+
* a singleton keyed `genesis-warrant:self`. `seedGenesis` is create-once — a second
|
|
27
|
+
* seed is a dup-create refusal, so the birth principal is fixed forever-on-chain.
|
|
28
|
+
*/
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
import { aggregate, instance } from "../aggregate.js";
|
|
31
|
+
import { t } from "../fields.js";
|
|
32
|
+
import { Lww } from "../drivers.js";
|
|
33
|
+
import { directive } from "../directive.js";
|
|
34
|
+
import { set, type PlannedOp } from "../ops.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Deterministic singleton id of the founding warrant — MUST equal
|
|
38
|
+
* `executor::GENESIS_WARRANT_ID` (the gate reads this exact key). One per chain.
|
|
39
|
+
*/
|
|
40
|
+
export const GENESIS_WARRANT_ID = "genesis-warrant:self";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The founding-warrant record. It does THREE jobs at once:
|
|
44
|
+
* - `principal` — the x-nomos-auth-verified OWNER (the genesis clause binds the founding grant to it);
|
|
45
|
+
* - `workspaceId` — this workspace's UNIQUE SELF-ID (the source binding the author signature covers,
|
|
46
|
+
* re-derived locally by the gate — NOT the shared/deterministic genesis commit, which is identical
|
|
47
|
+
* across all workspaces and so bound nothing);
|
|
48
|
+
* - its mere PRESENCE — the SIGNED-AUTHOR REGIME signal (the "era"): a workspace with a warrant
|
|
49
|
+
* REQUIRES a verified author block (forge closed); one without falls back to the legacy
|
|
50
|
+
* host-stamped principal (pre-feature chains replay green, per-position).
|
|
51
|
+
*/
|
|
52
|
+
export const GenesisWarrant = aggregate("GenesisWarrant", {
|
|
53
|
+
principal: t.string().merge(Lww), // the x-nomos-auth-verified owner, recorded at birth
|
|
54
|
+
workspaceId: t.string().merge(Lww), // this workspace's unique self-id (the author-signature source binding)
|
|
55
|
+
bornAt: t.string().merge(Lww), // ISO-8601, caller-stamped (determinism)
|
|
56
|
+
});
|
|
57
|
+
export type GenesisWarrantHandle = typeof GenesisWarrant;
|
|
58
|
+
|
|
59
|
+
/** The framework PRIMITIVE plan: record the founding warrant (singleton). */
|
|
60
|
+
export function writeGenesisWarrant(p: { principal: string; workspaceId: string; bornAt: string }): PlannedOp[] {
|
|
61
|
+
const w = instance(GenesisWarrant, GENESIS_WARRANT_ID);
|
|
62
|
+
return [
|
|
63
|
+
set(w, "principal", p.principal),
|
|
64
|
+
set(w, "workspaceId", p.workspaceId),
|
|
65
|
+
set(w, "bornAt", p.bornAt),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `seedGenesis` — the birth-door directive that records the verified founding principal + this
|
|
71
|
+
* workspace's unique self-id at the FIXED singleton id `genesis-warrant:self`. `.ensures` (NOT
|
|
72
|
+
* `.creates`): the warrant is a deterministic-KEYED singleton, not a minted record — `.creates` would
|
|
73
|
+
* demand a runtime-minted `<Type>_<uuid>` id and the id-mint gate refuses a hand-written one. The
|
|
74
|
+
* gate reads the warrant at THIS exact key, so the id is law, not minted. A second seed UPSERTs the
|
|
75
|
+
* same leaf rather than minting a duplicate; re-seeding is itself prevented out-of-band — seedGenesis
|
|
76
|
+
* is a CONTROL-PLANE birth-door write (the open session lane refuses it) and post-genesis bootstrap
|
|
77
|
+
* intents are refused once the lifecycle controller is active.
|
|
78
|
+
*/
|
|
79
|
+
export const seedGenesis = directive("seedGenesis")
|
|
80
|
+
.ensures(GenesisWarrant)
|
|
81
|
+
.payload(
|
|
82
|
+
z.object({
|
|
83
|
+
principal: z.string().min(1),
|
|
84
|
+
workspaceId: z.string().min(1),
|
|
85
|
+
bornAt: z.string().min(1),
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
.plan((p) => writeGenesisWarrant(p));
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* response-mutation). This is exactly the proven `ExportJobAggregate` shape
|
|
42
42
|
* (the tenant domains package `thing_structures`) generalised into a reusable framework pattern so a
|
|
43
43
|
* domain dev does NOT hand-roll the {aggregate + order + complete + fail} quartet each
|
|
44
|
-
* time — they DECLARE the task's params and get the quartet for free, like `
|
|
44
|
+
* time — they DECLARE the task's params and get the quartet for free, like `relations.ts`
|
|
45
45
|
* gives grant/revoke for free.
|
|
46
46
|
*
|
|
47
47
|
* ── What is framework vs tenant ──────────────────────────────────────────────
|
|
@@ -168,7 +168,7 @@ export function impureCapabilityAggregate<
|
|
|
168
168
|
* captured + committed — so the `order` create is idempotent on replay by capture, and the
|
|
169
169
|
* `complete`/`fail` receipt addresses the SAME instance by reading the committed minted id.
|
|
170
170
|
* Trivial today (identity), but centralised here so the keying rule has ONE home (the
|
|
171
|
-
* symmetry with `
|
|
171
|
+
* symmetry with `relations.ts`'s `relationTupleInstanceId`).
|
|
172
172
|
*/
|
|
173
173
|
export function impureCapabilityInstanceId(requestId: string): string {
|
|
174
174
|
return requestId;
|
|
@@ -215,7 +215,7 @@ const deadLetterSchema = z.object({
|
|
|
215
215
|
* The three plan builders. Each takes the task aggregate handle + the (already
|
|
216
216
|
* Zod-validated) payload and returns the `PlannedOp[]` writing the envelope. A tenant
|
|
217
217
|
* routes its directive `.plan` through these so the envelope shape can never drift —
|
|
218
|
-
* exactly how `
|
|
218
|
+
* exactly how `relations.ts`'s relate/unrelate route through `writeTuple`/`removeTuple`.
|
|
219
219
|
*/
|
|
220
220
|
|
|
221
221
|
/**
|
|
@@ -61,11 +61,30 @@ const OBJECT_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]*:[A-Za-z0-9][A-Za-z0-9_.:/-]*$/;
|
|
|
61
61
|
/** A relation name (the edge label) — a bare identifier, like a directive/cap name. */
|
|
62
62
|
const RELATION_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
63
63
|
/**
|
|
64
|
-
* A SUBJECT: `user:<uid>`, the public wildcard `user:*`, or a
|
|
65
|
-
*
|
|
66
|
-
*
|
|
64
|
+
* A SUBJECT: `user:<uid>`, the public wildcard `user:*`, a KEY-ADDRESSED `key:<64hex>`, or a
|
|
65
|
+
* USERSET `<type>:<id>#<relation>` (group membership / indirection — P2). A raw uid carrying the
|
|
66
|
+
* `:` delimiter or being the bare `*` is refused so it can never forge a wildcard/userset.
|
|
67
67
|
*/
|
|
68
68
|
const USER_SUBJECT_RE = /^user:([A-Za-z0-9][A-Za-z0-9_.-]*|\*)$/;
|
|
69
|
+
/**
|
|
70
|
+
* A KEY-ADDRESSED subject: `key:<64 lowercase hex>` — a sha256 keyHash, the CONTROL-PLANE authority
|
|
71
|
+
* subject (an AdminGrant/CreationGrant/CapabilityBinding key becomes `…@key:<keyHash>`). Pinned to
|
|
72
|
+
* EXACTLY 64 hex with NO wildcard: the Check wildcard arm is `user:*`-only, so a `key:*` would be a
|
|
73
|
+
* forge surface (any key claiming admin). Distinct from `user:` so a keyHash is never read as a uid.
|
|
74
|
+
*/
|
|
75
|
+
const KEY_SUBJECT_RE = /^key:[0-9a-f]{64}$/;
|
|
76
|
+
/**
|
|
77
|
+
* A USERSET subject `<type>:<id>#<relation>` — group membership / indirection (P2, the native-Zanzibar
|
|
78
|
+
* recursion EDGE the `RewriteEngine` resolves: `Doc#viewer@group:eng#member`). The body is OBJECT_RE
|
|
79
|
+
* (`<type>:<id>`) + `#` + RELATION_RE (`#` is never in either part, so the split is unambiguous). The
|
|
80
|
+
* CONTROL-PLANE CARVE-OUT (an admin/creator relation must NOT accept a userset — else admin-via-group)
|
|
81
|
+
* is enforced at the WRITE GATE by the relation's declared `AllowedType` (P4), not here: this generic
|
|
82
|
+
* boundary only checks the subject is well-FORMED. A relation that declares no userset allowed-type
|
|
83
|
+
* refuses one fail-closed at the gate. (Governance does not use the relation schema until P5, and P4
|
|
84
|
+
* lands the AllowedType gate before it — so no control-plane userset is admissible in the interim.)
|
|
85
|
+
*/
|
|
86
|
+
const USERSET_SUBJECT_RE =
|
|
87
|
+
/^[A-Za-z0-9][A-Za-z0-9_.-]*:[A-Za-z0-9][A-Za-z0-9_.:/-]*#[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
69
88
|
|
|
70
89
|
/** Deterministic per-(object, relation) aggregate instance id — replay-stable. */
|
|
71
90
|
export function relationTupleInstanceId(object: string, relation: string): string {
|
|
@@ -166,7 +185,12 @@ export function removeTuple(
|
|
|
166
185
|
const relatePayload = z.object({
|
|
167
186
|
object: z.string().regex(OBJECT_RE, "object must be <type>:<id>"),
|
|
168
187
|
relation: z.string().regex(RELATION_RE),
|
|
169
|
-
subject: z
|
|
188
|
+
subject: z
|
|
189
|
+
.string()
|
|
190
|
+
.refine(
|
|
191
|
+
(s) => USER_SUBJECT_RE.test(s) || KEY_SUBJECT_RE.test(s) || USERSET_SUBJECT_RE.test(s),
|
|
192
|
+
"subject must be user:<uid>, user:*, key:<64hex>, or a userset <type>:<id>#<relation>",
|
|
193
|
+
),
|
|
170
194
|
grantedBy: z.string().min(1),
|
|
171
195
|
grantedAt: z.string().min(1), // ISO-8601, caller-stamped (determinism)
|
|
172
196
|
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
|
|
4
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* framework/signing_identity.ts — THE `signer` RELATION (the key↔identity binding,
|
|
8
|
+
* `architecture/identity_keys.md`). NATIVE ZANZIBAR, not a bespoke roster.
|
|
9
|
+
*
|
|
10
|
+
* THE CLEAN SPLIT. Proving WHO authored an intent has exactly two parts:
|
|
11
|
+
* 1. The SIGNATURE — "this ed25519 device key signed these bytes." That is the ONE
|
|
12
|
+
* irreducibly-crypto primitive (the attestation layer beneath Zanzibar; relations
|
|
13
|
+
* can't prove a signature). It is the kernel/provenance `verify` over the
|
|
14
|
+
* source-bound author bytes — NOT modelled here.
|
|
15
|
+
* 2. WHICH KEY BELONGS TO WHICH IDENTITY — a RELATION:
|
|
16
|
+
* `identity:<principal>#signer@key:<sha256hex(pubkey)>`
|
|
17
|
+
* "this device key may AUTHOR as this principal." ENROLL with `relateSubject`,
|
|
18
|
+
* REVOKE with `unrelateSubject` (remove-wins — already native), CHECK with the
|
|
19
|
+
* relation engine (`RewriteEngine.check`). No aggregate, no roster fold; the pubkey
|
|
20
|
+
* rides the signature and its hash is just the subject. The gate reads THIS
|
|
21
|
+
* workspace's own signer tuples (local — each workspace holds its own guest list).
|
|
22
|
+
*
|
|
23
|
+
* So: the gate derives a PROVEN principal by `verify(sig)` (1) then
|
|
24
|
+
* `Check(identity:P # signer @ key:<hash>)` (2) — and only THEN runs the directive's
|
|
25
|
+
* own `.requires(...)` relation gate. Two of the three steps are relations; only the
|
|
26
|
+
* signature is crypto.
|
|
27
|
+
*
|
|
28
|
+
* BOOTSTRAP: the FOUNDING signer tuple of a principal (on a virgin `identity:P#signer`)
|
|
29
|
+
* is admitted by the genesis clause (`framework/genesis.ts`, token-bound), exactly like
|
|
30
|
+
* the founding admin grant; every later device is enrolled by proof-of-possession (an
|
|
31
|
+
* intent signed by an already-enrolled key).
|
|
32
|
+
*/
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
import { directive } from "../directive.js";
|
|
35
|
+
import { relationType, self, key } from "../relation_schema.js";
|
|
36
|
+
import { writeTuple, RelationTuple } from "./relations.js";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The framework `identity` relation schema — the `signer` relation, LEAF-ONLY
|
|
40
|
+
* (`allow: [key()]`, never a userset/wildcard: a `key:*` would let any key claim to author
|
|
41
|
+
* as anyone). A workspace that admits signed authorship composes this into its law.
|
|
42
|
+
*/
|
|
43
|
+
export const identityRelations = relationType("identity").relation("signer", {
|
|
44
|
+
rewrite: self(),
|
|
45
|
+
allow: [key()],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** The Zanzibar OBJECT for a principal's signer relation — `identity:<principal>` (the gate mirrors
|
|
49
|
+
* this as `executor::signer_object`; the subject is `key:<sha256hex(pubkey)>`). */
|
|
50
|
+
export function signerObject(principal: string): string {
|
|
51
|
+
return `identity:${principal}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The `signer` relation name — the relation a device key holds on its `identity:<principal>` object. */
|
|
55
|
+
export const SIGNER_RELATION = "signer";
|
|
56
|
+
|
|
57
|
+
const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256 hex (the device pubkey hash)");
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `enrollSigner` — author the founding `identity:<principal>#signer@key:<keyHash>` tuple (the bootstrap
|
|
61
|
+
* E of the warrant-flip, `architecture/identity_keys.md`). A `relateSubject` SPECIALISATION:
|
|
62
|
+
* - `.ensures(RelationTuple)` so the deterministic-keyed `(identity:<principal>, signer)` head is
|
|
63
|
+
* admitted by the id-mint gate (upsert-by-key) — exactly like `grantAdmin`;
|
|
64
|
+
* - **NO `.requires`** — DELIBERATE. At genesis the founding signer enrolls on a still-WARRANT-FREE
|
|
65
|
+
* prefix (it precedes `seedGenesis`), so `verify_captured_principal` is in its legacy fast path and
|
|
66
|
+
* admits it UNSIGNED; there is no admin/signer yet to gate it, so a `.requires` would re-create the
|
|
67
|
+
* bootstrap chicken-and-egg. The forge surface a bare `relateSubject` would open is closed by
|
|
68
|
+
* DOMAIN PLACEMENT: this directive is composed into the CONTROL-PLANE governance law (domain
|
|
69
|
+
* `workspaces`/`nomos`), which the open session lane structurally refuses
|
|
70
|
+
* (`cloud/holon-host/src/engine.mjs` — only the birth door + the secret-gated `/author` + admin
|
|
71
|
+
* lanes author it), so no tenant can forge a signer for another principal.
|
|
72
|
+
*
|
|
73
|
+
* THE ORDER IS THE CONTRACT (verified, zero gate change): `enrollSigner` (E, unsigned/pre-warrant) →
|
|
74
|
+
* `seedGenesis` (W, unsigned/pre-warrant, the warrant lands in its output) → `grantAdmin` (A, the FIRST
|
|
75
|
+
* SIGNED intent — its prefix now carries the warrant, the E-tuple proves the key, the virgin `#admin` is
|
|
76
|
+
* admitted by the genesis clause). NB: a GENERAL tenant-callable enrolment (additional devices) needs a
|
|
77
|
+
* self-binding (captured principal == payload principal) or admin-gating — NOT this founding lane.
|
|
78
|
+
*/
|
|
79
|
+
export const enrollSigner = directive("enrollSigner")
|
|
80
|
+
.ensures(RelationTuple)
|
|
81
|
+
.payload(
|
|
82
|
+
z.object({
|
|
83
|
+
principal: z.string().min(1), // the auth-provider uid the key may author as
|
|
84
|
+
keyHash: sha256Hex, // sha256hex of the ed25519 device PUBLIC key
|
|
85
|
+
grantedBy: z.string().min(1),
|
|
86
|
+
grantedAt: z.number().int().nonnegative(), // epoch ms, caller-stamped (determinism)
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
.plan((p) =>
|
|
90
|
+
writeTuple({
|
|
91
|
+
object: signerObject(p.principal),
|
|
92
|
+
relation: SIGNER_RELATION,
|
|
93
|
+
subject: "key:" + p.keyHash,
|
|
94
|
+
grantedBy: p.grantedBy,
|
|
95
|
+
grantedAt: String(p.grantedAt),
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
@@ -69,8 +69,10 @@ export function refAs(name: string, aggregateType: string, id: string): Invarian
|
|
|
69
69
|
return { name, aggregate: aggregateType, id };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/** Derive the BOUNDED declared read-set from the intent payload. Pure — no IO, no live read.
|
|
73
|
-
|
|
72
|
+
/** Derive the BOUNDED declared read-set from the intent payload. Pure — no IO, no live read.
|
|
73
|
+
* `ctx.principal` is the captured, authenticated AUTHOR identity (e.g. `key:<keyHash>` / a verified uid)
|
|
74
|
+
* — the same value on every replaying peer — so an invariant can gate by WHO authored (law-gated authz). */
|
|
75
|
+
export type WorkspaceInvariantReads = (ctx: { intent: Record<string, unknown>; principal?: string }) => InvariantRef[];
|
|
74
76
|
|
|
75
77
|
/**
|
|
76
78
|
* The post-apply predicate over the resolved snapshots, keyed by each ref's binding `name`.
|
|
@@ -84,7 +86,7 @@ export type WorkspaceInvariantReads = (ctx: { intent: Record<string, unknown> })
|
|
|
84
86
|
*/
|
|
85
87
|
export type WorkspaceInvariantAssert = (
|
|
86
88
|
snapshots: Record<string, Record<string, unknown>>,
|
|
87
|
-
ctx?: { intent: Record<string, unknown
|
|
89
|
+
ctx?: { intent: Record<string, unknown>; principal?: string },
|
|
88
90
|
) => WorkspaceInvariantVerdict;
|
|
89
91
|
|
|
90
92
|
/** A complete, registered workspace-invariant declaration (the `.assert` terminal yields this). */
|