@githolon/dsl 0.16.3 → 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";
@@ -96,18 +97,24 @@ export const WORKSPACE_ROLES = ["cloud-root", "platform", "tenant"] as const;
96
97
  const WORKSPACE_SELF_ID = "workspaceself-self";
97
98
  const SELF_ADMIN_OBJECT = "workspace:self";
98
99
  export const WorkspaceSelf = aggregate("WorkspaceSelf", {
100
+ // VESTIGIAL node-class label (the same smell as `kind`): NOTHING decides on its value — the kernel, host,
101
+ // and gate read it nowhere; the write-once invariants below key on `name`'s PRESENCE (the "born" marker),
102
+ // never role. No longer set on new records (birthCloud/ensureWorkspaceSelf stopped writing it). Kept only
103
+ // for era-safety (old records carry it); slated for removal with `kind` in one #58 disposition pass.
99
104
  role: t.enum(WORKSPACE_ROLES).merge(Lww),
100
105
  name: t.string().merge(Lww),
101
106
  });
102
107
  // The one-off SELF-IDENTITY backfill for an already-born workspace (root predates WorkspaceSelf). UNGATED
103
108
  // by the admin gate — it must run BEFORE the admin tuple/gate are live — but WRITE-ONCE (the invariant
104
- // below refuses a re-set once the role exists), so a race or a malicious re-set cannot flip the role and
105
- // freeze governance. The HOST authors this in the serialized genesis re-entry, winning any race.
109
+ // below refuses a re-set once the self-record exists), so a race or a malicious re-set cannot freeze
110
+ // governance. The HOST authors this in the serialized genesis re-entry, winning any race.
106
111
  export const ensureWorkspaceSelf = directive("ensureWorkspaceSelf")
107
112
  .ensures(WorkspaceSelf)
108
- .payload(z.object({ role: z.enum(WORKSPACE_ROLES), name: z.string().min(1) }))
113
+ // `role` accepted-but-ignored (vestigial — see WorkspaceSelf): callers may still pass it; the plan no
114
+ // longer sets it. `name` is the self-record's only meaningful field + the born marker.
115
+ .payload(z.object({ role: z.enum(WORKSPACE_ROLES).optional(), name: z.string().min(1) }))
109
116
  .plan((p) => {
110
- ensure(WorkspaceSelf, WORKSPACE_SELF_ID).set("role", p.role).set("name", p.name);
117
+ ensure(WorkspaceSelf, WORKSPACE_SELF_ID).set("name", p.name);
111
118
  return [];
112
119
  });
113
120
  export const workspaceSelfWriteOnce = workspaceInvariant("WorkspaceSelfWriteOnce")
