@githolon/dsl 0.15.0 → 0.16.1
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 +2 -0
- package/dart/FINDINGS.md +2 -2
- package/dart/LICENSE +45 -0
- package/dart/README.md +9 -0
- package/dart/example/nomos_webview_transport.dart +4 -4
- package/dart/lib/src/offer.dart +2 -2
- package/dart/lib/src/subscriptions.dart +1 -1
- package/dart/lib/src/webview_bridge.dart +52 -0
- package/dart/pubspec.yaml +3 -3
- package/dart/test/schema_validation_test.dart +1 -1
- package/dart/test/webview_bridge_test.dart +1 -1
- package/dart/test/wire_test.dart +1 -1
- package/package.json +1 -1
- package/src/aggregate.ts +9 -1
- package/src/authoring.ts +10 -1
- package/src/codegen_dart.ts +25 -15
- package/src/codegen_dart_client.ts +54 -5
- package/src/codegen_openapi.ts +239 -0
- package/src/codegen_proof.ts +16 -9
- package/src/codegen_ts.ts +83 -22
- package/src/compile_package_main.ts +117 -29
- package/src/ctx.ts +13 -0
- package/src/directive.ts +1 -1
- package/src/engine_entry.ts +22 -5
- package/src/fields.ts +44 -0
- package/src/flow_gate.ts +208 -0
- package/src/framework/capability.ts +19 -1
- package/src/framework/encryption_scope.ts +9 -1
- package/src/framework/genesis.ts +5 -1
- package/src/framework/relations.ts +23 -4
- package/src/framework/repair.ts +2 -2
- package/src/framework/signing_identity.ts +1 -1
- package/src/framework/workspace_invariant.ts +29 -8
- package/src/framework/workspaces.ts +147 -15
- package/src/index.ts +24 -0
- package/src/manifest.ts +12 -18
- package/src/ops.ts +42 -1
- package/src/principal_evidence.ts +112 -0
- package/src/relation_sugar.ts +10 -4
- package/src/shape_compat.ts +208 -0
- package/src/usd.ts +131 -0
- package/src/usd_layers.ts +43 -0
- package/src/wire.ts +8 -0
- package/src/wire_encode.ts +75 -2
- package/src/workspace_sharding.ts +196 -18
- package/src/workspace_type.ts +3 -2
- /package/dart/lib/{nomos_dsl.dart → nomos_client.dart} +0 -0
|
@@ -73,8 +73,17 @@ import { t } from "../fields.js";
|
|
|
73
73
|
import { set, withMarker } from "../ops.js";
|
|
74
74
|
import { query } from "../query.js";
|
|
75
75
|
import { workspaceInvariant, refAs } from "./workspace_invariant.js";
|
|
76
|
-
import { writeTuple, removeTuple, RelationTuple } from "./relations.js";
|
|
76
|
+
import { writeTuple, removeTuple, RelationTuple, relationTupleInstanceId } from "./relations.js";
|
|
77
77
|
import { relationType, self, user, key } from "../relation_schema.js";
|
|
78
|
+
import { GENESIS_WARRANT_ID } from "./genesis.js";
|
|
79
|
+
import { signerObject, SIGNER_RELATION } from "./signing_identity.js";
|
|
80
|
+
// THE GENESIS BOOTSTRAP (platform-birth genesis bootstrap, Approach A): governance carries the
|
|
81
|
+
// warrant-flip primitives so the birth door can lawfully bootstrap a virgin platform's founding admin —
|
|
82
|
+
// E `enrollSigner` (the founding signer tuple) → W `seedGenesis` (the GenesisWarrant) → A signed
|
|
83
|
+
// `grantAdmin` (admitted by the kernel genesis clause). Re-exported so the package's shape-discovery picks
|
|
84
|
+
// them up as part of the `workspaces` domain. (architecture/platform_birth_genesis_bootstrap.md)
|
|
85
|
+
export { GenesisWarrant, seedGenesis } from "./genesis.js";
|
|
86
|
+
export { identityRelations, enrollSigner } from "./signing_identity.js";
|
|
78
87
|
|
|
79
88
|
// ── WORKSPACE SELF-IDENTITY (the law reads its OWN install-context) ──────────────────────────────────
|
|
80
89
|
// The governance bundle runs on root AND every born platform. A workspace knows its ROLE via this
|
|
@@ -204,6 +213,10 @@ export const CloudWorkspace = aggregate("CloudWorkspace", {
|
|
|
204
213
|
createdAt: t.int().merge(Lww),
|
|
205
214
|
/** Epoch-ms the worker completed physical provisioning; absent until then. */
|
|
206
215
|
provisionedAt: t.int().merge(Lww).optional(),
|
|
216
|
+
/** sha256 hex of the founding-admin key for a kind:platform birth — DATA the birth capability provider
|
|
217
|
+
* grants admin to (never the key). The record (status:requested, kind:platform) IS the birth obligation;
|
|
218
|
+
* the provider drains it, materializes, and authors provisionWorkspace (the receipt). Absent for plain ws. */
|
|
219
|
+
foundingAdminHash: t.string().merge(Lww).optional(),
|
|
207
220
|
/**
|
|
208
221
|
* Per-workspace cumulative-ledger byte cap OVERRIDE (bytes); absent = the
|
|
209
222
|
* cloud-wide {@link CloudPolicy} default applies. Raised on the record via
|
|
@@ -374,6 +387,8 @@ export const createWorkspace = directive("createWorkspace")
|
|
|
374
387
|
.set("ownerGrantHash", "") // no owner credential until provisioned
|
|
375
388
|
.set("createdBy", p.requestedBy)
|
|
376
389
|
.set("createdAt", p.requestedAt);
|
|
390
|
+
// A plain workspace record (no kind:platform) — boot materializes it directly; the birth capability
|
|
391
|
+
// provider only drains kind:platform records, so this is a lineage record, not a genesis order.
|
|
377
392
|
return [];
|
|
378
393
|
})
|
|
379
394
|
.requires("creator"); // posture-gated creation: only a #creator holder may request a workspace
|
|
@@ -462,7 +477,8 @@ export const rotateWorkspaceOwner = directive("rotateWorkspaceOwner")
|
|
|
462
477
|
.plan((p) => {
|
|
463
478
|
const ws = instance(CloudWorkspace, p.workspaceId);
|
|
464
479
|
return [set(ws, "ownerGrantHash", p.ownerGrantHash)];
|
|
465
|
-
})
|
|
480
|
+
})
|
|
481
|
+
.requires("admin"); // lifecycle authority is law-state, not a host allowlist
|
|
466
482
|
|
|
467
483
|
/**
|
|
468
484
|
* setWorkspaceTier — move a workspace between `edge` and `heavy` ON THE RECORD;
|
|
@@ -482,7 +498,8 @@ export const setWorkspaceTier = directive("setWorkspaceTier")
|
|
|
482
498
|
.plan((p) => {
|
|
483
499
|
const ws = instance(CloudWorkspace, p.workspaceId);
|
|
484
500
|
return [set(ws, "tier", p.tier)];
|
|
485
|
-
})
|
|
501
|
+
})
|
|
502
|
+
.requires("admin");
|
|
486
503
|
|
|
487
504
|
/**
|
|
488
505
|
* retireWorkspace — the law withdraws the workspace. The worker thereafter
|
|
@@ -501,7 +518,8 @@ export const retireWorkspace = directive("retireWorkspace")
|
|
|
501
518
|
.plan((p) => {
|
|
502
519
|
const ws = instance(CloudWorkspace, p.workspaceId);
|
|
503
520
|
return [set(ws, "status", "retired" as const)];
|
|
504
|
-
})
|
|
521
|
+
})
|
|
522
|
+
.requires("admin");
|
|
505
523
|
|
|
506
524
|
/**
|
|
507
525
|
* reclaimWorkspace — THE LAWFUL CUSTODY DISCARD. Where {@link retireWorkspace}
|
|
@@ -531,7 +549,8 @@ export const reclaimWorkspace = directive("reclaimWorkspace")
|
|
|
531
549
|
.plan((p) => {
|
|
532
550
|
const ws = instance(CloudWorkspace, p.workspaceId);
|
|
533
551
|
return [set(ws, "status", "reclaimed" as const)];
|
|
534
|
-
})
|
|
552
|
+
})
|
|
553
|
+
.requires("admin");
|
|
535
554
|
|
|
536
555
|
/**
|
|
537
556
|
* birthPlatform — A PLATFORM IS BORN: the cloud, one level down. Records the
|
|
@@ -552,7 +571,10 @@ export const birthPlatform = directive("birthPlatform")
|
|
|
552
571
|
parent: z.string().default(""),
|
|
553
572
|
/** The pool: how many live children the platform may parent. */
|
|
554
573
|
pool: z.number().int().positive().default(50),
|
|
555
|
-
tier: z.enum(WORKSPACE_TIERS).default("
|
|
574
|
+
tier: z.enum(WORKSPACE_TIERS).default("heavy"),
|
|
575
|
+
/** sha256 of the founding-admin key the requester PRESENTED (the provider grants admin to the hash;
|
|
576
|
+
* the requester keeps the key — it is the platform's admin credential, used to govern + deploy). */
|
|
577
|
+
foundingAdminHash: sha256Hex,
|
|
556
578
|
requestedBy: z.string().min(1),
|
|
557
579
|
requestedAt: epochMs,
|
|
558
580
|
}),
|
|
@@ -567,12 +589,32 @@ export const birthPlatform = directive("birthPlatform")
|
|
|
567
589
|
.set("tier", p.tier)
|
|
568
590
|
.set("status", "requested" as const)
|
|
569
591
|
.set("ownerGrantHash", "")
|
|
592
|
+
.set("foundingAdminHash", p.foundingAdminHash) // the provider grants admin to this hash at materialize
|
|
570
593
|
.set("createdBy", p.requestedBy)
|
|
571
594
|
.set("createdAt", p.requestedAt);
|
|
595
|
+
// THE RECORD IS THE OBLIGATION: a kind:platform CloudWorkspace at status:requested with no ownerGrantHash
|
|
596
|
+
// is an order the birth capability provider drains (materialize + install the parent's law + seed genesis
|
|
597
|
+
// + grant the founding admin + birthCloud), closing it with provisionWorkspace (the receipt). No separate
|
|
598
|
+
// obligation aggregate — the minted CloudWorkspace IS it (sidesteps the id-mint gate; reuses the lifecycle).
|
|
572
599
|
return [];
|
|
573
600
|
})
|
|
574
601
|
.requires("platformCreator"); // invite-only: an active #platformCreator tuple admits the birth
|
|
575
602
|
|
|
603
|
+
// ─────────────────────────── BIRTH AS A NOMOS PRIMITIVE ──────────────────────────────────────────────
|
|
604
|
+
// THE RECORD IS THE OBLIGATION (architecture/githolon_host_contract.md "Birth is a capability"). Birth is
|
|
605
|
+
// an IMPURE capability: the parent ORDERS (the law-gated birthPlatform mints a CloudWorkspace at
|
|
606
|
+
// status:requested, kind:platform, carrying foundingAdminHash — gated by .requires("platformCreator")),
|
|
607
|
+
// the BIRTH CAPABILITY PROVIDER fulfils it (materialize: install the PARENT's law + seed genesis + grant
|
|
608
|
+
// the founding admin + birthCloud — the child's OWN kernel admitting each), and authors provisionWorkspace
|
|
609
|
+
// (the receipt) the parent TRUSTS. No separate obligation aggregate — the minted CloudWorkspace IS the
|
|
610
|
+
// order (it sidesteps the id-mint gate and reuses the requested→provisioned lifecycle). The provider knows
|
|
611
|
+
// HOW (the Capability Exception sanctions a declared effect); the law owns WHO/WHAT; the parent trusts the
|
|
612
|
+
// DID-IT-HAPPEN. A child born this way runs THIS SAME law and births its children identically.
|
|
613
|
+
|
|
614
|
+
/** workspacesByStatus — the birth provider drains `requested` CloudWorkspaces (kind:platform, no
|
|
615
|
+
* ownerGrantHash) → materialize → provisionWorkspace. The law's order queue; the host polls, never decides. */
|
|
616
|
+
export const workspacesByStatus = query("workspacesByStatus").key("status").returns(CloudWorkspace);
|
|
617
|
+
|
|
576
618
|
/** setPlatformPool — the pool edition of a granted quota request. */
|
|
577
619
|
export const setPlatformPool = directive("setPlatformPool")
|
|
578
620
|
.mutates(CloudWorkspace)
|
|
@@ -729,7 +771,8 @@ export const claimHostname = directive("claimHostname")
|
|
|
729
771
|
.set("claimedAt", p.claimedAt)
|
|
730
772
|
.set("status", "active" as const);
|
|
731
773
|
return [];
|
|
732
|
-
})
|
|
774
|
+
})
|
|
775
|
+
.requires("admin"); // hostname authority is law-state
|
|
733
776
|
|
|
734
777
|
/** releaseHostname — free the name (the record stays — audit, never erasure). */
|
|
735
778
|
export const releaseHostname = directive("releaseHostname")
|
|
@@ -744,7 +787,8 @@ export const releaseHostname = directive("releaseHostname")
|
|
|
744
787
|
.plan((p) => {
|
|
745
788
|
const c = instance(HostnameClaim, p.claimId);
|
|
746
789
|
return [set(c, "status", "released" as const)];
|
|
747
|
-
})
|
|
790
|
+
})
|
|
791
|
+
.requires("admin");
|
|
748
792
|
|
|
749
793
|
/**
|
|
750
794
|
* bindCapability — record the FACT that a workspace holds a capability
|
|
@@ -782,7 +826,8 @@ export const bindCapability = directive("bindCapability")
|
|
|
782
826
|
if (p.scope !== undefined) b.set("scope", p.scope);
|
|
783
827
|
if (p.expiresAt !== undefined) b.set("expiresAt", p.expiresAt);
|
|
784
828
|
return [];
|
|
785
|
-
})
|
|
829
|
+
})
|
|
830
|
+
.requires("admin"); // capability-bind authority is law-state
|
|
786
831
|
|
|
787
832
|
/**
|
|
788
833
|
* revokeCapability — flip a CapabilityBinding to `revoked`. The credential
|
|
@@ -804,7 +849,8 @@ export const revokeCapability = directive("revokeCapability")
|
|
|
804
849
|
.plan((p) => {
|
|
805
850
|
const b = instance(CapabilityBinding, p.bindingId);
|
|
806
851
|
return [set(b, "status", "revoked" as const)];
|
|
807
|
-
})
|
|
852
|
+
})
|
|
853
|
+
.requires("admin");
|
|
808
854
|
|
|
809
855
|
/**
|
|
810
856
|
* setCloudPolicy — set (or amend) THE QUOTA LAW. An Ensure on the law-state
|
|
@@ -1025,11 +1071,20 @@ export const capabilityBindingByKeyHash = query("capabilityBindingByKeyHash")
|
|
|
1025
1071
|
// UserPermission): admin=`workspace:self#admin@key:<keyHash>`, creator=`…#creator@key:…`,
|
|
1026
1072
|
// platformCreator=`workspace:self#platformCreator@user:<principal>`. grantAdmin/grantCreation/
|
|
1027
1073
|
// grantPlatformCreation write them via `writeTuple`; the bespoke AdminGrant/CreationGrant aggregates +
|
|
1028
|
-
// their queries are GONE.
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
|
|
1074
|
+
// their queries are GONE. RelationTuple's stable id rides into the lockfile + identity manifest via the
|
|
1075
|
+
// `export { RelationTuple }` below (grantAdmin/grantCreation/… `.ensures(RelationTuple)`); the projected
|
|
1076
|
+
// relationTuplesByObject read routes via its own re-export. (QuotaGrant stays an aggregate — a numeric
|
|
1077
|
+
// allowance, not a membership.)
|
|
1078
|
+
//
|
|
1079
|
+
// THE RAW TUPLE WRITERS ARE NOT GOVERNANCE OFFERS (the security pass). `relateSubject`/`unrelateSubject`
|
|
1080
|
+
// are the framework PRIMITIVE (still the DSL public API for domains that knowingly compose their own
|
|
1081
|
+
// schema); governance never authored through them — every governance tuple write is a GATED wrapper
|
|
1082
|
+
// (grantAdmin/grantCreation/grantPlatformCreation → `writeTuple`, each `.requires("admin")`). Re-exporting
|
|
1083
|
+
// the bare directives only registered an UNGATED `workspace:self#admin@…` writer for dispatch — a
|
|
1084
|
+
// self-grant-admin hole the moment the /v2 gateway routes every offer. Dropping the re-export removes them
|
|
1085
|
+
// from governance's law/dispatch/spec ENTIRELY (not hidden — genuinely not governance's API; the true
|
|
1086
|
+
// generic "you may only grant a relation you hold" rule is a future `relationFrom` kernel arc).
|
|
1087
|
+
export { relationTuplesByObject } from "./relations.js";
|
|
1033
1088
|
export { RelationTuple };
|
|
1034
1089
|
|
|
1035
1090
|
// ── GOVERNANCE WRITE-AUTHZ IS NATIVE ZANZIBAR — the ONE gate, no bespoke predicates ───────────────────
|
|
@@ -1045,3 +1100,80 @@ export { RelationTuple };
|
|
|
1045
1100
|
// in the pre-apply state). SELF-RELATIVE: custody guarantees a workspace only folds its OWN tuples, so the
|
|
1046
1101
|
// SAME law governs root and every born platform. The host's `_adminOk`/`_creationLawOk`/`_platformCheck`
|
|
1047
1102
|
// pre-checks are GONE too (a pure bailiff) — the native gate refuses a non-holder at admission.
|
|
1103
|
+
|
|
1104
|
+
// ── THE SECURITY PASS: genesis + receipt gating as LAW (nomos-improving-nomos / the spec audit) ────────
|
|
1105
|
+
// The /v2 gateway routes EVERY offer (nothing hidden — Jack's "make the surface attack-proof"). An offer
|
|
1106
|
+
// with neither a `.requires` gate nor an invariant is a RED FLAG: a raw ungated primitive. The genesis
|
|
1107
|
+
// directives (seedGenesis/enrollSigner) + the self-birth (birthCloud) + the birth receipt
|
|
1108
|
+
// (provisionWorkspace) CANNOT use `.requires` — they run pre-admin / pre-warrant or carry no actor — so
|
|
1109
|
+
// the protection that USED to live in the HOST session-lane refusal of control-plane intents moves here,
|
|
1110
|
+
// onto the ledger, as workspaceInvariants: pure predicates over pre-apply state + the payload, fired at the
|
|
1111
|
+
// ONE gate (author / edge admission / verify_chain replay), era-keyed (nomosInvariantGate). No kernel
|
|
1112
|
+
// change, no host gate — exactly the "evolve security THROUGH nomos" the north star demands.
|
|
1113
|
+
//
|
|
1114
|
+
// WRITE-ONCE-BY-VALUE, not strict create-once: an IDENTICAL re-offer (root's by-hash idempotent genesis
|
|
1115
|
+
// re-entry) passes; only a DIFFERENT binding is refused. So idempotence survives while a forged re-seed
|
|
1116
|
+
// (regime-flip / owner-rebind / signer-graft) becomes a typed refusal on every door.
|
|
1117
|
+
|
|
1118
|
+
/** seedGenesis: the founding GenesisWarrant is write-once-by-value. A virgin chain may seed; re-seeding
|
|
1119
|
+
* the SAME (principal, workspaceId) is idempotent; a DIFFERENT binding — the signed-author regime-flip /
|
|
1120
|
+
* owner-rebind the host session-lane used to refuse — is rejected. */
|
|
1121
|
+
export const genesisWarrantWriteOnce = workspaceInvariant("GenesisWarrantWriteOnce")
|
|
1122
|
+
.on("seedGenesis")
|
|
1123
|
+
.reads(() => [refAs("warrant", "GenesisWarrant", "pre:" + GENESIS_WARRANT_ID)])
|
|
1124
|
+
.assert(({ warrant }, ctx) => {
|
|
1125
|
+
const cur = warrant as { principal?: string; workspaceId?: string } | undefined;
|
|
1126
|
+
if (!cur?.principal) return { accept: true }; // virgin — genesis may seed the founding warrant
|
|
1127
|
+
const p = (ctx?.intent ?? {}) as { principal?: string; workspaceId?: string };
|
|
1128
|
+
return cur.principal === p.principal && (cur.workspaceId ?? "") === (p.workspaceId ?? "")
|
|
1129
|
+
? { accept: true } // idempotent re-seed (the by-hash genesis re-entry)
|
|
1130
|
+
: { reject: "genesis-warrant-already-bound" };
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
/** enrollSigner: the FOUNDING signer is write-once per principal. A virgin `identity:<principal>#signer`
|
|
1134
|
+
* may enroll its founding key; re-offering the SAME key is idempotent; grafting ANY other key (the
|
|
1135
|
+
* author-as-someone-else impersonation the host session-lane used to refuse) is rejected — a later device
|
|
1136
|
+
* needs a self-bound / admin-gated enrolment lane, never this founding one. */
|
|
1137
|
+
export const founderSignerWriteOnce = workspaceInvariant("FounderSignerWriteOnce")
|
|
1138
|
+
.on("enrollSigner")
|
|
1139
|
+
.reads((ctx) => [
|
|
1140
|
+
refAs(
|
|
1141
|
+
"tuple",
|
|
1142
|
+
"RelationTuple",
|
|
1143
|
+
"pre:" + relationTupleInstanceId(signerObject(String((ctx.intent ?? {}).principal ?? "")), SIGNER_RELATION),
|
|
1144
|
+
),
|
|
1145
|
+
])
|
|
1146
|
+
.assert(({ tuple }, ctx) => {
|
|
1147
|
+
const subjects = (tuple as { subjects?: Record<string, unknown> })?.subjects ?? {};
|
|
1148
|
+
const want = "key:" + String((ctx?.intent ?? {}).keyHash ?? "");
|
|
1149
|
+
const others = Object.keys(subjects).filter((k) => k !== want);
|
|
1150
|
+
return others.length === 0 ? { accept: true } : { reject: "founding-signer-already-enrolled" };
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
/** birthCloud: a chain is born ONCE. A second birthCloud would mint a duplicate root-ish CloudWorkspace
|
|
1154
|
+
* AND could flip the self-role via its ensure(WorkspaceSelf) — the governance-freeze attack. Refuse once
|
|
1155
|
+
* WorkspaceSelf.role exists. Genesis births on a role-less chain; materialize births each child once. */
|
|
1156
|
+
export const birthCloudOnce = workspaceInvariant("BirthCloudOnce")
|
|
1157
|
+
.on("birthCloud")
|
|
1158
|
+
.reads(() => [refAs("self", "WorkspaceSelf", "pre:" + WORKSPACE_SELF_ID)])
|
|
1159
|
+
.assert(({ self }) =>
|
|
1160
|
+
(self as { role?: string })?.role ? { reject: "cloud-already-born" } : { accept: true },
|
|
1161
|
+
);
|
|
1162
|
+
|
|
1163
|
+
/** provisionWorkspace: the birth RECEIPT may only close a `requested` record (no hijacking a live
|
|
1164
|
+
* workspace's owner credential), and for a kind:platform birth the receipt's ownerGrantHash MUST equal
|
|
1165
|
+
* the pre-committed foundingAdminHash (the provider grants admin to the key the parent ORDERED, never an
|
|
1166
|
+
* arbitrary one). A plain workspace carries no foundingAdminHash, so its fresh-secret mint stays the
|
|
1167
|
+
* trusted impure capability — birth honesty: the law gates what it can prove, trusts the provider for the
|
|
1168
|
+
* rest. (Worker authors ownerGrantHash=foundingAdminHash for platforms, sha256(freshSecret) for plain.) */
|
|
1169
|
+
export const provisionReceiptGuard = workspaceInvariant("ProvisionReceiptGuard")
|
|
1170
|
+
.on("provisionWorkspace")
|
|
1171
|
+
.reads((ctx) => [refAs("ws", "CloudWorkspace", "pre:" + String((ctx.intent ?? {}).workspaceId ?? ""))])
|
|
1172
|
+
.assert(({ ws }, ctx) => {
|
|
1173
|
+
const rec = ws as { status?: string; foundingAdminHash?: string } | undefined;
|
|
1174
|
+
if (!rec || rec.status !== "requested") return { reject: "provision-requires-requested-workspace" };
|
|
1175
|
+
const p = (ctx?.intent ?? {}) as { ownerGrantHash?: string };
|
|
1176
|
+
if (rec.foundingAdminHash && rec.foundingAdminHash !== p.ownerGrantHash)
|
|
1177
|
+
return { reject: "provision-founding-hash-mismatch" };
|
|
1178
|
+
return { accept: true };
|
|
1179
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -32,7 +32,21 @@ export {
|
|
|
32
32
|
type FieldKind,
|
|
33
33
|
type EvidenceRefValue,
|
|
34
34
|
evidenceRefSchema,
|
|
35
|
+
// FIELD FACETS (the endorse model — security_model.md): authority/standing/attribution gate
|
|
36
|
+
// set() acceptance of PrincipalEvidence; secret/public are the confidentiality lattice.
|
|
37
|
+
authorityField,
|
|
38
|
+
standingField,
|
|
39
|
+
attributionField,
|
|
40
|
+
secretField,
|
|
41
|
+
publicField,
|
|
35
42
|
} from "./fields.js";
|
|
43
|
+
export {
|
|
44
|
+
type PrincipalEvidence,
|
|
45
|
+
type AttributionPrincipal,
|
|
46
|
+
type EndorsedAuthority,
|
|
47
|
+
type EndorsedStanding,
|
|
48
|
+
type FieldFacet,
|
|
49
|
+
} from "./principal_evidence.js";
|
|
36
50
|
export {
|
|
37
51
|
Lww,
|
|
38
52
|
LwwCausal,
|
|
@@ -410,6 +424,16 @@ export {
|
|
|
410
424
|
type InvariantVerdict,
|
|
411
425
|
} from "./relation.js";
|
|
412
426
|
export { generateDartDomain, type DomainModule } from "./codegen_dart.js";
|
|
427
|
+
// SHAPE COMPATIBILITY (#72) — the structural contract a client negotiates with a workspace
|
|
428
|
+
// by, instead of the law hash. Pure + deterministic; the editor/CI mirror imports these.
|
|
429
|
+
export {
|
|
430
|
+
domainInterface,
|
|
431
|
+
compatible,
|
|
432
|
+
type DomainInterface,
|
|
433
|
+
type CompatVerdict,
|
|
434
|
+
type DirectivePayloadShape,
|
|
435
|
+
type AggregateReadShape,
|
|
436
|
+
} from "./shape_compat.js";
|
|
413
437
|
// The OPTIONAL Graphviz `.dot` ER-diagram emitter (off-by-default presentation projection,
|
|
414
438
|
// turned on by the `--emit-dot` compiler flag). Pure `DomainModule -> string`, deterministic;
|
|
415
439
|
// NOT part of the domain identity. Barrel-safe (no node:* imports), so the engine bundle can
|
package/src/manifest.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { finishExtremum, type AnyExtremum } from "./extremum.js";
|
|
|
39
39
|
import { existsAsCount, type AnyExists } from "./exists.js";
|
|
40
40
|
import type { OrderedReadDecl } from "./ordered_read.js";
|
|
41
41
|
import type { CanonicalPred } from "./predicate.js";
|
|
42
|
+
import { assertFlowGate } from "./flow_gate.js";
|
|
42
43
|
import type { DerivedDecl } from "./derived.js";
|
|
43
44
|
import type { CombinedDecl } from "./combined.js";
|
|
44
45
|
import type { CertifiedReadDecl } from "./certified_read.js";
|
|
@@ -156,7 +157,7 @@ export interface CanonicalDirective {
|
|
|
156
157
|
*/
|
|
157
158
|
readonly capturedReads?: CanonicalCapturedRead[];
|
|
158
159
|
/**
|
|
159
|
-
* The directive's DECLARED required HOST CAPABILITY ports (`
|
|
160
|
+
* The directive's DECLARED required HOST CAPABILITY ports (`OFFER_KERNEL_ARCHITECTURE.dot` cluster_ports;
|
|
160
161
|
* the HOST/PORT axis, invariant 3): the capability ports the host must PROVIDE for the
|
|
161
162
|
* directive to run, checked FAIL-CLOSED (`required ⊆ provided`) at policy LOAD. SORTED (the
|
|
162
163
|
* SET is the contract; order is incidental). DISTINCT from {@link requiresCapability} (the
|
|
@@ -1055,22 +1056,10 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
|
|
|
1055
1056
|
}))
|
|
1056
1057
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
1057
1058
|
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1061
|
-
//
|
|
1062
|
-
// query can NEVER be a captured read. (The one gate re-asserts this at admission/replay
|
|
1063
|
-
// via `cap_for_query`, so a hand-crafted raw intent is refused on every peer too.)
|
|
1064
|
-
const privateTypes = new Set(aggregates.filter((a) => a.requiresRead !== undefined).map((a) => a.id));
|
|
1065
|
-
for (const d of directives) {
|
|
1066
|
-
for (const cr of d.capturedReads ?? []) {
|
|
1067
|
-
if (privateTypes.has(cr.returns)) {
|
|
1068
|
-
throw new Error(
|
|
1069
|
-
`directive '${d.id}': captured read '${cr.queryId}' returns the PRIVATE type '${cr.returns}' — a .reads() of a requires(cap) query would write its plaintext onto the chain as public data. Private data may only leave via the gated read path (query/qbyid), never a captured read.`,
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1059
|
+
// THE INFORMATION-FLOW GATE (architecture/security_model.md): rule 2 (no private
|
|
1060
|
+
// captured read on the chain) + rule 1 (no encrypt-then-index) are enforced as ONE
|
|
1061
|
+
// pass over the assembled canonical manifest, just before return (see assertFlowGate).
|
|
1062
|
+
// E2E ENCRYPTION — critical #5 (e2e_encryption.md §5): the captured-read seal is rule 2.
|
|
1074
1063
|
|
|
1075
1064
|
// exists_ entries are COUNT declarations in the IR — merge them into `counts` so the
|
|
1076
1065
|
// Rust engine sees them as ordinary counts (the marker lives only in the DSL/codegen
|
|
@@ -1081,7 +1070,7 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
|
|
|
1081
1070
|
// Omit-when-empty: a domain declaring NO query / NO count / NO sum contributes neither
|
|
1082
1071
|
// key, so its canonical manifest is byte-identical to before the read-closure layer
|
|
1083
1072
|
// existed. `sums` follows the same omit-when-empty discipline as `counts`.
|
|
1084
|
-
|
|
1073
|
+
const manifest: CanonicalManifest = {
|
|
1085
1074
|
domain: mod.name,
|
|
1086
1075
|
aggregates,
|
|
1087
1076
|
directives,
|
|
@@ -1109,6 +1098,11 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
|
|
|
1109
1098
|
...(mod.workspaceTypes !== undefined ? { workspaceTypes: mod.workspaceTypes } : {}),
|
|
1110
1099
|
}),
|
|
1111
1100
|
};
|
|
1101
|
+
// THE STATIC INFORMATION-FLOW GATE — leaky law is unrepresentable (a typed compile
|
|
1102
|
+
// error naming the field, the sink, and the rule). Rules 1/2/5 read the manifest IR;
|
|
1103
|
+
// rule 4 also needs the raw module (the executable invariant reads + declassify waivers).
|
|
1104
|
+
assertFlowGate(manifest, mod as never);
|
|
1105
|
+
return manifest;
|
|
1112
1106
|
}
|
|
1113
1107
|
|
|
1114
1108
|
/** Recursively sort object keys; arrays are already in sorted/stable order. */
|
package/src/ops.ts
CHANGED
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { AggregateHandle, BoundAggregate } from "./aggregate.js";
|
|
19
19
|
import type { Field, FieldValue } from "./fields.js";
|
|
20
|
+
import {
|
|
21
|
+
endorsementOf,
|
|
22
|
+
type AttributionPrincipal,
|
|
23
|
+
type EndorsedAuthority,
|
|
24
|
+
type EndorsedStanding,
|
|
25
|
+
type PrincipalFacet,
|
|
26
|
+
} from "./principal_evidence.js";
|
|
20
27
|
|
|
21
28
|
/**
|
|
22
29
|
* A FIELD op, retaining the source aggregate id + field for deterministic wire encoding.
|
|
@@ -41,6 +48,13 @@ export interface FieldOp {
|
|
|
41
48
|
readonly value?: unknown;
|
|
42
49
|
readonly items?: readonly string[];
|
|
43
50
|
readonly entryKey?: string;
|
|
51
|
+
/**
|
|
52
|
+
* THE PRINCIPAL-FLOW ENDORSEMENT (security_model.md — the endorse model): present iff this op's
|
|
53
|
+
* value came from a PrincipalEvidence conversion. The raw principal is materialised into `value`;
|
|
54
|
+
* this records the facet + endorsement reason so the discovery harvest can lift it into the
|
|
55
|
+
* manifest `principalFlows`, and the runtime witness can emit the observed flow. Internal.
|
|
56
|
+
*/
|
|
57
|
+
readonly endorsement?: { readonly facet: PrincipalFacet; readonly reason: string | null };
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
export type PlannedEventMarker = "creates" | "mutates" | "ensures" | "archives";
|
|
@@ -84,6 +98,20 @@ type Target<Id extends string = string, F extends Record<string, Field> = Record
|
|
|
84
98
|
|
|
85
99
|
type Fields<A> = A extends Target<string, infer F> ? F : never;
|
|
86
100
|
|
|
101
|
+
/**
|
|
102
|
+
* What `set()` ACCEPTS for a field, given its facet (security_model.md — the endorse model).
|
|
103
|
+
* A plain (un-faceted) field accepts its ordinary value. A faceted field ALSO accepts the matching
|
|
104
|
+
* PrincipalEvidence conversion — and ONLY that one, so writing un-endorsed identity (or the wrong
|
|
105
|
+
* endorsement) into an authority/standing sink does not typecheck. The read/wire type is unchanged.
|
|
106
|
+
*/
|
|
107
|
+
type AcceptedValue<F> = F extends { readonly facet: "authority" }
|
|
108
|
+
? FieldValue<F> | EndorsedAuthority<FieldValue<F>>
|
|
109
|
+
: F extends { readonly facet: "standing" }
|
|
110
|
+
? FieldValue<F> | EndorsedStanding<FieldValue<F>>
|
|
111
|
+
: F extends { readonly facet: "attribution" }
|
|
112
|
+
? FieldValue<F> | AttributionPrincipal
|
|
113
|
+
: FieldValue<F>;
|
|
114
|
+
|
|
87
115
|
/** Resolve a target's `{aggregateId, aggregateType}` — bound → instance id + type. */
|
|
88
116
|
function addressOf(agg: Target): { aggregateId: string; aggregateType?: string } {
|
|
89
117
|
if ((agg as BoundAggregate).__isBoundAggregate === true) {
|
|
@@ -110,7 +138,7 @@ export function set<
|
|
|
110
138
|
(Fields<A>[K] extends { kind: "set" }
|
|
111
139
|
? `TYPE ERROR: '${K}' is a set field — set() would OVERWRITE the merge; use addToSet(handle, "${K}", values)`
|
|
112
140
|
: K),
|
|
113
|
-
value:
|
|
141
|
+
value: AcceptedValue<Fields<A>[K]>,
|
|
114
142
|
): FieldOp {
|
|
115
143
|
const f = (agg as { fields?: Record<string, { kind?: string }> }).fields?.[field];
|
|
116
144
|
if (f?.kind === "set") {
|
|
@@ -119,6 +147,19 @@ export function set<
|
|
|
119
147
|
`(concurrent adds would be lost). Use addToSet(handle, '${field}', values) — the additive write.`,
|
|
120
148
|
);
|
|
121
149
|
}
|
|
150
|
+
// MATERIALISE PrincipalEvidence: the typed value may be an evidence conversion (an opaque token).
|
|
151
|
+
// Store the RAW principal in the op, and record the endorsement so the harvest + runtime witness
|
|
152
|
+
// see the flow (security_model.md). A plain value passes through untouched.
|
|
153
|
+
const endorsement = endorsementOf(value);
|
|
154
|
+
if (endorsement) {
|
|
155
|
+
return {
|
|
156
|
+
...addressOf(agg),
|
|
157
|
+
field,
|
|
158
|
+
kind: "set",
|
|
159
|
+
value: endorsement.principal,
|
|
160
|
+
endorsement: { facet: endorsement.facet, reason: endorsement.reason },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
122
163
|
return { ...addressOf(agg), field, kind: "set", value };
|
|
123
164
|
}
|
|
124
165
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// NOMOS — the integrity dual of the information-flow gate (architecture/security_model.md).
|
|
2
|
+
// "Identity may attribute freely. Identity may authorise only when endorsed by declared law."
|
|
3
|
+
//
|
|
4
|
+
// `ctx.principal()` returns opaque PrincipalEvidence — NOT a string. It cannot be compared,
|
|
5
|
+
// concatenated, branched on, or smuggled into a payload (the raw value is held in a module-
|
|
6
|
+
// private registry, unreadable by authors). Its ONLY exits are three typed conversions:
|
|
7
|
+
//
|
|
8
|
+
// .asAttribution() → AttributionPrincipal (free — createdBy/grantedBy/...)
|
|
9
|
+
// .asAuthoritySubject(reason) → EndorsedAuthority<T> (endorsed — a write-up into authority)
|
|
10
|
+
// .asStandingParty(reason) → EndorsedStanding<T> (endorsed — accountability: owedBy/...)
|
|
11
|
+
//
|
|
12
|
+
// Field facets gate `set()` acceptance (authorityField accepts only EndorsedAuthority, etc.), so an
|
|
13
|
+
// un-endorsed flow into an authority sink does not COMPILE — the confused deputy is closed by
|
|
14
|
+
// construction, not by plan-body taint analysis. `requires`/`assert` remain the only place an
|
|
15
|
+
// authority decision may be MADE (evidence can't branch, so `if (principal() === …)` won't typecheck).
|
|
16
|
+
//
|
|
17
|
+
// The `reason` is part of the LAW, not the data: a non-empty human string, harvested into the
|
|
18
|
+
// manifest `principalFlows` + the endorse era key (the audit surface), and witnessed fail-closed at
|
|
19
|
+
// runtime/replay (Slice B).
|
|
20
|
+
|
|
21
|
+
/** The endorsement facet — which kind of sink the evidence has been converted for. */
|
|
22
|
+
export type PrincipalFacet = "attribution" | "authority" | "standing";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A field's FACET — the integrity/confidentiality role of the sink. The integrity facets
|
|
26
|
+
* (authority/attribution/standing) gate `set()` acceptance of PrincipalEvidence conversions;
|
|
27
|
+
* the confidentiality facets (secret/public) mark the read-down lattice. Default (no facet) =
|
|
28
|
+
* an ordinary field that accepts a plain value.
|
|
29
|
+
*/
|
|
30
|
+
export type FieldFacet = "authority" | "attribution" | "standing" | "secret" | "public";
|
|
31
|
+
|
|
32
|
+
/** The record behind an evidence-derived value, readable ONLY by the framework (never the author). */
|
|
33
|
+
export interface EndorsementRecord {
|
|
34
|
+
readonly facet: PrincipalFacet;
|
|
35
|
+
/** The verified authoring principal (the captured `ctx.principal()` value). */
|
|
36
|
+
readonly principal: string;
|
|
37
|
+
/** The static endorsement reason (LAW) — null for free attribution. */
|
|
38
|
+
readonly reason: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The raw principal + endorsement live HERE, module-private — an author holding a converted value
|
|
42
|
+
// has no accessor to them (the brand below is phantom). The framework reads via `endorsementOf`.
|
|
43
|
+
const REGISTRY = new WeakMap<object, EndorsementRecord>();
|
|
44
|
+
|
|
45
|
+
declare const FACET_BRAND: unique symbol;
|
|
46
|
+
/** A principal converted for ATTRIBUTION — accepted by `attributionField` sinks. Opaque. */
|
|
47
|
+
export interface AttributionPrincipal {
|
|
48
|
+
readonly [FACET_BRAND]: "attribution";
|
|
49
|
+
}
|
|
50
|
+
/** A principal ENDORSED for an AUTHORITY sink (relation subject/relation/object, token, policy…). Opaque. */
|
|
51
|
+
export interface EndorsedAuthority<T = string> {
|
|
52
|
+
readonly [FACET_BRAND]: "authority";
|
|
53
|
+
/** Phantom — carries the sink's value type for `set()` inference; never materialised. */
|
|
54
|
+
readonly __t?: T;
|
|
55
|
+
}
|
|
56
|
+
/** A principal ENDORSED for a STANDING sink (owedBy/responsible/issuer…). Opaque. */
|
|
57
|
+
export interface EndorsedStanding<T = string> {
|
|
58
|
+
readonly [FACET_BRAND]: "standing";
|
|
59
|
+
readonly __t?: T;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The opaque identity capability `ctx.principal()` returns. No raw exit — only typed conversions. */
|
|
63
|
+
export interface PrincipalEvidence {
|
|
64
|
+
/** Free: record WHO acted, into an `attributionField` (createdBy/grantedBy/submittedBy). */
|
|
65
|
+
asAttribution(): AttributionPrincipal;
|
|
66
|
+
/** Endorsed write-up into an `authorityField` — `reason` is LAW (a static, non-empty string). */
|
|
67
|
+
asAuthoritySubject<T = string>(reason: string): EndorsedAuthority<T>;
|
|
68
|
+
/** Endorsed write-up into a `standingField` (accountability) — `reason` is LAW. */
|
|
69
|
+
asStandingParty<T = string>(reason: string): EndorsedStanding<T>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function requireReason(kind: string, reason: unknown): string {
|
|
73
|
+
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`${kind}(reason) requires a non-empty reason — endorsing identity into an authority sink is a ` +
|
|
76
|
+
`declared act of law, not data. Pass a static string explaining why this flow is intended.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return reason;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function mint(facet: PrincipalFacet, principal: string, reason: string | null): object {
|
|
83
|
+
const token = {};
|
|
84
|
+
REGISTRY.set(token, { facet, principal, reason });
|
|
85
|
+
return token;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Build the opaque evidence capability over a verified principal (the engine wires this into ctx). */
|
|
89
|
+
export function principalEvidence(principal: string): PrincipalEvidence {
|
|
90
|
+
return {
|
|
91
|
+
asAttribution: () => mint("attribution", principal, null) as AttributionPrincipal,
|
|
92
|
+
asAuthoritySubject: <T = string>(reason: string) =>
|
|
93
|
+
mint("authority", principal, requireReason("asAuthoritySubject", reason)) as EndorsedAuthority<T>,
|
|
94
|
+
asStandingParty: <T = string>(reason: string) =>
|
|
95
|
+
mint("standing", principal, requireReason("asStandingParty", reason)) as EndorsedStanding<T>,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* FRAMEWORK-INTERNAL: read the endorsement behind an evidence-derived value — used by the write
|
|
101
|
+
* path (to materialise the raw principal into the op), the discovery harvest (to record the flow),
|
|
102
|
+
* and the runtime/replay witness (to emit the observed flow). Returns null for a plain value (a
|
|
103
|
+
* non-evidence write). Authors cannot call this usefully — the REGISTRY is module-private.
|
|
104
|
+
*/
|
|
105
|
+
export function endorsementOf(value: unknown): EndorsementRecord | null {
|
|
106
|
+
return typeof value === "object" && value !== null && REGISTRY.has(value) ? (REGISTRY.get(value) ?? null) : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Is this value an evidence conversion (any facet)? */
|
|
110
|
+
export function isPrincipalEvidenceValue(value: unknown): boolean {
|
|
111
|
+
return endorsementOf(value) !== null;
|
|
112
|
+
}
|
package/src/relation_sugar.ts
CHANGED
|
@@ -210,10 +210,16 @@ export interface SugaredRelationTypeBuilder extends RelationTypeBuilder {
|
|
|
210
210
|
* builder; we re-wrap on every chain step so the audience methods survive interleaving with `.relation`.
|
|
211
211
|
*/
|
|
212
212
|
export function withAudiences(builder: RelationTypeBuilder): SugaredRelationTypeBuilder {
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// the
|
|
216
|
-
|
|
213
|
+
// Carry the decl's data as OWN enumerable properties (NOT `Object.create(builder)` — a prototype-only
|
|
214
|
+
// object serializes to `{}`, which collapses the compiler's `discover()` JSON dedup so every sugared
|
|
215
|
+
// type but the first is dropped). `relations` is the SAME Map reference the underlying builder mutates,
|
|
216
|
+
// so reads stay live as `.relation(...)`/the audience methods add to it.
|
|
217
|
+
const sugared = {
|
|
218
|
+
type: builder.type,
|
|
219
|
+
relations: builder.relations,
|
|
220
|
+
...(builder.caps !== undefined ? { caps: builder.caps } : {}),
|
|
221
|
+
__isRelationType: true as const,
|
|
222
|
+
} as SugaredRelationTypeBuilder;
|
|
217
223
|
|
|
218
224
|
sugared.relation = function (name, opts) {
|
|
219
225
|
builder.relation(name, opts);
|