@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
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* * `<name>.package.usda` — the deployable package (engine bundle + USD IR);
|
|
32
32
|
* its sha256 IS the deploy `domainHash` (`policy:{hash}`).
|
|
33
33
|
* * `<name>.deploy.json` — `{packageUsda}`: the EXACT body
|
|
34
|
-
* `POST /
|
|
34
|
+
* `POST /v2/workspaces/:ws/domains` accepts as `application/json` on Nomos Cloud.
|
|
35
35
|
* Admission/projection manifests are embedded in the package USDA and are not host
|
|
36
36
|
* sidecars.
|
|
37
37
|
* * `<name>.proof.mts` — the GENERATED PROOF (codegen_proof.ts): a
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
*/
|
|
48
48
|
import { execFileSync } from "node:child_process";
|
|
49
49
|
import { expandCapabilityExports, isCapabilityDecl } from "./capability_exports.js";
|
|
50
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
50
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
51
51
|
import { createRequire } from "node:module";
|
|
52
52
|
import path from "node:path";
|
|
53
53
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -103,6 +103,8 @@ import {
|
|
|
103
103
|
} from "./stable_ids.js";
|
|
104
104
|
import type { UsdLayer } from "./usd.js";
|
|
105
105
|
import { generateTsClient, tsClientFactoryName } from "./codegen_ts.js";
|
|
106
|
+
import { generateOpenApiFragment, mergeOpenApiFragments } from "./codegen_openapi.js";
|
|
107
|
+
import { domainInterface, type DomainInterface } from "./shape_compat.js";
|
|
106
108
|
import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
|
|
107
109
|
|
|
108
110
|
const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -149,6 +151,8 @@ interface DomainConfig {
|
|
|
149
151
|
};
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
type DartSupportImport = "vendored" | "package";
|
|
155
|
+
|
|
152
156
|
interface PackageConfig {
|
|
153
157
|
readonly name: string;
|
|
154
158
|
readonly domains: readonly DomainConfig[];
|
|
@@ -159,13 +163,16 @@ interface PackageConfig {
|
|
|
159
163
|
* Typed DART frontend (the Flutter sibling of the generated TS client; #codegen_dart).
|
|
160
164
|
* `false`/absent = skip (default). `true` = emit into `<outDir>/dart/`;
|
|
161
165
|
* `{ out: "relative/dir" }` = emit there (resolved against the config dir).
|
|
166
|
+
* `{ support: "package" }` keeps `package:nomos_client/nomos_client.dart` imports for Flutter apps that
|
|
167
|
+
* already depend on `nomos_client`/`nomos_flutter`. The default `"vendored"` mode preserves the old
|
|
168
|
+
* self-contained directory.
|
|
162
169
|
* Per composed domain one `<name>.dart` (payload classes + read models + typed read
|
|
163
170
|
* accessors via the SAME `generateDartDomain` the co2 tenant emit uses), PLUS the
|
|
164
|
-
* framework `
|
|
165
|
-
* `package:
|
|
171
|
+
* framework `nomos_client` Dart support files vendored alongside (the generated files'
|
|
172
|
+
* `package:nomos_client/...` import is rewritten to a relative one) — so a Flutter app
|
|
166
173
|
* can vendor the directory as-is, no pub deps.
|
|
167
174
|
*/
|
|
168
|
-
readonly dart?: boolean | { readonly out?: string };
|
|
175
|
+
readonly dart?: boolean | { readonly out?: string; readonly support?: DartSupportImport };
|
|
169
176
|
/**
|
|
170
177
|
* OPT-IN layered USD emission (also `--layered` on the CLI). When true, ALSO emit
|
|
171
178
|
* `build/layers/<domain>--<module>.usda` (+ `.layer.json`) — one REAL USD layer per
|
|
@@ -391,11 +398,11 @@ function resolveNamed<T>(merged: Mod, names: readonly string[] | undefined, what
|
|
|
391
398
|
* Vendored output rewrites it to the relative barrel so the directory is
|
|
392
399
|
* self-contained (no pub path-dependency on the framework package).
|
|
393
400
|
*/
|
|
394
|
-
const DART_PACKAGE_IMPORT = `import 'package:
|
|
395
|
-
const DART_RELATIVE_IMPORT = `import '
|
|
401
|
+
const DART_PACKAGE_IMPORT = `import 'package:nomos_client/nomos_client.dart';`;
|
|
402
|
+
const DART_RELATIVE_IMPORT = `import 'nomos_client.dart';`;
|
|
396
403
|
|
|
397
404
|
/**
|
|
398
|
-
* The framework `
|
|
405
|
+
* The framework `nomos_client` Dart support files the generated code resolves through
|
|
399
406
|
* the barrel (`dsl/dart/lib/`). `schema_validation.dart` is deliberately NOT
|
|
400
407
|
* vendored: nothing the codegen emits references it, and it would drag the external
|
|
401
408
|
* `json_schema` pub dependency into every vendoring app.
|
|
@@ -411,25 +418,41 @@ const DART_SUPPORT_SRC = [
|
|
|
411
418
|
"webview_bridge.dart",
|
|
412
419
|
] as const;
|
|
413
420
|
|
|
421
|
+
function removeVendoredDartSupport(dartOut: string): void {
|
|
422
|
+
rmSync(path.join(dartOut, "nomos_client.dart"), { force: true });
|
|
423
|
+
rmSync(path.join(dartOut, "src"), { recursive: true, force: true });
|
|
424
|
+
}
|
|
425
|
+
|
|
414
426
|
/**
|
|
415
427
|
* Emit the typed Dart for the composed domain modules into `dartOut`: one
|
|
416
428
|
* `<name>.dart` per domain (the SAME `generateDartDomain` path the co2 tenant's
|
|
417
|
-
* `emit_dart.ts` drives) + the vendored `
|
|
429
|
+
* `emit_dart.ts` drives) + the vendored `nomos_client.dart` barrel and its `src/`
|
|
418
430
|
* support files, with the generated files' `package:` import rewritten relative.
|
|
419
431
|
*/
|
|
420
|
-
function emitDart(
|
|
421
|
-
|
|
432
|
+
function emitDart(
|
|
433
|
+
modules: readonly DomainModule[],
|
|
434
|
+
dartOut: string,
|
|
435
|
+
domainHash: string,
|
|
436
|
+
packageInterface: Record<string, DomainInterface> = {},
|
|
437
|
+
supportImport: DartSupportImport = "vendored",
|
|
438
|
+
): string[] {
|
|
439
|
+
mkdirSync(dartOut, { recursive: true });
|
|
422
440
|
|
|
423
|
-
// 1.
|
|
441
|
+
// 1. optional vendored framework support (barrel + src/), copied byte-for-byte except
|
|
424
442
|
// the barrel's schema_validation export (see DART_SUPPORT_SRC).
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
443
|
+
if (supportImport === "vendored") {
|
|
444
|
+
mkdirSync(path.join(dartOut, "src"), { recursive: true });
|
|
445
|
+
const libDir = path.join(DSL_DIR, "dart", "lib");
|
|
446
|
+
const barrel = readFileSync(path.join(libDir, "nomos_client.dart"), "utf8")
|
|
447
|
+
.split("\n")
|
|
448
|
+
.filter((line) => !line.includes("src/schema_validation.dart"))
|
|
449
|
+
.join("\n");
|
|
450
|
+
writeFileSync(path.join(dartOut, "nomos_client.dart"), barrel, "utf8");
|
|
451
|
+
for (const f of DART_SUPPORT_SRC) {
|
|
452
|
+
copyFileSync(path.join(libDir, "src", f), path.join(dartOut, "src", f));
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
removeVendoredDartSupport(dartOut);
|
|
433
456
|
}
|
|
434
457
|
|
|
435
458
|
// 2. one generated file per composed domain — the TYPES (`<name>.dart`) + the async bridge
|
|
@@ -450,10 +473,18 @@ function emitDart(modules: readonly DomainModule[], dartOut: string, domainHash:
|
|
|
450
473
|
);
|
|
451
474
|
}
|
|
452
475
|
const p = path.join(dartOut, `${mod.name}.dart`);
|
|
453
|
-
writeFileSync(
|
|
476
|
+
writeFileSync(
|
|
477
|
+
p,
|
|
478
|
+
supportImport === "vendored" ? src.replace(DART_PACKAGE_IMPORT, DART_RELATIVE_IMPORT) : src,
|
|
479
|
+
"utf8",
|
|
480
|
+
);
|
|
454
481
|
written.push(p);
|
|
455
482
|
|
|
456
|
-
const clientSrc = generateDartBridgeClient(modForDart, {
|
|
483
|
+
const clientSrc = generateDartBridgeClient(modForDart, {
|
|
484
|
+
domainHash,
|
|
485
|
+
interface: packageInterface[modForDart.domain ?? modForDart.name],
|
|
486
|
+
supportImport,
|
|
487
|
+
});
|
|
457
488
|
const cp = path.join(dartOut, `${mod.name}.client.dart`);
|
|
458
489
|
writeFileSync(cp, clientSrc, "utf8");
|
|
459
490
|
written.push(cp);
|
|
@@ -990,6 +1021,30 @@ async function main(): Promise<void> {
|
|
|
990
1021
|
// identity across clean clones). Deterministic — re-compiles rewrite the same bytes.
|
|
991
1022
|
writeFileSync(lockPath, JSON.stringify(nextLock, null, 2) + "\n", "utf8");
|
|
992
1023
|
|
|
1024
|
+
// ── THE STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates (read
|
|
1025
|
+
// shapes, sid-keyed), keyed by DOMAIN. The SAME object is (a) baked into the generated
|
|
1026
|
+
// clients as their requirement and (b) staged on the deploy as the workspace's offer, so
|
|
1027
|
+
// `connect()` negotiates compatibility by SHAPE (stable-id subtyping), not by law hash.
|
|
1028
|
+
// architecture/shape_compatibility.md. Multiple modules sharing a domain key merge.
|
|
1029
|
+
const packageInterface: Record<string, DomainInterface> = {};
|
|
1030
|
+
for (const mod of domainModules) {
|
|
1031
|
+
const key = mod.domain ?? mod.name;
|
|
1032
|
+
const di = domainInterface(mod, nextLock.domains[mod.name]?.stableIds);
|
|
1033
|
+
const prior = packageInterface[key];
|
|
1034
|
+
packageInterface[key] = prior
|
|
1035
|
+
? {
|
|
1036
|
+
...prior,
|
|
1037
|
+
directives: { ...prior.directives, ...di.directives },
|
|
1038
|
+
aggregates: { ...prior.aggregates, ...di.aggregates },
|
|
1039
|
+
}
|
|
1040
|
+
: di;
|
|
1041
|
+
}
|
|
1042
|
+
writeFileSync(
|
|
1043
|
+
path.join(outDir, `${cfg.name}.interface.json`),
|
|
1044
|
+
JSON.stringify(packageInterface, null, 2) + "\n",
|
|
1045
|
+
"utf8",
|
|
1046
|
+
);
|
|
1047
|
+
|
|
993
1048
|
// ── reports ──
|
|
994
1049
|
const entryReports: string[] = [];
|
|
995
1050
|
for (const [reportId, ref] of Object.entries(cfg.reports ?? {})) {
|
|
@@ -1276,6 +1331,19 @@ async function main(): Promise<void> {
|
|
|
1276
1331
|
const identity = buildIdentity(domainModules);
|
|
1277
1332
|
const { manifestsPath } = writeIdentity(identity, outDir);
|
|
1278
1333
|
|
|
1334
|
+
// ── 3b. the OpenAPI spec — THE PACKAGE'S SELF-DESCRIPTION (the host's runtime routing table;
|
|
1335
|
+
// architecture/githolon_host_contract.md "The Self-Describing API"). Each domain emits its fragment
|
|
1336
|
+
// from its OWN zod (z.toJSONSchema); the package's spec is the law-blind merge. Deploy STAGES it like
|
|
1337
|
+
// the read overlay; the workspace's full spec is the runtime merge of installed packages' specs
|
|
1338
|
+
// (currentLaw); the host ingests it and builds its router from it — no domain/directive name in host
|
|
1339
|
+
// source. Self-describing for the whole web ecosystem (Swagger/codegen/Hono/agent tool-calling).
|
|
1340
|
+
const openapi = mergeOpenApiFragments(
|
|
1341
|
+
domainModules.map((m) => generateOpenApiFragment(m, { lawHash: domainHash })),
|
|
1342
|
+
{ title: `${cfg.name} (nomos package)`, lawHash: domainHash },
|
|
1343
|
+
);
|
|
1344
|
+
const openapiPath = path.join(outDir, `${cfg.name}.openapi.json`);
|
|
1345
|
+
writeFileSync(openapiPath, JSON.stringify(openapi, null, 2) + "\n", "utf8");
|
|
1346
|
+
|
|
1279
1347
|
// ── 4. the one-call deploy body (package only) ──
|
|
1280
1348
|
// SELF-DESCRIBING: `summary` states what the (otherwise hex-opaque) package
|
|
1281
1349
|
// contains — purely derived from the inputs (no timestamps), ignored by the
|
|
@@ -1301,6 +1369,12 @@ async function main(): Promise<void> {
|
|
|
1301
1369
|
// collector's doc above). The worker stages it on the manifest overlay:
|
|
1302
1370
|
// the deploy-time binding warning + the executor's scan map read it there.
|
|
1303
1371
|
...(capabilityLowerings.length > 0 ? { capabilities: capabilityLowerings } : {}),
|
|
1372
|
+
// THE STRUCTURAL OFFER (#72): the per-domain interface, staged on the manifest overlay so
|
|
1373
|
+
// `connect()` pulls the workspace's installed shapes and negotiates by structural subtyping.
|
|
1374
|
+
interface: packageInterface,
|
|
1375
|
+
// THE SELF-DESCRIBING API: this package's OpenAPI spec (law-derived). The worker stages it on the
|
|
1376
|
+
// manifest overlay; a read serves the workspace's merged spec; the host builds its router from it.
|
|
1377
|
+
openapi,
|
|
1304
1378
|
};
|
|
1305
1379
|
const deployPath = path.join(outDir, `${cfg.name}.deploy.json`);
|
|
1306
1380
|
writeFileSync(deployPath, JSON.stringify(deployBody) + "\n", "utf8");
|
|
@@ -1338,7 +1412,7 @@ async function main(): Promise<void> {
|
|
|
1338
1412
|
` directives: ${d.directives.join(", ") || "—"}`,
|
|
1339
1413
|
` queries: ${d.queries.join(", ") || "—"} counts: ${d.counts.join(", ") || "—"} sums: ${d.sums.join(", ") || "—"}`,
|
|
1340
1414
|
]),
|
|
1341
|
-
`deploy: POST ${cfg.name}.deploy.json to /
|
|
1415
|
+
`deploy: POST ${cfg.name}.deploy.json to /v2/workspaces/<ws>/domains (or: githolon deploy <ws>)`,
|
|
1342
1416
|
`typed client: ${cfg.name}.client.ts — ${tsClientFactoryName(domainModules[0]?.domain ?? domainModules[0]?.name ?? cfg.name)}(holon), law hash baked in`,
|
|
1343
1417
|
proofPath !== undefined
|
|
1344
1418
|
? `generated proof: ${cfg.name}.proof.mts — run: githolon proof (live; ends ALL GREEN or names the jam)`
|
|
@@ -1350,14 +1424,25 @@ async function main(): Promise<void> {
|
|
|
1350
1424
|
// ── 5. the typed TS client (#10 — the web sibling of codegen_dart), with the
|
|
1351
1425
|
// deployed law's content hash baked in (emitted AFTER the package is hashed) ──
|
|
1352
1426
|
const clientPath = path.join(outDir, `${cfg.name}.client.ts`);
|
|
1353
|
-
writeFileSync(clientPath, generateTsClient(domainModules, { packageName: cfg.name, domainHash }), "utf8");
|
|
1427
|
+
writeFileSync(clientPath, generateTsClient(domainModules, { packageName: cfg.name, domainHash, interface: packageInterface }), "utf8");
|
|
1354
1428
|
|
|
1355
1429
|
// ── 6. the typed Dart frontend (opt-in; the Flutter sibling of step 5, emitted
|
|
1356
1430
|
// after the package hash like the TS client) ──
|
|
1357
1431
|
const dartCfg = cfg.dart ?? false;
|
|
1358
|
-
if (
|
|
1359
|
-
|
|
1432
|
+
if (
|
|
1433
|
+
dartCfg !== false &&
|
|
1434
|
+
dartCfg !== true &&
|
|
1435
|
+
(typeof dartCfg !== "object" ||
|
|
1436
|
+
dartCfg === null ||
|
|
1437
|
+
(dartCfg.out !== undefined && typeof dartCfg.out !== "string") ||
|
|
1438
|
+
(dartCfg.support !== undefined && dartCfg.support !== "vendored" && dartCfg.support !== "package"))
|
|
1439
|
+
) {
|
|
1440
|
+
fail(
|
|
1441
|
+
`config key 'dart' must be false, true, or { out?: "relative/dir", support?: "vendored" | "package" }; got ${JSON.stringify(dartCfg)}`,
|
|
1442
|
+
);
|
|
1360
1443
|
}
|
|
1444
|
+
const dartSupportImport: DartSupportImport =
|
|
1445
|
+
typeof dartCfg === "object" && dartCfg.support !== undefined ? dartCfg.support : "vendored";
|
|
1361
1446
|
let dartOut: string | undefined;
|
|
1362
1447
|
let dartFiles: string[] = [];
|
|
1363
1448
|
if (dartCfg !== false) {
|
|
@@ -1365,7 +1450,7 @@ async function main(): Promise<void> {
|
|
|
1365
1450
|
cfgDir,
|
|
1366
1451
|
typeof dartCfg === "object" && dartCfg.out !== undefined ? dartCfg.out : path.join(outDir, "dart"),
|
|
1367
1452
|
);
|
|
1368
|
-
dartFiles = emitDart(domainModules, dartOut, domainHash);
|
|
1453
|
+
dartFiles = emitDart(domainModules, dartOut, domainHash, packageInterface, dartSupportImport);
|
|
1369
1454
|
}
|
|
1370
1455
|
|
|
1371
1456
|
// ── summary ──
|
|
@@ -1401,20 +1486,23 @@ async function main(): Promise<void> {
|
|
|
1401
1486
|
}
|
|
1402
1487
|
}
|
|
1403
1488
|
console.log(` client ${rel(clientPath)} (typed TS client — payloads, read models, query accessors)`);
|
|
1489
|
+
console.log(` openapi ${rel(openapiPath)} (${Object.keys(openapi.paths).length} operation(s) — the law's self-describing API; the host's runtime routing table)`);
|
|
1404
1490
|
if (proofPath !== undefined) {
|
|
1405
1491
|
console.log(` proof ${rel(proofPath)} (a runnable e2e GENERATED from your law — run: githolon proof)`);
|
|
1406
1492
|
} else {
|
|
1407
1493
|
console.log(` proof SKIPPED — ${proofSkip}`);
|
|
1408
1494
|
}
|
|
1409
1495
|
if (dartOut !== undefined) {
|
|
1410
|
-
|
|
1496
|
+
const supportText =
|
|
1497
|
+
dartSupportImport === "vendored" ? "vendored nomos_client support" : "package nomos_client imports";
|
|
1498
|
+
console.log(` dart ${rel(dartOut)}${path.sep} (${dartFiles.length} domain file(s) + ${supportText} — typed Dart for Flutter)`);
|
|
1411
1499
|
}
|
|
1412
1500
|
console.log(` deploy ${rel(deployPath)}`);
|
|
1413
1501
|
console.log(``);
|
|
1414
1502
|
console.log(`deploy (Nomos Cloud):`);
|
|
1415
1503
|
console.log(` githolon ws create <ws> && githolon deploy <ws> # the secret is saved + sent for you`);
|
|
1416
1504
|
console.log(` # raw lane: curl -X POST -H 'content-type: application/json' -H 'Authorization: Bearer <workspaceSecret>' \\`);
|
|
1417
|
-
console.log(` # --data-binary @${rel(deployPath)} https://nomos.captainapp.co.uk/
|
|
1505
|
+
console.log(` # --data-binary @${rel(deployPath)} https://nomos.captainapp.co.uk/v2/workspaces/<ws>/domains`);
|
|
1418
1506
|
if (proofPath !== undefined) {
|
|
1419
1507
|
console.log(`prove it OFFLINE first: npx githolon proof # the generated proof's offline legs on a LOCAL holon (no cloud workspace)`);
|
|
1420
1508
|
console.log(` inner loop: npx githolon dev # watch -> recompile -> law live locally -> offline proof, on every save`);
|
package/src/ctx.ts
CHANGED
|
@@ -14,11 +14,19 @@
|
|
|
14
14
|
* - ctx.rng -> Random port
|
|
15
15
|
*/
|
|
16
16
|
import type { WireHlc } from "./wire.js";
|
|
17
|
+
import { principalEvidence, type PrincipalEvidence } from "./principal_evidence.js";
|
|
17
18
|
|
|
18
19
|
export interface Ports {
|
|
19
20
|
clock(): WireHlc;
|
|
20
21
|
id(): string;
|
|
21
22
|
rng(): number;
|
|
23
|
+
/**
|
|
24
|
+
* THE IDENTITY PORT (security_model.md — the endorse model): the captured, gate-verified
|
|
25
|
+
* authoring principal as opaque {@link PrincipalEvidence}. NOT a string — it can only become an
|
|
26
|
+
* attribution or an ENDORSED authority/standing value via its conversions, so identity cannot
|
|
27
|
+
* decide authority in plan code (the confused-deputy defence). Replay-stable (a captured port).
|
|
28
|
+
*/
|
|
29
|
+
principal(): PrincipalEvidence;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
/** A deterministic stub set of ports — seeded, replayable. */
|
|
@@ -27,6 +35,7 @@ export function deterministicPorts(seed: {
|
|
|
27
35
|
logical?: number;
|
|
28
36
|
replica?: number;
|
|
29
37
|
idPrefix?: string;
|
|
38
|
+
principal?: string;
|
|
30
39
|
} = {}): Ports {
|
|
31
40
|
let physical = seed.physical ?? 1;
|
|
32
41
|
let logical = seed.logical ?? 0;
|
|
@@ -34,7 +43,11 @@ export function deterministicPorts(seed: {
|
|
|
34
43
|
const idPrefix = seed.idPrefix ?? "id";
|
|
35
44
|
let idCounter = 0;
|
|
36
45
|
let rngState = (seed.physical ?? 1) >>> 0 || 1;
|
|
46
|
+
const evidence = principalEvidence(seed.principal ?? "");
|
|
37
47
|
return {
|
|
48
|
+
principal(): PrincipalEvidence {
|
|
49
|
+
return evidence;
|
|
50
|
+
},
|
|
38
51
|
clock(): WireHlc {
|
|
39
52
|
const h: WireHlc = { physical, logical, replica };
|
|
40
53
|
// Advance: same physical -> bump logical (mirrors merge.md local-event rule).
|
package/src/directive.ts
CHANGED
|
@@ -143,7 +143,7 @@ export interface Directive<P = unknown> {
|
|
|
143
143
|
readonly declaredRelations: RelationDecl[];
|
|
144
144
|
/**
|
|
145
145
|
* The directive's DECLARED required HOST CAPABILITIES — the capability PORTS the HOST must
|
|
146
|
-
* PROVIDE for this directive to run (`
|
|
146
|
+
* PROVIDE for this directive to run (`OFFER_KERNEL_ARCHITECTURE.dot` cluster_ports / `OFFER_FLOW.dot`
|
|
147
147
|
* cluster_ports, invariant 3). This is the HOST/PORT axis — "can this host PHYSICALLY do
|
|
148
148
|
* this?" — checked ONCE at policy LOAD against what the loading host ADVERTISES, FAIL-CLOSED
|
|
149
149
|
* (`required ⊆ provided`). It is DISTINCT from the AUTHZ axis {@link requiresCapability}
|
package/src/engine_entry.ts
CHANGED
|
@@ -57,6 +57,7 @@ import { RelationTuple } from "./framework/relations.js";
|
|
|
57
57
|
import type { Directive } from "./directive.js";
|
|
58
58
|
import type { AggregateHandle, AggregateInvariantFn, AggregateInvariantVerdict } from "./aggregate.js";
|
|
59
59
|
import type { Ports } from "./ctx.js";
|
|
60
|
+
import { principalEvidence, type PrincipalEvidence } from "./principal_evidence.js";
|
|
60
61
|
import type { WireEvent, WireHlc } from "./wire.js";
|
|
61
62
|
import type { DerivedDecl } from "./derived.js";
|
|
62
63
|
import type { CombinedDecl } from "./combined.js";
|
|
@@ -365,12 +366,18 @@ export function collectEngineRouting(
|
|
|
365
366
|
* intent's HLC — the committed HLC/id is host-injected in Rust); `id()` derives a
|
|
366
367
|
* deterministic per-dispatch value; `rng()` replays the captured draw.
|
|
367
368
|
*/
|
|
368
|
-
function portsFromHost(): Ports {
|
|
369
|
+
function portsFromHost(principalRaw = ""): Ports {
|
|
369
370
|
const hostPorts = (globalThis as { __ports?: { clock(): number; rng(): number } }).__ports;
|
|
370
371
|
const clockScalar = hostPorts ? hostPorts.clock() : 0;
|
|
371
372
|
const rngValue = hostPorts ? hostPorts.rng() : 0;
|
|
372
373
|
let idCounter = 0;
|
|
374
|
+
// The captured, gate-verified principal (the engine serialises it into the dispatch job —
|
|
375
|
+
// host.rs EnginePortsSnapshot.principal) becomes the opaque PrincipalEvidence the plan reads.
|
|
376
|
+
const evidence = principalEvidence(principalRaw);
|
|
373
377
|
return {
|
|
378
|
+
principal(): PrincipalEvidence {
|
|
379
|
+
return evidence;
|
|
380
|
+
},
|
|
374
381
|
clock(): WireHlc {
|
|
375
382
|
return { physical: clockScalar, logical: 0, replica: 0 };
|
|
376
383
|
},
|
|
@@ -386,7 +393,7 @@ function portsFromHost(): Ports {
|
|
|
386
393
|
|
|
387
394
|
/** The compiled engine surface `registerEngine` returns (and assigns onto `globalThis`). */
|
|
388
395
|
export interface RegisteredEngine {
|
|
389
|
-
plan(job: unknown): WireEvent[] | { events: WireEvent[]; strikes: string[] };
|
|
396
|
+
plan(job: unknown): WireEvent[] | { events: WireEvent[]; strikes?: string[]; endorse?: { sink: string; facet: string; reason: string | null }[] };
|
|
390
397
|
planReport(job: unknown): string;
|
|
391
398
|
}
|
|
392
399
|
|
|
@@ -709,7 +716,8 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
709
716
|
|
|
710
717
|
function plan(job: {
|
|
711
718
|
intent?: { domain?: string; directiveId?: string; payload?: unknown };
|
|
712
|
-
|
|
719
|
+
capturedPorts?: { principal?: string };
|
|
720
|
+
}): WireEvent[] | { events: WireEvent[]; strikes?: string[]; endorse?: { sink: string; facet: string; reason: string | null }[] } {
|
|
713
721
|
const intent = job.intent ?? {};
|
|
714
722
|
const domain = intent.domain;
|
|
715
723
|
const directiveId = intent.directiveId;
|
|
@@ -723,11 +731,20 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
723
731
|
if (entry === undefined) {
|
|
724
732
|
throw new Error(`engine plan: no directive registered for (${domain}, ${directiveId})`);
|
|
725
733
|
}
|
|
726
|
-
const ctx = portsFromHost();
|
|
734
|
+
const ctx = portsFromHost(job.capturedPorts?.principal);
|
|
727
735
|
const wire = executeDirectiveToIntent(entry.directive, entry.agg, intent.payload as never, ctx);
|
|
728
736
|
// Emit {events, strikes} ONLY when a strike is present, else the bare event array —
|
|
729
737
|
// so every strikes-free directive canonicalizes byte-identically to the pre-strikeout contract.
|
|
730
|
-
|
|
738
|
+
// The OBSERVED ENDORSE footprint (Slice B) rides the object form too — TRANSIENT (the gate reads
|
|
739
|
+
// it to check the law's nomosFlowGate.endorse; decode_plan_output ignores it for the byte-compare),
|
|
740
|
+
// so a principal-free intent still emits the bare array, byte-identical.
|
|
741
|
+
const endorse = wire.endorse;
|
|
742
|
+
if (!wire.strikes.length && !(endorse && endorse.length)) return wire.events;
|
|
743
|
+
return {
|
|
744
|
+
events: wire.events,
|
|
745
|
+
...(wire.strikes.length ? { strikes: wire.strikes } : {}),
|
|
746
|
+
...(endorse && endorse.length ? { endorse } : {}),
|
|
747
|
+
};
|
|
731
748
|
}
|
|
732
749
|
|
|
733
750
|
// The engine evals the bundle as a classic script and FREEZES globalThis before
|
package/src/fields.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { Conflict, Counter, LastPosition, Lww, type Driver } from "./drivers.js";
|
|
19
19
|
import type { AggregateHandle } from "./aggregate.js";
|
|
20
|
+
import type { FieldFacet } from "./principal_evidence.js";
|
|
20
21
|
|
|
21
22
|
export type FieldKind =
|
|
22
23
|
| "string"
|
|
@@ -143,8 +144,20 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
|
|
|
143
144
|
* Flag named `isMeta` — the `meta()` METHOD sets it, mirroring `encrypted()`/`optional()`.
|
|
144
145
|
*/
|
|
145
146
|
readonly isMeta?: true;
|
|
147
|
+
/**
|
|
148
|
+
* THE INTEGRITY/CONFIDENTIALITY FACET (security_model.md — the endorse model). Set by the
|
|
149
|
+
* `authorityField`/`attributionField`/`standingField`/`secretField`/`publicField` wrappers (or the
|
|
150
|
+
* `.facet()` sugar). Drives `set()` acceptance of PrincipalEvidence: an `authority`/`standing` sink
|
|
151
|
+
* accepts only the matching ENDORSED conversion (never un-endorsed identity), so the confused deputy
|
|
152
|
+
* is unrepresentable. OMITTED on a plain field (accepts a plain value). The field's READ/wire type is
|
|
153
|
+
* unchanged — the facet governs WRITES, not what's stored.
|
|
154
|
+
*/
|
|
155
|
+
readonly facet?: FieldFacet;
|
|
146
156
|
/** Returns a copy with the chosen driver (kind literal preserved). */
|
|
147
157
|
merge(driver: Driver): Field<T, K>;
|
|
158
|
+
/** Tag this field's facet, threaded through merge/optional (the canonical form is the
|
|
159
|
+
* `authorityField(...)`/`attributionField(...)`/… wrappers, which call this). */
|
|
160
|
+
withFacet<Fa extends FieldFacet>(facet: Fa): Field<T, K> & { readonly facet: Fa };
|
|
148
161
|
/** Mark this field's value ENCRYPTED (sets {@link Field.isEncrypted}). Chainable + non-destructive. */
|
|
149
162
|
encrypted(): Field<T, K>;
|
|
150
163
|
/** Mark this field plaintext METADATA (sets {@link Field.isMeta}) — the indexable opt-out on an owned aggregate. */
|
|
@@ -173,6 +186,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
173
186
|
jsonShape?: "object";
|
|
174
187
|
encrypted?: true;
|
|
175
188
|
meta?: true;
|
|
189
|
+
facet?: FieldFacet;
|
|
176
190
|
} = {},
|
|
177
191
|
): Field<T, K> {
|
|
178
192
|
const isOptional = extra.isOptional ?? false;
|
|
@@ -181,6 +195,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
181
195
|
zod,
|
|
182
196
|
driver,
|
|
183
197
|
isOptional,
|
|
198
|
+
...(extra.facet !== undefined ? { facet: extra.facet } : {}),
|
|
184
199
|
...(extra.refWorkspace !== undefined ? { refWorkspace: extra.refWorkspace } : {}),
|
|
185
200
|
...(extra.refAggregateId !== undefined ? { refAggregateId: extra.refAggregateId } : {}),
|
|
186
201
|
...(extra.viaField !== undefined ? { viaField: extra.viaField } : {}),
|
|
@@ -211,10 +226,39 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
211
226
|
// Driver/schema unchanged; the fact rides the aggregate's audience surface, not the wire kind.
|
|
212
227
|
return makeField<T, K>(kind, zod, driver, { ...extra, meta: true });
|
|
213
228
|
},
|
|
229
|
+
withFacet<Fa extends FieldFacet>(facet: Fa): Field<T, K> & { readonly facet: Fa } {
|
|
230
|
+
// The facet governs WRITES (set() acceptance), never the wire/read type — so the schema +
|
|
231
|
+
// driver are unchanged. Threaded through merge/optional (it rides `extra`).
|
|
232
|
+
return makeField<T, K>(kind, zod, driver, { ...extra, facet }) as Field<T, K> & { readonly facet: Fa };
|
|
233
|
+
},
|
|
214
234
|
};
|
|
215
235
|
return f;
|
|
216
236
|
}
|
|
217
237
|
|
|
238
|
+
// ── FIELD FACETS (security_model.md — the endorse model) ─────────────────────────────────────
|
|
239
|
+
// Canonical wrappers that tag a field's facet; `set()` then gates which PrincipalEvidence
|
|
240
|
+
// conversions it accepts. The field's read/wire type is unchanged — only WRITES are governed.
|
|
241
|
+
|
|
242
|
+
/** An AUTHORITY sink (relation subject/relation/object, token audience/scope, policy/role/identity
|
|
243
|
+
* write). Accepts a plain value OR `principal().asAuthoritySubject(reason)` — never un-endorsed identity. */
|
|
244
|
+
export const authorityField = <T, K extends FieldKind>(field: Field<T, K>): Field<T, K> & { readonly facet: "authority" } =>
|
|
245
|
+
field.withFacet("authority");
|
|
246
|
+
/** A STANDING sink (accountability: owedBy/owedTo/responsible/issuer/escalation). Accepts a plain
|
|
247
|
+
* value OR `principal().asStandingParty(reason)`. */
|
|
248
|
+
export const standingField = <T, K extends FieldKind>(field: Field<T, K>): Field<T, K> & { readonly facet: "standing" } =>
|
|
249
|
+
field.withFacet("standing");
|
|
250
|
+
/** An ATTRIBUTION sink (audit: createdBy/grantedBy/submittedBy). Accepts a plain value OR
|
|
251
|
+
* `principal().asAttribution()` — free, no endorsement. Promote to `authorityField` if law later
|
|
252
|
+
* reads it to decide authority. */
|
|
253
|
+
export const attributionField = <T, K extends FieldKind>(field: Field<T, K>): Field<T, K> & { readonly facet: "attribution" } =>
|
|
254
|
+
field.withFacet("attribution");
|
|
255
|
+
/** A confidentiality SOURCE (the read-down lattice). */
|
|
256
|
+
export const secretField = <T, K extends FieldKind>(field: Field<T, K>): Field<T, K> & { readonly facet: "secret" } =>
|
|
257
|
+
field.withFacet("secret");
|
|
258
|
+
/** Explicitly PUBLIC (⊤) — the read-down lattice top. */
|
|
259
|
+
export const publicField = <T, K extends FieldKind>(field: Field<T, K>): Field<T, K> & { readonly facet: "public" } =>
|
|
260
|
+
field.withFacet("public");
|
|
261
|
+
|
|
218
262
|
/** Infer the authored value type of a Field. */
|
|
219
263
|
export type FieldValue<F> = F extends Field<infer T> ? T : never;
|
|
220
264
|
|