@@ -118,8 +125,11 @@ export const workspaceSelfWriteOnce = workspaceInvariant("WorkspaceSelfWriteOnce
118
125
  // 2026-06-15). Bare matches — exactly the convention the sharding invariants already fire under.
119
126
  .on("ensureWorkspaceSelf")
120
127
  .reads(() => [refAs("self", "WorkspaceSelf", "pre:" + WORKSPACE_SELF_ID)])
128
+ // WRITE-ONCE on the self-record's PRESENCE via `name` (not the vestigial `role`). At genesis name + role
129
+ // were always co-written, so an existing born workspace has `name` ⇒ behaviour preserved; a fresh one has
130
+ // neither ⇒ first set admitted.
121
131
  .assert(({ self }) =>
122
- (self as { role?: string })?.role ? { reject: "workspace-self-already-set" } : { accept: true },
132
+ (self as { name?: string })?.name ? { reject: "workspace-self-already-set" } : { accept: true },
123
133
  );
124
134
 
125
135
  // ── THE GOVERNANCE RELATION SCHEMA (full-native Zanzibar — the ONE authz model) ──────────────────────
@@ -136,8 +146,10 @@ export const workspaceSelfWriteOnce = workspaceInvariant("WorkspaceSelfWriteOnce
136
146
  // side) DISCARDED at evaluation. Each relation is a flat `self()` (directly-stored tuples) — NO rewrite
137
147
  // algebra (no computed/from/union): control-plane authority is direct membership only.
138
148
  // * admin — grantAdmin/revokeAdmin + every admin-mutating governance directive.
139
- // * creator — createWorkspace (under invite/closed posture; see the host's _creationLawOk).
140
- // * platformCreator — birthPlatform (always invite-only).
149
+ // * creator — THE birth authority: createWorkspace + birthChild (leaf OR platform). The tier
150
+ // collapsed onto this one relation (2026-06-24); a subject is user: or key:.
151
+ // * platformCreator — VESTIGIAL: its directives (birthPlatform/grant·revokePlatformCreation) are removed;
152
+ // no .requires references it. Kept until the #58 disposition pass (with kind/role).
141
153
  // SELF-RELATIVE: custody guarantees a workspace only ever folds its OWN relations table, so
142
154
  // `workspace:root#admin` collapses to `workspace:self#admin` — the SAME law governs root and every
143
155
  // born platform with no name plumbing. (A pure tenant runs no governance ⇒ no schema ⇒ the gate is
@@ -224,9 +236,12 @@ export const CloudWorkspace = aggregate("CloudWorkspace", {
224
236
  */
225
237
  byteCap: t.int().merge(Lww).optional(),
226
238
  /**
227
- * What this node IS: absent/`"workspace"` = a leaf; `"platform"` = a PLATFORM —
228
- * a workspace that hosts the SAME governance law and parents its own subtree
229
- * (the cloud, one level down; born via {@link birthPlatform}).
239
+ * DERIVED LABEL (the `kind`-collapse, architecture/provisioning_as_obligations.md): set to `"platform"`
240
+ * iff the birth order carried a genesis (a `foundingAdminHash`), else absent/`"workspace"`. It is NOT a
241
+ * node-class and NOTHING decides on it — the birth provider keys off `foundingAdminHash`, the genuine
242
+ * signal that a child is born running the governance law. Kept only so old readers/queries that filter by
243
+ * label still resolve; slated for removal (a #58 disposition) once no reader depends on it. "All
244
+ * workspaces are equal until a domain is installed" — a platform is one with the governance domain.
230
245
  */
231
246
  kind: t.enum(["workspace", "platform"]).merge(Lww).optional(),
232
247
  /**
@@ -237,6 +252,29 @@ export const CloudWorkspace = aggregate("CloudWorkspace", {
237
252
  * request at the cloud level).
238
253
  */
239
254
  pool: t.int().merge(Lww).optional(),
255
+ /**
256
+ * THE LAW-AUTHORED GENESIS CHAIN (P1, architecture/provisioning_as_obligations.md "law and law alone").
257
+ * For a genesis birth, `birthChild` authors the child's genesis RECIPE here as data — a JSON array of
258
+ * `{domain, directiveId, payload, actor}` steps (`seedGenesis → grantAdmin → birthCloud`). The birth
259
+ * capability provider REPLAYS this opaque list on the child (it names nothing — the directive ids come
260
+ * from the law, not host source); that is the difference between the host *reconstructing* the genesis
261
+ * (today) and the host *delivering* a law-authored one. Absent for plain births + pre-P1 records (the
262
+ * provider falls back to its inline recipe — flag-gated until live byte-equivalence is proven). Naming
263
+ * the genesis directives HERE is law naming law (legitimate); the prohibition binds only the host.
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(),
240
278
  });
241
279
 
242
280
  // AdminGrant / CreationGrant aggregates: DELETED (full-native Zanzibar). Admin/creation authority is
@@ -356,11 +394,125 @@ export const CapabilityBinding = aggregate("CapabilityBinding", {
356
394
 
357
395
  // --- workspace directives ---------------------------------------------------------
358
396
 
397
+ // ── THE ONE BIRTH RECORD (the `kind`-collapse — architecture/provisioning_as_obligations.md) ───────────
398
+ // Every birth — a plain child OR a platform — mints the SAME `CloudWorkspace` order; the ONLY difference is
399
+ // the GENESIS DATA the order carries. `genesis` present (a founding-admin hash + pool) ⇒ this child is born
400
+ // running the governance law: the birth capability provider drains it (the order carries a foundingAdminHash)
401
+ // and seeds the genesis. Absent ⇒ a plain lineage record, materialized by boot-on-touch (never drained).
402
+ // `kind` is set PURELY as a derived label off genesis presence — a vestige for old readers; NOTHING decides
403
+ // on it (the host now keys off foundingAdminHash, not the label). There is NO node-class: a platform is just
404
+ // a workspace whose birth order carried a genesis. (`birthChild` is the one unified directive — the tier
405
+ // was collapsed onto `#creator`; `birthPlatform`/`createWorkspace` remain only as deprecated compat aliases.)
406
+ // Returns the record builder so a caller (birthChild) can author the law-authored genesis chain onto it.
407
+ function planBirthRecord(
408
+ p: {
409
+ name: string;
410
+ parent: string;
411
+ principal: string;
412
+ tier: (typeof WORKSPACE_TIERS)[number];
413
+ requestedBy: string;
414
+ requestedAt: number;
415
+ },
416
+ genesis?: { foundingAdminHash: string; pool: number },
417
+ ) {
418
+ const rec = create(CloudWorkspace)
419
+ .set("name", p.name)
420
+ .set("parent", p.parent)
421
+ .set("principal", p.principal)
422
+ .set("tier", p.tier)
423
+ .set("status", "requested" as const)
424
+ .set("ownerGrantHash", "") // no owner credential until provisioned
425
+ .set("createdBy", p.requestedBy)
426
+ .set("createdAt", p.requestedAt);
427
+ if (genesis) {
428
+ rec
429
+ .set("kind", "platform" as const) // DERIVED LABEL ONLY — the order's foundingAdminHash is the real signal
430
+ .set("pool", genesis.pool)
431
+ .set("foundingAdminHash", genesis.foundingAdminHash); // the provider grants admin to this hash at materialize
432
+ }
433
+ return rec;
434
+ }
435
+
436
+ // ── THE LAW-AUTHORED GENESIS CHAIN (P1 — "law and law alone") ──────────────────────────────────────────
437
+ // The parent's `birthChild` AUTHORS the child's genesis recipe as DATA — byte-faithful to what the host's
438
+ // `/materialize` reconstructs today (seedGenesis → grantAdmin → birthCloud), but now owned by the LAW. The
439
+ // host replays this opaque list on the child (naming nothing). Timestamps ride IN the payload (the doctrine):
440
+ // `bornAt` (ISO) for the warrant, `requestedAt` (epoch-ms) for the ms-stamped grants — both caller-stamped,
441
+ // so the chain is a pure function of the payload (deterministic on every replaying peer). Naming the genesis
442
+ // directives here is law naming law; the prohibition binds only the host. The law package bytes are NOT in
443
+ // here — the install-by-hash stays a custody concern of the provider (the bytes-in-pack refinement is later).
444
+ export function buildGenesisChain(p: {
445
+ name: string;
446
+ principal: string;
447
+ parent: string;
448
+ tier: (typeof WORKSPACE_TIERS)[number];
449
+ foundingAdminHash: string;
450
+ bornAt: string;
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;
470
+ }): string {
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
+ });
496
+ return JSON.stringify([
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 } },
501
+ { domain: "workspaces", directiveId: "grantAdmin", actor: founder, payload: { keyHash: p.foundingAdminHash, label: `founding admin (born for ${p.principal})`, grantedBy: p.principal, grantedAt: p.requestedAt } },
502
+ // No `role` — it was a vestigial node-class label (collapsed with `kind`); birthCloud ignores it now.
503
+ { domain: "workspaces", directiveId: "birthCloud", actor: "", payload: { principal: p.principal, name: p.name, tier: p.tier, bornAt: p.requestedAt, bornUnder } },
504
+ ]);
505
+ }
506
+
359
507
  /**
360
508
  * createWorkspace — record the lawful REQUEST for a workspace. Nomos MINTS the
361
509
  * `CloudWorkspace` id (the payload carries none); the record is born
362
510
  * `status:"requested"` with `ownerGrantHash:""` — no bearer exists until the
363
511
  * worker provisions and authors {@link provisionWorkspace}.
512
+ *
513
+ * @deprecated SUPERSEDED by {@link birthChild} (the `kind`-collapse) — a plain birth is `birthChild`
514
+ * with no genesis. Kept as a compat alias (same `#creator` gate) during migration; the host still
515
+ * authors it by name for namespaced-child + upload births. Retire once those callers move to birthChild.
364
516
  */
365
517
  export const createWorkspace = directive("createWorkspace")
366
518
  .creates(CloudWorkspace)
@@ -378,17 +530,8 @@ export const createWorkspace = directive("createWorkspace")
378
530
  }),
379
531
  )
380
532
  .plan((p) => {
381
- create(CloudWorkspace)
382
- .set("name", p.name)
383
- .set("parent", p.parent)
384
- .set("principal", p.principal)
385
- .set("tier", p.tier)
386
- .set("status", "requested" as const)
387
- .set("ownerGrantHash", "") // no owner credential until provisioned
388
- .set("createdBy", p.requestedBy)
389
- .set("createdAt", p.requestedAt);
390
- // A plain workspace record (no kind:platform) — boot materializes it directly; the birth capability
391
- // provider only drains kind:platform records, so this is a lineage record, not a genesis order.
533
+ // No genesis ⇒ a plain lineage record (no foundingAdminHash) — never drained; materialized by boot-on-touch.
534
+ planBirthRecord(p);
392
535
  return [];
393
536
  })
394
537
  .requires("creator"); // posture-gated creation: only a #creator holder may request a workspace
@@ -409,15 +552,19 @@ export const birthCloud = directive("birthCloud")
409
552
  /** The genesis auth-provider principal uid upon which the world rests. */
410
553
  principal: z.string().min(1),
411
554
  name: z.string().min(1).default("root"),
412
- /** The workspace's ROLE for the governance domain — the host stamps "cloud-root" at root genesis,
413
- * "platform" when birthing a platform's own chain. Drives the self-relative authz gate. */
414
- role: z.enum(WORKSPACE_ROLES).default("cloud-root"),
555
+ /** VESTIGIAL (see WorkspaceSelf): accepted-but-ignored — the value drives nothing. The authz gate is
556
+ * self-relative (`workspace:self#<relation>`), NOT role-keyed. Kept optional for caller compat. */
557
+ role: z.enum(WORKSPACE_ROLES).optional(),
415
558
  tier: z.enum(WORKSPACE_TIERS).default("edge"),
416
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(),
417
564
  }),
