@githolon/dsl 0.16.0 → 0.16.2
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/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/codegen_dart.ts +3 -3
- package/src/codegen_dart_client.ts +3 -3
- package/src/codegen_openapi.ts +239 -0
- package/src/codegen_proof.ts +16 -9
- package/src/codegen_ts.ts +51 -15
- package/src/compile_package_main.ts +33 -15
- package/src/directive.ts +1 -1
- package/src/framework/repair.ts +2 -2
- package/src/framework/signing_identity.ts +1 -1
- package/src/framework/workspaces.ts +140 -15
- package/src/manifest.ts +1 -1
- package/src/workspace_sharding.ts +1 -1
- package/src/workspace_type.ts +2 -2
- /package/dart/lib/{nomos_dsl.dart → nomos_client.dart} +0 -0
package/dart/FINDINGS.md
CHANGED
|
@@ -16,14 +16,14 @@ design: the schema is the single source of truth → TS (done), proto (later),
|
|
|
16
16
|
|
|
17
17
|
## What was built
|
|
18
18
|
|
|
19
|
-
`nomos2/dsl/dart/` — a Dart package (`
|
|
19
|
+
`nomos2/dsl/dart/` — a Dart package (`nomos_client`):
|
|
20
20
|
|
|
21
21
|
| Path | Role | Hand-written / generated |
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| `lib/src/wire.dart` | Fixed kernel wire types: `Hlc`, `Value` (sealed: `ValueStr`/`ValueInt`/`ValueSet`/`ValueMap`), `WireField`, `Op` (sealed: `OpSet`/`OpAddToSet`/`OpSetEntry`), `FieldOp`, `Event` (`{aggregate, ops}`, #56), `Intent` (`{hlc, events}`, #56). | hand-written |
|
|
24
24
|
| `lib/src/driver.dart` | `WireDriver` (sealed: `DriverLww`/`DriverAddWins`/`DriverConflict`/`DriverMapOf`) + `Schema`. | hand-written |
|
|
25
25
|
| `lib/src/generated/trackable_asset.dart` | The `trackable_asset` domain: `TrackableAsset` aggregate, `StatusEnum`, and `CreateAssetPayload`/`MoveAssetPayload`/`TagAssetPayload`/`SetCustomFieldPayload`. | **generated** |
|
|
26
|
-
| `lib/
|
|
26
|
+
| `lib/nomos_client.dart` | Public barrel (exports the three above). | hand-written |
|
|
27
27
|
| `test/wire_test.dart` | Round-trip tests for every wire type + Driver/Schema. | — |
|
|
28
28
|
| `test/trackable_asset_test.dart` | Read-model aggregate deserialization + directive-payload → Event JSON. | — |
|
|
29
29
|
|
package/dart/LICENSE
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Nomos Pre-Release License (v1)
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Captain App Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This is a pre-release of Nomos. This license gives you what you need to BUILD
|
|
6
|
+
with it; we keep the rest for now.
|
|
7
|
+
|
|
8
|
+
## You may
|
|
9
|
+
|
|
10
|
+
- install and use these packages to author Nomos domains, compile them, and
|
|
11
|
+
deploy them to Nomos Cloud or any Nomos instance Captain App operates or
|
|
12
|
+
authorizes;
|
|
13
|
+
- build, run, and ship applications on top of them — including commercial ones;
|
|
14
|
+
- keep everything that's yours: code you write, and everything these tools
|
|
15
|
+
generate FOR you (scaffolds from `create-githolon` / `githolon generate`, generated
|
|
16
|
+
clients, compiled domain packages) carries NO restriction from us — it is
|
|
17
|
+
yours outright.
|
|
18
|
+
|
|
19
|
+
## You may not
|
|
20
|
+
|
|
21
|
+
- redistribute these packages or their source, in whole or in part, outside
|
|
22
|
+
your team;
|
|
23
|
+
- modify them or build derivative tools, SDKs, or runtimes from their source;
|
|
24
|
+
- offer the Nomos runtime, or anything materially similar, as a hosted service;
|
|
25
|
+
- reverse-engineer the holon wasm runtime.
|
|
26
|
+
|
|
27
|
+
## Data retention
|
|
28
|
+
|
|
29
|
+
This is a pre-release we are actively evaluating. Workspaces you retire stop
|
|
30
|
+
counting toward your quota but are NOT deleted: **we retain all workspace data
|
|
31
|
+
(ledgers, law, intents) and may examine it to evaluate how the product is
|
|
32
|
+
used.** Don't put anything in a pre-release workspace you wouldn't want the
|
|
33
|
+
builders to read. If you need something truly expunged, ask:
|
|
34
|
+
jack@captainapp.co.uk.
|
|
35
|
+
|
|
36
|
+
## The rest
|
|
37
|
+
|
|
38
|
+
Provided **as is**, with no warranty of any kind; to the maximum extent
|
|
39
|
+
permitted by law, Captain App Ltd accepts no liability arising from your use.
|
|
40
|
+
This license terminates automatically if you breach it.
|
|
41
|
+
|
|
42
|
+
Want the kernel source, broader rights, or to do something this doesn't cover?
|
|
43
|
+
**Ask: jack@captainapp.co.uk.** The plan is to open Nomos up gradually, with
|
|
44
|
+
the people actually using it — telling us what you're building is how that
|
|
45
|
+
happens faster.
|
package/dart/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# nomos_client
|
|
2
|
+
|
|
3
|
+
Dart frontend types for [Nomos](https://github.com/Captain-App/nomos2) — the hand-written kernel
|
|
4
|
+
wire types plus the code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
5
|
+
the exact serde JSON the Rust kernel + projection produce/consume, and defines the `NomosBridge`
|
|
6
|
+
protocol the runtime ([`nomos_flutter`](https://pub.dev/packages/nomos_flutter)) drives.
|
|
7
|
+
|
|
8
|
+
You normally depend on `nomos_flutter` (which re-exports the bridge surface) rather than this package
|
|
9
|
+
directly. Licensed under the Nomos Pre-Release License — see `LICENSE`.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// COPY THIS INTO YOUR FLUTTER APP. It is an EXAMPLE, not part of the `
|
|
2
|
-
// `webview_flutter`, which the pure-Dart `
|
|
1
|
+
// COPY THIS INTO YOUR FLUTTER APP. It is an EXAMPLE, not part of the `nomos_client` library: it depends on
|
|
2
|
+
// `webview_flutter`, which the pure-Dart `nomos_client` package deliberately does NOT depend on (so the bridge
|
|
3
3
|
// protocol logic stays Flutter-free and unit-testable). This ~50-line transport is the only app-specific glue.
|
|
4
4
|
//
|
|
5
|
-
// It implements `BridgeTransport` (from `package:
|
|
5
|
+
// It implements `BridgeTransport` (from `package:nomos_client/nomos_client.dart`) over a `webview_flutter`
|
|
6
6
|
// WebViewController loading the published `nomos-runner` bundle:
|
|
7
7
|
// • host → runner: `controller.runJavaScript("window.__nomosSend(<json>)")`
|
|
8
8
|
// • runner → host: a `JavaScriptChannel("NomosHost")` whose onMessage feeds the bridge's message stream.
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import 'dart:async';
|
|
25
25
|
import 'dart:convert';
|
|
26
26
|
|
|
27
|
-
import 'package:
|
|
27
|
+
import 'package:nomos_client/nomos_client.dart';
|
|
28
28
|
import 'package:webview_flutter/webview_flutter.dart';
|
|
29
29
|
|
|
30
30
|
/// A [BridgeTransport] over a `webview_flutter` controller hosting the `nomos-runner` bundle.
|
package/dart/lib/src/offer.dart
CHANGED
|
@@ -40,7 +40,7 @@ abstract interface class NomosIntent {
|
|
|
40
40
|
|
|
41
41
|
/// The kernel write seam: the FRB `NomosClient.author` signature, as a function
|
|
42
42
|
/// type. `offer` takes this (rather than importing the FRB client) so the
|
|
43
|
-
/// generated `
|
|
43
|
+
/// generated `nomos_client` package stays free of a `flutter_rust_bridge` dependency
|
|
44
44
|
/// — the app threads `client.author` in. Returns the new HEAD commit oid (hex).
|
|
45
45
|
typedef Authorer = String Function({
|
|
46
46
|
required String domain,
|
|
@@ -70,7 +70,7 @@ String offer(NomosIntent intent,
|
|
|
70
70
|
// this seam to RESERVE a typed, type-tagged `<Type>_<uuidv7>` id, which it ships in the
|
|
71
71
|
// create payload; the kernel's GATE verifies its shape + type-tag prefix on author. Like
|
|
72
72
|
// [Authorer], this is a function type the app threads in (the FRB
|
|
73
|
-
// `NomosClient.mintIdForWorkspace`), so the generated `
|
|
73
|
+
// `NomosClient.mintIdForWorkspace`), so the generated `nomos_client` package stays free of a
|
|
74
74
|
// `flutter_rust_bridge` dependency.
|
|
75
75
|
//
|
|
76
76
|
// WHERE THE RANDOMNESS LIVES (the model — the dev touches NONE of it): the KERNEL mints
|
|
@@ -38,7 +38,7 @@ import 'dart:convert';
|
|
|
38
38
|
/// data}` rows (O(matches), never a workspace scan — #140 READ-CLOSURE).
|
|
39
39
|
///
|
|
40
40
|
/// `subscriptions.dart` takes this (rather than importing a host client) so the
|
|
41
|
-
/// generated `
|
|
41
|
+
/// generated `nomos_client` package stays host-agnostic. The app threads `client.query`
|
|
42
42
|
/// in. `paramsJson` is a JSON object carrying the
|
|
43
43
|
/// key field + value, e.g. `{"estateId":"e-1"}`.
|
|
44
44
|
typedef Queryer = String Function(String queryId, String paramsJson);
|
package/dart/pubspec.yaml
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
# wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
|
|
6
6
|
# If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
7
|
|
|
8
|
-
name:
|
|
8
|
+
name: nomos_client
|
|
9
|
+
repository: https://github.com/Captain-App/nomos2
|
|
9
10
|
description: >-
|
|
10
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
11
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
12
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
13
|
-
version: 0.
|
|
14
|
-
publish_to: none
|
|
14
|
+
version: 0.16.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
|
|
6
6
|
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
7
|
|
|
8
|
-
import 'package:
|
|
8
|
+
import 'package:nomos_client/nomos_client.dart';
|
|
9
9
|
import 'package:test/test.dart';
|
|
10
10
|
|
|
11
11
|
void main() {
|
|
@@ -7,7 +7,7 @@ import 'dart:async';
|
|
|
7
7
|
import 'dart:convert';
|
|
8
8
|
import 'dart:io';
|
|
9
9
|
|
|
10
|
-
import 'package:
|
|
10
|
+
import 'package:nomos_client/nomos_client.dart';
|
|
11
11
|
import 'package:test/test.dart';
|
|
12
12
|
|
|
13
13
|
/// A fake runner-over-a-transport: records sent requests, replies the way `webview-runner.mjs` does, and can
|
package/dart/test/wire_test.dart
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/src/codegen_dart.ts
CHANGED
|
@@ -2742,11 +2742,11 @@ export function generateDartDomain(mod: DomainModule): string {
|
|
|
2742
2742
|
`// ignore_for_file: unused_import`,
|
|
2743
2743
|
``,
|
|
2744
2744
|
// The generated tenant domains live in the `co2_nomos_domains` package and depend
|
|
2745
|
-
// on the framework `
|
|
2745
|
+
// on the framework `nomos_client` package by its PUBLIC package: URI — NOT a relative
|
|
2746
2746
|
// `../wire.dart` path into the framework (which broke when the generated dir moved
|
|
2747
|
-
// out of
|
|
2747
|
+
// out of nomos_client into the co-located tenant package). The framework barrel
|
|
2748
2748
|
// re-exports wire/dispatch/subscriptions/provider, so one import covers them all.
|
|
2749
|
-
`import 'package:
|
|
2749
|
+
`import 'package:nomos_client/nomos_client.dart';`,
|
|
2750
2750
|
...(mod.dartImports ?? []).map(dartImportLine),
|
|
2751
2751
|
``,
|
|
2752
2752
|
`// ---- enums ----`,
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* * typed O(1) count/sum accessors.
|
|
23
23
|
*
|
|
24
24
|
* It IMPORTS the sibling `<name>.dart` (for the read-model + payload classes) and either the vendored
|
|
25
|
-
* `
|
|
25
|
+
* `nomos_client.dart` barrel or the package `nomos_client` barrel (for `NomosBridge`). NO manifest/hash impact —
|
|
26
26
|
* pure frontend codegen, exactly like `codegen_dart.ts`. NO FALLBACK: an unsupported shape throws at compile
|
|
27
27
|
* time.
|
|
28
28
|
*/
|
|
@@ -108,8 +108,8 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
108
108
|
const hashConst = dartHashConstName(mod.name);
|
|
109
109
|
const supportImport =
|
|
110
110
|
opts.supportImport === "package"
|
|
111
|
-
? "package:
|
|
112
|
-
: "
|
|
111
|
+
? "package:nomos_client/nomos_client.dart"
|
|
112
|
+
: "nomos_client.dart";
|
|
113
113
|
const aggById = new Map<string, AggregateHandle>();
|
|
114
114
|
for (const a of mod.aggregates) aggById.set(a.id, a);
|
|
115
115
|
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted remotes are
|
|
2
|
+
// replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon wasm32-wasip1 artifact.
|
|
3
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* codegen_openapi.ts — THE DOMAIN'S SELF-DESCRIPTION (architecture/githolon_host_contract.md,
|
|
7
|
+
* "The Self-Describing API"). Each domain emits an OpenAPI fragment FROM ITS OWN ZOD at compile
|
|
8
|
+
* time — the same schemas the engine validates, lowered via `z.toJSONSchema`. This is the law's
|
|
9
|
+
* own product, never host code: the host INGESTS the workspace's merged spec at runtime and builds
|
|
10
|
+
* its router from it (path → `offer`/`query` rewrite), so no domain or directive name ever lives in
|
|
11
|
+
* host source.
|
|
12
|
+
*
|
|
13
|
+
* Each operation is an OFFER you can make to the workspace (the one effectful move) or a read:
|
|
14
|
+
* - an offerable intent → `POST /{directiveId}` whose requestBody is the intent's payload JSON
|
|
15
|
+
* Schema. On the wire an intent names exactly one directive, so `directiveId` is the offer
|
|
16
|
+
* SELECTOR (it rides in `x-nomos`, not as "the API"); the directive's PLAN — its fan-out into
|
|
17
|
+
* aggregates/events — is private and never appears here;
|
|
18
|
+
* - every READ → a `GET`: declared queries (+ the hasMany inverse indexes) by their key, and the
|
|
19
|
+
* maintained primitives — counts / sums / extremums (min,max) / spatial — by `group`/`bbox`;
|
|
20
|
+
* - each operation carries an `x-nomos` extension — `{ kind: "offer"|"query"|"count"|"sum"|
|
|
21
|
+
* "extremum"|"spatial", domain, … }` — the ONLY thing the host reads to fold a matched request
|
|
22
|
+
* into the opaque intent/query/read envelope.
|
|
23
|
+
*
|
|
24
|
+
* THE WORKSPACE SPEC is the runtime MERGE of its installed domains' fragments (which are active is
|
|
25
|
+
* `currentLaw`). `mergeOpenApiFragments` is that merge: a plain union today (no domain cascades on
|
|
26
|
+
* another's API SURFACE — scoped reads / `.encrypted` / `requires(cap)` change runtime BEHAVIOUR,
|
|
27
|
+
* not operation SHAPE), with two law-blind structural touch-ups — NAMESPACE a cross-domain path
|
|
28
|
+
* collision under `/{domain}/…` so neither domain's operation clobbers the other (operations carry
|
|
29
|
+
* their domain in `operationId`, so two domains' same-named directives ARE distinct ops and collide),
|
|
30
|
+
* and DEDUPE a byte-identical operation (defensive — e.g. a fragment seen twice). The merge reads
|
|
31
|
+
* only structure; if a real cross-domain cascade ever lands, THIS is the seam for it.
|
|
32
|
+
*/
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
import type { DomainModule } from "./codegen_dart.js";
|
|
35
|
+
import type { Directive } from "./directive.js";
|
|
36
|
+
import type { QueryDecl } from "./query.js";
|
|
37
|
+
import { hasManyIndexes } from "./query.js";
|
|
38
|
+
import { finishCount } from "./count.js";
|
|
39
|
+
import { finishSum } from "./sum.js";
|
|
40
|
+
import { finishExtremum } from "./extremum.js";
|
|
41
|
+
|
|
42
|
+
type AnyDirective = Directive<unknown>;
|
|
43
|
+
|
|
44
|
+
const GROUP_PARAM = { name: "group", in: "query", required: false, schema: { type: "string" } } as const;
|
|
45
|
+
|
|
46
|
+
/** Minimal OpenAPI 3.1 document shape we emit (open object — extensions ride as `x-…` keys). */
|
|
47
|
+
export interface OpenApiDoc {
|
|
48
|
+
openapi: "3.1.0";
|
|
49
|
+
info: Record<string, unknown>;
|
|
50
|
+
paths: Record<string, Record<string, unknown>>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface OpenApiFragmentOptions {
|
|
54
|
+
/** The content hash of the installed law (the spec's version + ingest cache key). */
|
|
55
|
+
lawHash?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Lower a directive's payload zod to JSON Schema; a non-representable schema falls back to a
|
|
59
|
+
* permissive object rather than failing the whole compile (the engine still validates the real zod). */
|
|
60
|
+
function payloadJsonSchema(d: AnyDirective): unknown {
|
|
61
|
+
try {
|
|
62
|
+
return z.toJSONSchema((d as { payloadSchema: z.ZodTypeAny }).payloadSchema, { io: "input" });
|
|
63
|
+
} catch {
|
|
64
|
+
return { type: "object", additionalProperties: true };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The domain key the engine resolves intents against (the `author` registry key). */
|
|
69
|
+
function domainKey(module: DomainModule): string {
|
|
70
|
+
return module.domain ?? module.name;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* generateOpenApiFragment — one domain → its OpenAPI fragment. Pure: same module ⇒ same fragment.
|
|
75
|
+
* Paths are bare (`/{id}`); the workspace gateway mounts the merged spec under `/ws/{holonId}`, and
|
|
76
|
+
* the merge namespaces any cross-domain collision. The offer/query distinction rides on the HTTP
|
|
77
|
+
* method (POST/GET) AND the authoritative `x-nomos.kind` — the host reads the latter, never the name.
|
|
78
|
+
*/
|
|
79
|
+
export function generateOpenApiFragment(
|
|
80
|
+
module: DomainModule,
|
|
81
|
+
opts: OpenApiFragmentOptions = {},
|
|
82
|
+
): OpenApiDoc {
|
|
83
|
+
const domain = domainKey(module);
|
|
84
|
+
const paths: Record<string, Record<string, unknown>> = {};
|
|
85
|
+
|
|
86
|
+
// THE GATE AUDIT INDEX: every offer must show its protection, so enumerating the spec IS the security
|
|
87
|
+
// audit. `requires` = the .requires relation gate; `invariants` = the workspaceInvariants that judge it
|
|
88
|
+
// (e.g. provisionWorkspace's ownerGrantHash==foundingAdminHash). An offer with NEITHER is a VISIBLE
|
|
89
|
+
// red flag (a raw ungated primitive like relateSubject). The spec hides nothing — that's the point.
|
|
90
|
+
const invariantsOf = (directiveId: string) =>
|
|
91
|
+
(module.workspaceInvariants ?? []).filter((i) => i.on === directiveId).map((i) => i.id);
|
|
92
|
+
|
|
93
|
+
// OFFERABLE INTENTS → POST offers. The payload JSON Schema is the intent's own zod, lowered.
|
|
94
|
+
for (const dir of module.directives as AnyDirective[]) {
|
|
95
|
+
const invs = invariantsOf(dir.id);
|
|
96
|
+
const op: Record<string, unknown> = {
|
|
97
|
+
operationId: `${domain}__${dir.id}`,
|
|
98
|
+
tags: [domain],
|
|
99
|
+
summary: `offer: ${dir.id}`,
|
|
100
|
+
description: dir.marker && dir.aggregateId
|
|
101
|
+
? `Offer the \`${dir.id}\` intent (${dir.marker} ${dir.aggregateId}). Admitted or refused by the law; the plan is private.`
|
|
102
|
+
: `Offer the \`${dir.id}\` intent. Admitted or refused by the law.`,
|
|
103
|
+
"x-nomos": {
|
|
104
|
+
kind: "offer",
|
|
105
|
+
domain,
|
|
106
|
+
directiveId: dir.id,
|
|
107
|
+
...(dir.marker ? { marker: dir.marker } : {}),
|
|
108
|
+
...(dir.aggregateId ? { aggregate: dir.aggregateId } : {}),
|
|
109
|
+
...(dir.requiresRelationGate ? { requires: dir.requiresRelationGate.relation } : {}),
|
|
110
|
+
...(invs.length ? { invariants: invs } : {}),
|
|
111
|
+
},
|
|
112
|
+
requestBody: {
|
|
113
|
+
required: true,
|
|
114
|
+
content: { "application/json": { schema: payloadJsonSchema(dir) } },
|
|
115
|
+
},
|
|
116
|
+
responses: {
|
|
117
|
+
"200": { description: "admitted — the sealed intent head" },
|
|
118
|
+
"401": { description: "no identity / identity refused" },
|
|
119
|
+
"422": { description: "the law refused the intent (typed refusal)" },
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
(paths[`/${dir.id}`] ??= {}).post = op;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// QUERIES → GET reads. Declared queries + the implicit hasMany inverse indexes (the relation IS
|
|
126
|
+
// the index) — both are reads the workspace serves; both belong in the spec.
|
|
127
|
+
const queries: QueryDecl[] = [
|
|
128
|
+
...(module.queries ?? []),
|
|
129
|
+
...module.aggregates.flatMap((agg) => hasManyIndexes(agg)),
|
|
130
|
+
];
|
|
131
|
+
for (const q of queries) {
|
|
132
|
+
const op: Record<string, unknown> = {
|
|
133
|
+
operationId: `${domain}__${q.id}`,
|
|
134
|
+
tags: [domain],
|
|
135
|
+
summary: `read ${q.returns} by ${q.key.join(", ") || "—"}`,
|
|
136
|
+
"x-nomos": { kind: "query", domain, queryId: q.id, key: q.key, returns: q.returns },
|
|
137
|
+
parameters: q.key.map((k) => ({ name: k, in: "query", required: true, schema: { type: "string" } })),
|
|
138
|
+
responses: { "200": { description: `rows of ${q.returns}` } },
|
|
139
|
+
};
|
|
140
|
+
(paths[`/${q.id}`] ??= {}).get = op;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// THE MAINTAINED READ PRIMITIVES — counts / sums / extremums (min,max) / spatial. Each is an O(1)
|
|
144
|
+
// read the workspace serves; each gets its GET. (Declared queries above are the indexed-row reads;
|
|
145
|
+
// these are the tallies + the geo index. Derived/combined fields are read-MODEL fields, surfaced on
|
|
146
|
+
// the aggregate's query rows — not their own endpoints.)
|
|
147
|
+
const getRead = (id: string, summary: string, xnomos: Record<string, unknown>, params: unknown[]) => {
|
|
148
|
+
(paths[`/${id}`] ??= {}).get = {
|
|
149
|
+
operationId: `${domain}__${id}`,
|
|
150
|
+
tags: [domain],
|
|
151
|
+
summary,
|
|
152
|
+
"x-nomos": { domain, ...xnomos },
|
|
153
|
+
parameters: params,
|
|
154
|
+
responses: { "200": { description: summary } },
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
for (const raw of module.counts ?? []) {
|
|
158
|
+
const c = finishCount(raw);
|
|
159
|
+
getRead(c.id, `count: ${c.of}${c.by ? ` by ${c.by}` : ""}`, { kind: "count", countId: c.id, ...(c.by ? { group: c.by } : {}) }, c.by ? [GROUP_PARAM] : []);
|
|
160
|
+
}
|
|
161
|
+
for (const raw of module.sums ?? []) {
|
|
162
|
+
const s = finishSum(raw);
|
|
163
|
+
getRead(s.id, `sum: ${s.sumField} of ${s.of}${s.by ? ` by ${s.by}` : ""}`, { kind: "sum", sumId: s.id, sumField: s.sumField, ...(s.by ? { group: s.by } : {}) }, s.by ? [GROUP_PARAM] : []);
|
|
164
|
+
}
|
|
165
|
+
for (const [key, kind] of [["mins", "min"], ["maxes", "max"]] as const) {
|
|
166
|
+
for (const raw of (module as unknown as Record<string, unknown[]>)[key] ?? []) {
|
|
167
|
+
const e = finishExtremum(raw as never);
|
|
168
|
+
getRead(e.id, `${kind}: ${e.valueField} of ${e.of}${e.by ? ` by ${e.by}` : ""}`, { kind: "extremum", extremumId: e.id, extremumKind: e.kind, ...(e.by ? { group: e.by } : {}) }, e.by ? [GROUP_PARAM] : []);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const sp of module.spatials ?? []) {
|
|
172
|
+
getRead(sp.id, `spatial: ${sp.of} by ${sp.on} bbox`, { kind: "spatial", spatialId: sp.id, on: sp.on }, [
|
|
173
|
+
{ name: "bbox", in: "query", required: true, schema: { type: "string" }, description: "minLng,minLat,maxLng,maxLat" },
|
|
174
|
+
]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
openapi: "3.1.0",
|
|
179
|
+
info: {
|
|
180
|
+
title: `${domain} (nomos domain)`,
|
|
181
|
+
version: opts.lawHash ? opts.lawHash.slice(0, 12) : "0",
|
|
182
|
+
"x-nomos-domain": domain,
|
|
183
|
+
...(opts.lawHash ? { "x-nomos-law-hash": opts.lawHash } : {}),
|
|
184
|
+
},
|
|
185
|
+
paths,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Stable JSON of an operation, to detect a genuine duplicate vs a real collision in the merge. */
|
|
190
|
+
function operationKey(op: unknown): string {
|
|
191
|
+
return JSON.stringify(op);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* mergeOpenApiFragments — the WORKSPACE spec = the union of its installed domains' fragments. The
|
|
196
|
+
* runtime step (driven by `currentLaw`), kept here as a pure, law-blind function:
|
|
197
|
+
* - UNION the paths;
|
|
198
|
+
* - DEDUPE an operation declared identically by several domains (framework aggregates) — one entry;
|
|
199
|
+
* - NAMESPACE a genuine collision (same path+method, DIFFERENT operation, different domains) under
|
|
200
|
+
* `/{domain}/{path}` so neither is clobbered.
|
|
201
|
+
* It reads only structure (paths, methods, the `x-nomos.domain` tag) — never a directive's meaning.
|
|
202
|
+
*/
|
|
203
|
+
export function mergeOpenApiFragments(
|
|
204
|
+
fragments: readonly OpenApiDoc[],
|
|
205
|
+
opts: { title?: string; lawHash?: string } = {},
|
|
206
|
+
): OpenApiDoc {
|
|
207
|
+
const paths: Record<string, Record<string, unknown>> = {};
|
|
208
|
+
for (const frag of fragments) {
|
|
209
|
+
for (const [path, methods] of Object.entries(frag.paths)) {
|
|
210
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
211
|
+
const existing = paths[path]?.[method];
|
|
212
|
+
if (existing === undefined) {
|
|
213
|
+
(paths[path] ??= {})[method] = op;
|
|
214
|
+
} else if (operationKey(existing) === operationKey(op)) {
|
|
215
|
+
// identical (a framework operation shared by many domains) — keep the one entry.
|
|
216
|
+
} else {
|
|
217
|
+
// genuine collision: two domains, same path+method, different operations. Namespace BOTH
|
|
218
|
+
// under their domain so neither wins silently.
|
|
219
|
+
const domOf = (o: unknown): string =>
|
|
220
|
+
String((o as { "x-nomos"?: { domain?: string } })?.["x-nomos"]?.domain ?? "domain");
|
|
221
|
+
const bucket = paths[path]!;
|
|
222
|
+
(paths[`/${domOf(existing)}${path}`] ??= {})[method] = existing;
|
|
223
|
+
(paths[`/${domOf(op)}${path}`] ??= {})[method] = op;
|
|
224
|
+
delete bucket[method];
|
|
225
|
+
if (Object.keys(bucket).length === 0) delete paths[path];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
openapi: "3.1.0",
|
|
232
|
+
info: {
|
|
233
|
+
title: opts.title ?? "nomos workspace",
|
|
234
|
+
version: opts.lawHash ? opts.lawHash.slice(0, 12) : "0",
|
|
235
|
+
...(opts.lawHash ? { "x-nomos-law-hash": opts.lawHash } : {}),
|
|
236
|
+
},
|
|
237
|
+
paths,
|
|
238
|
+
};
|
|
239
|
+
}
|
package/src/codegen_proof.ts
CHANGED
|
@@ -332,6 +332,12 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
332
332
|
: JSON.stringify(baseValue);
|
|
333
333
|
const queryParamsObj = query ? Object.fromEntries(query.key.map((k) => [k, baseValue[k]])) : undefined;
|
|
334
334
|
const queryParams = queryParamsObj ? JSON.stringify(queryParamsObj) : "";
|
|
335
|
+
// The gateway routes a declared query by its id: GET /v2/workspaces/<ws>/<queryId>?<key>=<val>
|
|
336
|
+
// (the host reads each declared key from the querystring — never a generic /query?params= lane).
|
|
337
|
+
const queryQs =
|
|
338
|
+
query && queryParamsObj
|
|
339
|
+
? query.key.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(String(queryParamsObj[k]))}`).join("&")
|
|
340
|
+
: "";
|
|
335
341
|
const listAccessor = `list${agg.id}s`;
|
|
336
342
|
|
|
337
343
|
// THE MACHINE-READABLE LEGS (parsed by `githolon`'s offline proof runner —
|
|
@@ -376,7 +382,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
376
382
|
`try {`,
|
|
377
383
|
``,
|
|
378
384
|
`// ${n()}. a throwaway workspace (the ONE-TIME secret comes back on create)`,
|
|
379
|
-
`let r = await fetch(\`\${CLOUD}/
|
|
385
|
+
`let r = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}\`, { method: "POST", headers: { "x-nomos-principal": "githolon-proof" } });`,
|
|
380
386
|
`let d = await r.json();`,
|
|
381
387
|
`if (!d.ok) fail(\`create workspace: \${JSON.stringify(d)}\`);`,
|
|
382
388
|
`SECRET = d.workspaceSecret;`,
|
|
@@ -384,7 +390,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
384
390
|
`ok(\`workspace \${WS} created (one-time secret held for cleanup — PROOF_KEEP=1 keeps the workspace and prints it)\`);`,
|
|
385
391
|
``,
|
|
386
392
|
`// ${n()}. deploy YOUR compiled law (build/${opts.packageName}.deploy.json) WITH the secret`,
|
|
387
|
-
`r = await fetch(\`\${CLOUD}/
|
|
393
|
+
`r = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/domains\`, { method: "POST", headers: { "content-type": "application/json", authorization: \`Bearer \${SECRET}\` }, body: JSON.stringify(deploy) });`,
|
|
388
394
|
`d = await r.json();`,
|
|
389
395
|
`if (!d.ok || d.installation?.[0]?.data?.["status.phase"] !== "Active") fail(\`deploy: \${JSON.stringify(d)}\`);`,
|
|
390
396
|
`if (d.domainHash !== ${hashConst}) fail(\`deploy hash \${d.domainHash} != the generated client's \${${hashConst}}\`);`,
|
|
@@ -463,8 +469,9 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
463
469
|
|
|
464
470
|
if (query) {
|
|
465
471
|
out.push(
|
|
466
|
-
`// ${n()}. the CLOUD answers the SAME declared query
|
|
467
|
-
|
|
472
|
+
`// ${n()}. the CLOUD answers the SAME declared query — routed by the workspace's OWN OpenAPI`,
|
|
473
|
+
`// (GET /v2/workspaces/<ws>/${query.id}?${queryQs}); the host learns the route from the law's spec.`,
|
|
474
|
+
`d = await (await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/${query.id}${queryQs ? `?${queryQs}` : ``}\`)).json();`,
|
|
468
475
|
`if (!(d.ok && d.rows.length === 1 && d.rows[0].id === createdId)) fail(\`cloud declared query: \${JSON.stringify(d)}\`);`,
|
|
469
476
|
`ok("cloud declared query ${query.id} returns the admitted row — full loop closed");`,
|
|
470
477
|
``,
|
|
@@ -472,7 +479,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
472
479
|
} else {
|
|
473
480
|
out.push(
|
|
474
481
|
`// ${n()}. the CLOUD answers the by-id read (edge projection of the admitted write)`,
|
|
475
|
-
`d = await (await fetch(\`\${CLOUD}/
|
|
482
|
+
`d = await (await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/aggregates/\${encodeURIComponent(createdId)}\`)).json();`,
|
|
476
483
|
`if (!(d.rows?.length === 1 && d.rows[0].id === createdId)) fail(\`cloud by-id read: \${JSON.stringify(d)}\`);`,
|
|
477
484
|
`ok("cloud read returns the admitted row — full loop closed");`,
|
|
478
485
|
``,
|
|
@@ -482,7 +489,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
482
489
|
if (count) {
|
|
483
490
|
out.push(
|
|
484
491
|
`// ${n()}. the EDGE maintains the same count (same wasm, same law)`,
|
|
485
|
-
`d = await (await fetch(\`\${CLOUD}/
|
|
492
|
+
`d = await (await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/${count.id}${count.by != null ? `?group=\${encodeURIComponent(${JSON.stringify(String(baseValue[count.by]))})}` : ``}\`)).json();`,
|
|
486
493
|
`if (!(d.ok && d.count === 1)) fail(\`cloud count ${count.id}: \${JSON.stringify(d)}\`);`,
|
|
487
494
|
`ok("cloud count ${count.id} = 1 — the O(1) maintained read, at the edge too");`,
|
|
488
495
|
``,
|
|
@@ -510,7 +517,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
510
517
|
`await ${factory}(writerB).${cd.id}(${concPayload("b")});`,
|
|
511
518
|
`await writerA.sync();`,
|
|
512
519
|
`await writerB.sync();`,
|
|
513
|
-
`d = await (await fetch(\`\${CLOUD}/
|
|
520
|
+
`d = await (await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/aggregates/\${encodeURIComponent(createdId)}\`)).json();`,
|
|
514
521
|
`const merged: string[] = d.rows?.[0]?.data?.[${JSON.stringify(setField)}] ?? [];`,
|
|
515
522
|
`if (!(merged.includes("proof-a") && merged.includes("proof-b"))) fail(\`AddWins union lost an add: \${JSON.stringify(merged)}\`);`,
|
|
516
523
|
`ok(\`two blind writers' concurrent adds BOTH survive the merge — ${setField} [\${[...merged].sort().join(", ")}]\`);`,
|
|
@@ -529,7 +536,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
529
536
|
`// ${n()}. CONVERGENCE: the original instance adopts canonical main in place — no reconnect`,
|
|
530
537
|
`await holon.pull();`,
|
|
531
538
|
`const head = await holon.head();`,
|
|
532
|
-
`const refsAdv = await (await fetch(\`\${CLOUD}/
|
|
539
|
+
`const refsAdv = await (await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/git/info/refs?service=git-upload-pack\`)).text();`,
|
|
533
540
|
`const remoteMain = (refsAdv.match(/([0-9a-f]{40}) refs\\/heads\\/main/) || [])[1];`,
|
|
534
541
|
`if (head !== remoteMain) fail(\`not converged in place: local \${head?.slice(0, 10)} vs main \${remoteMain?.slice(0, 10)}\`);`,
|
|
535
542
|
`ok("converged in place — local head == canonical main");`,
|
|
@@ -546,7 +553,7 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
546
553
|
` console.log(\`workspace \${WS} KEPT — secret \${SECRET}\`);`,
|
|
547
554
|
` console.log(\` persist it: githolon secret set \${WS} <the secret above> (then: githolon ws retire \${WS} when done)\`);`,
|
|
548
555
|
` } else if (SECRET) {`,
|
|
549
|
-
` const rr = await fetch(\`\${CLOUD}/
|
|
556
|
+
` const rr = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/retire\`, { method: "POST", headers: { authorization: \`Bearer \${SECRET}\` } }).then((x) => x.json()).catch(() => ({}));`,
|
|
550
557
|
` console.log(\` retired \${WS}: \${(rr as { ok?: boolean }).ok === true}\`);`,
|
|
551
558
|
` }`,
|
|
552
559
|
` process.exit(runError ? 1 : 0);`,
|
package/src/codegen_ts.ts
CHANGED
|
@@ -419,8 +419,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
419
419
|
|
|
420
420
|
// ── DERIVED BIRTH LANES (sharding §10 RATIFIED, slice 1) ──
|
|
421
421
|
// A `workspaceType(...).hasMany(...)` taxonomy DERIVES typed birth helpers over
|
|
422
|
-
// the EXISTING
|
|
423
|
-
//
|
|
422
|
+
// the EXISTING birth machinery (the boot lane + the parent's birthPlatform/createWorkspace
|
|
423
|
+
// governance offers, `POST /v2/workspaces/...` — derived, never forked). Fully typesafe
|
|
424
|
+
// per Jack's ruling: the caller passes LOGICAL names
|
|
424
425
|
// (parent param named after the parent TYPE); the `p--ws` composition is internal
|
|
425
426
|
// plumbing — the composed workspace name comes back on the result for `connect()`.
|
|
426
427
|
// PACKED child types derive NO holon-birth helper: a packed birth is a shard-map
|
|
@@ -495,16 +496,25 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
495
496
|
` [k: string]: unknown;`,
|
|
496
497
|
`}`,
|
|
497
498
|
``,
|
|
498
|
-
`const nomosBirth = async (cloud: string, path: string, auth: NomosBirthAuth): Promise<NomosBirthResult> => {`,
|
|
499
|
+
`const nomosBirth = async (cloud: string, path: string, auth: NomosBirthAuth, body?: unknown): Promise<NomosBirthResult> => {`,
|
|
499
500
|
` const headers: Record<string, string> = {};`,
|
|
500
501
|
` if (auth.authToken !== undefined) headers["x-nomos-auth"] = auth.authToken;`,
|
|
501
502
|
` if (auth.principal !== undefined) headers["x-nomos-principal"] = auth.principal;`,
|
|
502
503
|
` if (auth.bearer !== undefined) headers["authorization"] = \`Bearer \${auth.bearer}\`;`,
|
|
503
504
|
` if (auth.platformAdminKey !== undefined) headers["x-nomos-platform-admin-key"] = auth.platformAdminKey;`,
|
|
504
|
-
`
|
|
505
|
+
` if (body !== undefined) headers["content-type"] = "application/json";`,
|
|
506
|
+
` const res = await fetch(\`\${cloud.replace(/\\/$/, "")}\${path}\`, {`,
|
|
507
|
+
` method: "POST", headers, ...(body !== undefined ? { body: JSON.stringify(body) } : {}),`,
|
|
508
|
+
` });`,
|
|
505
509
|
` return (await res.json()) as NomosBirthResult;`,
|
|
506
510
|
`};`,
|
|
507
511
|
``,
|
|
512
|
+
`/** sha256-hex of the chosen platform admin key — the on-chain \`foundingAdminHash\` (the host never mints it). */`,
|
|
513
|
+
`const nomosKeyHash = async (key: string): Promise<string> => {`,
|
|
514
|
+
` const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(key));`,
|
|
515
|
+
` return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");`,
|
|
516
|
+
`};`,
|
|
517
|
+
``,
|
|
508
518
|
);
|
|
509
519
|
const emittedBirths = new Set<string>();
|
|
510
520
|
for (const mod of birthModules) {
|
|
@@ -518,13 +528,40 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
518
528
|
}
|
|
519
529
|
const childIds = new Set([...types.values()].flatMap((t) => [...t.childIds]));
|
|
520
530
|
const fnName = (typeId: string) => `birth${cap(camel(typeId))}`;
|
|
521
|
-
|
|
531
|
+
// THE GATEWAY BIRTH LANES (post-/v2): a top-level WORKSPACE births on the boot lane
|
|
532
|
+
// (POST /v2/workspaces/<name> — returns the one-time secret); every other birth is a
|
|
533
|
+
// GOVERNANCE OFFER routed by the parent's OpenAPI (POST /v2/workspaces/<parentWs>/<directiveId>),
|
|
534
|
+
// the obligation the birth provider materializes. parentWs = "root" for a top-level platform,
|
|
535
|
+
// else the parent holon's own name. timestamps ride IN the payload (the determinism doctrine);
|
|
536
|
+
// a platform's foundingAdminHash = sha256(your chosen platformAdminKey) — the host never mints it.
|
|
537
|
+
const emit = (name: string, doc: string, params: string[], lane: { kind: "boot" } | { kind: "offer"; parentWs: string; directiveId: "createWorkspace" | "birthPlatform" }) => {
|
|
522
538
|
if (emittedBirths.has(name)) return;
|
|
523
539
|
emittedBirths.add(name);
|
|
540
|
+
const sig = `opts: NomosBirthAuth & { cloud: string; ${params.join("; ")} }`;
|
|
541
|
+
if (lane.kind === "boot") {
|
|
542
|
+
out.push(
|
|
543
|
+
`/** ${doc} */`,
|
|
544
|
+
`export const ${name} = (${sig}): Promise<NomosBirthResult> =>`,
|
|
545
|
+
` nomosBirth(opts.cloud, \`/v2/workspaces/\${encodeURIComponent(opts.name)}\`, opts);`,
|
|
546
|
+
``,
|
|
547
|
+
);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const isPlatformOffer = lane.directiveId === "birthPlatform";
|
|
551
|
+
const bodyFields = [
|
|
552
|
+
`name: opts.name`,
|
|
553
|
+
`parent: ${lane.parentWs}`,
|
|
554
|
+
`principal: opts.principal ?? ""`,
|
|
555
|
+
`requestedBy: opts.principal ?? ""`,
|
|
556
|
+
`requestedAt: Date.now()`,
|
|
557
|
+
...(isPlatformOffer ? [`foundingAdminHash: await nomosKeyHash(opts.platformAdminKey)`] : []),
|
|
558
|
+
];
|
|
524
559
|
out.push(
|
|
525
560
|
`/** ${doc} */`,
|
|
526
|
-
`export const ${name} = (
|
|
527
|
-
` nomosBirth(opts.cloud, ${
|
|
561
|
+
`export const ${name} = async (${sig}): Promise<NomosBirthResult> =>`,
|
|
562
|
+
` nomosBirth(opts.cloud, \`/v2/workspaces/\${encodeURIComponent(${lane.parentWs})}/${lane.directiveId}\`, opts, {`,
|
|
563
|
+
` ${bodyFields.join(", ")},`,
|
|
564
|
+
` });`,
|
|
528
565
|
``,
|
|
529
566
|
);
|
|
530
567
|
};
|
|
@@ -532,14 +569,15 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
532
569
|
if (t.mode === "packed") continue; // placement, not a holon birth (slice 2)
|
|
533
570
|
const isPlatform = t.childIds.length > 0;
|
|
534
571
|
if (!childIds.has(t.id)) {
|
|
535
|
-
// A TOP-LEVEL type: births
|
|
572
|
+
// A TOP-LEVEL type: a workspace births on the boot lane; a platform-kind
|
|
573
|
+
// coordinator births via the root holon's birthPlatform offer.
|
|
536
574
|
emit(
|
|
537
575
|
fnName(t.id),
|
|
538
|
-
`Birth a '${t.id}' workspace${isPlatform ? " (a platform-kind coordinator: hosts its own subtree; PRESENT your chosen platformAdminKey)" : "
|
|
576
|
+
`Birth a '${t.id}' workspace${isPlatform ? " (a platform-kind coordinator: hosts its own subtree; PRESENT your chosen platformAdminKey) via root's birthPlatform offer" : " on the boot lane (the one-time secret comes back)"} — derived from the taxonomy. Placement is automatic (measured shape) — no tier to pick.`,
|
|
539
577
|
isPlatform ? [`name: string`, `platformAdminKey: string`] : [`name: string`],
|
|
540
578
|
isPlatform
|
|
541
|
-
?
|
|
542
|
-
:
|
|
579
|
+
? { kind: "offer", parentWs: `"root"`, directiveId: "birthPlatform" }
|
|
580
|
+
: { kind: "boot" },
|
|
543
581
|
);
|
|
544
582
|
}
|
|
545
583
|
for (const childId of t.childIds) {
|
|
@@ -549,13 +587,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
549
587
|
const childIsPlatform = child.childIds.length > 0;
|
|
550
588
|
emit(
|
|
551
589
|
fnName(child.id),
|
|
552
|
-
`Birth a '${child.id}' under a '${t.id}' — the taxonomy's hasMany edge
|
|
590
|
+
`Birth a '${child.id}' under a '${t.id}' — the taxonomy's hasMany edge, a governance offer on the parent holon (POST /v2/workspaces/<${parentParam}>/${childIsPlatform ? "birthPlatform" : "createWorkspace"}; lineage + pool enforced by the parent's law).${childIsPlatform ? " PRESENT your chosen platformAdminKey." : ""} Placement is automatic (measured shape).`,
|
|
553
591
|
childIsPlatform
|
|
554
592
|
? [`${parentParam}: string`, `name: string`, `platformAdminKey: string`]
|
|
555
593
|
: [`${parentParam}: string`, `name: string`],
|
|
556
|
-
childIsPlatform
|
|
557
|
-
? `\`/v1/platforms/\${encodeURIComponent(opts.${parentParam})}/platforms/\${encodeURIComponent(opts.name)}\``
|
|
558
|
-
: `\`/v1/platforms/\${encodeURIComponent(opts.${parentParam})}/workspaces/\${encodeURIComponent(opts.name)}\``,
|
|
594
|
+
{ kind: "offer", parentWs: `opts.${parentParam}`, directiveId: childIsPlatform ? "birthPlatform" : "createWorkspace" },
|
|
559
595
|
);
|
|
560
596
|
}
|
|
561
597
|
}
|
|
@@ -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
|
|
@@ -103,6 +103,7 @@ 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";
|
|
106
107
|
import { domainInterface, type DomainInterface } from "./shape_compat.js";
|
|
107
108
|
import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
|
|
108
109
|
|
|
@@ -162,13 +163,13 @@ interface PackageConfig {
|
|
|
162
163
|
* Typed DART frontend (the Flutter sibling of the generated TS client; #codegen_dart).
|
|
163
164
|
* `false`/absent = skip (default). `true` = emit into `<outDir>/dart/`;
|
|
164
165
|
* `{ out: "relative/dir" }` = emit there (resolved against the config dir).
|
|
165
|
-
* `{ support: "package" }` keeps `package:
|
|
166
|
-
* already depend on `
|
|
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
|
|
167
168
|
* self-contained directory.
|
|
168
169
|
* Per composed domain one `<name>.dart` (payload classes + read models + typed read
|
|
169
170
|
* accessors via the SAME `generateDartDomain` the co2 tenant emit uses), PLUS the
|
|
170
|
-
* framework `
|
|
171
|
-
* `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
|
|
172
173
|
* can vendor the directory as-is, no pub deps.
|
|
173
174
|
*/
|
|
174
175
|
readonly dart?: boolean | { readonly out?: string; readonly support?: DartSupportImport };
|
|
@@ -397,11 +398,11 @@ function resolveNamed<T>(merged: Mod, names: readonly string[] | undefined, what
|
|
|
397
398
|
* Vendored output rewrites it to the relative barrel so the directory is
|
|
398
399
|
* self-contained (no pub path-dependency on the framework package).
|
|
399
400
|
*/
|
|
400
|
-
const DART_PACKAGE_IMPORT = `import 'package:
|
|
401
|
-
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';`;
|
|
402
403
|
|
|
403
404
|
/**
|
|
404
|
-
* The framework `
|
|
405
|
+
* The framework `nomos_client` Dart support files the generated code resolves through
|
|
405
406
|
* the barrel (`dsl/dart/lib/`). `schema_validation.dart` is deliberately NOT
|
|
406
407
|
* vendored: nothing the codegen emits references it, and it would drag the external
|
|
407
408
|
* `json_schema` pub dependency into every vendoring app.
|
|
@@ -418,14 +419,14 @@ const DART_SUPPORT_SRC = [
|
|
|
418
419
|
] as const;
|
|
419
420
|
|
|
420
421
|
function removeVendoredDartSupport(dartOut: string): void {
|
|
421
|
-
rmSync(path.join(dartOut, "
|
|
422
|
+
rmSync(path.join(dartOut, "nomos_client.dart"), { force: true });
|
|
422
423
|
rmSync(path.join(dartOut, "src"), { recursive: true, force: true });
|
|
423
424
|
}
|
|
424
425
|
|
|
425
426
|
/**
|
|
426
427
|
* Emit the typed Dart for the composed domain modules into `dartOut`: one
|
|
427
428
|
* `<name>.dart` per domain (the SAME `generateDartDomain` path the co2 tenant's
|
|
428
|
-
* `emit_dart.ts` drives) + the vendored `
|
|
429
|
+
* `emit_dart.ts` drives) + the vendored `nomos_client.dart` barrel and its `src/`
|
|
429
430
|
* support files, with the generated files' `package:` import rewritten relative.
|
|
430
431
|
*/
|
|
431
432
|
function emitDart(
|
|
@@ -442,11 +443,11 @@ function emitDart(
|
|
|
442
443
|
if (supportImport === "vendored") {
|
|
443
444
|
mkdirSync(path.join(dartOut, "src"), { recursive: true });
|
|
444
445
|
const libDir = path.join(DSL_DIR, "dart", "lib");
|
|
445
|
-
const barrel = readFileSync(path.join(libDir, "
|
|
446
|
+
const barrel = readFileSync(path.join(libDir, "nomos_client.dart"), "utf8")
|
|
446
447
|
.split("\n")
|
|
447
448
|
.filter((line) => !line.includes("src/schema_validation.dart"))
|
|
448
449
|
.join("\n");
|
|
449
|
-
writeFileSync(path.join(dartOut, "
|
|
450
|
+
writeFileSync(path.join(dartOut, "nomos_client.dart"), barrel, "utf8");
|
|
450
451
|
for (const f of DART_SUPPORT_SRC) {
|
|
451
452
|
copyFileSync(path.join(libDir, "src", f), path.join(dartOut, "src", f));
|
|
452
453
|
}
|
|
@@ -1330,6 +1331,19 @@ async function main(): Promise<void> {
|
|
|
1330
1331
|
const identity = buildIdentity(domainModules);
|
|
1331
1332
|
const { manifestsPath } = writeIdentity(identity, outDir);
|
|
1332
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
|
+
|
|
1333
1347
|
// ── 4. the one-call deploy body (package only) ──
|
|
1334
1348
|
// SELF-DESCRIBING: `summary` states what the (otherwise hex-opaque) package
|
|
1335
1349
|
// contains — purely derived from the inputs (no timestamps), ignored by the
|
|
@@ -1358,6 +1372,9 @@ async function main(): Promise<void> {
|
|
|
1358
1372
|
// THE STRUCTURAL OFFER (#72): the per-domain interface, staged on the manifest overlay so
|
|
1359
1373
|
// `connect()` pulls the workspace's installed shapes and negotiates by structural subtyping.
|
|
1360
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,
|
|
1361
1378
|
};
|
|
1362
1379
|
const deployPath = path.join(outDir, `${cfg.name}.deploy.json`);
|
|
1363
1380
|
writeFileSync(deployPath, JSON.stringify(deployBody) + "\n", "utf8");
|
|
@@ -1395,7 +1412,7 @@ async function main(): Promise<void> {
|
|
|
1395
1412
|
` directives: ${d.directives.join(", ") || "—"}`,
|
|
1396
1413
|
` queries: ${d.queries.join(", ") || "—"} counts: ${d.counts.join(", ") || "—"} sums: ${d.sums.join(", ") || "—"}`,
|
|
1397
1414
|
]),
|
|
1398
|
-
`deploy: POST ${cfg.name}.deploy.json to /
|
|
1415
|
+
`deploy: POST ${cfg.name}.deploy.json to /v2/workspaces/<ws>/domains (or: githolon deploy <ws>)`,
|
|
1399
1416
|
`typed client: ${cfg.name}.client.ts — ${tsClientFactoryName(domainModules[0]?.domain ?? domainModules[0]?.name ?? cfg.name)}(holon), law hash baked in`,
|
|
1400
1417
|
proofPath !== undefined
|
|
1401
1418
|
? `generated proof: ${cfg.name}.proof.mts — run: githolon proof (live; ends ALL GREEN or names the jam)`
|
|
@@ -1469,6 +1486,7 @@ async function main(): Promise<void> {
|
|
|
1469
1486
|
}
|
|
1470
1487
|
}
|
|
1471
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)`);
|
|
1472
1490
|
if (proofPath !== undefined) {
|
|
1473
1491
|
console.log(` proof ${rel(proofPath)} (a runnable e2e GENERATED from your law — run: githolon proof)`);
|
|
1474
1492
|
} else {
|
|
@@ -1476,7 +1494,7 @@ async function main(): Promise<void> {
|
|
|
1476
1494
|
}
|
|
1477
1495
|
if (dartOut !== undefined) {
|
|
1478
1496
|
const supportText =
|
|
1479
|
-
dartSupportImport === "vendored" ? "vendored
|
|
1497
|
+
dartSupportImport === "vendored" ? "vendored nomos_client support" : "package nomos_client imports";
|
|
1480
1498
|
console.log(` dart ${rel(dartOut)}${path.sep} (${dartFiles.length} domain file(s) + ${supportText} — typed Dart for Flutter)`);
|
|
1481
1499
|
}
|
|
1482
1500
|
console.log(` deploy ${rel(deployPath)}`);
|
|
@@ -1484,7 +1502,7 @@ async function main(): Promise<void> {
|
|
|
1484
1502
|
console.log(`deploy (Nomos Cloud):`);
|
|
1485
1503
|
console.log(` githolon ws create <ws> && githolon deploy <ws> # the secret is saved + sent for you`);
|
|
1486
1504
|
console.log(` # raw lane: curl -X POST -H 'content-type: application/json' -H 'Authorization: Bearer <workspaceSecret>' \\`);
|
|
1487
|
-
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`);
|
|
1488
1506
|
if (proofPath !== undefined) {
|
|
1489
1507
|
console.log(`prove it OFFLINE first: npx githolon proof # the generated proof's offline legs on a LOCAL holon (no cloud workspace)`);
|
|
1490
1508
|
console.log(` inner loop: npx githolon dev # watch -> recompile -> law live locally -> offline proof, on every save`);
|
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/framework/repair.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* ── Why this module exists ──────────────────────────────────────────────────
|
|
12
12
|
* "Nomos is powerful enough to define its own repair surface as a Nomos domain"
|
|
13
|
-
* (Jack 2026-06-05,
|
|
13
|
+
* (Jack 2026-06-05, REPAIR.dot). The dogfood proof: repair intents go through
|
|
14
14
|
* THE ONE GATE — no bespoke crate, no AssumeVerified, no 2nd write path. This is Stage
|
|
15
15
|
* 2 (the `revert` directive); supersede / upcast / drain are later stages.
|
|
16
16
|
*
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
*
|
|
37
37
|
* ── Scope (Stage 2 only — do NOT add supersede / upcast / drain here) ──────
|
|
38
38
|
* supersede needs policy-control event shapes; upcast is an on-read migration;
|
|
39
|
-
* drain is a re-dispatch. All are later stages (
|
|
39
|
+
* drain is a re-dispatch. All are later stages (REPAIR.dot build order §2-5).
|
|
40
40
|
*/
|
|
41
41
|
import { z } from "zod";
|
|
42
42
|
import { aggregate, instance } from "../aggregate.js";
|
|
@@ -67,7 +67,7 @@ const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256
|
|
|
67
67
|
* bootstrap chicken-and-egg. The forge surface a bare `relateSubject` would open is closed by
|
|
68
68
|
* DOMAIN PLACEMENT: this directive is composed into the CONTROL-PLANE governance law (domain
|
|
69
69
|
* `workspaces`/`nomos`), which the open session lane structurally refuses
|
|
70
|
-
* (`
|
|
70
|
+
* (`adapter/engine.mjs` — only the birth door + the secret-gated `/author` + admin
|
|
71
71
|
* lanes author it), so no tenant can forge a signer for another principal.
|
|
72
72
|
*
|
|
73
73
|
* THE ORDER IS THE CONTRACT (verified, zero gate change): `enrollSigner` (E, unsigned/pre-warrant) →
|
|
@@ -73,8 +73,10 @@ 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";
|
|
78
80
|
// THE GENESIS BOOTSTRAP (platform-birth genesis bootstrap, Approach A): governance carries the
|
|
79
81
|
// warrant-flip primitives so the birth door can lawfully bootstrap a virgin platform's founding admin —
|
|
80
82
|
// E `enrollSigner` (the founding signer tuple) → W `seedGenesis` (the GenesisWarrant) → A signed
|
|
@@ -211,6 +213,10 @@ export const CloudWorkspace = aggregate("CloudWorkspace", {
|
|
|
211
213
|
createdAt: t.int().merge(Lww),
|
|
212
214
|
/** Epoch-ms the worker completed physical provisioning; absent until then. */
|
|
213
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(),
|
|
214
220
|
/**
|
|
215
221
|
* Per-workspace cumulative-ledger byte cap OVERRIDE (bytes); absent = the
|
|
216
222
|
* cloud-wide {@link CloudPolicy} default applies. Raised on the record via
|
|
@@ -381,6 +387,8 @@ export const createWorkspace = directive("createWorkspace")
|
|
|
381
387
|
.set("ownerGrantHash", "") // no owner credential until provisioned
|
|
382
388
|
.set("createdBy", p.requestedBy)
|
|
383
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.
|
|
384
392
|
return [];
|
|
385
393
|
})
|
|
386
394
|
.requires("creator"); // posture-gated creation: only a #creator holder may request a workspace
|
|
@@ -469,7 +477,8 @@ export const rotateWorkspaceOwner = directive("rotateWorkspaceOwner")
|
|
|
469
477
|
.plan((p) => {
|
|
470
478
|
const ws = instance(CloudWorkspace, p.workspaceId);
|
|
471
479
|
return [set(ws, "ownerGrantHash", p.ownerGrantHash)];
|
|
472
|
-
})
|
|
480
|
+
})
|
|
481
|
+
.requires("admin"); // lifecycle authority is law-state, not a host allowlist
|
|
473
482
|
|
|
474
483
|
/**
|
|
475
484
|
* setWorkspaceTier — move a workspace between `edge` and `heavy` ON THE RECORD;
|
|
@@ -489,7 +498,8 @@ export const setWorkspaceTier = directive("setWorkspaceTier")
|
|
|
489
498
|
.plan((p) => {
|
|
490
499
|
const ws = instance(CloudWorkspace, p.workspaceId);
|
|
491
500
|
return [set(ws, "tier", p.tier)];
|
|
492
|
-
})
|
|
501
|
+
})
|
|
502
|
+
.requires("admin");
|
|
493
503
|
|
|
494
504
|
/**
|
|
495
505
|
* retireWorkspace — the law withdraws the workspace. The worker thereafter
|
|
@@ -508,7 +518,8 @@ export const retireWorkspace = directive("retireWorkspace")
|
|
|
508
518
|
.plan((p) => {
|
|
509
519
|
const ws = instance(CloudWorkspace, p.workspaceId);
|
|
510
520
|
return [set(ws, "status", "retired" as const)];
|
|
511
|
-
})
|
|
521
|
+
})
|
|
522
|
+
.requires("admin");
|
|
512
523
|
|
|
513
524
|
/**
|
|
514
525
|
* reclaimWorkspace — THE LAWFUL CUSTODY DISCARD. Where {@link retireWorkspace}
|
|
@@ -538,7 +549,8 @@ export const reclaimWorkspace = directive("reclaimWorkspace")
|
|
|
538
549
|
.plan((p) => {
|
|
539
550
|
const ws = instance(CloudWorkspace, p.workspaceId);
|
|
540
551
|
return [set(ws, "status", "reclaimed" as const)];
|
|
541
|
-
})
|
|
552
|
+
})
|
|
553
|
+
.requires("admin");
|
|
542
554
|
|
|
543
555
|
/**
|
|
544
556
|
* birthPlatform — A PLATFORM IS BORN: the cloud, one level down. Records the
|
|
@@ -559,7 +571,10 @@ export const birthPlatform = directive("birthPlatform")
|
|
|
559
571
|
parent: z.string().default(""),
|
|
560
572
|
/** The pool: how many live children the platform may parent. */
|
|
561
573
|
pool: z.number().int().positive().default(50),
|
|
562
|
-
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,
|
|
563
578
|
requestedBy: z.string().min(1),
|
|
564
579
|
requestedAt: epochMs,
|
|
565
580
|
}),
|
|
@@ -574,12 +589,32 @@ export const birthPlatform = directive("birthPlatform")
|
|
|
574
589
|
.set("tier", p.tier)
|
|
575
590
|
.set("status", "requested" as const)
|
|
576
591
|
.set("ownerGrantHash", "")
|
|
592
|
+
.set("foundingAdminHash", p.foundingAdminHash) // the provider grants admin to this hash at materialize
|
|
577
593
|
.set("createdBy", p.requestedBy)
|
|
578
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).
|
|
579
599
|
return [];
|
|
580
600
|
})
|
|
581
601
|
.requires("platformCreator"); // invite-only: an active #platformCreator tuple admits the birth
|
|
582
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
|
+
|
|
583
618
|
/** setPlatformPool — the pool edition of a granted quota request. */
|
|
584
619
|
export const setPlatformPool = directive("setPlatformPool")
|
|
585
620
|
.mutates(CloudWorkspace)
|
|
@@ -736,7 +771,8 @@ export const claimHostname = directive("claimHostname")
|
|
|
736
771
|
.set("claimedAt", p.claimedAt)
|
|
737
772
|
.set("status", "active" as const);
|
|
738
773
|
return [];
|
|
739
|
-
})
|
|
774
|
+
})
|
|
775
|
+
.requires("admin"); // hostname authority is law-state
|
|
740
776
|
|
|
741
777
|
/** releaseHostname — free the name (the record stays — audit, never erasure). */
|
|
742
778
|
export const releaseHostname = directive("releaseHostname")
|
|
@@ -751,7 +787,8 @@ export const releaseHostname = directive("releaseHostname")
|
|
|
751
787
|
.plan((p) => {
|
|
752
788
|
const c = instance(HostnameClaim, p.claimId);
|
|
753
789
|
return [set(c, "status", "released" as const)];
|
|
754
|
-
})
|
|
790
|
+
})
|
|
791
|
+
.requires("admin");
|
|
755
792
|
|
|
756
793
|
/**
|
|
757
794
|
* bindCapability — record the FACT that a workspace holds a capability
|
|
@@ -789,7 +826,8 @@ export const bindCapability = directive("bindCapability")
|
|
|
789
826
|
if (p.scope !== undefined) b.set("scope", p.scope);
|
|
790
827
|
if (p.expiresAt !== undefined) b.set("expiresAt", p.expiresAt);
|
|
791
828
|
return [];
|
|
792
|
-
})
|
|
829
|
+
})
|
|
830
|
+
.requires("admin"); // capability-bind authority is law-state
|
|
793
831
|
|
|
794
832
|
/**
|
|
795
833
|
* revokeCapability — flip a CapabilityBinding to `revoked`. The credential
|
|
@@ -811,7 +849,8 @@ export const revokeCapability = directive("revokeCapability")
|
|
|
811
849
|
.plan((p) => {
|
|
812
850
|
const b = instance(CapabilityBinding, p.bindingId);
|
|
813
851
|
return [set(b, "status", "revoked" as const)];
|
|
814
|
-
})
|
|
852
|
+
})
|
|
853
|
+
.requires("admin");
|
|
815
854
|
|
|
816
855
|
/**
|
|
817
856
|
* setCloudPolicy — set (or amend) THE QUOTA LAW. An Ensure on the law-state
|
|
@@ -1032,11 +1071,20 @@ export const capabilityBindingByKeyHash = query("capabilityBindingByKeyHash")
|
|
|
1032
1071
|
// UserPermission): admin=`workspace:self#admin@key:<keyHash>`, creator=`…#creator@key:…`,
|
|
1033
1072
|
// platformCreator=`workspace:self#platformCreator@user:<principal>`. grantAdmin/grantCreation/
|
|
1034
1073
|
// grantPlatformCreation write them via `writeTuple`; the bespoke AdminGrant/CreationGrant aggregates +
|
|
1035
|
-
// their queries are GONE.
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
|
|
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";
|
|
1040
1088
|
export { RelationTuple };
|
|
1041
1089
|
|
|
1042
1090
|
// ── GOVERNANCE WRITE-AUTHZ IS NATIVE ZANZIBAR — the ONE gate, no bespoke predicates ───────────────────
|
|
@@ -1052,3 +1100,80 @@ export { RelationTuple };
|
|
|
1052
1100
|
// in the pre-apply state). SELF-RELATIVE: custody guarantees a workspace only folds its OWN tuples, so the
|
|
1053
1101
|
// SAME law governs root and every born platform. The host's `_adminOk`/`_creationLawOk`/`_platformCheck`
|
|
1054
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/manifest.ts
CHANGED
|
@@ -157,7 +157,7 @@ export interface CanonicalDirective {
|
|
|
157
157
|
*/
|
|
158
158
|
readonly capturedReads?: CanonicalCapturedRead[];
|
|
159
159
|
/**
|
|
160
|
-
* The directive's DECLARED required HOST CAPABILITY ports (`
|
|
160
|
+
* The directive's DECLARED required HOST CAPABILITY ports (`OFFER_KERNEL_ARCHITECTURE.dot` cluster_ports;
|
|
161
161
|
* the HOST/PORT axis, invariant 3): the capability ports the host must PROVIDE for the
|
|
162
162
|
* directive to run, checked FAIL-CLOSED (`required ⊆ provided`) at policy LOAD. SORTED (the
|
|
163
163
|
* SET is the contract; order is incidental). DISTINCT from {@link requiresCapability} (the
|
|
@@ -725,7 +725,7 @@ export const nomosRecordDeepVerify = directive("nomosRecordDeepVerify")
|
|
|
725
725
|
* SEAL A CHECKPOINT (§5.5, slice 4): bind the held per-shard frontiers + the
|
|
726
726
|
* subtotal state hash to the next sequence point. Host-authored on the cadence
|
|
727
727
|
* (the worker counts admitted deltas against the policy dial) or explicitly
|
|
728
|
-
* (`POST /
|
|
728
|
+
* (`POST /v2/workspaces/<coordinator>/checkpoint`) — but gate-verified either
|
|
729
729
|
* way: the `nomosCheckpointGate` invariant pins every claimed frontier to the
|
|
730
730
|
* HELD `NomosSummaryFrontier` watermark (`pre:` reads) and the sequence to
|
|
731
731
|
* held-head + 1. A seal can never claim a frontier the delta lane never
|
package/src/workspace_type.ts
CHANGED
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
* Everything else is DERIVED — Jack's ratified requirement is a FULLY TYPESAFE
|
|
34
34
|
* surface: a domain dev NEVER sees or manages a compound key. Homing is derived
|
|
35
35
|
* from the aggregates' existing `t.ref` chains (zero annotation); birth lanes are
|
|
36
|
-
* derived from `hasMany` over the EXISTING
|
|
37
|
-
* (`POST /
|
|
36
|
+
* derived from `hasMany` over the EXISTING birth machinery
|
|
37
|
+
* (`POST /v2/workspaces/...` birth offers — derived, never forked); minted-id home keys are
|
|
38
38
|
* internal plumbing (slice 2).
|
|
39
39
|
*
|
|
40
40
|
* THE HOMING WALK (this file): `home(aggregate)` = the NEAREST PACKED AXIS its
|
|
File without changes
|