@githolon/dsl 0.7.1 → 0.9.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.
@@ -66,12 +66,77 @@ export const GOVERNANCE_REVISION = "2026-06-12-capability-bindings"; // hash-bea
66
66
  */
67
67
  import { z } from "zod";
68
68
  import { aggregate, instance } from "../aggregate.js";
69
- import { create } from "../authoring.js";
69
+ import { create, ensure } from "../authoring.js";
70
70
  import { directive } from "../directive.js";
71
71
  import { Lww } from "../drivers.js";
72
72
  import { t } from "../fields.js";
73
73
  import { set, withMarker } from "../ops.js";
74
74
  import { query } from "../query.js";
75
+ import { workspaceInvariant, refAs } from "./workspace_invariant.js";
76
+ import { writeTuple, removeTuple, RelationTuple } from "./relations.js";
77
+ import { relationType, self, user, key } from "../relation_schema.js";
78
+
79
+ // ── WORKSPACE SELF-IDENTITY (the law reads its OWN install-context) ──────────────────────────────────
80
+ // The governance bundle runs on root AND every born platform. A workspace knows its ROLE via this
81
+ // singleton, at a CONSTANT fixed instance id (the RelationTuple discipline — a literal, never minted, so
82
+ // the id is identical on every peer + the sid is additive/era-safe). Seeded by the birth plan (ungated).
83
+ // The gate keys authority off the CONSTANT object literal `workspace:self` — because custody guarantees a
84
+ // workspace only ever folds its OWN relations table, `workspace:root#admin` collapses to `workspace:self#
85
+ // admin`, so the gate is self-relative with NO name plumbing. A tenant runs no governance ⇒ no WorkspaceSelf.
86
+ export const WORKSPACE_ROLES = ["cloud-root", "platform", "tenant"] as const;
87
+ const WORKSPACE_SELF_ID = "workspaceself-self";
88
+ const SELF_ADMIN_OBJECT = "workspace:self";
89
+ export const WorkspaceSelf = aggregate("WorkspaceSelf", {
90
+ role: t.enum(WORKSPACE_ROLES).merge(Lww),
91
+ name: t.string().merge(Lww),
92
+ });
93
+ // The one-off SELF-IDENTITY backfill for an already-born workspace (root predates WorkspaceSelf). UNGATED
94
+ // by the admin gate — it must run BEFORE the admin tuple/gate are live — but WRITE-ONCE (the invariant
95
+ // below refuses a re-set once the role exists), so a race or a malicious re-set cannot flip the role and
96
+ // freeze governance. The HOST authors this in the serialized genesis re-entry, winning any race.
97
+ export const ensureWorkspaceSelf = directive("ensureWorkspaceSelf")
98
+ .ensures(WorkspaceSelf)
99
+ .payload(z.object({ role: z.enum(WORKSPACE_ROLES), name: z.string().min(1) }))
100
+ .plan((p) => {
101
+ ensure(WorkspaceSelf, WORKSPACE_SELF_ID).set("role", p.role).set("name", p.name);
102
+ return [];
103
+ });
104
+ export const workspaceSelfWriteOnce = workspaceInvariant("WorkspaceSelfWriteOnce")
105
+ // BARE directive id — the gate filters `on == directiveId` and the engine dispatches by the
106
+ // BARE id within a domain slice (`directive("ensureWorkspaceSelf")`), so the produced intent
107
+ // carries `directiveId:"ensureWorkspaceSelf"`. A domain-QUALIFIED `on` ("workspaces.…") would
108
+ // NEVER match the bare runtime id ⇒ the gate stays silently INERT (the #266 side-door bug,
109
+ // 2026-06-15). Bare matches — exactly the convention the sharding invariants already fire under.
110
+ .on("ensureWorkspaceSelf")
111
+ .reads(() => [refAs("self", "WorkspaceSelf", "pre:" + WORKSPACE_SELF_ID)])
112
+ .assert(({ self }) =>
113
+ (self as { role?: string })?.role ? { reject: "workspace-self-already-set" } : { accept: true },
114
+ );
115
+
116
+ // ── THE GOVERNANCE RELATION SCHEMA (full-native Zanzibar — the ONE authz model) ──────────────────────
117
+ // The control-plane authority of the cloud is a NATIVE Zanzibar namespace. A directive declares its
118
+ // requirement with `.requires("<relation>")` (object defaults to the self-relative `"workspace:self"`),
119
+ // and the ONE wasm gate runs `engine.check(schema, "workspace:self", relation, principal)` over the
120
+ // pre-apply folded relation tuples — refusing any author who does not HOLD the relation. The schema +
121
+ // the `.requires` together arm the gate (the `nomosRelations` + `nomosRelationGate` era keys); the
122
+ // bespoke `workspaceInvariant` authz gates are GONE (this IS their replacement, as real native law).
123
+ //
124
+ // THE CONTROL-PLANE CARVE-OUT — LEAF-ONLY (`allow: [user(), key()]`, never a userset/wildcard). A
125
+ // `workspace:self#admin@<group>#member` or `@<type>:*` would let a group/wildcard escalate to admin
126
+ // (the admin-via-group forge); the fail-closed `allow` makes such a subject UNSTORABLE + (the Check
127
+ // side) DISCARDED at evaluation. Each relation is a flat `self()` (directly-stored tuples) — NO rewrite
128
+ // algebra (no computed/from/union): control-plane authority is direct membership only.
129
+ // * admin — grantAdmin/revokeAdmin + every admin-mutating governance directive.
130
+ // * creator — createWorkspace (under invite/closed posture; see the host's _creationLawOk).
131
+ // * platformCreator — birthPlatform (always invite-only).
132
+ // SELF-RELATIVE: custody guarantees a workspace only ever folds its OWN relations table, so
133
+ // `workspace:root#admin` collapses to `workspace:self#admin` — the SAME law governs root and every
134
+ // born platform with no name plumbing. (A pure tenant runs no governance ⇒ no schema ⇒ the gate is
135
+ // inert there; an un-backfilled workspace with no admin tuple refuses fail-closed.)
136
+ export const workspaceRelations = relationType("workspace")
137
+ .relation("admin", { rewrite: self(), allow: [user(), key()] })
138
+ .relation("creator", { rewrite: self(), allow: [user(), key()] })
139
+ .relation("platformCreator", { rewrite: self(), allow: [user(), key()] });
75
140
 
