@githolon/dsl 0.17.0 → 0.18.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.
@@ -67,6 +67,7 @@ export const GOVERNANCE_REVISION = "2026-06-12-capability-bindings"; // hash-bea
67
67
  import { z } from "zod";
68
68
  import { aggregate, instance } from "../aggregate.js";
69
69
  import { create, ensure } from "../authoring.js";
70
+ import { birth } from "../births.js";
70
71
  import { directive } from "../directive.js";
71
72
  import { Lww } from "../drivers.js";
72
73
  import { t } from "../fields.js";
@@ -262,6 +263,18 @@ export const CloudWorkspace = aggregate("CloudWorkspace", {
262
263
  * the genesis directives HERE is law naming law (legitimate); the prohibition binds only the host.
263
264
  */
264
265
  genesisChain: t.json().merge(Lww).optional(),
266
+ /**
267
+ * THE LINEAGE-AUTHORIZATION BINDING (proof-friendly — architecture/sovereign_adapter.md "birth is a kernel
268
+ * offer-effect"). The child's genesis SELF-ATTESTS who authorized it: a JSON commitment
269
+ * `{parent, principal, authorizedAt}` — the law-available lineage anchor — that the child's OWN chain
270
+ * carries, so a verifier of the child alone sees its claimed parentage, cross-checkable against the
271
+ * parent's chain (which holds the child's CloudWorkspace + the gated `birthChild` that minted it). It is
272
+ * the clean public-input shape a future recursive-proof circuit (Nova / zkWASM) would expose; the
273
+ * kernel-structural commitments (the parent chain head + the `birthChild` intent hash + the law hash) join
274
+ * it when the KERNEL produces the genesis (B's later slice — until then those ride as `""`). `t.string`
275
+ * (JSON), never `t.json`: it rides verbatim as a scalar (the kernel `Value` has no JSON variant — the same
276
+ * reasoning as the auth keys). Absent on root (the base of the world tree, born under nothing). */
277
+ bornUnder: t.string().merge(Lww).optional(),
265
278
  });
266
279
 
267
280
  // AdminGrant / CreationGrant aggregates: DELETED (full-native Zanzibar). Admin/creation authority is
@@ -431,17 +444,63 @@ function planBirthRecord(
431
444
  export function buildGenesisChain(p: {
432
445
  name: string;
433
446
  principal: string;
447
+ parent: string;
434
448
  tier: (typeof WORKSPACE_TIERS)[number];
435
449
  foundingAdminHash: string;
436
450
  bornAt: string;
437
451
  requestedAt: number;
452
+ /** THE ORDER DECLARES THE LAW (G3 — no inheritance, no `_parentLawHash`): the content hashes of the
453
+ * bootstrap framework controller + the governance law package the child installs IN-CHAIN as its first
454
+ * intents. Frozen facts on the birth order (the parent captured them at `birthChild` time). When present,
455
+ * the recipe PREPENDS `bootstrap/installDomain(framework)` + `nomos/installDomain(law)` — the two-layer
456
+ * install the genesis bootstrap does — so the child installs ITS OWN law as intent 0 and self-validates
457
+ * from there. Absent ⇒ the recipe resolves an already-installed law by shape (pre-G3 / test lanes). */
458
+ frameworkHash?: string;
459
+ lawHash?: string;
460
+ /**
461
+ * THE WARRANT-FLIP (VA-1, architecture/identity_keys.md). When TRUE — set iff the genesis recipe
462
+ * declares an AuthProvider (an IdP vouches for this subtree's principals) — `seedGenesis` records a
463
+ * REAL `workspaceId` (the workspace name as the unique self-id), so the chain is born WARRANTED:
464
+ * `verify_captured_principal` enforces signed authorship on all lanes (author / receive / verify_chain).
465
+ * FALSE (the default, the legacy lane) keeps `workspaceId: ""` so the workspace stays unwarranted
466
+ * (host-stamped authorship, like root) — pre-VA / non-AuthProvider births replay byte-identically.
467
+ * NO flag-day: the era is per-workspace, decided at genesis by whether the recipe carries an AuthProvider.
468
+ */
469
+ requiresWarrant?: boolean;
438
470
  }): string {
439
471
  const founder = "user:" + p.principal;
472
+ // THE ORDER-DECLARED LAW INSTALL (G3): the child installs the EXACT law the order names — bytes are NOT in
473
+ // the recipe (the kernel resolves them by hash from its own custody when it folds the birth, never the
474
+ // host). `domainHash` names the law; `domainPackageB64` stays empty here (the kernel supplies bytes-by-hash
475
+ // for the in-chain install). Two layers, matching the genesis bootstrap: bootstrap controller, then law.
476
+ const installSteps =
477
+ p.frameworkHash && p.lawHash
478
+ ? [
479
+ { domain: "bootstrap", directiveId: "installDomain", actor: founder, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
480
+ { domain: "nomos", directiveId: "installDomain", actor: founder, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
481
+ ]
482
+ : [];
483
+ // THE LINEAGE-AUTHORIZATION BINDING (proof-friendly): the child's birthCloud records who authorized it, so
484
+ // the child's OWN chain self-attests its parentage — cross-checkable against the parent's chain. The
485
+ // law-available anchor now; the kernel-structural commitments (parent chain head + birthChild intent hash
486
+ // + law hash) join as `""` placeholders until the kernel produces the genesis (B's later slice). The
487
+ // ordering of keys is fixed so the commitment is a stable, content-addressed shape (proof public inputs).
488
+ const bornUnder = JSON.stringify({
489
+ parent: p.parent,
490
+ principal: p.principal,
491
+ authorizedAt: p.requestedAt,
492
+ lawHash: "",
493
+ authorizationHash: "",
494
+ parentHead: "",
495
+ });
440
496
  return JSON.stringify([
441
- { domain: "workspaces", directiveId: "seedGenesis", actor: founder, payload: { principal: founder, workspaceId: "", bornAt: p.bornAt } },
497
+ ...installSteps,
498
+ // THE WARRANT-FLIP (VA-1): a REAL self-id iff the recipe declares an AuthProvider (born warranted ⇒
499
+ // signed authorship enforced on every lane); "" keeps the legacy host-stamped lane (byte-identical).
500
+ { domain: "workspaces", directiveId: "seedGenesis", actor: founder, payload: { principal: founder, workspaceId: p.requiresWarrant ? p.name : "", bornAt: p.bornAt } },
442
501
  { domain: "workspaces", directiveId: "grantAdmin", actor: founder, payload: { keyHash: p.foundingAdminHash, label: `founding admin (born for ${p.principal})`, grantedBy: p.principal, grantedAt: p.requestedAt } },
443
502
  // No `role` — it was a vestigial node-class label (collapsed with `kind`); birthCloud ignores it now.
444
- { domain: "workspaces", directiveId: "birthCloud", actor: "", payload: { principal: p.principal, name: p.name, tier: p.tier, bornAt: p.requestedAt } },
503
+ { domain: "workspaces", directiveId: "birthCloud", actor: "", payload: { principal: p.principal, name: p.name, tier: p.tier, bornAt: p.requestedAt, bornUnder } },
445
504
  ]);
446
505
  }
447
506
 
@@ -498,10 +557,14 @@ export const birthCloud = directive("birthCloud")
498
557
  role: z.enum(WORKSPACE_ROLES).optional(),
499
558
  tier: z.enum(WORKSPACE_TIERS).default("edge"),
500
559
  bornAt: epochMs,
560
+ /** THE LINEAGE-AUTHORIZATION BINDING (proof-friendly): the JSON commitment to who authorized this
561
+ * birth, recorded onto the child's self-record so its OWN chain self-attests its parentage. Absent on
562
+ * root (born under nothing). Authored by {@link buildGenesisChain} for a child birth. */
563
+ bornUnder: z.string().optional(),
501
564
  }),
502
565
  )
