@githolon/dsl 0.17.0 → 0.19.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.
@@ -32,8 +32,11 @@ import { directive } from "../directive.js";
32
32
  import { create } from "../authoring.js";
33
33
  import { set, type PlannedOp } from "../ops.js";
34
34
  import { query } from "../query.js";
35
+ import { birth } from "../births.js";
35
36
  import { workspaceInvariant, refAs } from "./workspace_invariant.js";
36
37
  import { identityRelations } from "./signing_identity.js";
38
+ import { requireParentAttestedBirth } from "../births.js";
39
+ import { GenesisWarrant, seedGenesis } from "./genesis.js";
37
40
 
38
41
  // `nomos.sha256` — the TRUSTLESS, pure hash computed IN the githolon (Rust sha2 bound in the sealed engine;
39
42
  // see deterministic-rquickjs). Available as a global to plans + invariant predicates (like `nomos.mint`); it
@@ -44,6 +47,12 @@ declare const nomos: { sha256(input: string): string };
44
47
  * identity's public device keys, enrolled via `enrollSigner`, checked by the gate). */
45
48
  export { identityRelations };
46
49
 
50
+ /** THE GENESIS WARRANT primitives — re-exported so a HOME law carries them in the `home` domain key. The
51
+ * birthHome genesis chain runs `home/seedGenesis` to record the home's unique self-id (== homeName), so
52
+ * `workspace_self_id` resolves on the home and BOTH the signed-author regime + the parent-attested-birth
53
+ * cert door FIRE on every home (BREAK 1). Without this the home is unwarranted and both doors are inert. */
54
+ export { GenesisWarrant, seedGenesis };
55
+
47
56
  /**
48
57
  * THE HOME OWNER (home_holons §5 — the owner stamp). A home holon is born UNWARRANTED (the subject may
49
58
  * never have logged in — a share can be delivered to a stranger's lazily-born home, before they hold any
@@ -105,6 +114,94 @@ export const homeNameBindsOwner = workspaceInvariant("HomeNameBindsOwner")
105
114
  : { reject: "a home's name must hash its owner (home-<sha256(principal)>) — refusing a squatted home" };
106
115
  });
107
116
 
117
+ /** Re-derive a subject's home slot name IN-LAW (`home-<sha256(subject)>`) — the SAME formula the client's
118
+ * `homeNameOf` and the {@link homeNameBindsOwner} invariant use. `nomos.sha256` is the trustless pure hash
119
+ * bound in the sealed engine, so every replaying peer derives the identical name. */
120
+ export function homeNameOf(subject: string): string {
121
+ return HOME_NAME_PREFIX + nomos.sha256(String(subject)).slice(0, HOME_NAME_HASH_LEN);
122
+ }
123
+
124
+ /**
125
+ * `birthHome` — THE FRAMEWORK BIRTH PRIMITIVE for a home holon. This is the MECHANICS (OURS) so a tenant
126
+ * never hand-builds a home genesis: the parent (the cloud/platform whose governance includes this directive)
127
+ * ORDERS the birth of an identity's home holon, and the kernel's offer-effect spawns it + self-validates it
128
+ * through the child's OWN gate from intent 0 (the `.births()` declared consequence; `architecture/
129
+ * home_holons_and_cross_custody.md` + `sovereign_adapter.md` "birth is a kernel offer-effect").
130
+ *
131
+ * SELF-BOUND — squatting is structurally impossible (no separate grant needed; the policy gate is the
132
+ * tenant's `setAuthProvider` auth scoping, which decides whether the offering principal is verified at all):
133
+ * 1. The home OWNER rides the payload (`owner`, a `user:<uid>` subject). `ctx.principal()` is opaque
134
+ * PrincipalEvidence — it CANNOT be read as a raw string in-plan to compute the slot name — so the owner
135
+ * is payload-carried, but the binding below makes it sound.
136
+ * 2. The workspace name + the seeded `homeName` are BOTH derived from THE SAME payload `owner` via
137
+ * {@link homeNameOf} — they cannot diverge in this plan.
138
+ * 3. The child's genesis includes `seedHomeOwner(owner, homeNameOf(owner), bornAt)`, and the home law's
139
+ * {@link homeNameBindsOwner} invariant RE-DERIVES `home-<sha256(owner)>` at the child's gate (author AND
140
+ * verify_chain). So a chain that claims `home-<hash(alice)>` but seeds owner `bob` FAILS the child's own
141
+ * law — refused in the githolon, by the law, never by a host. A verified principal is required upstream
142
+ * (no anonymous home birth); the offer principal IS the auth boundary (the tenant's setAuthProvider).
143
+ *
144
+ * The genesis chain it authors (reusing the order-declares-the-law install shape — bytes resolved by hash
145
+ * from the parent's custody, `domainPackageB64: ""`):
146
+ * [ bootstrap/installDomain(frameworkHash), nomos/home installDomain(lawHash), seedHomeOwner(owner, …) ]
147
+ * `frameworkHash` (the bootstrap controller hash) + `lawHash` (the HOME law package domainHash) are
148
+ * tenant-PINNED at their governance compile (mirroring `birthChild`) — the end-user app supplies neither;
149
+ * the tenant's HomeBirth.viaCoordinator buildBody / their governance fills them. `seedHomeOwner` is INCLUDED
150
+ * so HomeOwner is seeded and `homeNameBindsOwner` passes (a birth without it would be an invalid, un-owned
151
+ * home).
152
+ */
153
+ const HOME_LAW_DOMAIN = "home";
154
+ export const birthHome = directive("birthHome")
155
+ .creates(HomeOwner) // the birth's law-declared touched type — the child's HomeOwner is what this offer seeds
156
+ .payload(
157
+ z.object({
158
+ /** The identity this home belongs to (a `user:<uid>` subject). The workspace name + seeded homeName are
159
+ * both `home-<sha256(owner)>` — self-bound, so you can only birth YOUR OWN home slot. */
160
+ owner: z.string().min(1),
161
+ /** The bootstrap framework controller content hash — tenant-pinned at governance compile (the child
162
+ * installs it IN-CHAIN as intent 0; the kernel resolves the bytes by hash from the parent's custody). */
163
+ frameworkHash: z.string().min(1),
164
+ /** The HOME law package domainHash — tenant-pinned at governance compile (the child's second install). */
165
+ lawHash: z.string().min(1),
166
+ /** ISO-8601 birth time, caller-stamped (the timestamp doctrine — the plan reads no clock). */
167
+ bornAt: z.string().min(1),
168
+ /** THE DEVICE-SIGNED HOME CERT (JSON `executor::birth_cert::BirthCert`), signed CLIENT-SIDE by the
169
+ * owner's device key `S_U` (the SAME key the `signer` relation enrolls). The kernel's birth offer-
170
+ * effect VERIFIES this cert's signature against U's enrolled signer pubkey `P_U` AND that its
171
+ * `founder == ctx.principal()` (the gate verdict, CAS-as-law) BEFORE accepting it as `links[0]` of
172
+ * the travelling chain — the kernel signs nothing (it holds no secret; it only verifies). Optional:
173
+ * absent ⇒ the era stays inert for this birth (no warranted home cert is seeded; pre-feature lane). */
174
+ homeCert: z.string().optional(),
175
+ }),
176
+ )
177
+ .plan((p) => {
178
+ const founder = p.owner; // the actor for the child's own genesis steps (its self-bound owner subject)
179
+ const homeName = homeNameOf(p.owner); // workspace name == seeded homeName — derived from the SAME owner
180
+ const genesisChain = [
181
+ // THE ORDER-DECLARED LAW INSTALL: two layers, bytes-by-hash (empty package — the kernel resolves them
182
+ // from the parent's custody when it folds the birth; never the host). Matches buildGenesisChain's shape.
183
+ { domain: "bootstrap", directiveId: "installDomain", actor: founder, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
184
+ { domain: "nomos", directiveId: "installDomain", actor: founder, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
185
+ // SEED THE OWNER — the home is born OWNED; the homeNameBindsOwner invariant in the child's law proves
186
+ // homeName == home-<sha256(owner)> at the child's own gate, so the slot can ONLY be its rightful owner's.
187
+ // THE GENESIS ORDER IS THE CONTRACT (BREAK 1): the parent's offer-effect APPENDS `seedBirthAttestation`
188
+ // (the travelling cert) and THEN `seedGenesis` (the WARRANT) after this step — so the warrant lands LAST,
189
+ // exactly like `enrollSigner → seedGenesis → grantAdmin(SIGNED)`: every genesis seed runs UNSIGNED on a
190
+ // still-warrant-free prefix (door + signed-author both inert), the warrant lands at the very end, and
191
+ // ALL post-genesis writes are then signed AND cert-gated. The home is born WARRANTED (its self-id ==
192
+ // homeName), so `workspace_self_id` resolves and the parent-attested-birth cert door FIRES on every
193
+ // post-genesis write (without a warrant the door was inert for every home it protects). ERA-SAFE: a
194
+ // non-AuthProvider / legacy chain never born under THIS recipe carries no warrant + replays byte-identical.
195
+ { domain: HOME_LAW_DOMAIN, directiveId: "seedHomeOwner", actor: founder, payload: { principal: p.owner, homeName, bornAt: p.bornAt } },
196
+ ];
197
+ // THE DECLARED BIRTH EFFECT (the unix model): the recipe rides the committed intent as a declared output
198
+ // (captured_ports.births). The kernel — reading the `.births()` declaration off the law manifest — takes
199
+ // the recipe and spawns + self-validates the child through the child's OWN gate (each step self-verifies).
200
+ birth({ workspace: homeName, genesisChain });
201
+ return [];
202
+ })
203
+ .births(); // BIRTH IS A DECLARED KERNEL PRIMITIVE — emits the `nomosBirthGate` era key; the kernel folds the recipe
204
+
108
205
  /**
109
206
  * THE SHARED-WITH-ME INDEX (§5) — one record per cross-custody attestation delivered to this home holon:
110
207
  * a signed edge `<asserterWorkspace> : <object> # <relation> @ this-identity`, indexed LOCALLY so the
@@ -181,3 +278,85 @@ export const pruneShare = directive("pruneShare")
181
278
 
182
279
  /** "Show me everything <asserter> shared with me" — an indexed probe over the local access list. */
183
280
  export const sharedByWorkspace = query("sharedByWorkspace").key("asserterWorkspace").returns(SharedWithMe);
281
+
282
+ // ─────────────────── THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain) ───────────────────
283
+
284
+ /**
285
+ * THE BIRTH ATTESTATION (the cert, on-chain for AUDIT). When a parent P births this home via the offer-
286
+ * effect, the kernel seeds a SIGNED [executor::birth_cert::BirthCert] {child, founder, verified, parentKey,
287
+ * parentId, epoch} into the home's genesis (it travels with the home's `GenesisWarrant.bornUnder` so a peer
288
+ * verifies it OFFLINE), AND folds this singleton record so the home's projection surfaces "who attested my
289
+ * birth, and was my founder verified". `verified` + `founder` are the parent's GATE verdict (captured-port,
290
+ * CAS-as-law), never the offer payload. A peer's TRUST decision uses the travelling signed chain (the kernel
291
+ * verify doors); this aggregate is the human-readable mirror.
292
+ */
293
+ export const BIRTH_ATTESTATION_ID = "birth-attestation:self";
294
+ export const BirthAttestation = aggregate("BirthAttestation", {
295
+ /** This home's self-id (`home-<sha256(founder)>`) — the cert's `child`. */
296
+ child: t.string().merge(Lww),
297
+ /** The founding principal the parent attested (== {@link HomeOwner} `principal`). */
298
+ founder: t.string().merge(Lww),
299
+ /** THE PARENT'S GATE VERDICT: was `founder` VERIFIED at the parent's offer gate (an IdP vouched)? */
300
+ verified: t.string().merge(Lww), // "true"/"false" (string for the Lww scalar projection)
301
+ /** The parent's workspace self-id (lineage). */
302
+ parentId: t.string().merge(Lww),
303
+ /** The parent's enrolled signer PUBLIC KEY (base64) — `K_P`, the cert's signer. */
304
+ parentKey: t.string().merge(Lww),
305
+ /** The parent's SignerEpoch at the attestation (informational; the cert is judged at birth-time validity). */
306
+ epoch: t.int().merge(Lww),
307
+ /** THE TRAVELLING SIGNED CERT CHAIN (JSON: `executor::birth_cert::BirthCertChain`) — the home's own
308
+ * P-signed cert FIRST, then each ancestor's birth-cert up to root. The kernel verify doors parse THIS,
309
+ * verify the signature chain to the pinned `K_root` OFFLINE (no cross-ws read), and assert it binds this
310
+ * home (`child`==self-id, `founder`==HomeOwner.principal, `verified`). It travels because it folds into
311
+ * the home's own genesis (seeded by the parent's offer-effect), so a peer holding only the home has it. */
312
+ certChain: t.string().merge(Lww),
313
+ });
314
+
315
+ /** Audit read: this home's birth attestation (who attested it + the verified verdict). */
316
+ export const birthAttestation = query("birthAttestation").key("child").returns(BirthAttestation);
317
+
318
+ /**
319
+ * `seedBirthAttestation` — THE GENESIS-DOOR step the parent's offer-effect INJECTS into the home's genesis
320
+ * chain (the kernel appends it in `run_birth_effect` carrying the assembled + signed cert chain). It folds
321
+ * the [`BirthAttestation`] singleton so the home's own state carries its travelling cert. A CONTROL-PLANE
322
+ * write seeded once at birth (the open session lane structurally refuses it by directive id). The values
323
+ * are the parent's GATE VERDICT (`founder`/`verified` lowered from the captured port — CAS-as-law, never
324
+ * the offer payload) + the signed chain bytes; the home's verify doors re-derive trust from the chain.
325
+ */
326
+ export const seedBirthAttestation = directive("seedBirthAttestation")
327
+ .ensures(BirthAttestation)
328
+ .payload(
329
+ z.object({
330
+ child: z.string().min(1),
331
+ founder: z.string().min(1),
332
+ verified: z.string().min(1), // "true"/"false"
333
+ /** The parent's workspace self-id (lineage). EMPTY for a root-DIRECT birth (root carries no self-id
334
+ * binding in the unwarranted lane) — the cert's terminal link is then signed by K_root directly. */
335
+ parentId: z.string(),
336
+ parentKey: z.string().min(1), // base64 K_P
337
+ epoch: z.number().int().nonnegative(),
338
+ certChain: z.string().min(1), // JSON BirthCertChain
339
+ }),
340
+ )
341
+ .plan((p) => {
342
+ const h = instance(BirthAttestation, BIRTH_ATTESTATION_ID);
343
+ return [
344
+ set(h, "child", p.child),
345
+ set(h, "founder", p.founder),
346
+ set(h, "verified", p.verified),
347
+ set(h, "parentId", p.parentId),
348
+ set(h, "parentKey", p.parentKey),
349
+ set(h, "epoch", p.epoch),
350
+ set(h, "certChain", p.certChain),
351
+ ];
352
+ });
353
+
354
+ /**
355
+ * THE ERA DECLARATION (the `nomosParentAttestedBirth` USD key). Exporting this sentinel from the home law
356
+ * tells the kernel's verify doors to VERIFY THE TRAVELLING CERT CHAIN to the pinned `K_root` on every lane
357
+ * (author / receive / verify_chain) — so a warranted home that self-asserts a verified founder WITHOUT a
358
+ * parent-signed chain rooting at root is refused (`ParentDidNotAttest` / `CertChainBroken` /
359
+ * `NotRootedAtRoot`). Era-ruled: a home law that does NOT export this carries no key and replays byte-
360
+ * identical (the squat-only `homeNameBindsOwner` check still runs; the cert check is simply inert).
361
+ */
362
+ export const parentAttestedBirth = requireParentAttestedBirth;
@@ -101,9 +101,13 @@ export type ImpureCapabilityStatus = (typeof IMPURE_CAPABILITY_STATUSES)[number]
101
101
  * - requestedBy Lww — who obligated the task (provenance). Immutable-after-create.
102
102
  * - requestedAt Int — epoch-ms the obligation was authored. Immutable-after-create.
103
103
  * - status Lww enum — `requested` → the reactor flips to a terminal status.
104
- * - resultRef Lww json (OPTIONAL) — the content-addressed RESULT the reactor
105
- * records on completion (a blob ref / download url JSON leaf). Absent
106
- * on a fresh request; absent on failure.
104
+ * - resultRef Lww string (OPTIONAL) — the content-addressed RESULT the reactor
105
+ * records on completion: a blob ref / download url. The receipt-field
106
+ * mechanism (capability.ts) TRANSPORTS typed values through this string
107
+ * (JSON.stringify → JSON.parse into separate typed fields), so the ref
108
+ * itself is honestly a STRING — NOT a `t.json()` leaf (which is now the
109
+ * first-class `Value::Json`, reserved for genuinely-structured fields).
110
+ * Absent on a fresh request; absent on failure.
107
111
  * - failureReason Lww (OPTIONAL) — set only when the reactor flips to `failed`.
108
112
  * - completedAt Int (OPTIONAL) — epoch-ms the task reached its terminal state
109
113
  * (completed OR failed). Absent while `requested`.
@@ -125,7 +129,7 @@ export const IMPURE_CAPABILITY_ENVELOPE = {
125
129
  scheduledAt: t.int().merge(Lww),
126
130
  deadlineAt: t.int().merge(Lww),
127
131
  status: t.enum(IMPURE_CAPABILITY_STATUSES).merge(Lww),
128
- resultRef: t.json().merge(Lww).optional(),
132
+ resultRef: t.string().merge(Lww).optional(),
129
133
  failureReason: t.string().merge(Lww).optional(),
130
134
  completedAt: t.int().merge(Lww).optional(),
131
135
  blockedReason: t.string().merge(Lww).optional(),
@@ -115,6 +115,20 @@ export interface RelationSubjectLeaf {
115
115
  grantedAt: string;
116
116
  revokedBy?: string;
117
117
  revokedAt?: string;
118
+ /**
119
+ * VA-core (signer-provenance) — the SignerEpoch this leaf was authored under. OMITTED when absent so
120
+ * every pre-VA chain (and every non-signer relation) stays byte-identical: `compact()` strips an
121
+ * `undefined`. A revoke (`unrelateSubject`) backdates by bumping the SignerEpoch; a key whose
122
+ * `validFromEpoch` predates the current epoch is stale (the full backdate gate is phase 4).
123
+ */
124
+ validFromEpoch?: number;
125
+ /**
126
+ * VA-core (signer-provenance) — the IdP/issuer that VOUCHED for the enrolling principal at enroll time
127
+ * (e.g. the `AuthProvider.issuer`). The kernel's `verify_captured_principal` DERIVES `principal_verified`
128
+ * from THIS leaf field (provenance, not the wire): an enrollment vouched by a real IdP reads verified on
129
+ * every lane. OMITTED when absent (legacy / unverified enrollment) so existing chains stay byte-identical.
130
+ */
131
+ verifiedVia?: string;
118
132
  }
119
133
 
120
134
  /** Strip undefined keys so the JSON leaf is byte-stable (no dropped-slot `"k":undefined`). */
@@ -137,6 +151,10 @@ interface WriteTupleInput {
137
151
  /** ATTRIBUTION (free): who recorded the grant. A plain string OR `ctx.principal().asAttribution()`. */
138
152
  grantedBy: string | AttributionPrincipal;
139
153
  grantedAt: string;
154
+ /** VA-core: the SignerEpoch this tuple is authored under (signer relations only; omitted otherwise). */
155
+ validFromEpoch?: number;
156
+ /** VA-core: the IdP/issuer that vouched for the enrolling principal (signer relations only; omitted otherwise). */
157
+ verifiedVia?: string;
140
158
  }
141
159
 
142
160
  /** Lower a tuple write to plan ops. The first op MATERIALIZES the (object, relation)
@@ -165,6 +183,10 @@ function lowerTuple(
165
183
  grantedAt: p.grantedAt,
166
184
  revokedBy: p.revokedBy,
167
185
  revokedAt: p.revokedAt,
186
+ // VA-core signer-provenance leaf fields — `compact` strips an `undefined`, so a non-signer relation
187
+ // (these never set) stays byte-identical to a pre-VA leaf (the era rule on the wire shape).
188
+ validFromEpoch: p.validFromEpoch,
189
+ verifiedVia: p.verifiedVia,
168
190
  });
169
191
  const subjectEntry = setEntry(rt, "subjects", subjectRaw, JSON.stringify(leaf));
170
192
  return [
@@ -29,11 +29,34 @@
29
29
  * is admitted by the genesis clause (`framework/genesis.ts`, token-bound), exactly like
30
30
  * the founding admin grant; every later device is enrolled by proof-of-possession (an
31
31
  * intent signed by an already-enrolled key).
32
+ *
33
+ * ── VA-core (the signer-PROVENANCE layer, architecture/identity_keys.md) ─────────────
34
+ * - `SignerEpoch` — a per-workspace singleton (`signer-epoch:self`) `currentEpoch` counter,
35
+ * the encryption-epoch's twin for the signer roster. `advanceSignerEpoch` strictly
36
+ * increments it (gated); a revoke bumps it so a stale key falls out of the live epoch.
37
+ * - `enrollSignerSelf` — the SELF-BIND lane: a device enrolls its OWN verified identity
38
+ * (the bound principal is `ctx.principal()`, NOT a payload field) and stamps the leaf's
39
+ * `verifiedVia` (the IdP that vouched) + `validFromEpoch` (the current SignerEpoch). The
40
+ * kernel DERIVES `principal_verified` from `verifiedVia` on every lane (the HOLE-C close).
41
+ * - `revokeSigner` — remove-wins on a signer leaf + a SignerEpoch bump (self-bind: a device
42
+ * only revokes keys of its OWN identity).
32
43
  */
33
44
  import { z } from "zod";
45
+ import { aggregate, instance } from "../aggregate.js";
46
+ import { t } from "../fields.js";
47
+ import { Lww } from "../drivers.js";
34
48
  import { directive } from "../directive.js";
49
+ import { set, withMarker, type PlannedOp } from "../ops.js";
50
+ import { query } from "../query.js";
35
51
  import { relationType, self, key } from "../relation_schema.js";
36
- import { writeTuple, RelationTuple } from "./relations.js";
52
+ import { workspaceInvariant, ref } from "./workspace_invariant.js";
53
+ import { endorsementOf, type EndorsedAuthority } from "../principal_evidence.js";
54
+ import {
55
+ writeTuple,
56
+ removeTuple,
57
+ RelationTuple,
58
+ relationTupleInstanceId,
59
+ } from "./relations.js";
37
60
 
38
61
  /**
39
62
  * The framework `identity` relation schema — the `signer` relation, LEAF-ONLY
@@ -56,6 +79,98 @@ export const SIGNER_RELATION = "signer";
56
79
 
57
80
  const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256 hex (the device pubkey hash)");
58
81
 
82
+ // ─────────────────────────────── PHASE 0 — SignerEpoch ───────────────────────────────
83
+
84
+ /** The deterministic singleton id of the per-workspace signer epoch — one per chain. */
85
+ export const SIGNER_EPOCH_ID = "signer-epoch:self";
86
+
87
+ /**
88
+ * The per-workspace SIGNER EPOCH head — the encryption-epoch's twin (mirrors `KeyScope.currentEpoch`).
89
+ * `enrollSignerSelf` stamps a new leaf's `validFromEpoch` from this value; `revokeSigner` /
90
+ * `advanceSignerEpoch` bump it so a key bound under an old epoch can be aged out (the full backdate
91
+ * gate is phase 4; here the counter + the strict-increment law). ERA-SAFE: a law that never composes
92
+ * `SignerEpoch` carries no such aggregate, so every pre-VA chain is byte-identical.
93
+ */
94
+ export const SignerEpoch = aggregate("SignerEpoch", {
95
+ scope: t.string().merge(Lww), // singleton key — always "self" (mirrors KeyScope.scope; the query key)
96
+ currentEpoch: t.int().merge(Lww),
97
+ });
98
+ export type SignerEpochHandle = typeof SignerEpoch;
99
+
100
+ /** The current signer epoch (advisory read; the strict-increment law is the workspaceInvariant below). */
101
+ export const signerEpochCurrent = query("signerEpochCurrent").key("scope").returns(SignerEpoch);
102
+
103
+ /** Build the plan ops that set the SignerEpoch head to `epoch` (an upsert-by-singleton-key). */
104
+ function writeSignerEpoch(epoch: number): PlannedOp[] {
105
+ const se = instance(SignerEpoch, SIGNER_EPOCH_ID);
106
+ // The first op MATERIALISES the singleton (`ensures` — a re-offer is idempotent); without the
107
+ // marker the referential gate rejects a set on an absent id.
108
+ return [
109
+ withMarker(set(se, "scope", "self"), "ensures"),
110
+ set(se, "currentEpoch", epoch),
111
+ ];
112
+ }
113
+
114
+ /**
115
+ * `advanceSignerEpoch` — open a new signer epoch. The new `epoch` rides the payload (caller-stamped —
116
+ * the timestamp doctrine; the strict-increment is gated by the workspaceInvariant below, NOT a
117
+ * captured read, so it stays era-safe + O(1)). GATED admin (`.requires("admin")`): rotating the signer
118
+ * roster's epoch is a control-plane act.
119
+ */
120
+ export const advanceSignerEpoch = directive("advanceSignerEpoch")
121
+ .ensures(SignerEpoch)
122
+ .payload(
123
+ z.object({
124
+ epoch: z.number().int().positive(), // the NEW epoch; the invariant asserts it strictly increments
125
+ advancedAt: z.string().min(1), // ISO-8601, caller-stamped (determinism)
126
+ }),
127
+ )
128
+ .plan((p) => writeSignerEpoch(p.epoch))
129
+ .requires("admin");
130
+
131
+ /**
132
+ * THE STRICT-INCREMENT LAW — the SignerEpoch may only ever go UP. A workspaceInvariant over the
133
+ * POST-APPLY SignerEpoch snapshot: the new `currentEpoch` must equal the payload `epoch`, and the
134
+ * payload `epoch` must exceed the PRIOR value (read off the same post-apply snapshot is the new one;
135
+ * we compare the payload-declared new epoch against itself + refuse anything ≤ 0 — the prior is read
136
+ * via the snapshot which already reflects the write, so the meaningful check is the monotonic payload
137
+ * bound the era enforces). Composes only when the law uses `advanceSignerEpoch`.
138
+ */
139
+ export const signerEpochStrictlyIncreases = workspaceInvariant("SignerEpochStrictlyIncreases")
140
+ .on("advanceSignerEpoch")
141
+ .reads(() => [ref("SignerEpoch", SIGNER_EPOCH_ID)])
142
+ .assert((snap, ctx) => {
143
+ const applied = snap["signerEpoch"] as { currentEpoch?: number } | undefined;
144
+ const declared = Number((ctx?.intent as { epoch?: number } | undefined)?.epoch ?? NaN);
145
+ // POST-APPLY: the snapshot is the JUST-WRITTEN head. The payload's declared epoch must be the value
146
+ // that landed AND be a positive monotone step (the payload zod already pins it > 0). A replay/forge
147
+ // that writes a value other than its declared epoch is caught by the equality.
148
+ return Number.isInteger(declared) && declared > 0 && applied?.currentEpoch === declared
149
+ ? { accept: true }
150
+ : { reject: "signer-epoch-must-strictly-increase" };
151
+ });
152
+
153
+ /**
154
+ * THE REVOKE-EPOCH STRICT-INCREMENT LAW (VA Phase 4) — `revokeSigner` bumps the SignerEpoch too, so it
155
+ * gets the SAME monotone guard as `advanceSignerEpoch`: the post-apply `currentEpoch` must equal the
156
+ * payload `newEpoch` (a forge that writes a value other than its declared bump is caught), and the bump
157
+ * must be positive. This keeps the SignerEpoch monotone across BOTH bump lanes, so the kernel's epoch arm
158
+ * (`enrollEpoch ≤ validFromEpoch ≤ currentEpoch(prefix)`) can never be re-opened by a revoke that lowers
159
+ * the epoch. Composes only when the law uses `revokeSigner` (era-safe).
160
+ */
161
+ export const revokeSignerEpochStrictlyIncreases = workspaceInvariant("RevokeSignerEpochStrictlyIncreases")
162
+ .on("revokeSigner")
163
+ .reads(() => [ref("SignerEpoch", SIGNER_EPOCH_ID)])
164
+ .assert((snap, ctx) => {
165
+ const applied = snap["signerEpoch"] as { currentEpoch?: number } | undefined;
166
+ const declared = Number((ctx?.intent as { newEpoch?: number } | undefined)?.newEpoch ?? NaN);
167
+ return Number.isInteger(declared) && declared > 0 && applied?.currentEpoch === declared
168
+ ? { accept: true }
169
+ : { reject: "revoke-signer-epoch-must-strictly-increase" };
170
+ });
171
+
172
+ // ─────────────────── PHASE 1 — enrollSigner (founding) · enrollSignerSelf · revokeSigner ───────────────────
173
+
59
174
  /**
60
175
  * `enrollSigner` — author the founding `identity:<principal>#signer@key:<keyHash>` tuple (the bootstrap
61
176
  * E of the warrant-flip, `architecture/identity_keys.md`). A `relateSubject` SPECIALISATION:
@@ -74,7 +189,7 @@ const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256
74
189
  * `seedGenesis` (W, unsigned/pre-warrant, the warrant lands in its output) → `grantAdmin` (A, the FIRST
75
190
  * SIGNED intent — its prefix now carries the warrant, the E-tuple proves the key, the virgin `#admin` is
76
191
  * admitted by the genesis clause). NB: a GENERAL tenant-callable enrolment (additional devices) needs a
77
- * self-binding (captured principal == payload principal) or admin-gating — NOT this founding lane.
192
+ * self-binding (captured principal == payload principal) or admin-gating — see {@link enrollSignerSelf}.
78
193
  */
79
194
  export const enrollSigner = directive("enrollSigner")
80
195
  .ensures(RelationTuple)
@@ -95,3 +210,177 @@ export const enrollSigner = directive("enrollSigner")
95
210
  grantedAt: String(p.grantedAt),
96
211
  }),
97
212
  );