418
565
  )
419
566
  .plan((p) => {
420
- create(CloudWorkspace)
567
+ const rec = create(CloudWorkspace)
421
568
  .set("name", p.name)
422
569
  .set("parent", "") // the root of the world tree
423
570
  .set("principal", p.principal)
@@ -427,9 +574,12 @@ export const birthCloud = directive("birthCloud")
427
574
  .set("createdBy", p.principal)
428
575
  .set("createdAt", p.bornAt)
429
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);
430
579
  // The self-identity singleton at the CONSTANT id (ungated — birthCloud is not admin-mutating), so the
431
- // gate's self-record exists before any admin write. ensure() = deterministic-id sibling birth.
432
- ensure(WorkspaceSelf, WORKSPACE_SELF_ID).set("role", p.role).set("name", p.name);
580
+ // gate's self-record exists before any admin write. ensure() = deterministic-id sibling birth. `name` is
581
+ // the born marker; `role` is no longer written (vestigial node-class — see WorkspaceSelf).
582
+ ensure(WorkspaceSelf, WORKSPACE_SELF_ID).set("name", p.name);
433
583
  return [];
434
584
  });
435
585
 
@@ -524,18 +674,18 @@ export const retireWorkspace = directive("retireWorkspace")
524
674
  /**
525
675
  * reclaimWorkspace — THE LAWFUL CUSTODY DISCARD. Where {@link retireWorkspace}
526
676
  * withdraws the law (a status, the bytes untouched), reclaim is the law AUTHORIZING
527
- * the host to drop the custody: the worker bailiff, on seeing THIS fact folded,
528
- * `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).
529
679
  *
530
- * THE TWO-PHASE RULE (enforced worker-side, since a plan cannot read prior state):
531
- * 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.
532
682
  *
533
- * THE FIXED POINT: this is authored for CHILDREN only. The worker refuses to reclaim
534
- * ROOT — the genesis workspace is the seat of the law that authorizes reclaim, so the
535
- * law can reap everything it governs EXCEPT itself. Dropping root is the one act Nomos
536
- * cannot perform in-law; the custodian does it through Cloudflare directly. The
537
- * custody-reclaimed fact (this status flip) is append-only and outlives the bytes:
538
- * 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.
539
689
  */