503
566
  .plan((p) => {
504
- create(CloudWorkspace)
567
+ const rec = create(CloudWorkspace)
505
568
  .set("name", p.name)
506
569
  .set("parent", "") // the root of the world tree
507
570
  .set("principal", p.principal)
@@ -511,6 +574,8 @@ export const birthCloud = directive("birthCloud")
511
574
  .set("createdBy", p.principal)
512
575
  .set("createdAt", p.bornAt)
513
576
  .set("provisionedAt", p.bornAt);
577
+ // The child's chain self-attests its lineage authorization (the proof-friendly binding); root omits it.
578
+ if (p.bornUnder) rec.set("bornUnder", p.bornUnder);
514
579
  // The self-identity singleton at the CONSTANT id (ungated — birthCloud is not admin-mutating), so the
515
580
  // gate's self-record exists before any admin write. ensure() = deterministic-id sibling birth. `name` is
516
581
  // the born marker; `role` is no longer written (vestigial node-class — see WorkspaceSelf).
@@ -609,18 +674,18 @@ export const retireWorkspace = directive("retireWorkspace")
609
674
  /**
610
675
  * reclaimWorkspace — THE LAWFUL CUSTODY DISCARD. Where {@link retireWorkspace}
611
676
  * withdraws the law (a status, the bytes untouched), reclaim is the law AUTHORIZING
612
- * the host to drop the custody: the worker bailiff, on seeing THIS fact folded,
613
- * `ARTIFACTS.delete`s the workspace's git ledger and `deleteAll`s its DO storage.
677
+ * the bailiff to drop the custody: on seeing THIS fact folded, the bailiff drops the
678
+ * workspace's git ledger + DO storage (operator-gated soft-mark while pre-release).
614
679
  *
615
- * THE TWO-PHASE RULE (enforced worker-side, since a plan cannot read prior state):
616
- * only a `retired` workspace may be reclaimed — retire withdraws, reclaim discards.
680
+ * THE TWO-PHASE RULE is LAW (see {@link reclaimRequiresRetired}): only a `retired`
681
+ * workspace may be reclaimed — the invariant reads the pre-apply status at the one gate.
617
682
  *
618
- * THE FIXED POINT: this is authored for CHILDREN only. The worker refuses to reclaim
619
- * ROOT — the genesis workspace is the seat of the law that authorizes reclaim, so the
620
- * law can reap everything it governs EXCEPT itself. Dropping root is the one act Nomos
621
- * cannot perform in-law; the custodian does it through Cloudflare directly. The
622
- * custody-reclaimed fact (this status flip) is append-only and outlives the bytes:
623
- * Nomos never erases the FACT that a workspace existed and was reclaimed.
683
+ * THE FIXED POINT: this is authored for CHILDREN only. The law refuses to reclaim ROOT —
684
+ * the genesis workspace is the seat of the law that authorizes reclaim, so the law can
685
+ * reap everything it governs EXCEPT itself. Dropping root is the one act Nomos cannot
686
+ * perform in-law; the custodian does it through Cloudflare directly. The custody-reclaimed
687
+ * fact (this status flip) is append-only and outlives the bytes: Nomos never erases the
688
+ * FACT that a workspace existed and was reclaimed.
624
689
  */
625
690
  export const reclaimWorkspace = directive("reclaimWorkspace")
626
691
  .mutates(CloudWorkspace)
@@ -684,6 +749,17 @@ export const birthChild = directive("birthChild")
684
749
  /** GENESIS birth time as an ISO string (caller-stamped — the timestamp doctrine). Arms the warrant
685
750
  * in the law-authored genesis chain; required for (and only used by) a genesis birth. */
686
751
  bornAt: z.string().optional(),
752
+ /** THE ORDER DECLARES THE LAW (G3 — no inheritance): the content hashes of the bootstrap framework
753
+ * controller + the governance law package the child installs IN-CHAIN as its genesis intent 0. Frozen
754
+ * facts the requester presents (the parent's current law, captured at birth time). When present, the
755
+ * genesis recipe prepends the two-layer installDomain; the kernel resolves the BYTES by hash from its
756
+ * own custody (never the host). Absent ⇒ the recipe resolves an already-installed law by shape. */
757
+ frameworkHash: sha256Hex.optional(),
758
+ lawHash: sha256Hex.optional(),
759
+ /** THE WARRANT-FLIP (VA-1): TRUE iff the child's genesis recipe declares an AuthProvider — the child
760
+ * is born WARRANTED (signed authorship enforced on every lane). Absent/false ⇒ the legacy
761
+ * host-stamped lane (byte-identical to a pre-VA birth). Only meaningful with a genesis. */
762
+ requiresWarrant: z.boolean().optional(),
687
763
  })