76
141
  // --- vocabulary ----------------------------------------------------------------
77
142
 
@@ -161,43 +226,11 @@ export const CloudWorkspace = aggregate("CloudWorkspace", {
161
226
  pool: t.int().merge(Lww).optional(),
162
227
  });
163
228
 
164
- /**
165
- * AdminGrant — one admin credential of the cloud itself. An `active` AdminGrant's
166
- * `keyHash` is the WHOLE of admin power: the worker admits an admin bearer iff
167
- * `adminGrantByKeyHash(sha256(bearer))` yields an active grant. Genesis seeds the
168
- * first one; after that, admins make admins — on the record.
169
- */
170
- export const AdminGrant = aggregate("AdminGrant", {
171
- /** sha256 hex of the admin bearer secret (the secret itself never lands here). */
172
- keyHash: t.string().merge(Lww),
173
- /** Human label for audit ("jack", "ci-deployer", ...). */
174
- label: t.string().merge(Lww),
175
- /** The principal that authored the grant (attribution, on the ledger forever). */
176
- grantedBy: t.string().merge(Lww),
177
- /** Epoch-ms the grant was authored (caller-stamped). */
178
- grantedAt: t.int().merge(Lww),
179
- /** `active` | `revoked` — revocation is the law's kill switch for a key. */
180
- status: t.enum(GRANT_STATUSES).merge(Lww),
181
- });
182
-
183
- /**
184
- * CreationGrant — the narrower power to CREATE workspaces (the invite key,
185
- * lawful edition). Same shape and same enforcement path as AdminGrant, but the
186
- * worker honours it ONLY for `createWorkspace`-shaped access — separation of the
187
- * "may invite tenants" power from the "may govern the cloud" power.
188
- */
189
- export const CreationGrant = aggregate("CreationGrant", {
190
- /** sha256 hex of the creation bearer secret. */
191
- keyHash: t.string().merge(Lww),
192
- /** Human label for audit. */
193
- label: t.string().merge(Lww),
194
- /** The principal that authored the grant. */
195
- grantedBy: t.string().merge(Lww),
196
- /** Epoch-ms the grant was authored (caller-stamped). */
197
- grantedAt: t.int().merge(Lww),
198
- /** `active` | `revoked`. */
199
- status: t.enum(GRANT_STATUSES).merge(Lww),
200
- });
229
+ // AdminGrant / CreationGrant aggregates: DELETED (full-native Zanzibar). Admin/creation authority is
230
+ // now a NATIVE RELATION TUPLE — `workspace:self#admin@key:<keyHash>` / `…#creator@key:<keyHash>` —
231
+ // written by grantAdmin/grantCreation via `writeTuple`, gated by `.requires("admin")`, and read by the
232
+ // host's `_rootCheck` (the wasm `check` op) and the one gate's `engine.check`. A grant WAS just a
233
+ // relation; the aggregate + its `adminGrantByKeyHash`/`creationGrantByKeyHash` queries are gone.
201
234
 
202
235
  /**
203
236
  * CloudPolicy — THE QUOTA LAW, a law-state singleton (id `cloud-policy:cloud`).
@@ -342,7 +375,8 @@ export const createWorkspace = directive("createWorkspace")
342
375
  .set("createdBy", p.requestedBy)
343
376
  .set("createdAt", p.requestedAt);
344
377
  return [];
345
- });
378
+ })
379
+ .requires("creator"); // posture-gated creation: only a #creator holder may request a workspace
346
380
 
347
381
  /**
348
382
  * birthCloud — THE CLOUD'S OWN BIRTH INTENT. Root authors this once, into its own
@@ -360,6 +394,9 @@ export const birthCloud = directive("birthCloud")
360
394
  /** The genesis auth-provider principal uid upon which the world rests. */