540
690
  export const reclaimWorkspace = directive("reclaimWorkspace")
541
691
  .mutates(CloudWorkspace)
@@ -552,67 +702,96 @@ export const reclaimWorkspace = directive("reclaimWorkspace")
552
702
  })
553
703
  .requires("admin");
554
704
 
555
- /**
556
- * birthPlatform — A PLATFORM IS BORN: the cloud, one level down. Records the
557
- * platform-kind workspace (parented to root, owned by the invited principal,
558
- * carrying its POOL). The worker then provisions it exactly like the cloud
559
- * provisioned itself: deploys the SAME governance law into it, seeds ITS founding
560
- * AdminGrant, and the platform claims itself with its own birth intent on its own
561
- * chain. Recursion, not new machinery.
562
- */
563
- export const birthPlatform = directive("birthPlatform")
705
+ // ─────────────────────────── BIRTH AS A NOMOS PRIMITIVE ──────────────────────────────────────────────
706
+ // (birthPlatform is GONE — the tier collapsed onto #creator; birthChild is the one birth directive.)
707
+ // THE RECORD IS THE OBLIGATION (architecture/githolon_host_contract.md "Birth is a capability" +
708
+ // provisioning_as_obligations.md). Birth is an IMPURE capability: the parent ORDERS (the law-gated
709
+ // birthChild mints a CloudWorkspace at status:requested carrying foundingAdminHash + the LAW-AUTHORED
710
+ // genesisChain — gated by .requires("creator")), the BIRTH CAPABILITY PROVIDER fulfils it (materialize:
711
+ // install the parent's law + REPLAY the genesisChain opaquely — the child's OWN kernel admitting each), and
712
+ // authors provisionWorkspace (the receipt) the parent TRUSTS. No separate obligation aggregate — the minted
713
+ // CloudWorkspace IS the order. The provider owns only the impure boot + replay; the LAW owns the recipe (P1)
714
+ // AND who/what (the gate); the parent trusts the DID-IT-HAPPEN. A child born this way runs THIS SAME law and
715
+ // births its children identically — the recursion is the law.
716
+
717
+ // ─────────────────────────── birthChild — THE ONE BIRTH DIRECTIVE (the `kind`-collapse) ───────────────
718
+ // "There are only workspaces, all equal until a domain is installed." birthChild is the SINGLE directive
719
+ // that births any child — a plain leaf OR a platform; the difference is the GENESIS DATA the payload carries,
720
+ // never a node-class. THE TIER IS COLLAPSED (Jack, 2026-06-24): `#creator` is THE one birth authority — a
721
+ // creator may spawn a leaf AND a platform (the prior `#platformCreator` second tier is retired; platform
722
+ // birth under root is still invite-only because `creator` on root is admin-granted, just renamed/unified).
723
+ // • genesis ABSENT → a plain lineage record (no foundingAdminHash); boot-on-touch, never drained.
724
+ // • genesis PRESENT → carries {foundingAdminHash, pool}: the birth capability provider drains it
725
+ // (the order carries a foundingAdminHash) → materialize + seed genesis. A platform.
726
+ // Self-relative: the gate is `workspace:self#creator`, so the SAME directive on root births a platform and
727
+ // on a platform births a child — no per-level host code (the recursion is the law). Supersedes
728
+ // createWorkspace + birthPlatform (kept as compat aliases during migration). See
729
+ // architecture/provisioning_as_obligations.md "The `kind`-collapse".
730
+ export const birthChild = directive("birthChild")
564
731
  .creates(CloudWorkspace)