688
764
  // A genesis birth (a platform) must carry a principal AND a bornAt — it owns a subtree, a lineage
689
765
  // anchor, and a law-authored genesis chain whose warrant is stamped at bornAt.
@@ -694,13 +770,23 @@ export const birthChild = directive("birthChild")
694
770
  )
695
771
  .plan((p) => {
696
772
  const rec = planBirthRecord(p, p.foundingAdminHash ? { foundingAdminHash: p.foundingAdminHash, pool: p.pool } : undefined);
697
- // LAW AUTHORS THE GENESIS RECIPE (P1): a genesis birth carries its child's seedGenesis→grantAdmin→
698
- // birthCloud chain as data; the provider replays it opaquely. (refine guarantees principal + bornAt here.)
773
+ // LAW AUTHORS THE GENESIS RECIPE (P1): a genesis birth carries its child's [installDomain×2 →]
774
+ // seedGenesis→grantAdmin→birthCloud chain. (refine guarantees principal + bornAt here.)
699
775
  if (p.foundingAdminHash) {
700
- rec.set("genesisChain", buildGenesisChain({ name: p.name, principal: p.principal, tier: p.tier, foundingAdminHash: p.foundingAdminHash, bornAt: p.bornAt as string, requestedAt: p.requestedAt }));
776
+ 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 ? { requiresWarrant: true } : {}) });
777
+ // THE GENESIS RECIPE IS ALSO A LINEAGE/AUDIT FACT on the parent's chain (the parent records what it
778
+ // ORDERED — law naming law, legitimate). The kernel does NOT read this field to detect the birth.
779
+ rec.set("genesisChain", genesisChain);
780
+ // THE DECLARED BIRTH EFFECT (G3 — the unix model): the recipe rides the committed intent as a declared
781
+ // output (`captured_ports.births`). The kernel — reading the `.births` DECLARATION off the law manifest
782
+ // (the `nomosBirthGate` era key, NEVER this CloudWorkspace aggregate) — takes the recipe off the intent
783
+ // after admit and spawns + self-validates the child through the child's OWN gate. So birth is an
784
+ // EXPLICIT, declared consequence of the offer, not a side-effect inferred by scanning domain state.
785
+ birth({ workspace: p.name, genesisChain: JSON.parse(genesisChain) });
701
786
  }
702
787
  return [];
703
788
  })
789
+ .births() // BIRTH IS A DECLARED KERNEL PRIMITIVE (G3): emits the `nomosBirthGate` era key; the kernel folds the carried recipe
704
790
  .requires("creator"); // THE ONE birth authority — a #creator may birth a leaf OR a platform (tier collapsed)
705
791
 
706
792
  /** workspacesByStatus — the birth provider drains `requested` CloudWorkspaces that CARRY A foundingAdminHash
@@ -745,13 +831,31 @@ export const AuthProvider = aggregate("AuthProvider", {
745
831
  scope: t.string().merge(Lww),
746
832
  /** The token issuer (the `iss` claim tokens must carry). */
747
833
  issuer: t.string().merge(Lww),
748
- /** The provider's public JWKS endpoint (asymmetric verification keys). */
834
+ /** The provider's public JWKS endpoint (asymmetric verification keys). The KERNEL never fetches — it's a
835
+ * reference for the rotation capability that re-fetches + re-sets {@link keys}. */
749
836
  jwksUrl: t.string().merge(Lww),
837
+ /** THE VERIFYING KEYS — the JWKS JSON (`{keys:[{kid,kty,alg,crv,x,y,n,e,...}]}`) as an OPAQUE STRING the
838
+ * KERNEL parses + verifies a presented token against (sovereign-auth arc — see
839
+ * architecture/sovereign_adapter.md). `t.string` (NOT `t.json`): the kernel `Value` has no JSON variant,
840
+ * so the raw JWKS text must ride verbatim as a scalar — the kernel does `serde_json` on it, no projection
841
+ * parse, no double-encoding. LAW-STATE: set by {@link setAuthProvider} (gated), refreshed by the
842
+ * JWKS-fetch rotation capability (the host FETCHES — impure — but the law OWNS the keys + the kernel does
843
+ * the verification). Absent ⇒ no in-kernel verify for this workspace (shell-issuer fallback, transitional). */
844
+ keys: t.string().merge(Lww).optional(),
750
845
  /** OPTIONAL expected `aud` claim. When the platform sets it, a token's `aud` MUST carry this value
751
846
  * (defense-in-depth beyond the issuer match — e.g. a Firebase project id, an Auth0 API audience).
752
847
  * Absent ⇒ issuer match alone. This is LAW-STATE the PLATFORM owns; the host merely enforces it as a
753
848
  * capability (it never chooses the audience). */
754
849
  audience: t.string().merge(Lww),