361
395
  principal: z.string().min(1),
362
396
  name: z.string().min(1).default("root"),
397
+ /** The workspace's ROLE for the governance domain — the host stamps "cloud-root" at root genesis,
398
+ * "platform" when birthing a platform's own chain. Drives the self-relative authz gate. */
399
+ role: z.enum(WORKSPACE_ROLES).default("cloud-root"),
363
400
  tier: z.enum(WORKSPACE_TIERS).default("edge"),
364
401
  bornAt: epochMs,
365
402
  }),
@@ -375,6 +412,9 @@ export const birthCloud = directive("birthCloud")
375
412
  .set("createdBy", p.principal)
376
413
  .set("createdAt", p.bornAt)
377
414
  .set("provisionedAt", p.bornAt);
415
+ // The self-identity singleton at the CONSTANT id (ungated — birthCloud is not admin-mutating), so the
416
+ // gate's self-record exists before any admin write. ensure() = deterministic-id sibling birth.
417
+ ensure(WorkspaceSelf, WORKSPACE_SELF_ID).set("role", p.role).set("name", p.name);
378
418
  return [];
379
419
  });
380
420
 
@@ -530,7 +570,8 @@ export const birthPlatform = directive("birthPlatform")
530
570
  .set("createdBy", p.requestedBy)
531
571
  .set("createdAt", p.requestedAt);
532
572
  return [];
533
- });
573
+ })
574
+ .requires("platformCreator"); // invite-only: an active #platformCreator tuple admits the birth
534
575
 
535
576
  /** setPlatformPool — the pool edition of a granted quota request. */
536
577
  export const setPlatformPool = directive("setPlatformPool")
@@ -546,14 +587,18 @@ export const setPlatformPool = directive("setPlatformPool")
546
587
  .plan((p) => {
547
588
  const ws = instance(CloudWorkspace, p.workspaceId);
548
589
  return [set(ws, "pool", p.pool)];
549
- });
590
+ })
591
+ .requires("admin");
550
592
 
551
593
  /**
552
594
  * grantPlatformCreation / revokePlatformCreation — open and close the
553
595
  * become-a-platform door for one principal (see {@link PlatformGrant}).
554
596
  */
597
+ // grantPlatformCreation — become-a-platform authority IS the self-relative `#platformCreator` tuple
598
+ // (one authz spine, all the way down). Writes `workspace:self#platformCreator@user:<principal>` (no
599
+ // PlatformGrant aggregate) — the host's platform-birth invite check reads the SAME tuple via Check.
555
600
  export const grantPlatformCreation = directive("grantPlatformCreation")
556
- .creates(PlatformGrant)
601
+ .ensures(RelationTuple)
557
602
  .payload(
558
603
  z.object({
559
604
  principal: z.string().min(1),
@@ -562,29 +607,22 @@ export const grantPlatformCreation = directive("grantPlatformCreation")
562
607
  grantedAt: epochMs,
563
608
  }),
564
609
  )
565
- .plan((p) => {
566
- create(PlatformGrant)
567
- .set("principal", p.principal)
568
- .set("label", p.label)
569
- .set("grantedBy", p.grantedBy)
570
- .set("grantedAt", p.grantedAt)
571
- .set("status", "active" as const);
572
- return [];
573
- });
610
+ .plan((p) => writeTuple({ object: SELF_ADMIN_OBJECT, relation: "platformCreator", subject: "user:" + p.principal, grantedBy: p.grantedBy, grantedAt: String(p.grantedAt) }))
611
+ .requires("admin"); // only an admin opens the become-a-platform door
574
612
 
613
+ // revokePlatformCreation — flip the `#platformCreator@user:<principal>` leaf to revoked. BREAKING vs the
614
+ // aggregate model: keyed by PRINCIPAL (the tuple subject), not a minted grantId.
575
615
  export const revokePlatformCreation = directive("revokePlatformCreation")
576
- .mutates(PlatformGrant)
616
+ .ensures(RelationTuple)
577
617
  .payload(
578
618
  z.object({
579
- grantId: z.string().min(1),
619
+ principal: z.string().min(1),
580
620
  revokedBy: z.string().min(1),
581
621
  revokedAt: epochMs,
582
622
  }),
583
623
  )
584
- .plan((p) => {
585
- const grant = instance(PlatformGrant, p.grantId);
586
- return [set(grant, "status", "revoked" as const)];
587
- });
624
+ .plan((p) => removeTuple({ object: SELF_ADMIN_OBJECT, relation: "platformCreator", subject: "user:" + p.principal, grantedBy: p.revokedBy, grantedAt: String(p.revokedAt), revokedBy: p.revokedBy, revokedAt: String(p.revokedAt) }))
625
+ .requires("admin");
588
626
 