565
732
  .payload(
566
- z.object({
567
- name: z.string().min(1),
568
- /** The auth-provider uid of the platform OWNER (must hold a PlatformGrant). */
569
- principal: z.string().min(1),
570
- /** Parent CloudWorkspace aggregate id (root's record). */
571
- parent: z.string().default(""),
572
- /** The pool: how many live children the platform may parent. */
573
- pool: z.number().int().positive().default(50),
574
- tier: z.enum(WORKSPACE_TIERS).default("heavy"),
575
- /** sha256 of the founding-admin key the requester PRESENTED (the provider grants admin to the hash;
576
- * the requester keeps the key — it is the platform's admin credential, used to govern + deploy). */
577
- foundingAdminHash: sha256Hex,
578
- requestedBy: z.string().min(1),
579
- requestedAt: epochMs,
580
- }),
733
+ z
734
+ .object({
735
+ name: z.string().min(1),
736
+ /** The auth-provider principal uid the request claimed; `""` = unclaimed (a plain leaf may be). */
737
+ principal: z.string().default(""),
738
+ /** Parent CloudWorkspace aggregate id; `""` (default) = root. */
739
+ parent: z.string().default(""),
740
+ /** Hosting tier; defaults to the edge DO (a genesis/platform birth typically passes `heavy`). */
741
+ tier: z.enum(WORKSPACE_TIERS).default("edge"),
742
+ requestedBy: z.string().min(1),
743
+ requestedAt: epochMs,
744
+ /** GENESIS (optional): present ⇒ this child is born running the governance law (a platform). The
745
+ * sha256 of the founding-admin key the requester presented — the provider grants admin to the hash. */
746
+ foundingAdminHash: sha256Hex.optional(),
747
+ /** PLATFORM POOL: how many live children the born platform may parent. Only meaningful with genesis. */
748
+ pool: z.number().int().positive().default(50),
749
+ /** GENESIS birth time as an ISO string (caller-stamped — the timestamp doctrine). Arms the warrant
750
+ * in the law-authored genesis chain; required for (and only used by) a genesis birth. */
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(),
763
+ })
764
+ // A genesis birth (a platform) must carry a principal AND a bornAt — it owns a subtree, a lineage
765
+ // anchor, and a law-authored genesis chain whose warrant is stamped at bornAt.
766
+ .refine((d) => !d.foundingAdminHash || (d.principal.length > 0 && !!d.bornAt), {
767
+ message: "a genesis birth (foundingAdminHash present) requires a non-empty principal and bornAt (ISO)",
768
+ path: ["principal"],
769
+ }),
581
770
  )
