create-githolon 0.98.3 → 0.100.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.
@@ -1,213 +0,0 @@
1
- # Birthing child workspaces (law-at-birth)
2
-
3
- A directive in your law can **birth a child workspace** that is born already running its own domain law.
4
- A home births project children; a platform births workspaces; root births platforms — all the **same one
5
- way**. You do *not* ask Nomos for a new primitive and you do *not* hand a finished workspace to the cloud.
6
-
7
- ## How it works (birth is a kernel concern, defined by the ledger)
8
-
9
- Your directive declares `.births()` and, in its `plan`, authors the child's **genesis recipe** — the
10
- ordered intents the child will run — then calls `birth({ workspace, genesisChain })`. That's it. When the
11
- parent's offer is admitted, the kernel's birth offer-effect:
12
-
13
- 1. spawns the child's (empty) custody,
14
- 2. **installs the child's law in-chain** and folds the genesis recipe **through the child's own gate**,
15
- 3. so the child **self-validates from intent 0** — it is not trusted, it proves itself.
16
-
17
- The host installs nothing and decides nothing. The recipe is a **ledger fact**, not a runtime trick.
18
-
19
- ## The leaf recipe = two installs + your seed
20
-
21
- A leaf child (a home, a project) is born with exactly:
22
-
23
- ```
24
- [ bootstrap/installDomain(<frameworkHash>), // the bootstrap controller (bytes-by-hash, kernel-resolved)
25
- nomos/installDomain(<lawHash>), // YOUR child's domain law package
26
- <your own seed step(s)> ] // e.g. seed the owner / initial state
27
- ```
28
-
29
- `frameworkHash` + `lawHash` are the content hashes of the two packages, pinned at compile time (the kernel
30
- resolves the bytes from custody by hash — they are never shipped on the wire).
31
-
32
- ## Every birth is **keyed** — so you can find the workspace again
33
-
34
- A workspace exists to be **used**, and using it — sync it, open it, add a task to it — means **finding it
35
- again**, often from another device or after a restart. So a born workspace's name is **never** opaque or
36
- minted-and-forgotten: it is always **derived from a key you already own** (a business id like `projectId`, or
37
- an owner uid). One verb: **`birth.keyed`**.
38
-
39
- > **The one rule.** The name comes from a key you own → the client re-derives the same name from that same
40
- > key with `deriveWorkspaceName(namespace, key)` → the workspace is addressable **forever, with zero
41
- > persistence**. Same key ⇒ same workspace (re-birth is idempotent). You never hand-roll a naming scheme, and
42
- > you never store a minted name to find it later.
43
-
44
- ```ts
45
- import { directive, birth, create, z } from "@githolon/dsl";
46
-
47
- export const birthProject = directive("birthProject")
48
- .creates(ProjectBirth) // your lineage/audit row
49
- .payload(z.object({
50
- projectId: z.string().min(1), // THE KEY — an id you mint app-side; the workspace is FOR it
51
- owner: z.string().min(1),
52
- frameworkHash: z.string().min(1),
53
- lawHash: z.string().min(1),
54
- bornAt: z.string().min(1), // ISO-8601, caller-stamped (determinism)
55
- }))
56
- .plan((p) => {
57
- const actor = "user:" + p.owner;
58
- birth.keyed({
59
- key: p.projectId, // ← the workspace name is derived from this
60
- namespace: "project", // → project-<sha256(project ⏎ projectId)[:40]>
61
- genesisChain: (ws) => [ // the builder receives the resolved name
62
- { domain: "bootstrap", directiveId: "installDomain", actor, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
63
- { domain: "nomos", directiveId: "installDomain", actor, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
64
- { domain: "project", directiveId: "seedProject", actor, payload: { projectId: p.projectId, workspaceName: ws, bornAt: p.bornAt } },
65
- ],
66
- });
67
- return [];
68
- })
69
- .births()
70
- .requires("projectCreator");
71
- ```
72
-
73
- Then in the app you address that project from just its id — **no lookup table, no persisted name**:
74
-
75
- ```dart
76
- // Dart (nomos_client)
77
- final ws = deriveWorkspaceName('project', projectId);
78
- final session = await engine.open(ws); // open / sync / add tasks — any device, any time
79
- ```
80
- ```ts
81
- // TS (@githolon/client)
82
- const ws = await deriveWorkspaceName('project', projectId);
83
- const holon = await connect({ cloud, workspace: ws, clientId });
84
- ```
85
-
86
- `deriveWorkspaceName` computes the byte-identical name the law's `birth.keyed` produced (contract-tested
87
- across the engine, Dart, and TS). This is the helper that replaces every hand-rolled
88
- `projectWorkspaceForProjectId` — delete yours and use this.
89
-
90
- ### One-per-user things (a home): `birth.singleton`
91
-
92
- A "singleton" is just a keyed birth whose key is a natural single key — one home per user, so `key = owner`.
93
- Sugar over `birth.keyed`:
94
-
95
- ```ts
96
- birth.singleton({ keyedBy: p.owner, namespace: "home", genesisChain: (home) => [ /* installs + seed */ ] });
97
- // app: deriveWorkspaceName('home', owner)
98
- ```
99
-
100
- > ⚠️ **`birth.instance` is deprecated — don't use it.** It mints a name you throw away, so the born
101
- > workspace can only be reached via the birth result. There is no workspace you make and never want to find
102
- > again. Always `birth.keyed` (or `birth.singleton`).
103
-
104
- (Raw `birth({ workspace, genesisChain })` stays the low-level escape hatch for a name that is genuinely
105
- externally-determined — but if you're deriving it from your own data, use `birth.keyed`.)
106
-
107
- ## From the app (Dart) — parentless, session-bound
108
-
109
- The generated Dart client emits a typed method per `.births()` directive, bound to the SESSION whose
110
- workspace IS the parent — no loose parent string, and the born child comes back as a followable ref:
111
-
112
- ```dart
113
- final parentClient = await MyPlatformClient.bind(parentSession);
114
- final birth = await parentClient.birthProjectChild(payload: order); // parent = the session workspace
115
- final child = await engine.follow(birth.bornRefs.single); // mount the born child
116
- final childClient = await ProjectClient.bind(child); // typed client on the child
117
- ```
118
-
119
- (The old `parent:` parameter survives one minor version as a deprecated legacy-gateway shim; its verdict
120
- rides `outcome.gatewayVerdict`.)
121
-
122
- ## Authority — who may birth
123
-
124
- `.requires("creator")` gates the birth on a relation tuple. Grant the home owner `#creator` on their own
125
- home (self-bound at birth, the same way the owner is seeded) so a verified owner can birth their project
126
- children — and only their own. (Relations: `grant` / `writeTuple`, see `07-security.md`. Use whatever
127
- relation models your policy; `creator` is the convention `birthChild` uses.)
128
-
129
- ## What NOT to do
130
-
131
- - **Don't hand-roll the child's name** (`project-<sha256(owner)>`, or your own `projectWorkspaceForProjectId`).
132
- `birth.keyed` owns the naming and `deriveWorkspaceName` gives the client the identical name — hand-rolling
133
- either half is how the two drift and a workspace becomes unfindable.
134
- - **Don't use `birth.instance`.** It's deprecated — it mints a name you can't re-derive, so the workspace is
135
- addressable only via the birth result. Every workspace needs to be found again; birth it keyed.
136
- - **Don't ask for a "child-birth primitive."** It already exists — `.births()` + `birth.keyed`. A home is a
137
- first-class workspace; it births children exactly as root births platforms.
138
- - **Don't hand-roll a second recipe *builder*** across your domains — keep the recipe inline (it's small)
139
- or share one tiny helper *in your own code*; the framework deliberately keeps **one** birth primitive.
140
- - **Don't put a clock/random in the plan** — `bornAt` rides the payload (the timestamp doctrine).
141
-
142
- ## Prove it — the 30-second loop
143
-
144
- Every compile validates your genesis recipe statically (child domain composed in the package? seed
145
- directive exists? recipe payload satisfies its schema? every plan returns an **array** of ops) and prints
146
- any problem as a `⚠ births` warning **containing the fix**. Then:
147
-
148
- ```bash
149
- npx githolon compile # ⚠ births warnings = fix these first (the message says how)
150
- npx githolon proof # OFFLINE: drives your .births() directive on the real kernel, folds the
151
- # child through its OWN gate, reads the seed back, verify_chain green
152
- ```
153
-
154
- A red proof names the exact genesis step the child's gate refused. The most common first-timer halt: a
155
- plan that returns a fluent builder — builders record themselves, so end the plan with `return [];`.
156
- A complete working two-domain births package to copy: `bench/scale/fixtures/home-delegated-estate` (nomos2
157
- repo) — it births child workspaces with `birth.keyed` (addressable by key) and is exercised by
158
- `bench/scale/home_delegated_estate.smoke.mjs`.
159
-
160
- ## Posture at birth
161
-
162
- `birthChild` / `createWorkspace` take an optional `posture: "managed" | "sovereign"` (default **managed**)
163
- — the child's declared governance posture, mirrored onto its lineage record. A **Managed** child can be
164
- recovered by the operator on request (a rebirth heal = "ask us"); a **Sovereign** child's rebirth requires
165
- the child's own consent fact first (a heal = consent first). See
166
- [11-governance-postures.md](./11-governance-postures.md) for the two postures, the consent ceremony, and
167
- the exit rights.
168
-
169
- ## Offline workspace creation via delegated authority (`delegatedRole` / `.delegatedFrom(parent)`)
170
-
171
- Sometimes a CHILD must birth its own children **offline**, under authority a PARENT granted — with no
172
- contact with the parent at authoring time. Example: a platform **P** lets a user **U** create projects; U
173
- holds a **home** holon **H** on-device; U births a project **E** as a child of H, offline, gated by H's
174
- law. The parent's grant cannot be an author-time attested read (that needs connectivity). Instead the
175
- grant is **delivered into H as a signed cert and folded as a local authority fact** — then read offline.
176
-
177
- Author the delivery directive with `.delegatedRole(relation).delegatedFrom(parent)`:
178
-
179
- ```ts
180
- export const acceptDelegatedRole = directive("acceptDelegatedRole")
181
- .creates(ProjectCreatorGrant)
182
- .payload(z.object({ subject: roleSubject, delegationCert: z.string().min(1), grantedBy: roleSubject, grantedAt: z.string() }))
183
- .plan((p) => {
184
- create(ProjectCreatorGrant).set("subject", p.subject).set("grantedAt", p.grantedAt);
185
- return writeTuple({ object: "workspace:self", relation: "projectCreator", subject: p.subject, grantedBy: p.grantedBy, grantedAt: p.grantedAt });
186
- })
187
- .grants("projectCreator")
188
- .delegatedRole("projectCreator", { subjectField: "subject", certField: "delegationCert" })
189
- .delegatedFrom({ parentKeyObject: "birth-attestation:self", parentKeyField: "parentKey" }); // H's parent P's key
190
-
191
- // the birth itself is gated on the DELIVERED local tuple — read OFFLINE, no parent contact:
192
- export const birthProjectWorkspace = directive("birthProjectWorkspace")
193
- .creates(ProjectBirthOrder).payload(/* … */).plan(/* birth(...) */)
194
- .births()
195
- .requires("projectCreator"); // ← reads the delegated tuple folded by acceptDelegatedRole
196
- ```
197
-
198
- How it works:
199
- - **The one gate re-verifies P's grant on every lane** (author / edge admission / verify_chain): the
200
- carried `DelegationCert` must be signed by a grantor whose key chains to the pinned `K_root`, bind the
201
- same `(relation, subject)` the tuple writes, and — `.delegatedFrom(parent)` — whose grantor key IS this
202
- workspace's parent key. A forged / mis-bound / wrong-parent grant fails closed. This is the
203
- `nomosDelegationGate` era key — a law that declares no `.delegatedRole()` is byte-identical.
204
- - **The birth is authored OFFLINE**, gated only by H's LOCAL folded tuple; `verify_chain` re-verifies the
205
- delegation to `K_root` with no cross-workspace read.
206
- - **Revocation is forward-only + evidence-preserving.** P delivers a revoke (`removeTuple`, same gate);
207
- the folded tuple flips revoked (`RemoveWins`), so U's NEXT birth refuses — but a project born WHILE the
208
- grant was live re-verifies green forever (verify_chain re-folds the chain prefix at each intent's
209
- position). Both the grant and the revoke are permanent ledger facts.
210
-
211
- The platform produces the cert with the `delegation_cert_sign` holon op (grantor secret injected per call,
212
- never persisted), carrying its lineage to `K_root`; the bailiff delivers it host-free. Worked end-to-end:
213
- `bench/scale/home_delegated_estate.smoke.mjs`; doctrine: `architecture/delegated_authority_births.md`.
@@ -1,87 +0,0 @@
1
- # 09 — Attested cross-workspace reads (slice B)
2
-
3
- Your law can gate a write on a FACT IN ANOTHER WORKSPACE — without ever trusting the host,
4
- and without any peer opening the foreign chain at replay. The source's kernel SIGNS the
5
- answer; your law declares whose signature it trusts; every door (author, edge admission,
6
- `verify_chain` forever) re-verifies the signed envelope.
7
-
8
- ## Declare the trust edge in law
9
-
10
- ```ts
11
- import { z, directive, read, trustSource, foreignQuery } from "@githolon/dsl";
12
-
13
- const platformPolicy = foreignQuery("platformPolicy"); // the SOURCE's declared query, by id
14
-
15
- // module level — the trust boundary IS law (rides the domainHash):
16
- export const platformTrust = trustSource("acme-platform", {
17
- genesis: "9d1c…", // the source's genesis oid (its true identity — names are routing)
18
- keyHashes: ["41aa…"], // sha256 of the source's read-attestor public key(s)
19
- maxAgeMs: 86_400_000, // how stale an envelope may be vs the intent's own clock
20
- minSourceEpoch: "birth",
21
- monotone: true, // refuse an answer older than one this chain already saw
22
- });
23
-
24
- export const enrollDevice = directive("enrollDevice")
25
- .creates(HomeDevice)
26
- .payload(z.object({ device: z.string() }))
27
- .readsFrom("acme-platform", platformPolicy) // the declared foreign read
28
- .plan((p) => {
29
- const rows = read(platformPolicy, { scope: "homes" }, { from: "acme-platform" });
30
- if (rows[0]?.data?.enrollmentOpen !== "true") throw new Error("enrollment closed");
31
- create(HomeDevice).set("device", p.device);
32
- return [];
33
- });
34
- ```
35
-
36
- ## Who fetches the evidence? THE AUTHOR — never the host
37
-
38
- The host never initiates a fetch (that would be orchestration — host-purge doctrine). Three
39
- lanes, in precedence order:
40
-
41
- 1. **Carried** — the offer already contains `attestations: [{queryId, result, attestation}]`.
42
- The gate judges the carried envelope; nothing ever replaces it.
43
- 2. **Colocated (kernel-internal)** — when the source workspace is mounted in the SAME engine
44
- instance (the container pool colocates workspaces), the engine plane serves + signs the
45
- read in-kernel and injects it. Zero HTTP. The envelope is byte-identical to the HTTP lane —
46
- colocation is invisible to the gate.
47
- 3. **Client-carried (`?attest=1`)** — otherwise the CLIENT pre-fetches from the source's open
48
- read lane and carries the envelope on the offer.
49
-
50
- ## `@githolon/client` does the dance for you
51
-
52
- ```ts
53
- const holon = await connect({ cloud, workspace, clientId,
54
- attestedSources: { "acme-platform": "https://nomos.captainapp.co.uk" } }); // optional override
55
- await holon.dispatch("home", "enrollDevice", { device: "d-1" });
56
- // → the kernel refuses TYPED naming the missing read; the client fetches
57
- // GET <sourceCloud>/v2/workspaces/acme-platform/platformPolicy?scope=homes&attest=1
58
- // injects the signed envelope, retries — automatically.
59
- ```
60
-
61
- **Offline:** if the source is unreachable, the offer FAILS with the named
62
- `attested-read-unreachable` error. There is NO stale fallback — a plan without its live
63
- premise cannot author. Retry when the source is reachable. You can also pre-fetch ahead of
64
- time (`holon.fetchAttestedRead(from, queryId, args)`) and pass
65
- `dispatch(..., { attestations: [entry] })`.
66
-
67
- ## The two-step dance for THIN clients (raw HTTP, no SDK)
68
-
69
- 1. `POST /v2/workspaces/<ws>/<directiveId>` with your payload. If the law declares foreign
70
- reads and your body carries no matching attestation, you get **422** with:
71
- `attested-read-unreachable: read('<queryId>', {from: '<source>'}) args={…} has no injected
72
- attestation — pre-fetch it from the source's open read lane (?attest=1 …)`.
73
- 2. `GET /v2/workspaces/<source>/<queryId>?<args>&attest=1` → `{ok, rows, attestation}` —
74
- the source KERNEL signed it; the host relayed opaque bytes. Retry the POST with
75
- `"attestations": [{"queryId": "<queryId>", "result": <rows>, "attestation": <attestation>}]`
76
- in the body (it rides beside your payload fields, not inside them).
77
-
78
- No server fills this in for you — the refusal is the kernel INSTRUCTING you what evidence to
79
- carry. That is the design, not a limitation: the author carries the premise; every replayer
80
- verifies the signature forever, offline.
81
-
82
- ## What the gate checks (every door, every peer, forever)
83
-
84
- declared edge ✓ → key hash ∈ your law's pins ∧ genesis matches ∧ signature verifies over the
85
- result ✓ → size bounds ✓ → freshness (`maxAgeMs` vs the intent's frozen clock, `minSourceEpoch`,
86
- the `monotone` frontier) ✓ → the plan re-runs with the replayed capture and byte-compares.
87
- A forged/tampered/stale/wrong-key envelope is a TYPED refusal. No peer ever contacts the source.
@@ -1,79 +0,0 @@
1
- # 10 — Flutter clients: layered packages + humane names
2
-
3
- `nomos-compile` can emit your generated Dart as **three sibling pub packages** instead of one
4
- flat tree, so a Flutter app can enforce a layered architecture with the package graph itself:
5
-
6
- ```js
7
- // nomos.package.mjs
8
- export default {
9
- name: "acme",
10
- domains: [ /* … */ ],
11
- dart: {
12
- out: "build/dart",
13
- packages: {
14
- types: "acme_nomos_types", // pure data — frontend-safe
15
- client: "acme_nomos_client", // effectful runtime clients
16
- test: "acme_nomos_test", // headless harness — dev_dependencies only
17
- },
18
- names: {
19
- // public aliases over the stable generated identities (typedefs in the types barrel)
20
- AcmeProjectCustomFieldDefinitionFieldType: "FieldType",
21
- AcmeProjectDrawingTransform2d: "DrawingTransform2d",
22
- },
23
- },
24
- };
25
- ```
26
-
27
- ## The layering line
28
-
29
- **Don't ban generated types from UI; ban generated clients/runtime from UI.**
30
-
31
- ```text
32
- your_frontend -> acme_nomos_types (payloads, read models, VOs/enums,
33
- typed watch/read accessors, aggregate meta)
34
- your_composition -> acme_nomos_client + types (offer methods over NomosBridge,
35
- sessions, sync, workspace lifecycle)
36
- apps/your_app -> your_frontend + your_composition
37
- widget/e2e tests -> acme_nomos_test (dev_dependencies only)
38
- ```
39
-
40
- - `types` depends ONLY on the pure `nomos_types` core — no bridge, no sessions, no lifecycle.
41
- A type-only law change reaches the UI; a client/runtime change does not.
42
- - `client` depends on the types package + `nomos_client`. It re-exports the types barrel, so
43
- composition code needs one import.
44
- - `test` re-exports the headless harness (`package:nomos_client/testing.dart`) plus the full
45
- client layer.
46
-
47
- The generated `watch*/read*` accessors live in **types** on purpose: they are pure typed
48
- wrappers over an injected `NomosReads` seam (plain function fields) — the UI can render live
49
- data without ever seeing the runtime. Your composition layer constructs the one `NomosReads`
50
- from its bridge and hands it down.
51
-
52
- ## What is generated-owned vs yours
53
-
54
- - `lib/src/generated/**` and each package barrel (`lib/<pkg>.dart`) are **generated-owned** —
55
- wiped and rewritten on every compile. Never hand-edit them.
56
- - Each `pubspec.yaml` is scaffolded **only if missing** — you own versions, publishing
57
- (`publish_to`), and any extra dependencies. Version the three packages independently: that
58
- independence IS the blast-radius win; releasing them in lockstep rebuilds the god artifact
59
- with more pubspecs.
60
-
61
- ## Aliases (`dart.names`)
62
-
63
- Generated names are fully qualified (`<Domain><Aggregate><Field>…`) because identity must be
64
- collision-free across your whole law. The `names` map layers humane public names on top as
65
- `typedef` aliases in the types barrel. Validation is fail-closed at compile: the generated
66
- name must exist, and an alias may not collide with any generated public name or another alias.
67
- The compile summary prints long generated names that still lack an alias.
68
-
69
- Renaming an alias is free (it's a label over a stable identity — the law and the wire never
70
- see it). `names` works in the single-package emit too.
71
-
72
- ## Notes
73
-
74
- - `dart.packages` implies `support: "package"` (pub dependencies on `nomos_types` /
75
- `nomos_client`); combining it with `support: "vendored"` is a named compile refusal.
76
- - The scaffolded client/test pubspecs wire the sibling dependency as a `path:` (they are
77
- co-emitted); replace with hosted constraints when you publish.
78
- - Types-file purity is enforced by the compiler, not convention: a generated types file that
79
- imported anything beyond the `nomos_types` core would refuse the compile.
@@ -1,89 +0,0 @@
1
- # Governance postures — Managed and Sovereign
2
-
3
- Every platform you run on Nomos Cloud **declares a governance posture**. It decides one thing: **may the
4
- operator (the root custodian) run recovery verbs — rebirth, certifyEra, repairs — FOR your workspace
5
- without asking you first?** Both postures are first-class; you choose, and you can change your mind.
6
-
7
- The honest foundation, stated plainly and never hidden:
8
-
9
- - **Validity sovereignty is absolute for everyone.** Holons self-verify. The operator can *act* but can
10
- never *lie* — every operator act is a signed, ordered, permanent fact in root's replayable chain.
11
- - **Custody power is irreducibly the host's while you are hosted.** No cryptography stops a landlord
12
- changing the locks. What Nomos gives you is **detectability** (a custody break fails closed) and
13
- **exit** (clone the ledger and leave — the byte-identical wasm makes self-hosting the ultimate exit).
14
- - Therefore **hosted sovereignty = law-encoded restraint + total detectability + credible exit.** Never
15
- prevention.
16
-
17
- ## Posture A — Managed (the default)
18
-
19
- Your platform's law folds an **OperatorGrant**: the root custodian may run recovery verbs for you — each
20
- a signed, attributed law fact + certificate you audit forever. This is *the assurance of being able to
21
- ask us for help*: if your chain scars unrecoverably, we can rebirth it for you without a ceremony.
22
-
23
- A workspace with **no posture fact at all is Managed** — that is the default, and nothing changes for a
24
- platform that never touches postures.
25
-
26
- ## Posture B — Sovereign
27
-
28
- No OperatorGrant. Your founding key is terminal for your subtree's law. **The teeth:** root's
29
- `rebirthWorkspace` REFUSES for a Sovereign workspace unless the offer carries **your consent** — a
30
- `RebirthConsent` fact folded in **your own chain**, signed by your own admin, in your own history. The
31
- two-key ceremony: the custody act cannot move without BOTH root's law fact AND your consent fact.
32
- Operator restraint becomes law that replays, not policy you have to trust.
33
-
34
- ### The honest ceiling (read this)
35
-
36
- > The operator controls root's law and could deploy law removing the consent check — but that act is
37
- > itself a signed, ordered, permanent fact in root's chain, visible to every tenant running `replay root`,
38
- > and grounds for exit-with-everything. Not "we can't" — "we can't secretly, and you can always leave."
39
-
40
- ## Declaring and changing your posture
41
-
42
- Posture is **law-state in your own chain** (you own the fact), mirrored onto root's lineage record so the
43
- bailiff can consult it on the hot path without reading every tenant chain. **The tenant chain is
44
- authoritative — a mirror mismatch fails closed to Sovereign.**
45
-
46
- ```bash
47
- # see the current posture, the consent state, and exactly what it means
48
- githolon posture <platform>
49
-
50
- # withdraw the operator grant (Managed → Sovereign) — forward-only; help already given stays in history
51
- githolon posture set <platform> sovereign --reason "we run our own recovery from here"
52
-
53
- # re-invite help (Sovereign → Managed)
54
- githolon posture set <platform> managed --reason "hand recovery back to the operator"
55
- ```
56
-
57
- You can also declare a posture **at birth**: `birthChild` / `createWorkspace` take an optional
58
- `posture: "managed" | "sovereign"` (default managed), mirrored onto the child's lineage record.
59
-
60
- ## The consent ceremony (Sovereign rebirth)
61
-
62
- When a Sovereign workspace needs a rebirth, the operator plans it but **cannot proceed until you consent
63
- in your own chain**:
64
-
65
- ```bash
66
- # operator: sees the block up front
67
- githolon rebirth plan <ws> # GOVERNANCE POSTURE → SOVEREIGN, consent ABSENT → refused pre-seed
68
-
69
- # tenant: fold the consent fact in YOUR OWN chain (signed by your admin)
70
- githolon consent rebirth <ws> --reason "authorising the reborn incarnation" [--expires-at <epoch-ms>]
71
-
72
- # operator: now the two-key ceremony is satisfied — the rebirth admits
73
- githolon rebirth apply --plan rebirths/<date>-<ws>/plan.json --gcp-secret nomos-root-custodian-key
74
- ```
75
-
76
- Without a live consent fact, both the CLI (`rebirth apply`) and the custody act itself (a `428` refusal)
77
- decline and name the exact `githolon consent rebirth` command you need. Consent is forward-only and may
78
- carry an `expiresAt` window.
79
-
80
- ## Recursion and exit
81
-
82
- A platform is root-for-its-subtree: it offers **its** children the same postures with its founding key in
83
- the operator role. **Custody does not nest** — a Sovereign sub-platform still rides the grandparent's
84
- bailiff; true custody independence is self-hosting (clone the ledger, run the byte-identical wasm
85
- yourself). No level ever rebirths itself; root operations stay the one extra-legal act, always named in
86
- CLI output.
87
-
88
- See also: [08-births.md](./08-births.md) (a Managed heal = "ask us"; a Sovereign heal = consent first) and
89
- the ratified spec `architecture/governance_postures.md`.
@@ -1,114 +0,0 @@
1
- // E2E: the FULL loop against a LIVE Nomos Cloud, driven through the GENERATED TYPED
2
- // CLIENT (`build/__APP_NAME__.client.ts` — emitted by githolon compile):
3
- //
4
- // compile → create workspace → deploy (package + manifest overlay, ONE POST) →
5
- // web client connects → TYPED createList + addTodo LOCALLY (offline writes) →
6
- // sync + edge admission → the CLOUD's declared query + count return the work.
7
- //
8
- // The docs travel with you, offline: docs/01-mental-model.md first, then
9
- // docs/03-client.md explains every call here. This file is the NARRATED
10
- // walkthrough you reshape by hand; its GENERATED sibling — build/__APP_NAME__.proof.mts,
11
- // emitted by every compile from your own law — needs no rewriting: `githolon proof`.
12
- //
13
- // The OFFLINE convergence proof (two peers, blind edits, byte-identical sync, no cloud)
14
- // lives in test/convergence.local.mjs: node test/convergence.local.mjs
15
- //
16
- // Run from this directory AFTER `npx githolon compile`: npx tsx test/e2e.mts
17
- import { readFileSync } from "node:fs";
18
- import { connect } from "@githolon/client";
19
- import { todoClient, __APP_HASH__ } from "../build/__APP_NAME__.client.ts";
20
-
21
- const CLOUD = (process.env.NOMOS_CLOUD || "https://nomos.captainapp.co.uk").replace(/\/+$/, "");
22
- const WS = process.env.NOMOS_WS || "todo-e2e-" + Math.random().toString(36).slice(2, 8);
23
-
24
- // explicit annotation on the const so TS's never-call narrowing applies after fail(...)
25
- // IDENTITY NOTE: this proof uses the bare x-nomos-principal lane so the test is
26
- // SELF-CONTAINED (a throwaway workspace, no stored credentials). Real apps use
27
- // `githolon login --agent` (a VERIFIED identity) + `githolon ws create/deploy`.
28
- const fail: (m: string) => never = (m) => { console.error("✗ " + m); process.exit(1); };
29
- const ok = (m: string) => console.log("✓ " + m);
30
-
31
- const deploy = JSON.parse(readFileSync(new URL("../build/__APP_NAME__.deploy.json", import.meta.url), "utf8"));
32
-
33
- // 1. create the workspace (capture the ONE-TIME secret)
34
- let r = await fetch(`${CLOUD}/v1/workspaces/${WS}`, { method: "POST", headers: { "x-nomos-principal": "nomos-e2e-suite" } });
35
- let d = await r.json();
36
- if (!d.ok) fail(`create workspace: ${JSON.stringify(d)}`);
37
- const SECRET: string = d.workspaceSecret;
38
- if (!SECRET?.startsWith("nws_v1_")) fail(`no workspaceSecret returned: ${JSON.stringify(d)}`);
39
- ok(`workspace ${WS} created — controller ${d.controller.domainHash.slice(0, 12)}…`);
40
-
41
- // ownership: a deploy WITHOUT the secret must be refused
42
- r = await fetch(`${CLOUD}/v1/workspaces/${WS}/domains`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(deploy) });
43
- if (r.status !== 401) fail(`unauthenticated deploy was not refused (got ${r.status})`);
44
- ok("ownership — deploy without the workspace secret refused (401)");
45
-
46
- // deploy WITH the secret. The generated client's baked hash IS the deployed law's hash.
47
- r = await fetch(`${CLOUD}/v1/workspaces/${WS}/domains`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${SECRET}` }, body: JSON.stringify(deploy) });
48
- d = await r.json();
49
- if (!d.ok || d.installation?.[0]?.data?.["status.phase"] !== "Active") fail(`deploy: ${JSON.stringify(d)}`);
50
- if (d.domainHash !== __APP_HASH__) fail(`deploy hash ${d.domainHash} != generated client's ${__APP_HASH__}`);
51
- ok(`law deployed Active — ${__APP_HASH__.slice(0, 12)}… (baked into the typed client)`);
52
-
53
- // 2. connect a web client (pulls wasm + manifests + ledger) and bind the typed surface
54
- const holon = await connect({ cloud: CLOUD, workspace: WS, clientId: "owner-1" });
55
- const app = todoClient(holon);
56
- ok(`web client connected — typed client bound (replica ${holon.replica.slice(0, 6)}…)`);
57
-
58
- // 3. LOCAL-FIRST writes — ENFORCED offline (fetch is trapped; any network touch fails).
59
- const realFetch = globalThis.fetch;
60
- globalThis.fetch = (() => { throw new Error("OFFLINE PROOF VIOLATED: local authoring touched the network"); }) as typeof fetch;
61
-
62
- // A directive returns the new local HEAD (a string). Aggregate ids are MINTED by the
63
- // engine — read them back from the declared query. `addTodo.listId` is a plain string
64
- // the app chooses (the grouping key), so we don't need createList's minted id to wire
65
- // todos to a list — we just key both on the same `listId`.
66
- const listId = "list-launch-plan";
67
- await app.createList({ title: "Launch plan" });
68
- await app.addTodo({ listId, text: "Draft the deck" });
69
- await app.addTodo({ listId, text: "Book the venue" });
70
- ok("typed offline writes — createList + 2 addTodo, under the pulled law");
71
-
72
- // the TYPED declared query routes LOCALLY; minted Todo ids come back on the rows
73
- const seeded = await app.todosByList({ listId });
74
- if (seeded.length !== 2) fail(`local query: expected 2 todos, got ${seeded.length}`);
75
- const t1 = seeded.find((row) => row.data.text === "Draft the deck")!.id;
76
- await app.toggleTodo({ todoId: t1, done: true }); // Lww boolean (t.bool)
77
- await app.tagTodo({ todoId: t1, tags: ["urgent", "q3"] }); // AddWins set
78
- ok(`read back a minted Todo id (${t1.slice(0, 16)}…) → toggled done + tagged`);
79
-
80
- const localOpen = await app.openTodosByList(listId);
81
- if (localOpen !== 1) fail(`local count: expected 1 open, got ${localOpen}`);
82
- ok("local reads route — todosByList = 2, openTodosByList = 1 (2 todos − 1 done), no network");
83
-
84
- globalThis.fetch = realFetch;
85
- ok("offline proof ENFORCED — fetch was trapped: local write/query/count touched no network");
86
-
87
- // 4. sync: push the session branch → edge admission merges to canonical main
88
- const s = await holon.sync();
89
- if (!s.admission?.ok) fail(`sync/admit: ${JSON.stringify(s)}`);
90
- ok("synced — session branch admitted onto canonical main");
91
-
92
- // 5. the CLOUD answers the declared query + the maintained count from canonical main
93
- d = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/query?queryId=todosByList&params=${encodeURIComponent(JSON.stringify({ listId }))}`)).json();
94
- if (!(d.ok && d.rows.length === 2)) fail(`cloud query: ${JSON.stringify(d)}`);
95
- ok("cloud query — todosByList returned both todos from canonical main");
96
- d = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/counts/openTodosByList?group=${encodeURIComponent(listId)}`)).json();
97
- if (!(d.ok && d.count === 1)) fail(`cloud count: ${JSON.stringify(d)}`);
98
- ok("cloud count — openTodosByList = 1 (the where-filtered O(1) tally, maintained)");
99
-
100
- // 6. SHARING — one native Zanzibar tuple. Author it through the owner's secret-gated
101
- // /author lane, then read it back from the cloud's audit query.
102
- r = await fetch(`${CLOUD}/v1/workspaces/${WS}/author`, {
103
- method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${SECRET}` },
104
- body: JSON.stringify({ domain: "todo", directiveId: "shareList", payload: {
105
- listId, userId: "bob", role: "editor", sharedBy: "owner-1", sharedAt: new Date().toISOString() } }),
106
- });
107
- d = await r.json();
108
- if (!d.ok) fail(`shareList author: ${JSON.stringify(d)}`);
109
- d = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/query?queryId=relationTuplesByObject&params=${encodeURIComponent(JSON.stringify({ object: `TodoList:${listId}` }))}`)).json();
110
- const editor = (d.rows ?? []).find((u: { data: { relation: string } }) => u.data.relation === "editor");
111
- if (editor?.data?.subjects?.["user:bob"]?.status !== "active") fail(`sharing tuple not recorded: ${JSON.stringify(d)}`);
112
- ok("sharing — TodoList#editor@user:bob recorded as a native relation tuple (active)");
113
-
114
- console.log(`\nALL GREEN — typed client: compile → deploy → connect → typed offline writes → query/count → admit → cloud reads → native sharing (${WS})`);