@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.
- package/dart/CHANGELOG.md +5 -0
- package/dart/lib/nomos_client.dart +5 -0
- package/dart/lib/src/errors.dart +79 -0
- package/dart/lib/src/home.dart +20 -0
- package/dart/lib/src/webview_bridge.dart +68 -0
- package/dart/lib/src/wire.dart +117 -0
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -1
- package/dart/test/home_parity_test.dart +9 -0
- package/dart/test/wire_test.dart +48 -0
- package/package.json +1 -1
- package/src/births.ts +82 -0
- package/src/build_package.ts +23 -27
- package/src/codegen_dart.ts +10 -0
- package/src/codegen_ts.ts +12 -8
- package/src/compile_package_main.ts +61 -109
- package/src/decl_shapes.ts +122 -0
- package/src/directive.ts +30 -0
- package/src/engine_entry.ts +109 -8
- package/src/framework/home.ts +179 -0
- package/src/framework/impure_capability.ts +8 -4
- package/src/framework/relations.ts +22 -0
- package/src/framework/signing_identity.ts +291 -2
- package/src/framework/workspace_invariant.ts +17 -2
- package/src/framework/workspaces.ts +392 -153
- package/src/index.ts +42 -0
- package/src/usd.ts +44 -0
- package/src/usd_layers.ts +41 -0
- package/src/wire.ts +13 -0
- package/src/wire_encode.ts +14 -2
|
@@ -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
|
|
105
|
-
//
|
|
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
|
-
|
|
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("
|
|
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 {
|
|
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 —
|
|
140
|
-
//
|
|
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
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
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
|
-
|
|
382
|
-
|
|
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
|
-
/**
|
|
413
|
-
*
|
|
414
|
-
role: z.enum(WORKSPACE_ROLES).
|
|
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
|
-
|
|
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
|
|
528
|
-
*
|
|
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
|
|
531
|
-
*
|
|
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
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
.
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
.set("
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
.
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
637
|
-
|
|
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
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
|
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
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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 {
|
|
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
|
+
});
|