create-githolon 0.34.1 → 0.36.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-githolon",
3
- "version": "0.34.1",
3
+ "version": "0.36.0",
4
4
  "type": "module",
5
5
  "description": "Scaffold a Nomos domain package: the starter domain + compile config + live e2e. `npm create githolon my-app`.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -0,0 +1,87 @@
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("co2-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("co2-platform", platformPolicy) // the declared foreign read
28
+ .plan((p) => {
29
+ const rows = read(platformPolicy, { scope: "homes" }, { from: "co2-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: { "co2-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/co2-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.