582
771
  .plan((p) => {
583
- create(CloudWorkspace)
584
- .set("name", p.name)
585
- .set("parent", p.parent)
586
- .set("principal", p.principal)
587
- .set("kind", "platform" as const)
588
- .set("pool", p.pool)
589
- .set("tier", p.tier)
590
- .set("status", "requested" as const)
591
- .set("ownerGrantHash", "")
592
- .set("foundingAdminHash", p.foundingAdminHash) // the provider grants admin to this hash at materialize
593
- .set("createdBy", p.requestedBy)
594
- .set("createdAt", p.requestedAt);
595
- // THE RECORD IS THE OBLIGATION: a kind:platform CloudWorkspace at status:requested with no ownerGrantHash
596
- // is an order the birth capability provider drains (materialize + install the parent's law + seed genesis
597
- // + grant the founding admin + birthCloud), closing it with provisionWorkspace (the receipt). No separate
598
- // obligation aggregate — the minted CloudWorkspace IS it (sidesteps the id-mint gate; reuses the lifecycle).
772
+ const rec = planBirthRecord(p, p.foundingAdminHash ? { foundingAdminHash: p.foundingAdminHash, pool: p.pool } : undefined);
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.)
775
+ if (p.foundingAdminHash) {
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) });
786
+ }
599
787
  return [];
600
788
  })
601
- .requires("platformCreator"); // invite-only: an active #platformCreator tuple admits the birth
789
+ .births() // BIRTH IS A DECLARED KERNEL PRIMITIVE (G3): emits the `nomosBirthGate` era key; the kernel folds the carried recipe
790
+ .requires("creator"); // THE ONE birth authority — a #creator may birth a leaf OR a platform (tier collapsed)
602
791
 
603
- // ─────────────────────────── BIRTH AS A NOMOS PRIMITIVE ──────────────────────────────────────────────
604
- // THE RECORD IS THE OBLIGATION (architecture/githolon_host_contract.md "Birth is a capability"). Birth is
605
- // an IMPURE capability: the parent ORDERS (the law-gated birthPlatform mints a CloudWorkspace at
606
- // status:requested, kind:platform, carrying foundingAdminHash — gated by .requires("platformCreator")),
607
- // the BIRTH CAPABILITY PROVIDER fulfils it (materialize: install the PARENT's law + seed genesis + grant
608
- // the founding admin + birthCloud — the child's OWN kernel admitting each), and authors provisionWorkspace
609
- // (the receipt) the parent TRUSTS. No separate obligation aggregate — the minted CloudWorkspace IS the
610
- // order (it sidesteps the id-mint gate and reuses the requested→provisioned lifecycle). The provider knows
611
- // HOW (the Capability Exception sanctions a declared effect); the law owns WHO/WHAT; the parent trusts the
612
- // DID-IT-HAPPEN. A child born this way runs THIS SAME law and births its children identically.
613
-
614
- /** workspacesByStatus — the birth provider drains `requested` CloudWorkspaces (kind:platform, no
615
- * ownerGrantHash) → materialize → provisionWorkspace. The law's order queue; the host polls, never decides. */
792
+ /** workspacesByStatus — the birth provider drains `requested` CloudWorkspaces that CARRY A foundingAdminHash
793
+ * (a genesis order, no ownerGrantHash) → materialize → provisionWorkspace. Plain requested records (no
794
+ * foundingAdminHash) are lineage only, boot-on-touch. The law's order queue; the host polls, never decides. */
616
795
  export const workspacesByStatus = query("workspacesByStatus").key("status").returns(CloudWorkspace);
617
796
 
618
797
  /** setPlatformPool — the pool edition of a granted quota request. */
@@ -632,39 +811,10 @@ export const setPlatformPool = directive("setPlatformPool")
632
811
  })
633
812
  .requires("admin");
634
813
 
635
- /**
636
- * grantPlatformCreation / revokePlatformCreation — open and close the
637
- * become-a-platform door for one principal (see {@link PlatformGrant}).
638
- */
639
- // grantPlatformCreation — become-a-platform authority IS the self-relative `#platformCreator` tuple
640
- // (one authz spine, all the way down). Writes `workspace:self#platformCreator@user:<principal>` (no
641
- // PlatformGrant aggregate) — the host's platform-birth invite check reads the SAME tuple via Check.
642
- export const grantPlatformCreation = directive("grantPlatformCreation")
643
- .ensures(RelationTuple)
644
- .payload(
645
- z.object({
646
- principal: z.string().min(1),
647
- label: z.string().min(1),
648
- grantedBy: z.string().min(1),
649
- grantedAt: epochMs,
650
- }),
651
- )
652
- .plan((p) => writeTuple({ object: SELF_ADMIN_OBJECT, relation: "platformCreator", subject: "user:" + p.principal, grantedBy: p.grantedBy, grantedAt: String(p.grantedAt) }))
653
- .requires("admin"); // only an admin opens the become-a-platform door
654
-
655
- // revokePlatformCreation — flip the `#platformCreator@user:<principal>` leaf to revoked. BREAKING vs the
656
- // aggregate model: keyed by PRINCIPAL (the tuple subject), not a minted grantId.
657
- export const revokePlatformCreation = directive("revokePlatformCreation")
658
- .ensures(RelationTuple)
659
- .payload(
660
- z.object({
661
- principal: z.string().min(1),
662
- revokedBy: z.string().min(1),
663
- revokedAt: epochMs,
664
- }),
665
- )
666
- .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) }))
667
- .requires("admin");
814
+ // grantPlatformCreation / revokePlatformCreation — REMOVED (the tier collapsed onto #creator, 2026-06-24).
815
+ // Platform-birth authority is now just #creator (grantCreation, which takes a user OR key subject). The
816
+ // `#platformCreator` relation + its (vestigial) PlatformGrant aggregate / platformGrantByPrincipal query
817
+ // retire in the same #58 disposition pass as kind/role; any historical #platformCreator tuple is inert data.
668
818
 