589
627
  /**
590
628
  * AuthProvider — BYOAuth: WHO VOUCHES FOR THIS SUBTREE'S PRINCIPALS. A law-state
@@ -638,7 +676,8 @@ export const setAuthProvider = directive("setAuthProvider")
638
676
  set(ap, "setBy", p.setBy),
639
677
  set(ap, "setAt", p.setAt),
640
678
  ];
641
- });
679
+ })
680
+ .requires("admin");
642
681
 
643
682
  /**
644
683
  * HostnameClaim — a CUSTOM HOSTNAME bound to a platform: `api.acme.com` serving
@@ -789,7 +828,8 @@ export const setCloudPolicy = directive("setCloudPolicy")
789
828
  set(pol, "setBy", p.setBy),
790
829
  set(pol, "setAt", p.setAt),
791
830
  ];
792
- });
831
+ })
832
+ .requires("admin");
793
833
 
794
834
  /**
795
835
  * grantQuota — grant a quota request: a principal's workspace allowance. The
@@ -812,7 +852,8 @@ export const grantQuota = directive("grantQuota")
812
852
  .set("grantedBy", p.grantedBy)
813
853
  .set("grantedAt", p.grantedAt);
814
854
  return [];
815
- });
855
+ })
856
+ .requires("admin");
816
857
 
817
858
  /**
818
859
  * setWorkspaceByteCap — grant one workspace a bigger (or smaller) ledger than the
@@ -831,7 +872,8 @@ export const setWorkspaceByteCap = directive("setWorkspaceByteCap")
831
872
  .plan((p) => {
832
873
  const ws = instance(CloudWorkspace, p.workspaceId);
833
874
  return [set(ws, "byteCap", p.byteCap)];
834
- });
875
+ })
876
+ .requires("admin");
835
877
 
836
878
  // --- grant directives ---------------------------------------------------------------
837
879
 
@@ -840,8 +882,12 @@ export const setWorkspaceByteCap = directive("setWorkspaceByteCap")
840
882
  * authors the FIRST one from the bootstrap env var — which is then spent forever;
841
883
  * every later grant is an admin granting, attributed and admitted under this law.
842
884
  */
885
+ // ZANZIBAR ALL THE WAY DOWN: admin authority IS the self-relative tuple the gate reads. grantAdmin writes
886
+ // `workspace:self#admin@key:<keyHash>` (no AdminGrant aggregate) — a typed alias for relateSubject(#admin);
887
+ // the AuthorHoldsAdmin gate's PRE-APPLY roster prevents self-grant. `label` stays in the payload contract
888
+ // (provenance) though the keyHash-keyed tuple leaf does not carry it.
843
889
  export const grantAdmin = directive("grantAdmin")
844
- .creates(AdminGrant)
890
+ .ensures(RelationTuple)
845
891
  .payload(
846
892
  z.object({
847
893
  keyHash: sha256Hex,
@@ -850,42 +896,39 @@ export const grantAdmin = directive("grantAdmin")
850
896
  grantedAt: epochMs,
851
897
  }),
852
898
  )
853
- .plan((p) => {
854
- create(AdminGrant)
855
- .set("keyHash", p.keyHash)
856
- .set("label", p.label)
857
- .set("grantedBy", p.grantedBy)
858
- .set("grantedAt", p.grantedAt)
859
- .set("status", "active" as const);
860
- return [];
861
- });
899
+ .plan((p) => writeTuple({ object: SELF_ADMIN_OBJECT, relation: "admin", subject: "key:" + p.keyHash, grantedBy: p.grantedBy, grantedAt: String(p.grantedAt) }))
900
+ .requires("admin"); // only an admin grants admin (pre-apply roster ⇒ no self-grant)
862
901
 
863
902
  /**
864
903
  * revokeAdmin — flip an AdminGrant to `revoked`. The key stops verifying at fold
865
904
  * time; the grant record (who granted it, when, who killed it) stays on the
866
905
  * ledger — audit is structural, not a log file.
867
906
  */
907
+ // revokeAdmin — flip the `#admin@key:<keyHash>` subject leaf to revoked (an Lww write over the same map
908
+ // entry; the projection drops the row, Check fails closed). Keyed by keyHash now (the tuple subject), not
909
+ // a minted grantId. Audit stays structural — the revoked leaf rides the chain forever.
868
910
  export const revokeAdmin = directive("revokeAdmin")
869
- .mutates(AdminGrant)
911
+ .ensures(RelationTuple)
870
912
  .payload(
871
913
  z.object({
872
- grantId: z.string().min(1),
914
+ keyHash: sha256Hex,
873
915
  revokedBy: z.string().min(1),
874
916
  revokedAt: epochMs,
875
917
  }),
876
918
  )