850
+ /** OPTIONAL — the JWT claim that carries the PRINCIPAL (default `"sub"` when absent). Azure AD's stable
851
+ * user id is `oid` (its `sub` is pairwise-per-app, useless as identity); some setups key on a custom
852
+ * claim. The KERNEL extracts THIS claim as the principal — never hardcoded to `sub`. */
853
+ principalClaim: t.string().merge(Lww).optional(),
854
+ /** OPTIONAL — the PINNED CA ROOT (base64 DER) for TRUSTLESS key rotation. When set, {@link rotateKeys}
855
+ * accepts a new JWKS IFF each key's `x5c` chain validates to THIS root IN THE KERNEL (RFC 5280, the IdP's
856
+ * CA vouches) and each key matches its leaf cert — no host trust, no admin. Pinned once via the
857
+ * admin-gated {@link setAuthProvider}. Absent ⇒ no trustless rotation (rotate via setAuthProvider). */
858
+ rootCert: t.string().merge(Lww).optional(),
755
859
  /** Human label for audit ("acme supabase", ...). */
756
860
  label: t.string().merge(Lww),
757
861
  /** Who set it (attribution). */
@@ -773,6 +877,13 @@ export const setAuthProvider = directive("setAuthProvider")
773
877
  issuer: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
774
878
  jwksUrl: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
775
879
  audience: z.string().min(1).optional(),
880
+ /** The JWKS JSON (the public verifying keys) the KERNEL checks tokens against. Optional so the
881
+ * rotation capability can re-set just the keys; absent keeps the prior keys (Lww). */
882
+ keys: z.string().min(1).optional(),
883
+ /** OPTIONAL — the claim carrying the principal (default "sub"; e.g. "oid" for Azure AD). */
884
+ principalClaim: z.string().min(1).optional(),
885
+ /** OPTIONAL — the pinned CA root (base64 DER) enabling trustless {@link rotateKeys}. */
886
+ rootCert: z.string().min(1).optional(),
776
887
  label: z.string().min(1),
777
888
  setBy: z.string().min(1),
778
889
  setAt: epochMs,
@@ -785,6 +896,9 @@ export const setAuthProvider = directive("setAuthProvider")
785
896
  set(ap, "issuer", p.issuer),
786
897
  set(ap, "jwksUrl", p.jwksUrl),
787
898
  ...(p.audience ? [set(ap, "audience", p.audience)] : []),
899
+ ...(p.keys ? [set(ap, "keys", p.keys)] : []),
900
+ ...(p.principalClaim ? [set(ap, "principalClaim", p.principalClaim)] : []),
901
+ ...(p.rootCert ? [set(ap, "rootCert", p.rootCert)] : []),
788
902
  set(ap, "label", p.label),
789
903
  set(ap, "setBy", p.setBy),
790
904
  set(ap, "setAt", p.setAt),
@@ -792,6 +906,30 @@ export const setAuthProvider = directive("setAuthProvider")
792
906
  })
793
907
  .requires("admin");
794
908
 