669
819
  /**
670
820
  * AuthProvider — BYOAuth: WHO VOUCHES FOR THIS SUBTREE'S PRINCIPALS. A law-state
@@ -681,13 +831,31 @@ export const AuthProvider = aggregate("AuthProvider", {
681
831
  scope: t.string().merge(Lww),
682
832
  /** The token issuer (the `iss` claim tokens must carry). */
683
833
  issuer: t.string().merge(Lww),
684
- /** 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}. */
685
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(),
686
845
  /** OPTIONAL expected `aud` claim. When the platform sets it, a token's `aud` MUST carry this value
687
846
  * (defense-in-depth beyond the issuer match — e.g. a Firebase project id, an Auth0 API audience).
688
847
  * Absent ⇒ issuer match alone. This is LAW-STATE the PLATFORM owns; the host merely enforces it as a
689
848
  * capability (it never chooses the audience). */
690
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(),
691
859
  /** Human label for audit ("acme supabase", ...). */
692
860
  label: t.string().merge(Lww),
693
861
  /** Who set it (attribution). */
@@ -709,6 +877,13 @@ export const setAuthProvider = directive("setAuthProvider")
709
877
  issuer: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
710
878
  jwksUrl: z.string().regex(/^https?:\/\/\S+$/, "expected an http(s) URL"),
711
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(),
712
887
  label: z.string().min(1),
713
888
  setBy: z.string().min(1),
714
889
  setAt: epochMs,
@@ -721,6 +896,9 @@ export const setAuthProvider = directive("setAuthProvider")
721
896
  set(ap, "issuer", p.issuer),
722
897
  set(ap, "jwksUrl", p.jwksUrl),
723
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)] : []),
724
902
  set(ap, "label", p.label),
725
903
  set(ap, "setBy", p.setBy),
726
904
  set(ap, "setAt", p.setAt),
@@ -728,6 +906,30 @@ export const setAuthProvider = directive("setAuthProvider")
728
906
  })
729
907
  .requires("admin");