877
- .plan((p) => {
878
- const grant = instance(AdminGrant, p.grantId);
879
- return [set(grant, "status", "revoked" as const)];
880
- });
919
+ .plan((p) => removeTuple({ object: SELF_ADMIN_OBJECT, relation: "admin", subject: "key:" + p.keyHash, grantedBy: p.revokedBy, grantedAt: String(p.revokedAt), revokedBy: p.revokedBy, revokedAt: String(p.revokedAt) }))
920
+ .requires("admin");
881
921
 
882
922
  /**
883
923
  * grantCreation — mint a workspace-creation credential record (`status:"active"`).
884
924
  * The lawful replacement for the `NOMOS_CLOUD_INVITE_KEY` env var: who may invite
885
925
  * tenants is law state, grantable and revocable on the record.
886
926
  */
927
+ // grantCreation — creation authority IS the self-relative `#creator` tuple the CreationGate reads.
928
+ // Writes `workspace:self#creator@key:<keyHash>` (no CreationGrant aggregate). Under posture invite/closed
929
+ // the gate admits only holders of an active #creator subject; under open/unset it admits anyone.
887
930
  export const grantCreation = directive("grantCreation")
888
- .creates(CreationGrant)
931
+ .ensures(RelationTuple)
889
932
  .payload(
890
933
  z.object({
891
934
  keyHash: sha256Hex,
@@ -894,30 +937,21 @@ export const grantCreation = directive("grantCreation")
894
937
  grantedAt: epochMs,
895
938
  }),
896
939
  )
897
- .plan((p) => {
898
- create(CreationGrant)
899
- .set("keyHash", p.keyHash)
900
- .set("label", p.label)
901
- .set("grantedBy", p.grantedBy)
902
- .set("grantedAt", p.grantedAt)
903
- .set("status", "active" as const);
904
- return [];
905
- });
940
+ .plan((p) => writeTuple({ object: SELF_ADMIN_OBJECT, relation: "creator", subject: "key:" + p.keyHash, grantedBy: p.grantedBy, grantedAt: String(p.grantedAt) }))
941
+ .requires("admin"); // only an admin grants creation authority
906
942
 
907
- /** revokeCreation — flip a CreationGrant to `revoked` (see {@link revokeAdmin}). */
943
+ /** revokeCreation — flip the `#creator@key:<keyHash>` leaf to revoked (see {@link revokeAdmin}). */
908
944
  export const revokeCreation = directive("revokeCreation")
909
- .mutates(CreationGrant)
945
+ .ensures(RelationTuple)
910
946
  .payload(
911
947
  z.object({
912
- grantId: z.string().min(1),
948
+ keyHash: sha256Hex,
913
949
  revokedBy: z.string().min(1),
914
950
  revokedAt: epochMs,
915
951
  }),
916
952
  )
917
- .plan((p) => {
918
- const grant = instance(CreationGrant, p.grantId);
919
- return [set(grant, "status", "revoked" as const)];
920
- });
953
+ .plan((p) => removeTuple({ object: SELF_ADMIN_OBJECT, relation: "creator", subject: "key:" + p.keyHash, grantedBy: p.revokedBy, grantedAt: String(p.revokedAt), revokedBy: p.revokedBy, revokedAt: String(p.revokedAt) }))
954
+ .requires("admin");
921
955
 
922
956
  // --- declared read queries (the worker's ENTIRE enforcement surface) -----------------
923
957
  //
@@ -928,15 +962,10 @@ export const revokeCreation = directive("revokeCreation")
928
962
  /** Resolve a workspace by its unique name (the `:ws` URL segment → the law record). */
929
963
  export const workspaceByName = query("workspaceByName").key("name").returns(CloudWorkspace);
930
964
 
931
- /** "Is this bearer an admin?" — probe by sha256(bearer); active grant = yes. */
932
- export const adminGrantByKeyHash = query("adminGrantByKeyHash")
933
- .key("keyHash")
934
- .returns(AdminGrant);
935
-
936
- /** "May this bearer create workspaces?" — probe by sha256(bearer). */
937
- export const creationGrantByKeyHash = query("creationGrantByKeyHash")
938
- .key("keyHash")
939
- .returns(CreationGrant);
965
+ // adminGrantByKeyHash / creationGrantByKeyHash queries: DELETED (full-native Zanzibar). "Is this bearer
966
+ // an admin / a creator?" is now `Check(workspace:self#admin@key:<keyHash>)` / `…#creator@…` over the
967
+ // projected relation tuples (the wasm `check` op / the one gate's `engine.check`) — never an aggregate
968
+ // probe. The AdminGrant/CreationGrant aggregates that backed these are gone; the tuple IS the grant.
940
969
 
941
970
  /** All children of a workspace (`""` = the roots) — the workspace tree's read side. */
942
971
  export const workspacesByParent = query("workspacesByParent")