909
+ /**
910
+ * rotateKeys — TRUSTLESS key rotation. Replace {@link AuthProvider.keys} with a new JWKS whose every key
911
+ * carries an `x5c` cert chain. DELIBERATELY UNGATED at the relation level — the KERNEL is the authority: it
912
+ * admits this ONLY if each key's x5c chain validates (RFC 5280) to the law-pinned {@link AuthProvider.rootCert}
913
+ * and each key matches its CA-vouched leaf cert (the bind). So a bailiff (host on a cron, or anyone) may
914
+ * FETCH the IdP's rotated JWKS and offer it, but cannot forge keys — forged keys wouldn't chain to the root.
915
+ * No host trust, no admin: the IdP's CA vouches. Workspaces with no pinned root rotate via setAuthProvider.
916
+ */
917
+ export const rotateKeys = directive("rotateKeys")
918
+ .ensures(AuthProvider)
919
+ .payload(
920
+ z.object({
921
+ /** The new JWKS JSON; EACH key MUST carry an `x5c` the kernel validates to {@link AuthProvider.rootCert}. */
922
+ keys: z.string().min(1),
923
+ rotatedAt: epochMs,
924
+ }),
925
+ )
926
+ .plan((p) => {
927
+ const ap = instance(AuthProvider, "auth-provider:platform");
928
+ // The kernel's x5c admission gate authorizes this (validate_rotate_keys); the plan just stores the
929
+ // CA-vouched JWKS. The `ensures` marker upserts onto the existing AuthProvider singleton.
930
+ return [withMarker(set(ap, "keys", p.keys), "ensures"), set(ap, "setAt", p.rotatedAt)];
931
+ });
932
+
795
933
  /**
796
934
  * HostnameClaim — a CUSTOM HOSTNAME bound to a platform: `api.acme.com` serving
797
935
  * acme's subtree. A root-custody fact (the hostname is the HOST's edge resource —
@@ -1006,14 +1144,21 @@ export const setWorkspaceByteCap = directive("setWorkspaceByteCap")
1006
1144
  export const grantAdmin = directive("grantAdmin")
1007
1145
  .ensures(RelationTuple)
1008
1146
  .payload(
1009
- z.object({
1010
- keyHash: sha256Hex,
1011
- label: z.string().min(1),
1012
- grantedBy: z.string().min(1),
1013
- grantedAt: epochMs,
1014
- }),
1147
+ z
1148
+ .object({
1149
+ // `principal` (a `user:<uid>` subject) is the PRINCIPAL-CENTRIC grant — authority lives on the
1150
+ // identity, so it survives device-key ROTATION (the signer indirection's whole point) and matches
1151
+ // the subject a SIGNED author resolves to. `keyHash` (a `key:<hash>` subject) is the legacy
1152
+ // key-centric grant (the bearer-secret lane). Exactly one — mirrors grantCreation.
1153
+ keyHash: sha256Hex.optional(),
1154
+ principal: z.string().min(1).optional(),
1155
+ label: z.string().min(1),
1156
+ grantedBy: z.string().min(1),
1157
+ grantedAt: epochMs,
1158
+ })
1159
+ .refine((d) => !!d.keyHash !== !!d.principal, { message: "grantAdmin needs exactly one of keyHash (a key: subject) or principal (a user: subject)", path: ["keyHash"] }),
1015
1160
  )
1016
- .plan((p) => writeTuple({ object: SELF_ADMIN_OBJECT, relation: "admin", subject: "key:" + p.keyHash, grantedBy: p.grantedBy, grantedAt: String(p.grantedAt) }))
1161
+ .plan((p) => writeTuple({ object: SELF_ADMIN_OBJECT, relation: "admin", subject: p.principal ? "user:" + p.principal : "key:" + p.keyHash, grantedBy: p.grantedBy, grantedAt: String(p.grantedAt) }))
1017
1162
  .requires("admin"); // only an admin grants admin (pre-apply roster ⇒ no self-grant)
1018
1163
 
1019
1164
  /**
@@ -1027,13 +1172,16 @@ export const grantAdmin = directive("grantAdmin")
1027
1172
  export const revokeAdmin = directive("revokeAdmin")
1028
1173
  .ensures(RelationTuple)
1029
1174
  .payload(
1030
- z.object({
1031
- keyHash: sha256Hex,
1032
- revokedBy: z.string().min(1),
1033
- revokedAt: epochMs,
1034
- }),
1175
+ z
1176
+ .object({
1177
+ keyHash: sha256Hex.optional(),
1178
+ principal: z.string().min(1).optional(),
1179
+ revokedBy: z.string().min(1),
1180
+ revokedAt: epochMs,
1181
+ })
1182
+ .refine((d) => !!d.keyHash !== !!d.principal, { message: "revokeAdmin needs exactly one of keyHash or principal", path: ["keyHash"] }),
1035
1183
  )
1036
- .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) }))
1184
+ .plan((p) => removeTuple({ object: SELF_ADMIN_OBJECT, relation: "admin", subject: p.principal ? "user:" + p.principal : "key:" + p.keyHash, grantedBy: p.revokedBy, grantedAt: String(p.revokedAt), revokedBy: p.revokedBy, revokedAt: String(p.revokedAt) }))
1037
1185
  .requires("admin");
1038
1186
 
1039
1187
  /**
@@ -1253,3 +1401,18 @@ export const provisionReceiptGuard = workspaceInvariant("ProvisionReceiptGuard")
1253
1401
  return { reject: "provision-founding-hash-mismatch" };
1254
1402
  return { accept: true };
1255
1403
  });
1404
+
1405
+ /** reclaimWorkspace: THE RETIRED→RECLAIMED TWO-PHASE INTERLOCK, AS LAW (#41/#58). Reclaim authorizes the
1406
+ * irreversible custody discard, so only a `retired` workspace may be reclaimed — retire WITHDRAWS (a status,
1407
+ * reversible), reclaim DISCARDS. The invariant reads the pre-apply status of the record being reclaimed (same
1408
+ * shape as ProvisionReceiptGuard) and refuses unless it is exactly `retired`. Fires at the ONE wasm gate on
1409
+ * every peer — author, edge admission, AND verify_chain — so the interlock is identical on cloud + client +
1410
+ * replay; the bailiff's drop merely executes the lawful `reclaimed` fact. (Root never reaches here: the law
1411
+ * refuses reclaiming itself; this guards children.) */
1412
+ export const reclaimRequiresRetired = workspaceInvariant("ReclaimRequiresRetired")
1413
+ .on("reclaimWorkspace")
1414
+ .reads((ctx) => [refAs("ws", "CloudWorkspace", "pre:" + String((ctx.intent ?? {}).workspaceId ?? ""))])
1415
+ .assert(({ ws }) => {
1416
+ const rec = ws as { status?: string } | undefined;
1417
+ return rec?.status === "retired" ? { accept: true } : { reject: "reclaim-requires-retired" };
1418
+ });
package/src/index.ts CHANGED
@@ -94,6 +94,18 @@ export { create, existing, ensure, type AggregateRef } from "./authoring.js";
94
94
  // `nomos-compile` REFUSES law that references read() (a named compile error, never a runtime halt).
95
95
  // The captured-reads wave (task #58) wires the capability in and lifts the refusal.
96
96
  export { read } from "./read.js";