730
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
+
731
933
  /**
732
934
  * HostnameClaim — a CUSTOM HOSTNAME bound to a platform: `api.acme.com` serving
733
935
  * acme's subtree. A root-custody fact (the hostname is the HOST's edge resource —
@@ -942,14 +1144,21 @@ export const setWorkspaceByteCap = directive("setWorkspaceByteCap")
942
1144
  export const grantAdmin = directive("grantAdmin")
943
1145
  .ensures(RelationTuple)
944
1146
  .payload(
945
- z.object({
946
- keyHash: sha256Hex,
947
- label: z.string().min(1),
948
- grantedBy: z.string().min(1),
949
- grantedAt: epochMs,
950
- }),
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"] }),
951
1160
  )
952
- .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) }))
953
1162
  .requires("admin"); // only an admin grants admin (pre-apply roster ⇒ no self-grant)
954
1163
 
955
1164
  /**
@@ -963,13 +1172,16 @@ export const grantAdmin = directive("grantAdmin")
963
1172
  export const revokeAdmin = directive("revokeAdmin")
964
1173
  .ensures(RelationTuple)
965
1174
  .payload(
966
- z.object({
967
- keyHash: sha256Hex,
968
- revokedBy: z.string().min(1),
969
- revokedAt: epochMs,
970
- }),
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"] }),
971
1183
  )
972
- .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) }))
973
1185
  .requires("admin");
974
1186
 
975
1187
  /**
@@ -980,30 +1192,41 @@ export const revokeAdmin = directive("revokeAdmin")
980
1192
  // grantCreation — creation authority IS the self-relative `#creator` tuple the CreationGate reads.
981
1193
  // Writes `workspace:self#creator@key:<keyHash>` (no CreationGrant aggregate). Under posture invite/closed
982
1194
  // the gate admits only holders of an active #creator subject; under open/unset it admits anyone.
1195
+ // #creator is ONE relation over ONE principal graph; a subject is `key:<hash>` (proof by holding a secret —
1196
+ // a Bearer) OR `user:<uid>` (proof of identity — an auth-provider token). The relation allows BOTH
1197
+ // (allow:[user(),key()]); these are not two authorities, just two ways a principal authenticates. So
1198
+ // grantCreation takes EITHER — exactly one of keyHash / principal. This is what lets a USER birth platforms
1199
+ // via birthChild (no "creator key" dance) and dissolves the user/key migration wrinkle.
983
1200
  export const grantCreation = directive("grantCreation")
984
1201
  .ensures(RelationTuple)
985
1202
  .payload(
986
- z.object({
987
- keyHash: sha256Hex,
988
- label: z.string().min(1),
989
- grantedBy: z.string().min(1),
990
- grantedAt: epochMs,
991
- }),
1203
+ z
1204
+ .object({
1205
+ keyHash: sha256Hex.optional(),
1206
+ principal: z.string().min(1).optional(),
1207
+ label: z.string().min(1),
1208
+ grantedBy: z.string().min(1),
1209
+ grantedAt: epochMs,
1210
+ })
1211
+ .refine((d) => !!d.keyHash !== !!d.principal, { message: "grantCreation needs exactly one of keyHash (a key: subject) or principal (a user: subject)", path: ["keyHash"] }),
992
1212
  )
993
- .plan((p) => writeTuple({ object: SELF_ADMIN_OBJECT, relation: "creator", subject: "key:" + p.keyHash, grantedBy: p.grantedBy, grantedAt: String(p.grantedAt) }))
1213
+ .plan((p) => writeTuple({ object: SELF_ADMIN_OBJECT, relation: "creator", subject: p.principal ? "user:" + p.principal : "key:" + p.keyHash, grantedBy: p.grantedBy, grantedAt: String(p.grantedAt) }))
994
1214
  .requires("admin"); // only an admin grants creation authority
995
1215
 
996
- /** revokeCreation — flip the `#creator@key:<keyHash>` leaf to revoked (see {@link revokeAdmin}). */
1216
+ /** revokeCreation — flip the `#creator@<subject>` leaf to revoked (key OR user, mirroring grantCreation). */
997
1217
  export const revokeCreation = directive("revokeCreation")
998
1218
  .ensures(RelationTuple)
999
1219
  .payload(
1000
- z.object({
1001
- keyHash: sha256Hex,
1002
- revokedBy: z.string().min(1),
1003
- revokedAt: epochMs,
1004
- }),
1220
+ z
1221
+ .object({
1222
+ keyHash: sha256Hex.optional(),
1223
+ principal: z.string().min(1).optional(),
1224
+ revokedBy: z.string().min(1),
1225
+ revokedAt: epochMs,
1226
+ })
1227
+ .refine((d) => !!d.keyHash !== !!d.principal, { message: "revokeCreation needs exactly one of keyHash or principal", path: ["keyHash"] }),
1005
1228
  )
1006
- .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) }))
1229
+ .plan((p) => removeTuple({ object: SELF_ADMIN_OBJECT, relation: "creator", subject: p.principal ? "user:" + p.principal : "key:" + p.keyHash, grantedBy: p.revokedBy, grantedAt: String(p.revokedAt), revokedBy: p.revokedBy, revokedAt: String(p.revokedAt) }))
1007
1230
  .requires("admin");
1008
1231
 
1009
1232
  // --- declared read queries (the worker's ENTIRE enforcement surface) -----------------
@@ -1156,8 +1379,9 @@ export const founderSignerWriteOnce = workspaceInvariant("FounderSignerWriteOnce
1156
1379
  export const birthCloudOnce = workspaceInvariant("BirthCloudOnce")
1157
1380
  .on("birthCloud")
1158
1381
  .reads(() => [refAs("self", "WorkspaceSelf", "pre:" + WORKSPACE_SELF_ID)])
1382
+ // WRITE-ONCE on the self-record's PRESENCE via `name` (not the vestigial `role`) — a workspace is born once.
1159
1383
  .assert(({ self }) =>
1160
- (self as { role?: string })?.role ? { reject: "cloud-already-born" } : { accept: true },
1384
+ (self as { name?: string })?.name ? { reject: "cloud-already-born" } : { accept: true },
1161
1385
  );
1162
1386
 
1163
1387
  /** provisionWorkspace: the birth RECEIPT may only close a `requested` record (no hijacking a live
@@ -1177,3 +1401,18 @@ export const provisionReceiptGuard = workspaceInvariant("ProvisionReceiptGuard")
1177
1401
  return { reject: "provision-founding-hash-mismatch" };
1178
1402
  return { accept: true };
1179
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
+ });