@githolon/dsl 0.16.3 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart/CHANGELOG.md +5 -0
- package/dart/lib/nomos_client.dart +5 -0
- package/dart/lib/src/errors.dart +79 -0
- package/dart/lib/src/home.dart +20 -0
- package/dart/lib/src/webview_bridge.dart +68 -0
- package/dart/lib/src/wire.dart +117 -0
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -1
- package/dart/test/home_parity_test.dart +9 -0
- package/dart/test/wire_test.dart +48 -0
- package/package.json +1 -1
- package/src/births.ts +82 -0
- package/src/build_package.ts +23 -27
- package/src/codegen_dart.ts +10 -0
- package/src/codegen_ts.ts +12 -8
- package/src/compile_package_main.ts +61 -109
- package/src/decl_shapes.ts +122 -0
- package/src/directive.ts +30 -0
- package/src/engine_entry.ts +109 -8
- package/src/framework/home.ts +179 -0
- package/src/framework/impure_capability.ts +8 -4
- package/src/framework/relations.ts +22 -0
- package/src/framework/signing_identity.ts +291 -2
- package/src/framework/workspace_invariant.ts +17 -2
- package/src/framework/workspaces.ts +392 -153
- package/src/index.ts +42 -0
- package/src/usd.ts +44 -0
- package/src/usd_layers.ts +41 -0
- package/src/wire.ts +13 -0
- package/src/wire_encode.ts +14 -2
package/src/engine_entry.ts
CHANGED
|
@@ -54,6 +54,13 @@
|
|
|
54
54
|
import { executeDirectiveToIntent } from "./wire_encode.js";
|
|
55
55
|
import { expandCapabilityExports } from "./capability_exports.js";
|
|
56
56
|
import { RelationTuple } from "./framework/relations.js";
|
|
57
|
+
import { generateOpenApiFragment, mergeOpenApiFragments, type OpenApiDoc } from "./codegen_openapi.js";
|
|
58
|
+
import type { QueryDecl } from "./query.js";
|
|
59
|
+
import type { AnyCount } from "./count.js";
|
|
60
|
+
import type { AnySum } from "./sum.js";
|
|
61
|
+
import type { AnyExtremum } from "./extremum.js";
|
|
62
|
+
import type { SpatialDecl } from "./spatial.js";
|
|
63
|
+
import type { DomainModule } from "./codegen_dart.js";
|
|
57
64
|
import type { Directive } from "./directive.js";
|
|
58
65
|
import type { AggregateHandle, AggregateInvariantFn, AggregateInvariantVerdict } from "./aggregate.js";
|
|
59
66
|
import type { Ports } from "./ctx.js";
|
|
@@ -120,6 +127,34 @@ export interface EngineEntryConfig {
|
|
|
120
127
|
* `sideEffects: false` (a bare side-effect import would be tree-shaken away).
|
|
121
128
|
*/
|
|
122
129
|
readonly seeds?: readonly unknown[];
|
|
130
|
+
/**
|
|
131
|
+
* THE SELF-DESCRIBE INPUT (G1 — openapi residue removal): the per-domain read/maintenance
|
|
132
|
+
* decls the OpenAPI fragment needs that are NOT discoverable by shape from the live module
|
|
133
|
+
* exports alone (queries/counts/sums/mins/maxes/spatials/workspaceInvariants — the compiler
|
|
134
|
+
* already auto-discovered + named-resolved them when it composed each DomainModule). The
|
|
135
|
+
* directives + aggregates ARE discovered from the modules; these ride alongside so the
|
|
136
|
+
* lump's `globalThis.describe` reproduces the EXACT `<name>.openapi.json` the compiler wrote
|
|
137
|
+
* (byte-identity, no Rust re-implementation, no drift). Keyed by domain dispatch key, in the
|
|
138
|
+
* same declaration order as `domains`. Omitted ⇒ describe still emits offers from directives.
|
|
139
|
+
*/
|
|
140
|
+
readonly openapiDecls?: Record<string, OpenApiDomainDecls>;
|
|
141
|
+
/**
|
|
142
|
+
* The package's OpenAPI document title (the compiler's `"<name> (nomos package)"`). Baked into
|
|
143
|
+
* the lump so `describe` reproduces the compiler's `<name>.openapi.json` byte-for-byte without
|
|
144
|
+
* the kernel knowing the package name. The kernel MAY override it (the workspace-level title).
|
|
145
|
+
*/
|
|
146
|
+
readonly packageTitle?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The openapi-relevant decls one domain contributes beyond its directives/aggregates. */
|
|
150
|
+
export interface OpenApiDomainDecls {
|
|
151
|
+
readonly queries?: readonly QueryDecl[];
|
|
152
|
+
readonly counts?: readonly AnyCount[];
|
|
153
|
+
readonly sums?: readonly AnySum[];
|
|
154
|
+
readonly mins?: readonly AnyExtremum[];
|
|
155
|
+
readonly maxes?: readonly AnyExtremum[];
|
|
156
|
+
readonly spatials?: readonly SpatialDecl[];
|
|
157
|
+
readonly workspaceInvariants?: readonly WorkspaceInvariantDecl[];
|
|
123
158
|
}
|
|
124
159
|
|
|
125
160
|
interface RegistryEntry {
|
|
@@ -395,6 +430,8 @@ function portsFromHost(principalRaw = ""): Ports {
|
|
|
395
430
|
export interface RegisteredEngine {
|
|
396
431
|
plan(job: unknown): WireEvent[] | { events: WireEvent[]; strikes?: string[]; endorse?: { sink: string; facet: string; reason: string | null }[] };
|
|
397
432
|
planReport(job: unknown): string;
|
|
433
|
+
/** The G1 self-describe verb: the lump's own merged OpenAPI, derived from its modules. */
|
|
434
|
+
describe(opts?: { lawHash?: string; title?: string }): OpenApiDoc;
|
|
398
435
|
}
|
|
399
436
|
|
|
400
437
|
/**
|
|
@@ -543,7 +580,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
543
580
|
/** `{workspaceInvariantReads:{id}, payload}` → the bounded ref-set `[{name,aggregate,id},…]`. */
|
|
544
581
|
function planWorkspaceInvariantReads(job: {
|
|
545
582
|
intent?: { workspaceInvariantReads?: { id?: string }; payload?: unknown };
|
|
546
|
-
capturedPorts?: { principal?: string };
|
|
583
|
+
capturedPorts?: { principal?: string; principal_verified?: boolean };
|
|
547
584
|
}): string {
|
|
548
585
|
const id = job.intent?.workspaceInvariantReads?.id;
|
|
549
586
|
if (typeof id !== "string") {
|
|
@@ -559,14 +596,17 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
559
596
|
// LAW-GATED AUTHZ: the captured author identity (a top-level sibling of `intent`, set by the wasm
|
|
560
597
|
// author leg + replayed verbatim) — the governance invariant reads it to gate by the AUTHOR.
|
|
561
598
|
const principal = (job.capturedPorts?.principal ?? "") as string;
|
|
562
|
-
|
|
599
|
+
// VA-core (Phase 3): the kernel-DERIVED author-verified flag (`captured_ports.principal_verified`),
|
|
600
|
+
// threaded the same way as `principal`, so an invariant may gate on a VERIFIED author (e.g. enrollSignerSelf).
|
|
601
|
+
const principalVerified = job.capturedPorts?.principal_verified === true;
|
|
602
|
+
return JSON.stringify(decl.reads({ intent, principal, principalVerified }));
|
|
563
603
|
}
|
|
564
604
|
|
|
565
605
|
/** `{workspaceInvariant:{id,on}, payload?}` + priorState (named snapshots) → {accept}|{reject}. */
|
|
566
606
|
function planWorkspaceInvariantAssert(job: {
|
|
567
607
|
intent?: { workspaceInvariant?: { id?: string }; payload?: unknown };
|
|
568
608
|
priorState?: Record<string, unknown>;
|
|
569
|
-
capturedPorts?: { principal?: string };
|
|
609
|
+
capturedPorts?: { principal?: string; principal_verified?: boolean };
|
|
570
610
|
}): string {
|
|
571
611
|
const id = job.intent?.workspaceInvariant?.id;
|
|
572
612
|
if (typeof id !== "string") {
|
|
@@ -581,7 +621,9 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
581
621
|
const snapshots = (job.priorState ?? {}) as Record<string, Record<string, unknown>>;
|
|
582
622
|
const intent = (job.intent?.payload ?? {}) as Record<string, unknown>;
|
|
583
623
|
const principal = (job.capturedPorts?.principal ?? "") as string;
|
|
584
|
-
|
|
624
|
+
// VA-core (Phase 3): the kernel-DERIVED author-verified flag — threaded like `principal`.
|
|
625
|
+
const principalVerified = job.capturedPorts?.principal_verified === true;
|
|
626
|
+
return JSON.stringify(decl.assert(snapshots, { intent, principal, principalVerified }));
|
|
585
627
|
}
|
|
586
628
|
|
|
587
629
|
function planDerive(job: {
|
|
@@ -717,7 +759,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
717
759
|
function plan(job: {
|
|
718
760
|
intent?: { domain?: string; directiveId?: string; payload?: unknown };
|
|
719
761
|
capturedPorts?: { principal?: string };
|
|
720
|
-
}): WireEvent[] | { events: WireEvent[]; strikes?: string[]; endorse?: { sink: string; facet: string; reason: string | null }[] } {
|
|
762
|
+
}): WireEvent[] | { events: WireEvent[]; strikes?: string[]; endorse?: { sink: string; facet: string; reason: string | null }[]; births?: { workspace: string; genesisChain: unknown }[] } {
|
|
721
763
|
const intent = job.intent ?? {};
|
|
722
764
|
const domain = intent.domain;
|
|
723
765
|
const directiveId = intent.directiveId;
|
|
@@ -739,18 +781,77 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
739
781
|
// it to check the law's nomosFlowGate.endorse; decode_plan_output ignores it for the byte-compare),
|
|
740
782
|
// so a principal-free intent still emits the bare array, byte-identical.
|
|
741
783
|
const endorse = wire.endorse;
|
|
742
|
-
|
|
784
|
+
// THE BIRTH EFFECT (G3): a `.births()` directive's plan recorded the child's genesis recipe; it rides the
|
|
785
|
+
// plan-output object as a TRANSIENT verify-input (like `endorse` — the kernel reads it onto the intent's
|
|
786
|
+
// `captured_ports.births` and folds it after admit; decode_plan_output ignores it for the byte-compare),
|
|
787
|
+
// so a birth-free intent still emits the bare event array, byte-identical (the era rule).
|
|
788
|
+
const births = wire.births;
|
|
789
|
+
if (!wire.strikes.length && !(endorse && endorse.length) && !(births && births.length)) return wire.events;
|
|
743
790
|
return {
|
|
744
791
|
events: wire.events,
|
|
745
792
|
...(wire.strikes.length ? { strikes: wire.strikes } : {}),
|
|
746
793
|
...(endorse && endorse.length ? { endorse } : {}),
|
|
794
|
+
...(births && births.length ? { births } : {}),
|
|
747
795
|
};
|
|
748
796
|
}
|
|
749
797
|
|
|
798
|
+
/**
|
|
799
|
+
* THE SELF-DESCRIBE VERB (G1) — the lump's OWN OpenAPI, derived from the SAME modules it
|
|
800
|
+
* dispatches. The kernel runs this over the FOLDED law (the lump IS `nomos:executable:
|
|
801
|
+
* javascriptHex` in the package USDA in custody), so the workspace's API surface is a pure,
|
|
802
|
+
* deterministic function of installed law — NOT a host-staged blob. It reproduces, byte for
|
|
803
|
+
* byte, the `<name>.openapi.json` the compiler wrote: per domain (declaration order) build the
|
|
804
|
+
* openapi-relevant DomainModule (directives + aggregates from the live exports; queries/counts/
|
|
805
|
+
* sums/mins/maxes/spatials/workspaceInvariants from `config.openapiDecls`), `generateOpenApi
|
|
806
|
+
* Fragment`, then `mergeOpenApiFragments` — the EXACT calls compile_package_main makes.
|
|
807
|
+
* `opts.lawHash`/`opts.title` are injected by the kernel at describe-time (the version stamp +
|
|
808
|
+
* package title), matching the compiler's `domainHash`/`"<name> (nomos package)"`.
|
|
809
|
+
*/
|
|
810
|
+
function describe(opts?: { lawHash?: string; title?: string }): OpenApiDoc {
|
|
811
|
+
const lawHash = opts?.lawHash;
|
|
812
|
+
const fragments: OpenApiDoc[] = [];
|
|
813
|
+
for (const domainName of domainNames) {
|
|
814
|
+
const sources = config.domains[domainName];
|
|
815
|
+
if (sources === undefined) continue;
|
|
816
|
+
const merged = mergeModules(sources.map((m) => (typeof m === "function" ? m() : m)));
|
|
817
|
+
// Aggregates dedup by wire id + RelationTuple auto-ensure (the compose contract) — openapi
|
|
818
|
+
// reads aggregates only for the hasMany inverse indexes, so this matches composeDomainModule.
|
|
819
|
+
const byId = aggregatesOf(merged);
|
|
820
|
+
const directives = [...directivesOf(merged).values()];
|
|
821
|
+
if (!byId.has(RelationTuple.id) && directives.some((d) => d.aggregateId === RelationTuple.id)) {
|
|
822
|
+
byId.set(RelationTuple.id, RelationTuple);
|
|
823
|
+
}
|
|
824
|
+
const decls = config.openapiDecls?.[domainName] ?? {};
|
|
825
|
+
const module = {
|
|
826
|
+
name: domainName,
|
|
827
|
+
domain: domainName,
|
|
828
|
+
aggregates: [...byId.values()],
|
|
829
|
+
directives,
|
|
830
|
+
...(decls.queries && decls.queries.length > 0 ? { queries: [...decls.queries] } : {}),
|
|
831
|
+
...(decls.counts && decls.counts.length > 0 ? { counts: [...decls.counts] } : {}),
|
|
832
|
+
...(decls.sums && decls.sums.length > 0 ? { sums: [...decls.sums] } : {}),
|
|
833
|
+
...(decls.mins && decls.mins.length > 0 ? { mins: [...decls.mins] } : {}),
|
|
834
|
+
...(decls.maxes && decls.maxes.length > 0 ? { maxes: [...decls.maxes] } : {}),
|
|
835
|
+
...(decls.spatials && decls.spatials.length > 0 ? { spatials: [...decls.spatials] } : {}),
|
|
836
|
+
...(decls.workspaceInvariants && decls.workspaceInvariants.length > 0
|
|
837
|
+
? { workspaceInvariants: [...decls.workspaceInvariants] }
|
|
838
|
+
: {}),
|
|
839
|
+
} as unknown as DomainModule;
|
|
840
|
+
fragments.push(generateOpenApiFragment(module, lawHash !== undefined ? { lawHash } : {}));
|
|
841
|
+
}
|
|
842
|
+
const title = opts?.title ?? config.packageTitle;
|
|
843
|
+
return mergeOpenApiFragments(fragments, {
|
|
844
|
+
...(title !== undefined ? { title } : {}),
|
|
845
|
+
...(lawHash !== undefined ? { lawHash } : {}),
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
750
849
|
// The engine evals the bundle as a classic script and FREEZES globalThis before
|
|
751
|
-
// dispatch — assign
|
|
850
|
+
// dispatch — assign all entries NOW, at top-level eval. `describe` is the G1 self-
|
|
851
|
+
// describe verb (the kernel `describe` run mode evals `globalThis.describe(opts)`).
|
|
752
852
|
(globalThis as { plan?: unknown }).plan = plan;
|
|
753
853
|
(globalThis as { planReport?: unknown }).planReport = planReport;
|
|
854
|
+
(globalThis as { describe?: unknown }).describe = describe;
|
|
754
855
|
|
|
755
|
-
return { plan, planReport } as RegisteredEngine;
|
|
856
|
+
return { plan, planReport, describe } as RegisteredEngine;
|
|
756
857
|
}
|
package/src/framework/home.ts
CHANGED
|
@@ -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
|
|
105
|
-
* records on completion
|
|
106
|
-
*
|
|
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.
|
|
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 [
|