97
+ // THE BIRTH EFFECT (G3 — birth is a declared kernel primitive): a `.births()` directive's plan records the
98
+ // child's genesis recipe via `birth(recipe)` (reuse `buildGenesisChain`); it rides the committed intent as a
99
+ // declared output, and the kernel — reading the `.births` declaration off the law manifest (the
100
+ // `nomosBirthGate` era key, never domain state) — spawns + self-validates the child after the offer admits.
101
+ export { birth, type BirthRecipe } from "./births.js";
102
+ // THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain): a home/child law exports
103
+ // `requireParentAttestedBirth` to declare it requires a parent-attested-birth cert chain (the
104
+ // `nomosParentAttestedBirth` era key); the kernel verify doors then verify the travelling chain to K_root.
105
+ export {
106
+ requireParentAttestedBirth,
107
+ type ParentAttestedBirthMarker,
108
+ } from "./births.js";
97
109
  export {
98
110
  query,
99
111
  hasManyIndexes,
@@ -247,6 +259,28 @@ export {
247
259
  signerObject,
248
260
  SIGNER_RELATION,
249
261
  enrollSigner,
262
+ // VA-core — SignerEpoch + self-bind enroll/revoke + the strict-increment & verified-enroll gates.
263
+ SignerEpoch,
264
+ SIGNER_EPOCH_ID,
265
+ signerEpochCurrent,
266
+ advanceSignerEpoch,
267
+ signerEpochStrictlyIncreases,
268
+ enrollSignerSelf,
269
+ enrollSignerSelfRequiresVerified,
270
+ revokeSigner,
271
+ revokeSignerEpochStrictlyIncreases,
272
+ signerTuplesByObject,
273
+ signerTupleInstanceId,
274
+ // THE ENROLL-ONCE DELEGATION (the peer-sovereign root-of-trust) — the per-user tenant-signed cert the
275
+ // kernel reads to assemble U's home cert chain at birth.
276
+ SignerDelegation,
277
+ SIGNER_DELEGATION_PREFIX,
278
+ signerDelegationInstanceId,
279
+ enrollDelegation,
280
+ signerDelegationByUser,
281
+ type SignerDelegationHandle,
282
+ type SignerEpochHandle,
283
+ type SignerAuthority,
250
284
  } from "./framework/signing_identity.js";
251
285
  // THE HOME HOLON (home_holons_and_cross_custody.md §4) — the identity-role law: the shared-with-me access
252
286
  // index + the owner lane to maintain it (the key directory is the `signer` relation above).
@@ -262,6 +296,14 @@ export {
262
296
  seedHomeOwner,
263
297
  writeHomeOwner,
264
298
  homeNameBindsOwner,
299
+ homeNameOf,
300
+ birthHome,
301
+ // THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain).
302
+ BirthAttestation,
303
+ BIRTH_ATTESTATION_ID,
304
+ seedBirthAttestation,
305
+ birthAttestation,
306
+ parentAttestedBirth,
265
307
  } from "./framework/home.js";
266
308
  // THE GENESIS WARRANT (identity_keys.md step 0) — the in-history birth fact that lets
267
309
  // the gate's genesis clause admit a founding control-plane grant on a virgin chain.
package/src/usd.ts CHANGED
@@ -285,6 +285,33 @@ export interface UsdLayer {
285
285
  }
286
286
  >;
287
287
  };