@@ -983,3 +1012,29 @@ export const capabilityBindingsByWorkspace = query("capabilityBindingsByWorkspac
983
1012
  export const capabilityBindingByKeyHash = query("capabilityBindingByKeyHash")
984
1013
  .key("keyHash")
985
1014
  .returns(CapabilityBinding);
1015
+
1016
+ // ── THE ZANZIBAR SPINE comes to the CONTROL PLANE (governance → tuples) ──────────────────────────
1017
+ // The authority grants above ARE relation tuples in disguise (the same smell as the deleted
1018
+ // UserPermission): admin=`workspace:self#admin@key:<keyHash>`, creator=`…#creator@key:…`,
1019
+ // platformCreator=`workspace:self#platformCreator@user:<principal>`. grantAdmin/grantCreation/
1020
+ // grantPlatformCreation write them via `writeTuple`; the bespoke AdminGrant/CreationGrant aggregates +
1021
+ // their queries are GONE. Re-export the framework tuple surface so the COMPILED governance package mints
1022
+ // RelationTuple's stable id into the lockfile + identity manifest, the engine registers relateSubject/
1023
+ // unrelateSubject for dispatch, and the projected relationTuplesByObject read routes. (QuotaGrant stays
1024
+ // an aggregate — a numeric allowance, not a membership.)
1025
+ export { relateSubject, unrelateSubject, relationTuplesByObject } from "./relations.js";
1026
+ export { RelationTuple };
1027
+
1028
+ // ── GOVERNANCE WRITE-AUTHZ IS NATIVE ZANZIBAR — the ONE gate, no bespoke predicates ───────────────────
1029
+ // The bespoke `workspaceInvariant` authz gates (AuthorHoldsAdmin / CreationGate / PlatformCreationGate)
1030
+ // are DELETED. Their job is now done by the full-native relation gate: each governance directive carries
1031
+ // `.requires("<relation>")` (above), the compiler emits `nomosRelations` (the schema) + `nomosRelationGate`
1032
+ // (directive→relation), and the ONE wasm gate runs `engine.check(schema, "workspace:self", relation,
1033
+ // principal)` over the PRE-APPLY folded relation tuples — refusing any author who does not HOLD the
1034
+ // relation, on every door (author / edge admission / verify_chain replay). The author is the CAPTURED
1035
+ // principal (`key:<sha256(bearer)>` for admin/creator, `user:<uid>` for platformCreator), pinned to the
1036
+ // sealed Attribution.actor on every peer — so "who may grant" is a LAW fact, never a host pre-check. The
1037
+ // pre-apply roster means `grantAdmin(@key:self)` cannot self-grant (the not-yet-written subject is absent
1038
+ // in the pre-apply state). SELF-RELATIVE: custody guarantees a workspace only folds its OWN tuples, so the
1039
+ // SAME law governs root and every born platform. The host's `_adminOk`/`_creationLawOk`/`_platformCheck`
1040
+ // pre-checks are GONE too (a pure bailiff) — the native gate refuses a non-holder at admission.
package/src/index.ts CHANGED
@@ -51,6 +51,9 @@ export {
51
51
  set,
52
52
  addToSet,
53
53
  setEntry,
54
+ increment,
55
+ decrement,
56
+ removeFromSet,
54
57
  withMarker,
55
58
  strike,
56
59
  type PlannedOp,
@@ -58,7 +61,7 @@ export {
58
61
  type PlannedEventMarker,
59
62
  type StrikeOp,
60
63
  } from "./ops.js";
61
- export { directive, type Directive, type ReferentialMarker } from "./directive.js";
64
+ export { directive, recordObject, type Directive, type ReferentialMarker } from "./directive.js";
62
65
  // FIRST-CLASS WORKSPACE TYPES (sharding §10 RATIFIED, slice 1) live on the SUBPATH
63
66
  // `@githolon/dsl/workspace-type` (`workspaceType("estate").root(Estate).hasMany(...)`),
64
67
  // NOT this runtime barrel — the barrel is bundled into EVERY tenant's engine lump, and
@@ -191,24 +194,11 @@ export {
191
194
  type EqFilter,
192
195
  type RequiredIndex,
193
196
  } from "./query/compile.js";
194
- export {
195
- PERMISSION_STATUSES,
196
- roleBindingSchema,
197
- userPermissionAggregate,
198
- userPermissionsInstanceId,
199
- permissionId,
200
- grant,
201
- revoke,
202
- delegate,
203
- revokeDelegation,
204
- updateMetadata,
205
- roleCatalogue,
206
- type PermissionStatus,
207
- type FrameworkRoleBinding,
208
- type UserPermissionAggregateHandle,
209
- type Scope,
210
- type RoleCatalogue,
211
- } from "./framework/rbac.js";
197
+ // THE LEGACY RBAC (framework/rbac.ts: userPermissionAggregate / grant / revoke / delegate /
198
+ // roleCatalogue) is DELETED — the one-model teardown. Read-visibility + grants are NATIVE relations:
199
+ // a `requires(cap)` aggregate's cap IS a read relation (synthesized into nomosRelations at compile),
200
+ // and a grant is a `relateSubject({ object:"workspace:self", relation:cap, ... })` tuple. See
201
+ // framework/relations.ts + relation_schema.ts below.
212
202
  // NATIVE ZANZIBAR / ReBAC (architecture/zanzibar_native_plan.md, slice 1) — object-keyed
213
203
  // relation-tuple events `object#relation@subject`, the clean-slate-native authz spine.
214
204
  export {
@@ -234,6 +224,37 @@ export {
234
224
  encKeyByPrincipal,
235
225
  type EncKeyHandle,
236
226
  } from "./framework/encryption_identity.js";
227
+ // IDENTITY KEYS (identity_keys.md) — the `signer` RELATION: the key↔identity binding is
228
+ // native Zanzibar (`identity:<principal>#signer@key:<hash>`, enrolled/revoked via
229
+ // relateSubject/unrelateSubject, checked by the engine), NOT a bespoke roster. The only
230
+ // sub-Zanzibar primitive is the SIGNATURE itself (the crypto attestation).
231
+ export {
232
+ identityRelations,
233
+ signerObject,
234
+ SIGNER_RELATION,
235
+ enrollSigner,
236
+ } from "./framework/signing_identity.js";
237
+ // THE HOME HOLON (home_holons_and_cross_custody.md §4) — the identity-role law: the shared-with-me access
238
+ // index + the owner lane to maintain it (the key directory is the `signer` relation above).
239
+ export {
240
+ SharedWithMe,
241
+ recordShare,
242
+ pruneShare,
243
+ sharedByWorkspace,
244
+ HomeOwner,
245
+ HOME_OWNER_ID,
246
+ seedHomeOwner,
247
+ writeHomeOwner,
248
+ } from "./framework/home.js";
249
+ // THE GENESIS WARRANT (identity_keys.md step 0) — the in-history birth fact that lets
250
+ // the gate's genesis clause admit a founding control-plane grant on a virgin chain.
251
+ export {
252
+ GenesisWarrant,
253
+ GENESIS_WARRANT_ID,
254
+ writeGenesisWarrant,
255
+ seedGenesis,
256
+ type GenesisWarrantHandle,
257
+ } from "./framework/genesis.js";
237
258
  // E2E ENCRYPTION (slice 4) — the group-key events (RFC 9420 sender-keys on the ledger).
238
259
  export {
239
260
  KeyScope,
@@ -307,6 +328,54 @@ export {
307
328
  type WorkspaceInvariantAssert,
308
329
  type InvariantRef,
309
330
  } from "./framework/workspace_invariant.js";
331
+ // NATIVE ZANZIBAR RELATION SCHEMA (P1) — the userset-rewrite law as pure data.
332
+ export {
333
+ relationType,
334
+ isRelationTypeDecl,
335
+ validateRelationType,
336
+ self,
337
+ computed,
338
+ via,
339
+ viaAll,
340
+ union,
341
+ intersection,
342
+ exclusion,
343
+ user,
344
+ key,
345
+ object,
346
+ anyUser,
347
+ group,
348
+ DEFAULT_RELATION_CAPS,
349
+ type RelationTypeDecl,
350
+ type RelationTypeBuilder,
351
+ type RelationDefAst,
352
+ type Rewrite,
353
+ type AllowedSubject,
354
+ type RelationCaps,
355
+ } from "./relation_schema.js";
356
+ // SECURE-BY-DEFAULT SUGAR over the relation schema — plain-English combinators, audiences, presets.
357
+ // Pure sugar: every export here BUILDS the existing Rewrite/RelationTypeDecl AST (no new wire shape).
358
+ export {
359
+ // tier 1 — plain-English combinator aliases (OpenFGA-shaped)
360
+ directly,
361
+ inheritedFrom,
362
+ fromParent,
363
+ fromEveryParent,
364
+ or,
365
+ and,
366
+ butNot,
367
+ // tier 1.5 — the un-forgettable type-restriction pairing
368
+ storedFor,
369
+ roles,
370
+ // tier 2 — secure-by-default audiences
371
+ withAudiences,
372
+ secureType,
373
+ // tier 3 — one-shot patterns
374
+ roleHierarchy,
375
+ docSharingModel,
376
+ type RelationOpts,
377
+ type SugaredRelationTypeBuilder,
378
+ } from "./relation_sugar.js";
310
379
  export { csv, csvRows } from "./report/csv.js";
311
380
  // CertifiedRead — the DECLARED write-path read surface (survival Constraint 5;
312
381
  // `certified_read.md`). A directive declares profiled reads via `.readsCertified({ name:
package/src/manifest.ts CHANGED
@@ -113,10 +113,6 @@ export interface CanonicalDirective {
113
113
  /** The target aggregate id. */
114
114
  readonly target: string;
115
115
  readonly marker: WireRefMode;
116
- /** The required capability — defaults to the directive id. */
117
- readonly requiresCapability: string;
118
- /** Whether the directive declares a `scopeFrom` (presence only, not behaviour). */
119
- readonly scopeFrom: boolean;
120
116
  /** The payload's top-level object shape as SORTED `[name, type]` pairs. */
121
117
  readonly payload: CanonicalPayloadField[];
122
118
  /**
@@ -1046,8 +1042,6 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
1046
1042
  id: d.id,
1047
1043
  target: d.aggregateId,
1048
1044
  marker: encodeRefMode(d.marker),
1049
- requiresCapability: d.requiresCapability ?? d.id,
1050
- scopeFrom: d.scopeFrom !== undefined,
1051
1045
  payload: canonicalPayload(d.payloadSchema),
1052
1046
  payloadJsonSchema: canonicalPayloadJsonSchema(d.payloadSchema),
1053
1047
  // Omit-when-empty: a directive declaring NO boundary contributes neither key,
package/src/ops.ts CHANGED
@@ -37,7 +37,7 @@ export interface FieldOp {
37
37
  */
38
38
  readonly marker?: PlannedEventMarker;
39
39
  readonly field: string;
40
- readonly kind: "set" | "addToSet" | "setEntry";
40
+ readonly kind: "set" | "addToSet" | "setEntry" | "increment" | "decrement" | "removeFromSet";
41
41
  readonly value?: unknown;
42
42
  readonly items?: readonly string[];
43
43
  readonly entryKey?: string;
@@ -147,6 +147,55 @@ export function addToSet<
147
147
  return { ...addressOf(agg), field, kind: "addToSet", items };
148
148
  }
149
149
 
150
+ /**
151
+ * `increment(handle, "field", amount)` — add to a PN-COUNTER field (`t.counter()`). The ONLY way to
152
+ * advance a counter (set() would clobber the convergent CRDT state). Concurrent increments on different
153
+ * replicas all land (the merge sums the per-replica totals), so nobody's count is lost. `amount` is a
154
+ * non-negative integer; the field must be `kind:"counter"`. `decrement` is the symmetric subtract.
155
+ */
156
+ function counterDelta<A extends Target, K extends CounterKey<A>>(
157
+ agg: A,
158
+ field: K,
159
+ amount: number,
160
+ kind: "increment" | "decrement",
161
+ ): FieldOp {
162
+ if (!Number.isInteger(amount) || amount < 0) {
163
+ throw new Error(`${kind}('${String(field)}', ${amount}): amount must be a non-negative integer.`);
164
+ }
165
+ return { ...addressOf(agg), field: field as string, kind, value: amount };
166
+ }
167
+ /** A counter field key — `kind:"counter"` only (an `int` is NOT a counter; that would not converge). */
168
+ type CounterKey<A> = {
169
+ [P in keyof Fields<A>]: Fields<A>[P] extends { kind: "counter" } ? P : never;
170
+ }[keyof Fields<A>] &
171
+ string;
172
+
173
+ export function increment<A extends Target, K extends CounterKey<A>>(agg: A, field: K, amount = 1): FieldOp {
174
+ return counterDelta(agg, field, amount, "increment");
175
+ }
176
+ /** `decrement(handle, "field", amount)` — subtract from a PN-counter field. See {@link increment}. */
177
+ export function decrement<A extends Target, K extends CounterKey<A>>(agg: A, field: K, amount = 1): FieldOp {
178
+ return counterDelta(agg, field, amount, "decrement");
179
+ }
180
+
181
+ /**
182
+ * `removeFromSet(handle, "field", items)` — tombstone items in a REMOVE-WINS set (`t.set(...).merge(
183
+ * RemoveWins)`). The remove permanently WINS over any concurrent or later add of the same element
184
+ * (2-phase-set semantics), so a concurrent add+remove converges to "removed". The additive partner is
185
+ * {@link addToSet}; on a RemoveWins field both are encoded to the kernel's 2-phase ops. (On a plain
186
+ * AddWins set there is no remove — that's the whole point of AddWins — so this targets set fields and
187
+ * the encoder refuses a non-RemoveWins set at compile output.)
188
+ */
189
+ export function removeFromSet<
190
+ A extends Target,
191
+ K extends {
192
+ [P in keyof Fields<A>]: FieldValue<Fields<A>[P]> extends string[] ? P : never;
193
+ }[keyof Fields<A>] &
194
+ string,
195
+ >(agg: A, field: K, items: readonly string[]): FieldOp {
196
+ return { ...addressOf(agg), field, kind: "removeFromSet", items };
197
+ }
198
+
150
199
  /** Set one entry of a map field. The map's value type is checked. */
151
200
  export function setEntry<
152
201
  A extends Target,