@githolon/dsl 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/aggregate.ts +19 -16
- package/src/build_package.ts +7 -0
- package/src/codegen_dart.ts +25 -0
- package/src/codegen_ts.ts +18 -10
- package/src/compile_package_main.ts +10 -0
- package/src/directive.ts +102 -16
- package/src/engine_entry.ts +8 -2
- package/src/fields.ts +24 -2
- package/src/framework/encryption_identity.ts +3 -3
- package/src/framework/genesis.ts +88 -0
- package/src/framework/home.ts +149 -0
- package/src/framework/impure_capability.ts +3 -3
- package/src/framework/relations.ts +28 -4
- package/src/framework/signing_identity.ts +97 -0
- package/src/framework/workspace_invariant.ts +5 -3
- package/src/framework/workspaces.ts +158 -103
- package/src/index.ts +88 -19
- package/src/manifest.ts +0 -6
- package/src/ops.ts +50 -1
- package/src/relation_schema.ts +298 -0
- package/src/relation_sugar.ts +324 -0
- package/src/usd.ts +202 -7
- package/src/usd_layers.ts +81 -0
- package/src/wire.ts +18 -3
- package/src/wire_encode.ts +28 -2
- package/src/framework/rbac.ts +0 -418
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
|
|
4
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* framework/genesis.ts — THE GENESIS WARRANT (the bootstrap base case;
|
|
8
|
+
* `architecture/identity_keys.md` step 0, `home_holons_and_cross_custody.md` §2).
|
|
9
|
+
*
|
|
10
|
+
* THE BOOTSTRAP PARADOX: the relation gate refuses a founding control-plane grant
|
|
11
|
+
* on a VIRGIN chain (an empty `#admin`/`#creator`/… roster) — there is no admin yet
|
|
12
|
+
* to gate the first admin. Resolved LAWFULLY (not by a host bypass): at birth the
|
|
13
|
+
* host verifies the `x-nomos-auth` token ONCE and records the verified principal as
|
|
14
|
+
* an in-history fact — this `GenesisWarrant`. The gate's GENESIS CLAUSE
|
|
15
|
+
* (`executor::genesis_admits`) then admits a founding grant on a virgin object IFF
|
|
16
|
+
* the warrant binds the chain to the captured principal. Strictly additive (only an
|
|
17
|
+
* otherwise-`Unauthorized` virgin grant), so every sealed chain replays unchanged.
|
|
18
|
+
*
|
|
19
|
+
* CONTROL-PLANE: `seedGenesis` is authored by the BIRTH DOOR (the host, after the
|
|
20
|
+
* one-time token verification), NOT by the open session lane — the session lane
|
|
21
|
+
* structurally refuses control-plane intents, so a tenant cannot forge a warrant for
|
|
22
|
+
* a principal it does not own. The warrant is the host's recorded verification, not
|
|
23
|
+
* a host authority decision: every replaying peer re-reads the chain fact.
|
|
24
|
+
*
|
|
25
|
+
* SINGLETON per chain: one founding principal per home/workspace, so the warrant is
|
|
26
|
+
* a singleton keyed `genesis-warrant:self`. `seedGenesis` is create-once — a second
|
|
27
|
+
* seed is a dup-create refusal, so the birth principal is fixed forever-on-chain.
|
|
28
|
+
*/
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
import { aggregate, instance } from "../aggregate.js";
|
|
31
|
+
import { t } from "../fields.js";
|
|
32
|
+
import { Lww } from "../drivers.js";
|
|
33
|
+
import { directive } from "../directive.js";
|
|
34
|
+
import { set, type PlannedOp } from "../ops.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Deterministic singleton id of the founding warrant — MUST equal
|
|
38
|
+
* `executor::GENESIS_WARRANT_ID` (the gate reads this exact key). One per chain.
|
|
39
|
+
*/
|
|
40
|
+
export const GENESIS_WARRANT_ID = "genesis-warrant:self";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The founding-warrant record. It does THREE jobs at once:
|
|
44
|
+
* - `principal` — the x-nomos-auth-verified OWNER (the genesis clause binds the founding grant to it);
|
|
45
|
+
* - `workspaceId` — this workspace's UNIQUE SELF-ID (the source binding the author signature covers,
|
|
46
|
+
* re-derived locally by the gate — NOT the shared/deterministic genesis commit, which is identical
|
|
47
|
+
* across all workspaces and so bound nothing);
|
|
48
|
+
* - its mere PRESENCE — the SIGNED-AUTHOR REGIME signal (the "era"): a workspace with a warrant
|
|
49
|
+
* REQUIRES a verified author block (forge closed); one without falls back to the legacy
|
|
50
|
+
* host-stamped principal (pre-feature chains replay green, per-position).
|
|
51
|
+
*/
|
|
52
|
+
export const GenesisWarrant = aggregate("GenesisWarrant", {
|
|
53
|
+
principal: t.string().merge(Lww), // the x-nomos-auth-verified owner, recorded at birth
|
|
54
|
+
workspaceId: t.string().merge(Lww), // this workspace's unique self-id (the author-signature source binding)
|
|
55
|
+
bornAt: t.string().merge(Lww), // ISO-8601, caller-stamped (determinism)
|
|
56
|
+
});
|
|
57
|
+
export type GenesisWarrantHandle = typeof GenesisWarrant;
|
|
58
|
+
|
|
59
|
+
/** The framework PRIMITIVE plan: record the founding warrant (singleton). */
|
|
60
|
+
export function writeGenesisWarrant(p: { principal: string; workspaceId: string; bornAt: string }): PlannedOp[] {
|
|
61
|
+
const w = instance(GenesisWarrant, GENESIS_WARRANT_ID);
|
|
62
|
+
return [
|
|
63
|
+
set(w, "principal", p.principal),
|
|
64
|
+
set(w, "workspaceId", p.workspaceId),
|
|
65
|
+
set(w, "bornAt", p.bornAt),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `seedGenesis` — the birth-door directive that records the verified founding principal + this
|
|
71
|
+
* workspace's unique self-id at the FIXED singleton id `genesis-warrant:self`. `.ensures` (NOT
|
|
72
|
+
* `.creates`): the warrant is a deterministic-KEYED singleton, not a minted record — `.creates` would
|
|
73
|
+
* demand a runtime-minted `<Type>_<uuid>` id and the id-mint gate refuses a hand-written one. The
|
|
74
|
+
* gate reads the warrant at THIS exact key, so the id is law, not minted. A second seed UPSERTs the
|
|
75
|
+
* same leaf rather than minting a duplicate; re-seeding is itself prevented out-of-band — seedGenesis
|
|
76
|
+
* is a CONTROL-PLANE birth-door write (the open session lane refuses it) and post-genesis bootstrap
|
|
77
|
+
* intents are refused once the lifecycle controller is active.
|
|
78
|
+
*/
|
|
79
|
+
export const seedGenesis = directive("seedGenesis")
|
|
80
|
+
.ensures(GenesisWarrant)
|
|
81
|
+
.payload(
|
|
82
|
+
z.object({
|
|
83
|
+
principal: z.string().min(1),
|
|
84
|
+
workspaceId: z.string().min(1),
|
|
85
|
+
bornAt: z.string().min(1),
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
.plan((p) => writeGenesisWarrant(p));
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
|
|
4
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* framework/home.ts — THE HOME HOLON (the identity role; `architecture/home_holons_and_cross_custody.md`
|
|
8
|
+
* §4). One holon, three roles — "workspace," "platform," "home" are the SAME githolon wearing the role
|
|
9
|
+
* its installed LAW gives it. A HOME holon runs THIS law: an identity's subject-side root.
|
|
10
|
+
*
|
|
11
|
+
* It holds only PUBLIC material + signed facts (private keys never leave the client, so the cloud hosting
|
|
12
|
+
* it stays operator-blind):
|
|
13
|
+
* - KEY DIRECTORY — the identity's public device keys: the `signer` relation
|
|
14
|
+
* (`identity:<principal>#signer@key:<hash>`, `framework/signing_identity.ts`). Re-exported here so a
|
|
15
|
+
* home holon's law carries it: "wrap the scope key to U's device" becomes "read U's home key roster."
|
|
16
|
+
* - SHARED-WITH-ME index (§5) — one record per verified cross-custody attestation delivered here, so
|
|
17
|
+
* "show me everything shared with me" is a LOCAL query, no fan-out.
|
|
18
|
+
* - INBOX — grants/invites/notifications/dead-letters addressed to the identity.
|
|
19
|
+
*
|
|
20
|
+
* THE FORWARD-CHECK DOCTRINE (§5): the index is a DISCOVERY HINT, not the authority. When the identity
|
|
21
|
+
* actually opens the asserting workspace, THAT workspace's gate does the live `Check` — so a stale share
|
|
22
|
+
* (the asserter revoked it) is fail-safe: the identity sees a workspace it can no longer open, the forward
|
|
23
|
+
* check denies, the hint is pruned. (The cross-custody ATTESTATION that proves a delivered share is signed
|
|
24
|
+
* + verifiable is §3; the host DELIVERY lane that fills the index is §5. This file is the law that holds
|
|
25
|
+
* the result, and the owner-authored lane to maintain it.)
|
|
26
|
+
*/
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { aggregate, instance } from "../aggregate.js";
|
|
29
|
+
import { t } from "../fields.js";
|
|
30
|
+
import { Lww } from "../drivers.js";
|
|
31
|
+
import { directive } from "../directive.js";
|
|
32
|
+
import { create } from "../authoring.js";
|
|
33
|
+
import { set, type PlannedOp } from "../ops.js";
|
|
34
|
+
import { query } from "../query.js";
|
|
35
|
+
import { identityRelations } from "./signing_identity.js";
|
|
36
|
+
|
|
37
|
+
/** The KEY DIRECTORY is the `signer` relation — re-exported so a home holon's law composes it (the
|
|
38
|
+
* identity's public device keys, enrolled via `enrollSigner`, checked by the gate). */
|
|
39
|
+
export { identityRelations };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* THE HOME OWNER (home_holons §5 — the owner stamp). A home holon is born UNWARRANTED (the subject may
|
|
43
|
+
* never have logged in — a share can be delivered to a stranger's lazily-born home, before they hold any
|
|
44
|
+
* device key, so there is no signed-author regime yet). This singleton records WHO the home belongs to: the
|
|
45
|
+
* §3 cross-custody gate binds a delivered quote's subject to THIS principal (`executor::HOME_OWNER_ID`), so
|
|
46
|
+
* a quote signed for anyone else fails closed. Seeded ONCE at birth by the host (the control-plane
|
|
47
|
+
* `seedHomeOwner`, kept off the open session lane), so a tenant cannot forge ownership.
|
|
48
|
+
*/
|
|
49
|
+
export const HOME_OWNER_ID = "home-owner:self";
|
|
50
|
+
export const HomeOwner = aggregate("HomeOwner", {
|
|
51
|
+
/** The identity this home belongs to — the canonical `user:<uid>` subject the §3 gate binds quotes to. */
|
|
52
|
+
principal: t.string().merge(Lww),
|
|
53
|
+
/** ISO-8601, caller-stamped (determinism — no clock in the plan). */
|
|
54
|
+
bornAt: t.string().merge(Lww),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** The framework PRIMITIVE plan: stamp the home's owner (singleton at the fixed id the gate reads). */
|
|
58
|
+
export function writeHomeOwner(p: { principal: string; bornAt: string }): PlannedOp[] {
|
|
59
|
+
const h = instance(HomeOwner, HOME_OWNER_ID);
|
|
60
|
+
return [set(h, "principal", p.principal), set(h, "bornAt", p.bornAt)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* `seedHomeOwner` — the birth-door directive that stamps the home's owner at the fixed singleton id. A
|
|
65
|
+
* CONTROL-PLANE write: the host authors it once at lazy birth (after resolving who the home is for); the
|
|
66
|
+
* open session lane structurally REFUSES it (by directive id, like the §6 handoff seals), so a tenant
|
|
67
|
+
* cannot overwrite ownership. `.ensures` (a deterministic-keyed singleton, not a minted record).
|
|
68
|
+
*/
|
|
69
|
+
export const seedHomeOwner = directive("seedHomeOwner")
|
|
70
|
+
.ensures(HomeOwner)
|
|
71
|
+
.payload(z.object({ principal: z.string().min(1), bornAt: z.string().min(1) }))
|
|
72
|
+
.plan((p) => writeHomeOwner(p));
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* THE SHARED-WITH-ME INDEX (§5) — one record per cross-custody attestation delivered to this home holon:
|
|
76
|
+
* a signed edge `<asserterWorkspace> : <object> # <relation> @ this-identity`, indexed LOCALLY so the
|
|
77
|
+
* access list is a local query (no fan-out across every resource holon). A DISCOVERY HINT — the live authz
|
|
78
|
+
* stays the forward `Check` on the asserting holon when the identity opens it.
|
|
79
|
+
*/
|
|
80
|
+
export const SharedWithMe = aggregate("SharedWithMe", {
|
|
81
|
+
/** The holon that signed + delivered this edge — WHERE the live forward `Check` runs when opened. */
|
|
82
|
+
asserterWorkspace: t.string().merge(Lww),
|
|
83
|
+
/** The shared OBJECT — the whole workspace (`workspace:self`) or a specific record (`Entry:<id>`). */
|
|
84
|
+
object: t.string().merge(Lww),
|
|
85
|
+
/** The relation granted (`reader` / `editor` / a read cap). */
|
|
86
|
+
relation: t.string().merge(Lww),
|
|
87
|
+
/** The asserter's OWNER principal — whose key signed the attestation (verified at §3 admission). */
|
|
88
|
+
asserterPrincipal: t.string().merge(Lww),
|
|
89
|
+
/** ISO-8601, caller-stamped (determinism — no clock in the plan). */
|
|
90
|
+
attestedAt: t.string().merge(Lww),
|
|
91
|
+
/** PRUNED (forward-check fail-safe): set when the live `Check` on the asserter denies — the hint is
|
|
92
|
+
* dropped from the list but the audit row stays. `""` = live. */
|
|
93
|
+
prunedAt: t.string().merge(Lww),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `recordShare` — index a cross-custody share into the home holon's access list. AUTHORED on the home
|
|
98
|
+
* holon (the identity's own subject-side root). The payload carries the §3 CROSS-CUSTODY ATTESTATION — a
|
|
99
|
+
* SIGNED QUOTE: the asserter's device `publicKey` + a `signature` over the edge
|
|
100
|
+
* `<asserterWorkspace>:<object>#<relation>@<this-home-owner>`. The home holon's OWN gate verifies it
|
|
101
|
+
* (`executor::record_share_attestation_violation`, bound to the warrant owner), so the BAILIFF can deliver
|
|
102
|
+
* it HOST-FREE (the host carries bytes, vouches nothing — the gate vouches). The owner lane authors the
|
|
103
|
+
* SAME shape (the owner holds the quote the asserter gave it). The signature is a verification input, not
|
|
104
|
+
* stored — only the edge facts fold into the index. The live authority stays the forward `Check` on the
|
|
105
|
+
* asserter when the identity opens it (§5 fail-safe); a stale/forged quote can't enter the list at all.
|
|
106
|
+
*/
|
|
107
|
+
export const recordShare = directive("recordShare")
|
|
108
|
+
.creates(SharedWithMe)
|
|
109
|
+
.payload(
|
|
110
|
+
z.object({
|
|
111
|
+
asserterWorkspace: z.string().min(1),
|
|
112
|
+
object: z.string().min(1),
|
|
113
|
+
relation: z.string().min(1),
|
|
114
|
+
asserterPrincipal: z.string().min(1),
|
|
115
|
+
attestedAt: z.string().min(1),
|
|
116
|
+
/** The asserter's device PUBLIC KEY (base64) — what the §3 gate verifies the quote against. */
|
|
117
|
+
publicKey: z.string().min(1),
|
|
118
|
+
/** The asserter's SIGNATURE (base64) over the edge bytes, subject = this home's owner. */
|
|
119
|
+
signature: z.string().min(1),
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
.plan((p) => {
|
|
123
|
+
create(SharedWithMe)
|
|
124
|
+
.set("asserterWorkspace", p.asserterWorkspace)
|
|
125
|
+
.set("object", p.object)
|
|
126
|
+
.set("relation", p.relation)
|
|
127
|
+
.set("asserterPrincipal", p.asserterPrincipal)
|
|
128
|
+
.set("attestedAt", p.attestedAt)
|
|
129
|
+
.set("prunedAt", "");
|
|
130
|
+
return [];
|
|
131
|
+
})
|
|
132
|
+
// CROSS-CUSTODY ATTESTATION-AUTHORIZED (§3/§5): the payload's signed quote IS the authorization (lowered
|
|
133
|
+
// into the `nomosAttestationGate` era key) — the one gate verifies it against the home's owner (the
|
|
134
|
+
// LAW-DECLARED owner-source below, NOT a kernel-hardcoded id) + pins the principal to the attestation key,
|
|
135
|
+
// so the bailiff delivers it host-free to an OFFLINE subject (no device key, no author block).
|
|
136
|
+
.attestationAuthorized({ ownerObject: HOME_OWNER_ID, ownerField: "principal" });
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `pruneShare` — fail-safe drop of a hint whose forward `Check` denied (the asserter revoked it). The row
|
|
140
|
+
* stays for audit; `prunedAt` removes it from the live list. (The forward check itself is the resource
|
|
141
|
+
* holon's job — this records the verdict locally.)
|
|
142
|
+
*/
|
|
143
|
+
export const pruneShare = directive("pruneShare")
|
|
144
|
+
.mutates(SharedWithMe)
|
|
145
|
+
.payload(z.object({ shareId: z.string().min(1), prunedAt: z.string().min(1) }))
|
|
146
|
+
.plan((p) => [set(instance(SharedWithMe, p.shareId), "prunedAt", p.prunedAt)]);
|
|
147
|
+
|
|
148
|
+
/** "Show me everything <asserter> shared with me" — an indexed probe over the local access list. */
|
|
149
|
+
export const sharedByWorkspace = query("sharedByWorkspace").key("asserterWorkspace").returns(SharedWithMe);
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* response-mutation). This is exactly the proven `ExportJobAggregate` shape
|
|
42
42
|
* (the tenant domains package `thing_structures`) generalised into a reusable framework pattern so a
|
|
43
43
|
* domain dev does NOT hand-roll the {aggregate + order + complete + fail} quartet each
|
|
44
|
-
* time — they DECLARE the task's params and get the quartet for free, like `
|
|
44
|
+
* time — they DECLARE the task's params and get the quartet for free, like `relations.ts`
|
|
45
45
|
* gives grant/revoke for free.
|
|
46
46
|
*
|
|
47
47
|
* ── What is framework vs tenant ──────────────────────────────────────────────
|
|
@@ -168,7 +168,7 @@ export function impureCapabilityAggregate<
|
|
|
168
168
|
* captured + committed — so the `order` create is idempotent on replay by capture, and the
|
|
169
169
|
* `complete`/`fail` receipt addresses the SAME instance by reading the committed minted id.
|
|
170
170
|
* Trivial today (identity), but centralised here so the keying rule has ONE home (the
|
|
171
|
-
* symmetry with `
|
|
171
|
+
* symmetry with `relations.ts`'s `relationTupleInstanceId`).
|
|
172
172
|
*/
|
|
173
173
|
export function impureCapabilityInstanceId(requestId: string): string {
|
|
174
174
|
return requestId;
|
|
@@ -215,7 +215,7 @@ const deadLetterSchema = z.object({
|
|
|
215
215
|
* The three plan builders. Each takes the task aggregate handle + the (already
|
|
216
216
|
* Zod-validated) payload and returns the `PlannedOp[]` writing the envelope. A tenant
|
|
217
217
|
* routes its directive `.plan` through these so the envelope shape can never drift —
|
|
218
|
-
* exactly how `
|
|
218
|
+
* exactly how `relations.ts`'s relate/unrelate route through `writeTuple`/`removeTuple`.
|
|
219
219
|
*/
|
|
220
220
|
|
|
221
221
|
/**
|
|
@@ -61,11 +61,30 @@ const OBJECT_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]*:[A-Za-z0-9][A-Za-z0-9_.:/-]*$/;
|
|
|
61
61
|
/** A relation name (the edge label) — a bare identifier, like a directive/cap name. */
|
|
62
62
|
const RELATION_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
63
63
|
/**
|
|
64
|
-
* A SUBJECT: `user:<uid>`, the public wildcard `user:*`, or a
|
|
65
|
-
*
|
|
66
|
-
*
|
|
64
|
+
* A SUBJECT: `user:<uid>`, the public wildcard `user:*`, a KEY-ADDRESSED `key:<64hex>`, or a
|
|
65
|
+
* USERSET `<type>:<id>#<relation>` (group membership / indirection — P2). A raw uid carrying the
|
|
66
|
+
* `:` delimiter or being the bare `*` is refused so it can never forge a wildcard/userset.
|
|
67
67
|
*/
|
|
68
68
|
const USER_SUBJECT_RE = /^user:([A-Za-z0-9][A-Za-z0-9_.-]*|\*)$/;
|
|
69
|
+
/**
|
|
70
|
+
* A KEY-ADDRESSED subject: `key:<64 lowercase hex>` — a sha256 keyHash, the CONTROL-PLANE authority
|
|
71
|
+
* subject (an AdminGrant/CreationGrant/CapabilityBinding key becomes `…@key:<keyHash>`). Pinned to
|
|
72
|
+
* EXACTLY 64 hex with NO wildcard: the Check wildcard arm is `user:*`-only, so a `key:*` would be a
|
|
73
|
+
* forge surface (any key claiming admin). Distinct from `user:` so a keyHash is never read as a uid.
|
|
74
|
+
*/
|
|
75
|
+
const KEY_SUBJECT_RE = /^key:[0-9a-f]{64}$/;
|
|
76
|
+
/**
|
|
77
|
+
* A USERSET subject `<type>:<id>#<relation>` — group membership / indirection (P2, the native-Zanzibar
|
|
78
|
+
* recursion EDGE the `RewriteEngine` resolves: `Doc#viewer@group:eng#member`). The body is OBJECT_RE
|
|
79
|
+
* (`<type>:<id>`) + `#` + RELATION_RE (`#` is never in either part, so the split is unambiguous). The
|
|
80
|
+
* CONTROL-PLANE CARVE-OUT (an admin/creator relation must NOT accept a userset — else admin-via-group)
|
|
81
|
+
* is enforced at the WRITE GATE by the relation's declared `AllowedType` (P4), not here: this generic
|
|
82
|
+
* boundary only checks the subject is well-FORMED. A relation that declares no userset allowed-type
|
|
83
|
+
* refuses one fail-closed at the gate. (Governance does not use the relation schema until P5, and P4
|
|
84
|
+
* lands the AllowedType gate before it — so no control-plane userset is admissible in the interim.)
|
|
85
|
+
*/
|
|
86
|
+
const USERSET_SUBJECT_RE =
|
|
87
|
+
/^[A-Za-z0-9][A-Za-z0-9_.-]*:[A-Za-z0-9][A-Za-z0-9_.:/-]*#[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
69
88
|
|
|
70
89
|
/** Deterministic per-(object, relation) aggregate instance id — replay-stable. */
|
|
71
90
|
export function relationTupleInstanceId(object: string, relation: string): string {
|
|
@@ -166,7 +185,12 @@ export function removeTuple(
|
|
|
166
185
|
const relatePayload = z.object({
|
|
167
186
|
object: z.string().regex(OBJECT_RE, "object must be <type>:<id>"),
|
|
168
187
|
relation: z.string().regex(RELATION_RE),
|
|
169
|
-
subject: z
|
|
188
|
+
subject: z
|
|
189
|
+
.string()
|
|
190
|
+
.refine(
|
|
191
|
+
(s) => USER_SUBJECT_RE.test(s) || KEY_SUBJECT_RE.test(s) || USERSET_SUBJECT_RE.test(s),
|
|
192
|
+
"subject must be user:<uid>, user:*, key:<64hex>, or a userset <type>:<id>#<relation>",
|
|
193
|
+
),
|
|
170
194
|
grantedBy: z.string().min(1),
|
|
171
195
|
grantedAt: z.string().min(1), // ISO-8601, caller-stamped (determinism)
|
|
172
196
|
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
|
|
4
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* framework/signing_identity.ts — THE `signer` RELATION (the key↔identity binding,
|
|
8
|
+
* `architecture/identity_keys.md`). NATIVE ZANZIBAR, not a bespoke roster.
|
|
9
|
+
*
|
|
10
|
+
* THE CLEAN SPLIT. Proving WHO authored an intent has exactly two parts:
|
|
11
|
+
* 1. The SIGNATURE — "this ed25519 device key signed these bytes." That is the ONE
|
|
12
|
+
* irreducibly-crypto primitive (the attestation layer beneath Zanzibar; relations
|
|
13
|
+
* can't prove a signature). It is the kernel/provenance `verify` over the
|
|
14
|
+
* source-bound author bytes — NOT modelled here.
|
|
15
|
+
* 2. WHICH KEY BELONGS TO WHICH IDENTITY — a RELATION:
|
|
16
|
+
* `identity:<principal>#signer@key:<sha256hex(pubkey)>`
|
|
17
|
+
* "this device key may AUTHOR as this principal." ENROLL with `relateSubject`,
|
|
18
|
+
* REVOKE with `unrelateSubject` (remove-wins — already native), CHECK with the
|
|
19
|
+
* relation engine (`RewriteEngine.check`). No aggregate, no roster fold; the pubkey
|
|
20
|
+
* rides the signature and its hash is just the subject. The gate reads THIS
|
|
21
|
+
* workspace's own signer tuples (local — each workspace holds its own guest list).
|
|
22
|
+
*
|
|
23
|
+
* So: the gate derives a PROVEN principal by `verify(sig)` (1) then
|
|
24
|
+
* `Check(identity:P # signer @ key:<hash>)` (2) — and only THEN runs the directive's
|
|
25
|
+
* own `.requires(...)` relation gate. Two of the three steps are relations; only the
|
|
26
|
+
* signature is crypto.
|
|
27
|
+
*
|
|
28
|
+
* BOOTSTRAP: the FOUNDING signer tuple of a principal (on a virgin `identity:P#signer`)
|
|
29
|
+
* is admitted by the genesis clause (`framework/genesis.ts`, token-bound), exactly like
|
|
30
|
+
* the founding admin grant; every later device is enrolled by proof-of-possession (an
|
|
31
|
+
* intent signed by an already-enrolled key).
|
|
32
|
+
*/
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
import { directive } from "../directive.js";
|
|
35
|
+
import { relationType, self, key } from "../relation_schema.js";
|
|
36
|
+
import { writeTuple, RelationTuple } from "./relations.js";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The framework `identity` relation schema — the `signer` relation, LEAF-ONLY
|
|
40
|
+
* (`allow: [key()]`, never a userset/wildcard: a `key:*` would let any key claim to author
|
|
41
|
+
* as anyone). A workspace that admits signed authorship composes this into its law.
|
|
42
|
+
*/
|
|
43
|
+
export const identityRelations = relationType("identity").relation("signer", {
|
|
44
|
+
rewrite: self(),
|
|
45
|
+
allow: [key()],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** The Zanzibar OBJECT for a principal's signer relation — `identity:<principal>` (the gate mirrors
|
|
49
|
+
* this as `executor::signer_object`; the subject is `key:<sha256hex(pubkey)>`). */
|
|
50
|
+
export function signerObject(principal: string): string {
|
|
51
|
+
return `identity:${principal}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The `signer` relation name — the relation a device key holds on its `identity:<principal>` object. */
|
|
55
|
+
export const SIGNER_RELATION = "signer";
|
|
56
|
+
|
|
57
|
+
const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256 hex (the device pubkey hash)");
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `enrollSigner` — author the founding `identity:<principal>#signer@key:<keyHash>` tuple (the bootstrap
|
|
61
|
+
* E of the warrant-flip, `architecture/identity_keys.md`). A `relateSubject` SPECIALISATION:
|
|
62
|
+
* - `.ensures(RelationTuple)` so the deterministic-keyed `(identity:<principal>, signer)` head is
|
|
63
|
+
* admitted by the id-mint gate (upsert-by-key) — exactly like `grantAdmin`;
|
|
64
|
+
* - **NO `.requires`** — DELIBERATE. At genesis the founding signer enrolls on a still-WARRANT-FREE
|
|
65
|
+
* prefix (it precedes `seedGenesis`), so `verify_captured_principal` is in its legacy fast path and
|
|
66
|
+
* admits it UNSIGNED; there is no admin/signer yet to gate it, so a `.requires` would re-create the
|
|
67
|
+
* bootstrap chicken-and-egg. The forge surface a bare `relateSubject` would open is closed by
|
|
68
|
+
* DOMAIN PLACEMENT: this directive is composed into the CONTROL-PLANE governance law (domain
|
|
69
|
+
* `workspaces`/`nomos`), which the open session lane structurally refuses
|
|
70
|
+
* (`cloud/holon-host/src/engine.mjs` — only the birth door + the secret-gated `/author` + admin
|
|
71
|
+
* lanes author it), so no tenant can forge a signer for another principal.
|
|
72
|
+
*
|
|
73
|
+
* THE ORDER IS THE CONTRACT (verified, zero gate change): `enrollSigner` (E, unsigned/pre-warrant) →
|
|
74
|
+
* `seedGenesis` (W, unsigned/pre-warrant, the warrant lands in its output) → `grantAdmin` (A, the FIRST
|
|
75
|
+
* SIGNED intent — its prefix now carries the warrant, the E-tuple proves the key, the virgin `#admin` is
|
|
76
|
+
* 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.
|
|
78
|
+
*/
|
|
79
|
+
export const enrollSigner = directive("enrollSigner")
|
|
80
|
+
.ensures(RelationTuple)
|
|
81
|
+
.payload(
|
|
82
|
+
z.object({
|
|
83
|
+
principal: z.string().min(1), // the auth-provider uid the key may author as
|
|
84
|
+
keyHash: sha256Hex, // sha256hex of the ed25519 device PUBLIC key
|
|
85
|
+
grantedBy: z.string().min(1),
|
|
86
|
+
grantedAt: z.number().int().nonnegative(), // epoch ms, caller-stamped (determinism)
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
.plan((p) =>
|
|
90
|
+
writeTuple({
|
|
91
|
+
object: signerObject(p.principal),
|
|
92
|
+
relation: SIGNER_RELATION,
|
|
93
|
+
subject: "key:" + p.keyHash,
|
|
94
|
+
grantedBy: p.grantedBy,
|
|
95
|
+
grantedAt: String(p.grantedAt),
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
@@ -69,8 +69,10 @@ export function refAs(name: string, aggregateType: string, id: string): Invarian
|
|
|
69
69
|
return { name, aggregate: aggregateType, id };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/** Derive the BOUNDED declared read-set from the intent payload. Pure — no IO, no live read.
|
|
73
|
-
|
|
72
|
+
/** Derive the BOUNDED declared read-set from the intent payload. Pure — no IO, no live read.
|
|
73
|
+
* `ctx.principal` is the captured, authenticated AUTHOR identity (e.g. `key:<keyHash>` / a verified uid)
|
|
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[];
|
|
74
76
|
|
|
75
77
|
/**
|
|
76
78
|
* The post-apply predicate over the resolved snapshots, keyed by each ref's binding `name`.
|
|
@@ -84,7 +86,7 @@ export type WorkspaceInvariantReads = (ctx: { intent: Record<string, unknown> })
|
|
|
84
86
|
*/
|
|
85
87
|
export type WorkspaceInvariantAssert = (
|
|
86
88
|
snapshots: Record<string, Record<string, unknown>>,
|
|
87
|
-
ctx?: { intent: Record<string, unknown
|
|
89
|
+
ctx?: { intent: Record<string, unknown>; principal?: string },
|
|
88
90
|
) => WorkspaceInvariantVerdict;
|
|
89
91
|
|
|
90
92
|
/** A complete, registered workspace-invariant declaration (the `.assert` terminal yields this). */
|