288
+ /**
289
+ * THE BIRTH GATE DECLARATION (G3 — birth is a declared kernel primitive, the unix model). Present IFF ≥1
290
+ * directive declares `.births()`: the SORTED set of their ids. A directive named here BIRTHS a child
291
+ * workspace as the defined CONSEQUENCE of its offer — after the offer admits, the kernel reads THIS
292
+ * declaration (the SAME way it reads `nomosReadGate`/`nomosInvariantGate` — never a domain aggregate),
293
+ * takes the genesis recipe the intent carried (its `captured_ports.births`), spawns the child's custody,
294
+ * and self-validates the child's whole genesis through the child's OWN gate (the `materialize_genesis`
295
+ * keystone). The kernel reads NO domain schema; it reads its own manifest declaration + the carried
296
+ * recipe. OMITTED for law with no birth directive (the era rule — byte-identical hash; every pre-feature
297
+ * chain replays green, the birth effect inert). SORTED keys for byte-determinism.
298
+ */
299
+ readonly nomosBirthGate?: {
300
+ readonly v: 1;
301
+ /** The directive ids declared `.births()` — SORTED. */
302
+ readonly directives: string[];
303
+ };
304
+ /**
305
+ * THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain). Present IFF the law declares
306
+ * `requireParentAttestedBirth`: a warranted workspace running this law MUST travel with a SIGNED cert
307
+ * chain proving its founder was VERIFIED by its parent at birth, rooting at the pinned `K_root`. The
308
+ * kernel's verify doors (author / receive / verify_chain) verify the travelling chain OFFLINE — no
309
+ * cross-ws read. OMITTED for law that doesn't declare it (the era rule — byte-identical hash; every
310
+ * pre-feature + legacy/non-warranted chain replays green, the check inert).
311
+ */
312
+ readonly nomosParentAttestedBirth?: {
313
+ readonly v: 1;
314
+ };
288
315
  /**
289
316
  * STABLE IDENTIFIERS (#58 — names are labels, identity is minted). The layer's
290
317
  * domain/aggregate/field sids + label lineages, carried verbatim from the canonical
@@ -721,6 +748,21 @@ export function emitUsd(
721
748
  }
722
749
  const nomosAttestationGate =
723
750
  Object.keys(attestationReq).length > 0 ? { v: 1 as const, directives: attestationReq } : undefined;
751
+ // THE BIRTH GATE (G3 — birth is a declared kernel primitive): the SORTED set of directive ids
752
+ // declaring `.births()`. The kernel reads this off the manifest (never a domain aggregate) to know an
753
+ // admitted directive births, then folds the recipe the intent carried. Omit-when-empty (era rule —
754
+ // a birth-free layer is byte-identical apart from the always-on stableIds key).
755
+ const birthDirectives = [...l.module.directives]
756
+ .filter((d) => (d as { birthsChild?: boolean }).birthsChild === true)
757
+ .map((d) => d.id)
758
+ .sort();
759
+ const nomosBirthGate =
760
+ birthDirectives.length > 0 ? { v: 1 as const, directives: birthDirectives } : undefined;
761
+ // THE PEER-SOVEREIGN ROOT-OF-TRUST: the home/child law declared it requires a parent-attested-birth
762
+ // cert chain (the `requireParentAttestedBirth` sentinel, discovered by composeDomainModule). Emit the
763
+ // era key iff true; omit otherwise (a law that doesn't require it is byte-identical — the era rule).
764
+ const nomosParentAttestedBirth =
765
+ (l.module as { parentAttestedBirth?: boolean }).parentAttestedBirth === true ? { v: 1 as const } : undefined;
724
766
  // THE CAPTURED-READ GATE DECLARATION (#58): collected from the canonical
725
767
  // manifest's per-directive `capturedReads` (omit-when-empty — read-free law's
726
768
  // USD is byte-identical apart from the always-on stableIds key).
@@ -850,6 +892,8 @@ export function emitUsd(
850
892
  ...(nomosRelations !== undefined ? { nomosRelations } : {}),
851
893
  ...(nomosRelationGate !== undefined ? { nomosRelationGate } : {}),
852
894
  ...(nomosAttestationGate !== undefined ? { nomosAttestationGate } : {}),
895
+ ...(nomosBirthGate !== undefined ? { nomosBirthGate } : {}),
896
+ ...(nomosParentAttestedBirth !== undefined ? { nomosParentAttestedBirth } : {}),
853
897
  ...(Object.keys(visAggregates).length > 0
854
898
  ? { nomosReadVisibility: { v: 1 as const, aggregates: visAggregates } }
855
899
  : {}),
package/src/usd_layers.ts CHANGED
@@ -177,6 +177,12 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
177
177
  ...(layer.nomosAttestationGate !== undefined
178
178
  ? [`${indent}custom string nomos:attestationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosAttestationGate)))}`]
179
179
  : []),
180
+ ...(layer.nomosBirthGate !== undefined
181
+ ? [`${indent}custom string nomos:birthGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosBirthGate)))}`]
182
+ : []),
183
+ ...(layer.nomosParentAttestedBirth !== undefined
184
+ ? [`${indent}custom string nomos:parentAttestedBirthHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosParentAttestedBirth)))}`]
185
+ : []),
180
186
  ];
181
187
  }
182
188
 
@@ -765,6 +771,23 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
765
771
  attestationGate = { v: 1, directives: rec };
766
772
  }
767
773
  }
774
+ // THE BIRTH GATE (G3): the SORTED union of every layer's `.births()` directive ids. Omit-when-empty
775
+ // (era rule). The set is the contract; order is incidental (sorted for byte-determinism).
776
+ let birthGate: UsdLayer["nomosBirthGate"];
777
+ {
778
+ const b = new Set<string>();
779
+ for (const l of stack) {
780
+ if (l.nomosBirthGate === undefined) continue;
781
+ for (const id of l.nomosBirthGate.directives) b.add(id);
782
+ }
783
+ if (b.size > 0) birthGate = { v: 1, directives: [...b].sort() };
784
+ }
785
+ // THE PEER-SOVEREIGN ROOT-OF-TRUST: the era key is present iff ANY layer in the stack declares it
786
+ // (a boolean union — once required, required). Omit-when-absent (era rule).
787
+ let parentAttestedBirth: UsdLayer["nomosParentAttestedBirth"];
788
+ if (stack.some((l) => l.nomosParentAttestedBirth !== undefined)) {
789
+ parentAttestedBirth = { v: 1 };
790
+ }
768
791
  return {
769
792
  path,
770
793
  prims: [...prims.keys()].sort().map((p) => prims.get(p)!),
@@ -781,6 +804,8 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
781
804
  ...(relations !== undefined ? { nomosRelations: relations } : {}),
782
805
  ...(relationGate !== undefined ? { nomosRelationGate: relationGate } : {}),
783
806
  ...(attestationGate !== undefined ? { nomosAttestationGate: attestationGate } : {}),
807
+ ...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
808
+ ...(parentAttestedBirth !== undefined ? { nomosParentAttestedBirth: parentAttestedBirth } : {}),
784
809
  };
785
810
  });
786
811
 
@@ -908,6 +933,8 @@ export function parseUsdaDocument(text: string): UsdDocument {
908
933
  const relationsByDomain = new Map<string, UsdLayer["nomosRelations"]>();
909
934
  const relationGateByDomain = new Map<string, UsdLayer["nomosRelationGate"]>();
910
935
  const attestationGateByDomain = new Map<string, UsdLayer["nomosAttestationGate"]>();
936
+ const birthGateByDomain = new Map<string, UsdLayer["nomosBirthGate"]>();
937
+ const parentAttestedBirthByDomain = new Map<string, UsdLayer["nomosParentAttestedBirth"]>();
911
938
  for (const p of prims) {
912
939
  if (p.attrs.get("nomos:kind") === "domain") {
913
940
  const domain = attrString(p, "nomos:domain");
@@ -959,6 +986,16 @@ export function parseUsdaDocument(text: string): UsdDocument {
959
986
  if (typeof attGateHex === "string") {
960
987
  attestationGateByDomain.set(domain, JSON.parse(unhexUtf8(attGateHex)) as UsdLayer["nomosAttestationGate"]);
961
988
  }
989
+ // THE BIRTH GATE (G3): the hex-carried `.births()` directive set.
990
+ const birthGateHex = p.attrs.get("nomos:birthGateHex");
991
+ if (typeof birthGateHex === "string") {
992
+ birthGateByDomain.set(domain, JSON.parse(unhexUtf8(birthGateHex)) as UsdLayer["nomosBirthGate"]);
993
+ }
994
+ // THE PEER-SOVEREIGN ROOT-OF-TRUST: the hex-carried parent-attested-birth era marker.
995
+ const pabHex = p.attrs.get("nomos:parentAttestedBirthHex");
996
+ if (typeof pabHex === "string") {
997
+ parentAttestedBirthByDomain.set(domain, JSON.parse(unhexUtf8(pabHex)) as UsdLayer["nomosParentAttestedBirth"]);
998
+ }
962
999
  }
963
1000
  }
964
1001
 
@@ -1056,6 +1093,8 @@ export function parseUsdaDocument(text: string): UsdDocument {
1056
1093
  const rel = relationsByDomain.get(domain);
1057
1094
  const relGate = relationGateByDomain.get(domain);
1058
1095
  const attGate = attestationGateByDomain.get(domain);
1096
+ const birthGate = birthGateByDomain.get(domain);
1097
+ const parentAttestedBirth = parentAttestedBirthByDomain.get(domain);
1059
1098
  return {
1060
1099
  path: `/Nomos/${domain}`,
1061
1100
  prims: [...b.prims].sort(byPath),
@@ -1072,6 +1111,8 @@ export function parseUsdaDocument(text: string): UsdDocument {
1072
1111
  ...(rel !== undefined ? { nomosRelations: rel } : {}),
1073
1112
  ...(relGate !== undefined ? { nomosRelationGate: relGate } : {}),
1074
1113
  ...(attGate !== undefined ? { nomosAttestationGate: attGate } : {}),
1114
+ ...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
1115
+ ...(parentAttestedBirth !== undefined ? { nomosParentAttestedBirth: parentAttestedBirth } : {}),
1075
1116
  };
1076
1117
  })
1077
1118
  .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
package/src/wire.ts CHANGED
@@ -40,6 +40,10 @@ export type WireValue =
40
40
  | { Set: string[] }
41
41
  | { Map: Record<string, WireField> }
42
42
  | { Evidence: WireEvidenceRef }
43
+ // First-class canonical JSON (RFC 8785 / JCS) — the committed form of a `t.json()` leaf. The
44
+ // value rides INLINE (`{Json: <any json>}`), not as a string-of-JSON; the kernel canonicalises
45
+ // it at the boundary (`canonical_json.rs`) and decodes the variant directly into structured JSON.
46
+ | { Json: unknown }
43
47
  // ── STRUCTURAL CRDT state values (only seen when reading RAW folded state back; the read MODEL
44
48
  // collapses them via the kernel `observed_value` to Int/Str/Set, so typed reads stay scalar) ──
45
49
  | { Counter: Record<string, { pos: number; neg: number }> }
@@ -135,6 +139,15 @@ export interface WireIntent {
135
139
  * empty so a principal-free intent is byte-identical.
136
140
  */