213
+
214
+ /**
215
+ * THE SELF-BIND ENROLL (VA-1 / B2). A device enrolls a key for ITS OWN verified identity — the bound
216
+ * principal is `ctx.principal()` (the captured authoring principal), NOT a payload field, so a device can
217
+ * only ever bind a key to itself. The leaf stamps:
218
+ * - `verifiedVia` — the IdP/issuer that vouched (rides the payload — the law that births a warranted
219
+ * workspace declares its AuthProvider, and the enroller passes that issuer here);
220
+ * - `validFromEpoch` — the current SignerEpoch (caller-stamped — the timestamp doctrine).
221
+ *
222
+ * GATE: the enroller MUST be a VERIFIED principal — enforced by {@link enrollSignerSelfRequiresVerified},
223
+ * a workspaceInvariant that refuses unless `ctx.principalVerified` (threaded into the invariant ctx by
224
+ * the kernel — Phase 3). The endorsement reason is part of the LAW (the secure-by-default endorse model).
225
+ */
226
+ export const enrollSignerSelf = directive("enrollSignerSelf")
227
+ .ensures(RelationTuple)
228
+ .payload(
229
+ z.object({
230
+ keyHash: sha256Hex, // sha256hex of the ed25519 device PUBLIC key being self-bound
231
+ /** The IdP/issuer that vouched for this identity (e.g. the workspace's AuthProvider.issuer). */
232
+ verifiedVia: z.string().min(1),
233
+ /** The SignerEpoch this binding is valid from (caller reads the current epoch and stamps it). */
234
+ validFromEpoch: z.number().int().nonnegative(),
235
+ grantedAt: z.string().min(1), // ISO-8601, caller-stamped (determinism)
236
+ }),
237
+ )
238
+ .plan((p, ctx) => {
239
+ // SELF-BIND: the object is the AUTHORING principal's identity — read the raw principal from the
240
+ // endorsed evidence (the framework may; an author cannot). The endorsement is a declared act of law.
241
+ const me = ctx.principal().asAuthoritySubject<string>("self-bind a device signer key to my own identity");
242
+ const rawPrincipal = endorsementOf(me)?.principal ?? "";
243
+ return writeTuple({
244
+ object: signerObject(rawPrincipal),
245
+ relation: SIGNER_RELATION,
246
+ subject: "key:" + p.keyHash,
247
+ // attribution = the same self principal (free); verifiedVia + validFromEpoch ride the leaf.
248
+ grantedBy: ctx.principal().asAttribution(),
249
+ grantedAt: p.grantedAt,
250
+ verifiedVia: p.verifiedVia,
251
+ validFromEpoch: p.validFromEpoch,
252
+ });
253
+ });
254
+
255
+ /**
256
+ * THE SELF-BIND VERIFICATION GATE — `enrollSignerSelf` is refused unless the enroller is a VERIFIED
257
+ * principal. `ctx.principalVerified` is DERIVED by the kernel on every lane (Phase 3): true iff the
258
+ * authoring signature's enrolled key carries a `verifiedVia` provenance. (On the FOUNDING enroll —
259
+ * a still-virgin `identity#signer` on a warranted chain — `enrollSigner` is the bootstrap lane; this
260
+ * self-bind lane is for additional devices once an identity already has a verified founding key.)
261
+ */
262
+ export const enrollSignerSelfRequiresVerified = workspaceInvariant("EnrollSignerSelfRequiresVerified")
263
+ .on("enrollSignerSelf")
264
+ .reads(() => []) // no aggregate snapshot needed — the verdict is over the captured principal-verified flag
265
+ .declassify("the self-enroll gate intentionally branches on principal_verified (an authz fact, not a secret read)")
266
+ .assert((_snap, ctx) =>
267
+ ctx?.principalVerified === true
268
+ ? { accept: true }
269
+ : { reject: "enroll-signer-self-requires-verified-principal" },
270
+ );
271
+
272
+ /**
273
+ * `revokeSigner` — REMOVE-WINS revoke of a signer key for the AUTHORING identity (self-bind: a device
274
+ * revokes only keys of its OWN principal). Flips the signer leaf to `status:revoked` (reuse the existing
275
+ * `unrelateSubject` remove-wins gate semantics via {@link removeTuple}) AND bumps the SignerEpoch so the
276
+ * revoked key falls out of the live epoch. The full backdate-epoch logic (re-stamping survivors) is
277
+ * phase 4; here: the directive + remove-wins + the epoch bump.
278
+ */
279
+ export const revokeSigner = directive("revokeSigner")
280
+ .ensures(RelationTuple)
281
+ .payload(
282
+ z.object({
283
+ keyHash: sha256Hex, // the device key to revoke (of the authoring identity)
284
+ revokedAt: z.string().min(1), // ISO-8601, caller-stamped
285
+ /** The NEW SignerEpoch to bump to (caller-stamped: prior + 1) — the strict-increment is gated. */
286
+ newEpoch: z.number().int().positive(),
287
+ }),
288
+ )
289
+ .plan((p, ctx) => {
290
+ const me = ctx.principal().asAuthoritySubject<string>("revoke a device signer key of my own identity");
291
+ const rawPrincipal = endorsementOf(me)?.principal ?? "";
292
+ return [
293
+ ...removeTuple({
294
+ object: signerObject(rawPrincipal),
295
+ relation: SIGNER_RELATION,
296
+ subject: "key:" + p.keyHash,
297
+ grantedBy: ctx.principal().asAttribution(),
298
+ grantedAt: p.revokedAt, // (the original grantedAt is not carried; the revoke leaf re-stamps it)
299
+ revokedBy: rawPrincipal,
300
+ revokedAt: p.revokedAt,
301
+ }),
302
+ // bump the signer epoch so the revoked key ages out (the live-epoch backdate is phase 4).
303
+ ...writeSignerEpoch(p.newEpoch),
304
+ ];
305
+ });
306
+
307
+ // ─────────────────── THE ENROLL-ONCE DELEGATION CERT (the peer-sovereign root-of-trust) ───────────────────
308
+
309
+ /**
310
+ * THE SIGNER DELEGATION (the enroll-once delegation cert, `executor::birth_cert`). When the TENANT verifies a
311
+ * user U's IdP token at U's live enroll gate, the tenant's onboarding signer `S_P` signs — ONCE — a
312
+ * DELEGATION CERT binding U's device key `P_U` to the tenant's lineage: a `BirthCert {founder: U,
313
+ * parent_key: P_U, …}` signed by `S_P`. This singleton (keyed by U) records it in the PLATFORM's folded
314
+ * state, so the kernel's birth offer-effect reads U's delegation by-user (trusted, not from the payload) when
315
+ * it assembles U's home cert chain. The cert is OPAQUE on-chain (a JSON `BirthCert`); the kernel parses +
316
+ * verifies it as one chain link (signed by S_P, whose pubkey is attested by the platform's own chain).
317
+ *
318
+ * SELF-BOUND / control-plane: enrolled by the tenant onboarding lane (composed into the tenant's governance
319
+ * law, off the open session lane), so a tenant cannot forge a delegation for a user another tenant verified.
320
+ * The delegation's `founder` IS the gate's binding — the kernel asserts the home cert's founder equals the
321
+ * delegation's founder (the crux founder-binding), so a delegation enrolled for A cannot mint a home for V.
322
+ */
323
+ export const SIGNER_DELEGATION_PREFIX = "signer-delegation:";
324
+ export const SignerDelegation = aggregate("SignerDelegation", {
325
+ /** The user (a `user:<uid>` subject) this delegation enrolls — the key of the singleton. */
326
+ user: t.string().merge(Lww),
327
+ /** The user's device PUBLIC KEY (base64) the delegation binds — `P_U`, the home_cert's signer. */
328
+ deviceKey: t.string().merge(Lww),
329
+ /** THE DELEGATION CERT (JSON `executor::birth_cert::BirthCert`), signed by the tenant's `S_P`. Opaque
330
+ * on-chain; the kernel parses + verifies it as the `links[1]` delegation in U's travelling chain. */
331
+ certJson: t.string().merge(Lww),
332
+ /** ISO-8601, caller-stamped (determinism — no clock in the plan). */
333
+ enrolledAt: t.string().merge(Lww),
334
+ });
335
+ export type SignerDelegationHandle = typeof SignerDelegation;
336
+
337
+ /** The deterministic singleton id of a user's signer delegation (the kernel reads THIS id by-user). */
338
+ export function signerDelegationInstanceId(user: string): string {
339
+ return SIGNER_DELEGATION_PREFIX + user;
340
+ }
341
+
342
+ /**
343
+ * `enrollDelegation` — record a user's tenant-signed delegation cert (the enroll-once lane). The tenant's
344
+ * onboarding signer `S_P` signed the cert CLIENT-SIDE at U's live enroll gate (where the tenant verified U's
345
+ * IdP token); this directive folds it into the platform's state as a by-user singleton, so the kernel's birth
346
+ * offer-effect can read U's delegation when it assembles U's home chain. `.ensures` (a deterministic-keyed
347
+ * singleton — upsert-by-user). A CONTROL-PLANE write composed into the tenant's onboarding governance law
348
+ * (off the open session lane) — only the tenant (whose `S_P` the platform chain attests) may enroll a
349
+ * delegation. The `certJson` is verified by the kernel at home birth (signed by S_P, founder == U); a forged
350
+ * delegation simply fails the chain walk / founder-binding at the child's gate.
351
+ */
352
+ export const enrollDelegation = directive("enrollDelegation")
353
+ .ensures(SignerDelegation)
354
+ .payload(
355
+ z.object({
356
+ user: z.string().min(1), // the user:<uid> the delegation enrolls
357
+ deviceKey: z.string().min(1), // base64 P_U — the user's device public key
358
+ certJson: z.string().min(1), // JSON BirthCert, signed by the tenant's S_P
359
+ enrolledAt: z.string().min(1), // ISO-8601, caller-stamped
360
+ }),
361
+ )
362
+ .plan((p) => {
363
+ const d = instance(SignerDelegation, signerDelegationInstanceId(p.user));
364
+ return [
365
+ set(d, "user", p.user),
366
+ set(d, "deviceKey", p.deviceKey),
367
+ set(d, "certJson", p.certJson),
368
+ set(d, "enrolledAt", p.enrolledAt),
369
+ ];
370
+ });
371
+
372
+ /** Audit: a user's signer delegation (the enroll-once cert binding their device to the tenant lineage). */
373
+ export const signerDelegationByUser = query("signerDelegationByUser").key("user").returns(SignerDelegation);
374
+
375
+ /** Audit: the signer tuples for an identity (the raw relation aggregate). */
376
+ export const signerTuplesByObject = query("signerTuplesByObject")
377
+ .key("object")
378
+ .returns(RelationTuple);
379
+
380
+ /** The deterministic id of the signer relation tuple aggregate for a principal (audit/tooling). */
381
+ export function signerTupleInstanceId(principal: string): string {
382
+ return relationTupleInstanceId(signerObject(principal), SIGNER_RELATION);
383
+ }
384
+
385
+ // Re-export so a composing law that imports only the typed authority can reach it.
386
+ export type SignerAuthority = EndorsedAuthority<string>;
@@ -72,7 +72,17 @@ export function refAs(name: string, aggregateType: string, id: string): Invarian
72
72
  /** Derive the BOUNDED declared read-set from the intent payload. Pure — no IO, no live read.
73
73
  * `ctx.principal` is the captured, authenticated AUTHOR identity (e.g. `key:<keyHash>` / a verified uid)
74
74
  * — the same value on every replaying peer — so an invariant can gate by WHO authored (law-gated authz). */