137
141
  endorse?: { sink: string; facet: string; reason: string | null }[];
142
+ /**
143
+ * THE BIRTH EFFECT (G3 — birth is a declared kernel primitive, the unix model): the genesis recipe(s) a
144
+ * `.births()` directive's plan produced (`birth(recipe)`). It rides the intent as a DECLARED OUTPUT — the
145
+ * kernel reads the `.births` DECLARATION off the law manifest (the `nomosBirthGate` era key, NOT domain
146
+ * state), then takes this recipe off the admitted intent and spawns + self-validates the child through the
147
+ * child's OWN gate. A verify-input WITNESS (rides the carried `captured_ports`, never the events nor the
148
+ * OID). OMITTED when empty so a birth-free intent is byte-identical to before this existed (the era rule).
149
+ */
150
+ births?: { workspace: string; genesisChain: unknown }[];
138
151
  }
139
152
 
140
153
  /**
@@ -22,6 +22,7 @@
22
22
  import { encodeDriver } from "./drivers.js";
23
23
  import { __resetAuthoring, __drainAuthoring } from "./authoring.js";
24
24
  import { __resetReads, __drainReads } from "./read.js";
25
+ import { __resetBirths, __drainBirths } from "./births.js";
25
26
  import type { AggregateHandle } from "./aggregate.js";
26
27
  import type { Field, EvidenceRefValue } from "./fields.js";
27
28
  import type { Directive, ReferentialMarker } from "./directive.js";
@@ -86,14 +87,18 @@ function encodeKernelValue(field: Field | undefined, value: unknown): WireValue
86
87
  },
87
88
  };
88
89
  }
90
+ // A `t.json()` leaf lowers to the FIRST-CLASS kernel `Value::Json` (RFC 8785 / JCS), NOT a
91
+ // `Value::Str` holding JSON-shaped text. Checked BEFORE the scalar fallthroughs so EVERY json
92
+ // value — object, array, string, number, boolean, null — commits as one consistent value kind
93
+ // (the kernel canonicalises it at the boundary). This is the new-era encoding; a package compiled
94
+ // before this change froze the old `{ Str: JSON.stringify(value) }` lump and replays unchanged.
95
+ if (field?.kind === "json") return { Json: value };
89
96
  // For scalar Set/SetEntry the kernel expects Str or Int. Maps store leaves;
90
97
  // a map's entry value is a leaf scalar.
91
98
  if (typeof value === "number" && Number.isInteger(value)) return { Int: value };
92
99
  if (typeof value === "string") return { Str: value };
93
100
  if (typeof value === "boolean") return { Str: String(value) };
94
101
  // Enum values are strings; refs are id strings — both handled above.
95
- // Anything else (object/array as a scalar) is JSON-encoded into a Str leaf.
96
- if (field?.kind === "json") return { Str: JSON.stringify(value) };
97
102
  throw new Error(
98
103
  `Cannot encode value ${JSON.stringify(value)} for field '${field?.kind ?? "?"}': ` +
99
104
  `kernel scalars are Str | Int only.`,
@@ -196,9 +201,15 @@ export function executeDirectiveToIntent<P>(
196
201
  // The plan may also READ (O(1) DSL queries via `read(...)`) — captured into the footprint below.
197
202
  __resetAuthoring();
198
203
  __resetReads();
204
+ __resetBirths();
199
205
  const returned = directive.plan(parsed, ctx);
200
206
  const planned = [...__drainAuthoring(), ...returned];
201
207
  const reads = __drainReads();
208
+ // THE BIRTH EFFECT (G3 — birth is a declared kernel primitive): a `.births()` directive's plan records
209
+ // the child's genesis recipe via `birth(recipe)`; it rides the intent as a DECLARED output (like a
210
+ // captured read), drained here. Empty for every non-birth directive — OMITTED below so a birth-free
211
+ // intent is byte-identical (the era rule).
212
+ const births = __drainBirths();
202
213
 
203
214
  // Partition the plan output: STRIKE ops route onto the intent's `strikes` channel
204
215
  // (the kernel's strikeout / revert facet, folded by parity); FIELD ops group into
@@ -302,6 +313,7 @@ export function executeDirectiveToIntent<P>(
302
313
  events,
303
314
  ...(reads.length ? { reads } : {}),
304
315
  ...(endorse.length ? { endorse } : {}),
316
+ ...(births.length ? { births } : {}),
305
317
  };
306
318
  }
307
319