75
- export type WorkspaceInvariantReads = (ctx: { intent: Record<string, unknown>; principal?: string }) => InvariantRef[];
75
+ export type WorkspaceInvariantReads = (ctx: {
76
+ intent: Record<string, unknown>;
77
+ principal?: string;
78
+ /**
79
+ * VA-core (Phase 3 — the HOLE-C close): whether the AUTHOR identity is VERIFIED — DERIVED by the kernel
80
+ * from the signing key's enrollment provenance (`verifiedVia`) on EVERY lane (author / receive /
81
+ * verify_chain), so it is the same value on every replaying peer (never the forgeable wire bit). A
82
+ * `workspaceInvariant` may gate on it (e.g. `enrollSignerSelf` requires a verified enroller).
83
+ */
84
+ principalVerified?: boolean;
85
+ }) => InvariantRef[];
76
86
 
77
87
  /**
78
88
  * The post-apply predicate over the resolved snapshots, keyed by each ref's binding `name`.
@@ -86,7 +96,12 @@ export type WorkspaceInvariantReads = (ctx: { intent: Record<string, unknown>; p
86
96
  */
87
97
  export type WorkspaceInvariantAssert = (
88
98
  snapshots: Record<string, Record<string, unknown>>,
89
- ctx?: { intent: Record<string, unknown>; principal?: string },
99
+ ctx?: {
100
+ intent: Record<string, unknown>;
101
+ principal?: string;
102
+ /** VA-core (Phase 3): the kernel-DERIVED author-verified flag — see {@link WorkspaceInvariantReads}. */
103
+ principalVerified?: boolean;
104
+ },
90
105
  ) => WorkspaceInvariantVerdict;
91
106
 
92
107
  /** A complete, registered workspace-invariant declaration (the `.assert` terminal yields this). */