@githolon/dsl 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart/lib/nomos_client.dart +5 -0
- package/dart/lib/src/errors.dart +79 -0
- package/dart/lib/src/home.dart +20 -0
- package/dart/lib/src/webview_bridge.dart +68 -0
- package/dart/lib/src/wire.dart +117 -0
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -1
- package/dart/test/home_parity_test.dart +9 -0
- package/dart/test/wire_test.dart +48 -0
- package/package.json +1 -1
- package/src/births.ts +82 -0
- package/src/build_package.ts +23 -27
- package/src/codegen_dart.ts +10 -0
- package/src/compile_package_main.ts +61 -109
- package/src/decl_shapes.ts +122 -0
- package/src/directive.ts +30 -0
- package/src/engine_entry.ts +109 -8
- package/src/framework/home.ts +179 -0
- package/src/framework/impure_capability.ts +8 -4
- package/src/framework/relations.ts +22 -0
- package/src/framework/signing_identity.ts +291 -2
- package/src/framework/workspace_invariant.ts +17 -2
- package/src/framework/workspaces.ts +193 -30
- package/src/index.ts +42 -0
- package/src/usd.ts +44 -0
- package/src/usd_layers.ts +41 -0
- package/src/wire.ts +13 -0
- package/src/wire_encode.ts +14 -2
|
@@ -26,11 +26,16 @@ library;
|
|
|
26
26
|
export 'src/wire.dart';
|
|
27
27
|
export 'src/driver.dart';
|
|
28
28
|
export 'src/offer.dart';
|
|
29
|
+
// homeNameOf(subject) — the canonical home-workspace name (parity with @githolon/client + the law side).
|
|
30
|
+
export 'src/home.dart';
|
|
29
31
|
export 'src/subscriptions.dart';
|
|
30
32
|
export 'src/schema_validation.dart';
|
|
31
33
|
// THE WEBVIEW BRIDGE (app-binding boundary) — the async Dart runtime that drives the REAL githolon running
|
|
32
34
|
// in a WebView, so the generated typed client never needs a `MemoryPersistence` fake.
|
|
33
35
|
export 'src/webview_bridge.dart';
|
|
36
|
+
// TYPED DIAGNOSTICS (co2 feedback req 7) — the fixed set of ways the home-birth ceremony can fail, each
|
|
37
|
+
// extending NomosBridgeError so existing handlers still catch them, named so the app can branch on `is`.
|
|
38
|
+
export 'src/errors.dart';
|
|
34
39
|
// The PROVIDER boundary (server-side capability-provider SDK runtime: TaskOutcome /
|
|
35
40
|
// TaskSuccess / TaskFailure / TaskResult / TaskProvider / HostClock). The symmetric twin
|
|
36
41
|
// of `offer.dart`; the generated per-task provider classes build on it.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
|
|
4
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/// TYPED DIAGNOSTICS for the home factory (co2 feedback req 7).
|
|
7
|
+
///
|
|
8
|
+
/// The generic [NomosBridgeError] (in `webview_bridge.dart`) is the catch-all for a refusal the githolon
|
|
9
|
+
/// surfaces verbatim. But the HOME-BIRTH CEREMONY ([NomosScope.home]) has a small, fixed set of ways it can
|
|
10
|
+
/// fail — and each one wants a DIFFERENT remedy from the app (re-auth vs. retry vs. "deploy the home law").
|
|
11
|
+
/// These typed errors name the exact failure so the app's `errorBuilder` can branch on `is`, and so a log
|
|
12
|
+
/// line says WHICH leg of the ceremony broke, not just "connect failed".
|
|
13
|
+
///
|
|
14
|
+
/// All extend [NomosBridgeError] so existing `catch (NomosBridgeError)` handlers still catch them.
|
|
15
|
+
library;
|
|
16
|
+
|
|
17
|
+
import 'webview_bridge.dart' show NomosBridgeError;
|
|
18
|
+
|
|
19
|
+
/// The auth-provider access token the cloud was given was missing, malformed, expired, or rejected — the
|
|
20
|
+
/// cloud answered 401 / "invalid token" on a lane that requires a verified principal (every birth does).
|
|
21
|
+
/// REMEDY: re-acquire a fresh token from the auth provider and reconnect.
|
|
22
|
+
class AuthTokenInvalid extends NomosBridgeError {
|
|
23
|
+
AuthTokenInvalid([String? message])
|
|
24
|
+
: super(message ?? 'the auth-provider token was missing or rejected (the cloud refused 401) — a birth needs a verified principal');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// The workspace name the cloud reported for the born home does NOT equal `homeNameOf(subject)`. The home's
|
|
28
|
+
/// own law commits to `home-<sha256(subject)>` (the squat invariant), so a mismatch means the cloud routed to
|
|
29
|
+
/// the wrong slot — a fail-closed safety stop, never silently mount the wrong holon.
|
|
30
|
+
class HomeNameMismatch extends NomosBridgeError {
|
|
31
|
+
/// What `homeNameOf(subject)` derived locally.
|
|
32
|
+
final String expected;
|
|
33
|
+
|
|
34
|
+
/// What the cloud reported (its status `lineage.id` / the name it served).
|
|
35
|
+
final String reported;
|
|
36
|
+
HomeNameMismatch({required this.expected, required this.reported, String? message})
|
|
37
|
+
: super(message ?? 'the cloud reported home "$reported" but this subject hashes to "$expected" — refusing to mount a home whose name does not bind its owner');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// The workspace exists and is born, but the HOME LAW is not installed/active on it — so it is a plain
|
|
41
|
+
/// workspace, not a home holon. Per req 2 there is NO generic fallback: a home that cannot be confirmed to
|
|
42
|
+
/// run the home law FAILS here rather than silently mounting a lawless workspace. REMEDY: the tenant's
|
|
43
|
+
/// governance must install the home law (the birth offer should author it; if it didn't, the deploy is wrong).
|
|
44
|
+
class HomeLawMissing extends NomosBridgeError {
|
|
45
|
+
/// The workspace that is missing the home law.
|
|
46
|
+
final String workspace;
|
|
47
|
+
HomeLawMissing({required this.workspace, String? message})
|
|
48
|
+
: super(message ?? 'workspace "$workspace" is born but the home law is not installed/active on it — refusing to mount a plain workspace as a home');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// The offer to the home's birth lane was REFUSED by the parent's gate (the law denied the birth — e.g. the
|
|
52
|
+
/// principal lacks creation authority, the pool is exhausted, or the genesis recipe was rejected). Carries
|
|
53
|
+
/// the cloud's verdict verbatim. REMEDY is tenant-specific (grant creation authority / raise the pool).
|
|
54
|
+
class BirthRejected extends NomosBridgeError {
|
|
55
|
+
/// The birth-lane directive id that was offered (e.g. the tenant's `birthHome` / `seedHomeOwner`).
|
|
56
|
+
final String? directiveId;
|
|
57
|
+
|
|
58
|
+
/// The HTTP status the cloud answered, when known.
|
|
59
|
+
final int? statusCode;
|
|
60
|
+
BirthRejected({String? message, this.directiveId, this.statusCode})
|
|
61
|
+
: super(message ?? 'the home birth offer was refused by the law${directiveId != null ? ' (directive "$directiveId")' : ''}${statusCode != null ? ' [$statusCode]' : ''}');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// A pack the cloud returned (or the local holon produced) was malformed — it could not be parsed/applied as
|
|
65
|
+
/// a git pack. This is a transport/custody integrity fault, not a law refusal. REMEDY: retry; if persistent,
|
|
66
|
+
/// the custody bytes are corrupt.
|
|
67
|
+
class PackMalformed extends NomosBridgeError {
|
|
68
|
+
PackMalformed([String? message]) : super(message ?? 'a returned pack was malformed and could not be applied (custody integrity fault)');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// The home law's content hash could not be resolved — the cloud reported no current law for the home's
|
|
72
|
+
/// domain key, so the app cannot confirm WHICH law the home runs (and thus cannot baked-hash-pin or judge
|
|
73
|
+
/// installed-ness). REMEDY: the home law must be deployed; if it was, retry once the deploy has folded.
|
|
74
|
+
class LawHashUnavailable extends NomosBridgeError {
|
|
75
|
+
/// The workspace whose current law hash could not be resolved.
|
|
76
|
+
final String workspace;
|
|
77
|
+
LawHashUnavailable({required this.workspace, String? message})
|
|
78
|
+
: super(message ?? 'could not resolve the current law hash for "$workspace" — the cloud reported no installed law for the home');
|
|
79
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import 'dart:convert';
|
|
2
|
+
|
|
3
|
+
import 'package:crypto/crypto.dart';
|
|
4
|
+
|
|
5
|
+
/// The canonical home-workspace name for an identity SUBJECT (e.g. `user:<firebaseUid>`).
|
|
6
|
+
///
|
|
7
|
+
/// BYTE-IDENTICAL to `@githolon/client`'s `homeNameOf` and the law side
|
|
8
|
+
/// (`dsl/src/framework/home.ts` `HOME_NAME_PREFIX`). A home holon is named by its SUBJECT — never the raw
|
|
9
|
+
/// uid and never an app-chosen workspace name — so the same principal always resolves the same home, on
|
|
10
|
+
/// every peer and on the cloud. Use it directly as the `workspace`:
|
|
11
|
+
///
|
|
12
|
+
/// ```dart
|
|
13
|
+
/// NomosScope(workspace: homeNameOf('user:$firebaseUid'), ...)
|
|
14
|
+
/// ```
|
|
15
|
+
///
|
|
16
|
+
/// Formula: `home-` + the first 40 hex chars of `sha256(utf8(subject))`.
|
|
17
|
+
String homeNameOf(String subject) {
|
|
18
|
+
final hex = sha256.convert(utf8.encode(subject)).toString(); // lowercase hex
|
|
19
|
+
return 'home-${hex.substring(0, 40)}';
|
|
20
|
+
}
|
|
@@ -156,12 +156,16 @@ class NomosBridge {
|
|
|
156
156
|
required String workspace,
|
|
157
157
|
String? clientId,
|
|
158
158
|
String? authToken,
|
|
159
|
+
String? authorSecret,
|
|
159
160
|
String? restoreFrom}) async {
|
|
160
161
|
await _call('connect', {
|
|
161
162
|
'cloud': cloud,
|
|
162
163
|
'workspace': workspace,
|
|
163
164
|
if (clientId != null) 'clientId': clientId,
|
|
164
165
|
if (authToken != null) 'authToken': authToken,
|
|
166
|
+
// The device signing secret (base64, from [authorKeygen]) — the warrant default for every offer.
|
|
167
|
+
// Stays client-side; the kernel mints the signed-author attestation so authorship is replay-provable.
|
|
168
|
+
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
165
169
|
if (restoreFrom != null) 'restoreFrom': restoreFrom
|
|
166
170
|
});
|
|
167
171
|
}
|
|
@@ -172,6 +176,70 @@ class NomosBridge {
|
|
|
172
176
|
return r['head'].toString();
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
/// Mint an ed25519 DEVICE KEY in-kernel → `{secret, public}` (base64). The SECRET stays on this device
|
|
180
|
+
/// (persist it in secure storage); pass it to [connect] as `authorSecret` (or per-offer `signWith`) so the
|
|
181
|
+
/// holon SIGNS each intent. The PUBLIC half is what gets enrolled so the gate re-verifies the signature on
|
|
182
|
+
/// replay — the durable, cryptographic answer to "who authored this?" (the warrant). Mint once per device.
|
|
183
|
+
Future<Map<String, Object?>> authorKeygen() async =>
|
|
184
|
+
_asMap(await _call('authorKeygen'));
|
|
185
|
+
|
|
186
|
+
/// Mint a DEVICE KEY for the VA root-of-trust (`{secret, public}`, base64) — an alias of [authorKeygen]
|
|
187
|
+
/// the birth-cert flow speaks. The SECRET stays on this device (secure storage); it signs birth certs
|
|
188
|
+
/// ([signBirthCert]) + writes ([connect]'s `authorSecret`) on the LOCAL kernel; the PUBLIC half is enrolled
|
|
189
|
+
/// on-chain ([enrollDevice]). Never put the secret on the chain.
|
|
190
|
+
Future<Map<String, Object?>> mintDeviceKey() async =>
|
|
191
|
+
_asMap(await _call('mintDeviceKey'));
|
|
192
|
+
|
|
193
|
+
/// VA ROOT-OF-TRUST: sign a BirthCert through the kernel `cert_sign` op (the SINGLE source of the cert
|
|
194
|
+
/// byte-layout — no Dart copy). `fields` = `{child, founder, verified, parentKey (this device's base64
|
|
195
|
+
/// ed25519 PUBLIC half), parentId, epoch}`; `deviceSecret` is that key's base64 SECRET half, handed to the
|
|
196
|
+
/// LOCAL kernel only (it never reaches a server). Returns the snake_case cert JSON `verify_to_root` parses.
|
|
197
|
+
Future<Map<String, Object?>> signBirthCert(
|
|
198
|
+
Map<String, Object?> fields, String deviceSecret) async =>
|
|
199
|
+
_asMap(await _call(
|
|
200
|
+
'signBirthCert', {'fields': fields, 'deviceSecret': deviceSecret}))['cert'] as Map<String, Object?>? ??
|
|
201
|
+
const {};
|
|
202
|
+
|
|
203
|
+
/// ENROLL THIS DEVICE — the LIVE gate: bind a device key's PUBLIC half to `principal` on-chain as a
|
|
204
|
+
/// `signer` so the holon's gate can PROVE this device's authorship on replay (and the VA chain attest it).
|
|
205
|
+
/// `publicKey` is the base64 PUBLIC half (from [mintDeviceKey]); `authToken` (defaults to the connect-time
|
|
206
|
+
/// bearer) is the VERIFIED identity that authorizes the enrollment. Returns the offer verdict.
|
|
207
|
+
Future<Map<String, Object?>> enrollDevice(
|
|
208
|
+
{required String principal,
|
|
209
|
+
required String publicKey,
|
|
210
|
+
String? authToken}) async =>
|
|
211
|
+
_asMap(await _call('enrollDevice', {
|
|
212
|
+
'principal': principal,
|
|
213
|
+
'publicKey': publicKey,
|
|
214
|
+
if (authToken != null) 'authToken': authToken,
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
/// VERIFY_CHAIN — the kernel's from-genesis self-verification of THIS holon's folded ledger (read-only:
|
|
218
|
+
/// contiguity → every intent re-admitted through the ONE gate against the state folded from the prefix). For
|
|
219
|
+
/// a WARRANTED home this RE-FIRES the parent-attested-birth cert door on the cold-replay lane — so a
|
|
220
|
+
/// freshly-connected peer that CLONES a home and runs this IS the offline peer-verify (replay IS validation;
|
|
221
|
+
/// no new op). Returns the kernel verdict `{valid, error?, plansRerun?}`.
|
|
222
|
+
Future<Map<String, Object?>> verifyChain() async =>
|
|
223
|
+
_asMap(await _call('verifyChain'));
|
|
224
|
+
|
|
225
|
+
/// BIRTH A HOME — THE CEREMONY (VA root-of-trust, drift-proof), run end-to-end inside the connected holon:
|
|
226
|
+
/// derive the deterministic home name, mint + enroll the device key, DEVICE-SIGN the home cert through the
|
|
227
|
+
/// kernel `cert_sign` op (the single source of the byte layout), OFFER the PARAMETERIZED parent's `birthHome`
|
|
228
|
+
/// directive carrying the homeCert, poll the parent for the born home, and CONFIRM the born home's own
|
|
229
|
+
/// `verify_chain` is GREEN (the offline peer-verify — replay IS validation). `spec` MUST carry `parent` (the
|
|
230
|
+
/// tenant's platform whose governance includes `birthHome`), `principal`, `frameworkHash`, and `lawHash`;
|
|
231
|
+
/// `subject`/`authToken`/`directiveId` are optional. OURS is the ceremony; the parent is the TENANT's.
|
|
232
|
+
/// Returns `{home, device:{secret,public}, homeCert, born, verify:{valid,…}}`.
|
|
233
|
+
Future<Map<String, Object?>> birthHome(Map<String, Object?> spec) async =>
|
|
234
|
+
_asMap(await _call('birthHome', spec));
|
|
235
|
+
|
|
236
|
+
/// Sign a cross-custody SHARE attestation `{secret, asserterWorkspace, object, relation, subject,
|
|
237
|
+
/// attestedAt}` → `{publicKey, signature}`. This is what drives `recordShare` into an invitee's home
|
|
238
|
+
/// (the free global invitation system): the granting workspace signs that it shares `object#relation`
|
|
239
|
+
/// with `subject`, and the invitee's home gate verifies the signature before recording it.
|
|
240
|
+
Future<Map<String, Object?>> attestationSign(Map<String, Object?> args) async =>
|
|
241
|
+
_asMap(await _call('attestationSign', {'args': args}));
|
|
242
|
+
|
|
175
243
|
/// A declared query's rows from the local projection.
|
|
176
244
|
Future<List<Map<String, dynamic>>> query(String queryId,
|
|
177
245
|
[Map<String, Object?> params = const {}]) async =>
|
package/dart/lib/src/wire.dart
CHANGED
|
@@ -92,6 +92,24 @@ sealed class Value {
|
|
|
92
92
|
(k, v) => MapEntry(k, WireField.fromJson(v as Map<String, dynamic>)),
|
|
93
93
|
));
|
|
94
94
|
}
|
|
95
|
+
// `Value::Evidence(EvidenceRef)` — the content-addressed BACKING of a claim. The hash is the
|
|
96
|
+
// git blob oid bound at author time; the gate re-derives + backs it (kernel/src/evidence.rs,
|
|
97
|
+
// evidence.md §1/§5). Capture-time-irreversible provenance — decode it so it round-trips
|
|
98
|
+
// (a claim's evidence must survive every peer, not be dropped to an "unknown variant").
|
|
99
|
+
if (json.containsKey('Evidence')) {
|
|
100
|
+
final r = json['Evidence'] as Map<String, dynamic>;
|
|
101
|
+
return ValueEvidence(
|
|
102
|
+
hash: r['hash'] as String,
|
|
103
|
+
mediaType: r['media_type'] as String,
|
|
104
|
+
byteLength: (r['byte_length'] as num).toInt(),
|
|
105
|
+
storageClass: r['storage_class'] as String, // "Git" | "External"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
// `Value::Json(CanonicalJson)` — first-class JSON committed INLINE (RFC 8785/JCS), replacing
|
|
109
|
+
// the old t.json-as-Value::Str. The held value is the decoded JSON tree.
|
|
110
|
+
if (json.containsKey('Json')) {
|
|
111
|
+
return ValueJson(json['Json']);
|
|
112
|
+
}
|
|
95
113
|
throw FormatException('Unknown Value variant: ${json.keys}');
|
|
96
114
|
}
|
|
97
115
|
|
|
@@ -165,6 +183,105 @@ class ValueMap extends Value {
|
|
|
165
183
|
String toString() => 'ValueMap($value)';
|
|
166
184
|
}
|
|
167
185
|
|
|
186
|
+
/// `Value::Evidence(EvidenceRef)` ->
|
|
187
|
+
/// `{"Evidence":{"hash":"<git-oid>","media_type":"application/pdf","byte_length":1024,"storage_class":"Git"}}`.
|
|
188
|
+
///
|
|
189
|
+
/// A content-addressed, framework-owned reference to the bytes that BACK a claim (`evidence.md` §1;
|
|
190
|
+
/// `kernel/src/evidence.rs`). The `hash` is the git blob oid of the bytes — bound AT AUTHOR TIME and
|
|
191
|
+
/// re-derived by the gate, so a claim's evidence binding is provable forever. The bytes live in the
|
|
192
|
+
/// Evidence Store, never the ledger op. This variant is capture-time-irreversible provenance: it must
|
|
193
|
+
/// round-trip on every peer, so the client decodes it (never drops it to "unknown variant").
|
|
194
|
+
class ValueEvidence extends Value {
|
|
195
|
+
final String hash;
|
|
196
|
+
final String mediaType;
|
|
197
|
+
final int byteLength;
|
|
198
|
+
final String storageClass; // "Git" | "External"
|
|
199
|
+
const ValueEvidence({
|
|
200
|
+
required this.hash,
|
|
201
|
+
required this.mediaType,
|
|
202
|
+
required this.byteLength,
|
|
203
|
+
required this.storageClass,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
@override
|
|
207
|
+
Map<String, dynamic> toJson() => {
|
|
208
|
+
'Evidence': {
|
|
209
|
+
'hash': hash,
|
|
210
|
+
'media_type': mediaType,
|
|
211
|
+
'byte_length': byteLength,
|
|
212
|
+
'storage_class': storageClass,
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
@override
|
|
217
|
+
bool operator ==(Object other) =>
|
|
218
|
+
other is ValueEvidence &&
|
|
219
|
+
other.hash == hash &&
|
|
220
|
+
other.mediaType == mediaType &&
|
|
221
|
+
other.byteLength == byteLength &&
|
|
222
|
+
other.storageClass == storageClass;
|
|
223
|
+
@override
|
|
224
|
+
int get hashCode => Object.hash('Evidence', hash, mediaType, byteLength, storageClass);
|
|
225
|
+
@override
|
|
226
|
+
String toString() =>
|
|
227
|
+
'ValueEvidence($hash, $mediaType, $byteLength, $storageClass)';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// `Value::Json(CanonicalJson)` -> `{"Json": <any canonical-JSON value>}`.
|
|
231
|
+
///
|
|
232
|
+
/// First-class JSON committed INLINE (the kernel canonicalizes to RFC 8785 / JCS), replacing the old
|
|
233
|
+
/// `t.json`-as-`Value::Str`. The held `value` is the decoded JSON tree (Map / List / num / String /
|
|
234
|
+
/// bool / null). Equality is structural over the decoded tree.
|
|
235
|
+
class ValueJson extends Value {
|
|
236
|
+
final dynamic value;
|
|
237
|
+
const ValueJson(this.value);
|
|
238
|
+
|
|
239
|
+
@override
|
|
240
|
+
Map<String, dynamic> toJson() => {'Json': value};
|
|
241
|
+
|
|
242
|
+
@override
|
|
243
|
+
bool operator ==(Object other) => other is ValueJson && _jsonEq(other.value, value);
|
|
244
|
+
@override
|
|
245
|
+
int get hashCode => Object.hash('Json', _jsonHash(value));
|
|
246
|
+
@override
|
|
247
|
+
String toString() => 'ValueJson($value)';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// Structural deep-equality over a decoded JSON tree (Map / List / scalar) — no `dart:convert` dep.
|
|
251
|
+
bool _jsonEq(dynamic a, dynamic b) {
|
|
252
|
+
if (identical(a, b)) return true;
|
|
253
|
+
if (a is Map && b is Map) {
|
|
254
|
+
if (a.length != b.length) return false;
|
|
255
|
+
for (final e in a.entries) {
|
|
256
|
+
if (!b.containsKey(e.key) || !_jsonEq(e.value, b[e.key])) return false;
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
if (a is List && b is List) {
|
|
261
|
+
if (a.length != b.length) return false;
|
|
262
|
+
for (var i = 0; i < a.length; i++) {
|
|
263
|
+
if (!_jsonEq(a[i], b[i])) return false;
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
return a == b;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Order-independent hash over a decoded JSON tree (consistent with [_jsonEq]).
|
|
271
|
+
int _jsonHash(dynamic v) {
|
|
272
|
+
if (v is Map) {
|
|
273
|
+
var h = 0;
|
|
274
|
+
for (final e in v.entries) {
|
|
275
|
+
h ^= Object.hash(e.key, _jsonHash(e.value));
|
|
276
|
+
}
|
|
277
|
+
return Object.hash('jmap', h);
|
|
278
|
+
}
|
|
279
|
+
if (v is List) {
|
|
280
|
+
return Object.hash('jlist', Object.hashAll(v.map(_jsonHash)));
|
|
281
|
+
}
|
|
282
|
+
return v.hashCode;
|
|
283
|
+
}
|
|
284
|
+
|
|
168
285
|
/// `Field { value: Value, stamp: Hlc }` — only used when reading state back.
|
|
169
286
|
///
|
|
170
287
|
/// Named `WireField` to avoid clashing with the DSL field-builder concept and
|
package/dart/pubspec.lock
CHANGED
package/dart/pubspec.yaml
CHANGED
|
@@ -11,13 +11,14 @@ description: >-
|
|
|
11
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
12
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
13
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
14
|
-
version: 0.
|
|
14
|
+
version: 0.18.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
18
18
|
|
|
19
19
|
dependencies:
|
|
20
20
|
json_schema: ^5.2.2
|
|
21
|
+
crypto: ^3.0.0
|
|
21
22
|
|
|
22
23
|
dev_dependencies:
|
|
23
24
|
test: ^1.25.0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import 'package:nomos_client/nomos_client.dart';
|
|
2
|
+
import 'package:test/test.dart';
|
|
3
|
+
void main() {
|
|
4
|
+
test('homeNameOf matches the JS/law formula', () {
|
|
5
|
+
expect(homeNameOf('user:abc-123'), 'home-5094641143043f9281b6b9146cc345a254a5d3af');
|
|
6
|
+
expect(homeNameOf('user:abc-123').startsWith('home-'), isTrue);
|
|
7
|
+
expect(homeNameOf('user:abc-123').length, 45); // 'home-' + 40 hex
|
|
8
|
+
});
|
|
9
|
+
}
|
package/dart/test/wire_test.dart
CHANGED
|
@@ -46,9 +46,57 @@ void main() {
|
|
|
46
46
|
(m) => Value.fromJson(m),
|
|
47
47
|
);
|
|
48
48
|
|
|
49
|
+
// Value::Evidence — the content-addressed claim BACKING (capture-time-irreversible provenance).
|
|
50
|
+
// The raw serde shape is snake_case (the Rust EvidenceRef field names) + storage_class as a bare
|
|
51
|
+
// unit-variant string ("Git"/"External"). It MUST round-trip, never drop to "unknown variant".
|
|
52
|
+
roundTrips(
|
|
53
|
+
'Value::Evidence',
|
|
54
|
+
'{"Evidence":{"hash":"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391","media_type":"application/pdf","byte_length":1024,"storage_class":"Git"}}',
|
|
55
|
+
(m) => Value.fromJson(m),
|
|
56
|
+
);
|
|
57
|
+
// Value::Json — first-class inline JSON (RFC 8785/JCS), nested object value.
|
|
58
|
+
roundTrips(
|
|
59
|
+
'Value::Json',
|
|
60
|
+
'{"Json":{"a":1,"b":["x","y"],"c":{"d":true}}}',
|
|
61
|
+
(m) => Value.fromJson(m),
|
|
62
|
+
);
|
|
63
|
+
|
|
49
64
|
test('Value::Int -> ValueInt', () {
|
|
50
65
|
expect(Value.fromJson({'Int': 10}), const ValueInt(10));
|
|
51
66
|
});
|
|
67
|
+
test('Value::Evidence decodes the snake_case ref + bare storage class', () {
|
|
68
|
+
final v = Value.fromJson({
|
|
69
|
+
'Evidence': {
|
|
70
|
+
'hash': 'abc123',
|
|
71
|
+
'media_type': 'image/png',
|
|
72
|
+
'byte_length': 42,
|
|
73
|
+
'storage_class': 'External',
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
expect(
|
|
77
|
+
v,
|
|
78
|
+
const ValueEvidence(
|
|
79
|
+
hash: 'abc123',
|
|
80
|
+
mediaType: 'image/png',
|
|
81
|
+
byteLength: 42,
|
|
82
|
+
storageClass: 'External',
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
test('Value::Json structural equality over the decoded tree', () {
|
|
87
|
+
expect(
|
|
88
|
+
Value.fromJson({
|
|
89
|
+
'Json': {
|
|
90
|
+
'a': 1,
|
|
91
|
+
'b': [true, null]
|
|
92
|
+
}
|
|
93
|
+
}),
|
|
94
|
+
const ValueJson({
|
|
95
|
+
'a': 1,
|
|
96
|
+
'b': [true, null]
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
52
100
|
});
|
|
53
101
|
|
|
54
102
|
group('Op variants', () {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
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/births.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* THE BIRTH EFFECT — birth is an EXPLICIT, DECLARED kernel primitive (G3, the unix model).
|
|
8
|
+
*
|
|
9
|
+
* "A syscall does a defined thing and finishes; no implicit side-effects inferred from scanning state."
|
|
10
|
+
* A `.births()` directive's `plan` PRODUCES the child's genesis recipe (reuse `buildGenesisChain`) and
|
|
11
|
+
* records it onto the intent's BIRTH EFFECT via `birth(recipe)`. The recipe rides the committed intent as
|
|
12
|
+
* a DECLARED OUTPUT — exactly like a captured read rides `captured_ports.reads` — NEVER as a field set on a
|
|
13
|
+
* domain aggregate the kernel would have to scan (a circular dependency the kernel must never have).
|
|
14
|
+
*
|
|
15
|
+
* The kernel reads the `.births` DECLARATION from the law manifest (the `nomosBirthGate` era key, the SAME
|
|
16
|
+
* mechanism it reads `.reads`/`.creates`/invariants by — NOT domain state) to KNOW an admitted directive
|
|
17
|
+
* births; it then takes the recipe off this carried effect and spawns + self-validates the child through its
|
|
18
|
+
* OWN gate (the `materialize_genesis` keystone). The dev never sees this sink — they call `birth(recipe)`;
|
|
19
|
+
* the library routes it here and `executeDirectiveToIntent` drains it onto the intent.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** ONE birth recipe the plan produced — the child's genesis chain (the `buildGenesisChain` JSON array of
|
|
23
|
+
* `{domain, directiveId, payload, actor[, domainPackageB64, domainHash]}` steps). Carried verbatim onto the
|
|
24
|
+
* intent's birth effect; the kernel folds it through the child's own gate (each step self-verifies). */
|
|
25
|
+
export interface BirthRecipe {
|
|
26
|
+
/** The child workspace name the parent asked to birth (lineage + the child's `:ws` segment). */
|
|
27
|
+
readonly workspace: string;
|
|
28
|
+
/** The genesis chain: the ordered steps the child runs through its OWN gate to validate itself from intent 0. */
|
|
29
|
+
readonly genesisChain: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── the per-dispatch birth footprint — library state drained by `executeDirectiveToIntent` (mirrors the
|
|
33
|
+
// captured-read footprint in read.ts; internal to the directive runtime, never a sandbox side channel). ──
|
|
34
|
+
let footprint: BirthRecipe[] = [];
|
|
35
|
+
/** Clear the footprint before a plan runs (called by `executeDirectiveToIntent`). */
|
|
36
|
+
export function __resetBirths(): void {
|
|
37
|
+
footprint = [];
|
|
38
|
+
}
|
|
39
|
+
/** Take + clear the births recorded during a plan (called by `executeDirectiveToIntent`). */
|
|
40
|
+
export function __drainBirths(): BirthRecipe[] {
|
|
41
|
+
const f = footprint;
|
|
42
|
+
footprint = [];
|
|
43
|
+
return f;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Record a BIRTH the plan declares: the child's genesis recipe rides the committed intent as a DECLARED
|
|
48
|
+
* effect (the kernel takes it off the intent after admit and spawns + self-validates the child). The
|
|
49
|
+
* directive MUST declare `.births()` (which lands the `nomosBirthGate` era key in the law's USD-IR) — the
|
|
50
|
+
* kernel executes the birth ONLY for a directive its resolved law names there (the era rule: pre-feature
|
|
51
|
+
* chains carry no key, so they replay byte-green and the kernel does nothing). A plan may birth at most one
|
|
52
|
+
* child per intent (one offer, one defined consequence — the unix model).
|
|
53
|
+
*/
|
|
54
|
+
export function birth(recipe: BirthRecipe): void {
|
|
55
|
+
footprint.push(recipe);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* THE PEER-SOVEREIGN ROOT-OF-TRUST SENTINEL (the BirthAttestation cert chain). A home/child law EXPORTS
|
|
60
|
+
* this marker to declare it REQUIRES a parent-attested-birth: a warranted workspace running this law must
|
|
61
|
+
* travel with a SIGNED cert chain proving its founder was VERIFIED by its parent at birth, rooting at the
|
|
62
|
+
* pinned `K_root`. The composer discovers this by shape (`__isParentAttestedBirth`) — like an aggregate
|
|
63
|
+
* handle — and lowers it to the `nomosParentAttestedBirth` USD era key (emitted iff present). The kernel's
|
|
64
|
+
* verify doors then verify the travelling cert chain OFFLINE (no cross-ws read). ERA-RULED: a law that
|
|
65
|
+
* never exports it carries no key and replays byte-identical (pre-feature + legacy/non-warranted homes).
|
|
66
|
+
*
|
|
67
|
+
* export const parentAttested = requireParentAttestedBirth; // in the home law module
|
|
68
|
+
*/
|
|
69
|
+
export interface ParentAttestedBirthMarker {
|
|
70
|
+
readonly __isParentAttestedBirth: true;
|
|
71
|
+
}
|
|
72
|
+
export const requireParentAttestedBirth: ParentAttestedBirthMarker = { __isParentAttestedBirth: true };
|
|
73
|
+
|
|
74
|
+
/** Shape-discover the {@link requireParentAttestedBirth} sentinel in a module's exports (the composer). */
|
|
75
|
+
export function moduleRequiresParentAttestedBirth(mod: Record<string, unknown>): boolean {
|
|
76
|
+
for (const v of Object.values(mod)) {
|
|
77
|
+
if (v && typeof v === "object" && (v as ParentAttestedBirthMarker).__isParentAttestedBirth === true) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
package/src/build_package.ts
CHANGED
|
@@ -19,14 +19,15 @@
|
|
|
19
19
|
* 15-positional-parameter `module(...)` helper (dedupe by wire id, the
|
|
20
20
|
* impure-capability directive check, default read-models, and the
|
|
21
21
|
* HASH-LOAD-BEARING omit-when-empty key discipline).
|
|
22
|
-
* * `buildReadManifest(modules)` — the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* * `buildIdentity(modules)`
|
|
27
|
-
*
|
|
28
|
-
* `
|
|
29
|
-
*
|
|
22
|
+
* * `buildReadManifest(modules)` — the read-projection descriptor (aggregateFieldKinds +
|
|
23
|
+
* query/count/spatial/derived/combined routing). Field KINDS, not drivers. NO LONGER WRITTEN to
|
|
24
|
+
* `build/` (the old `domain_manifests.json` sidecar was a local diagnostic the contract ignored); the
|
|
25
|
+
* read routing lives in the package OpenAPI now. Kept as a function for callers that want the value.
|
|
26
|
+
* * `buildIdentity(modules)` — the per-domain IDENTITY emit + admits_domain_hash registry (#193/#136),
|
|
27
|
+
* hoisted from `emit_identity.ts` (palette-gap domains recorded-and-excluded). The admission identity
|
|
28
|
+
* is carried in the package USD IR + the committed `nomos.stable-ids.json` lockfile; the old
|
|
29
|
+
* `domain_identity_*.json` sidecars (and `writeIdentity`) are REMOVED. The per-domain hash is surfaced
|
|
30
|
+
* in the compile summary.
|
|
30
31
|
* * `packageUsda(usdJson, javascript)` — the `.package.usda` envelope, byte-exact
|
|
31
32
|
* to `build_nomos.mjs` `packageUsda()` == Rust `domain_package_from_js`
|
|
32
33
|
* (deterministic-rquickjs/src/domain_package.rs).
|
|
@@ -57,6 +58,7 @@ import { domainHash, emitManifestBytes } from "./manifest.js";
|
|
|
57
58
|
import { emitUsd } from "./usd.js";
|
|
58
59
|
import { expandCapabilityExports } from "./capability_exports.js";
|
|
59
60
|
import { RelationTuple } from "./framework/relations.js";
|
|
61
|
+
import { moduleRequiresParentAttestedBirth } from "./births.js";
|
|
60
62
|
|
|
61
63
|
// ─────────────────────────────────────────────────────────────────────────────────
|
|
62
64
|
// composeDomainModule — the named-keys `module(...)`
|
|
@@ -181,6 +183,12 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
181
183
|
const workspaceInvariants = [...(spec.workspaceInvariants ?? [])];
|
|
182
184
|
const relationTypes = [...(spec.relationTypes ?? [])];
|
|
183
185
|
const dartImports = [...(spec.dartImports ?? [])];
|
|
186
|
+
// THE PEER-SOVEREIGN ROOT-OF-TRUST: a home/child law that exports `requireParentAttestedBirth` declares
|
|
187
|
+
// it requires a parent-attested-birth cert chain. Discovered by shape across the module list (the SAME
|
|
188
|
+
// duck-typing aggregates/directives use). Omit-when-false (era rule): a law that never declares it is
|
|
189
|
+
// byte-identical in the canonical manifest/USD to before the key existed.
|
|
190
|
+
const parentAttestedBirth =
|
|
191
|
+
moduleRequiresParentAttestedBirth(merged) || spec.modules.some((m) => moduleRequiresParentAttestedBirth(m));
|
|
184
192
|
|
|
185
193
|
return {
|
|
186
194
|
name: spec.name,
|
|
@@ -207,6 +215,9 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
207
215
|
// Omit-when-empty — the relation schema is hashed law (P1); a relation-free domain's
|
|
208
216
|
// canonical manifest/USD stays byte-identical (the nomosRelations era rule).
|
|
209
217
|
...(relationTypes.length > 0 ? { relationTypes } : {}),
|
|
218
|
+
// THE PEER-SOVEREIGN ROOT-OF-TRUST: omit-when-false — a law that doesn't require parent-attested
|
|
219
|
+
// birth is byte-identical in the canonical manifest/USD (the nomosParentAttestedBirth era rule).
|
|
220
|
+
...(parentAttestedBirth ? { parentAttestedBirth: true } : {}),
|
|
210
221
|
...(dartImports.length > 0 ? { dartImports } : {}),
|
|
211
222
|
...(spec.dartRefImports !== undefined && spec.dartRefImports.length > 0
|
|
212
223
|
? { dartRefImports: [...spec.dartRefImports] }
|
|
@@ -702,25 +713,10 @@ export function buildIdentity(modules: readonly DomainModule[]): IdentityEmit {
|
|
|
702
713
|
return { manifests, hashes, excluded };
|
|
703
714
|
}
|
|
704
715
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
* to a SEPARATE sidecar the gate never reads.
|
|
710
|
-
*/
|
|
711
|
-
export function writeIdentity(
|
|
712
|
-
emit: IdentityEmit,
|
|
713
|
-
outDir: string,
|
|
714
|
-
): { manifestsPath: string; hashesPath: string; excludedPath: string } {
|
|
715
|
-
mkdirSync(outDir, { recursive: true });
|
|
716
|
-
const manifestsPath = join(outDir, "domain_identity_manifests.json");
|
|
717
|
-
const hashesPath = join(outDir, "domain_identity_hashes.json");
|
|
718
|
-
const excludedPath = join(outDir, "domain_identity_excluded.json");
|
|
719
|
-
writeFileSync(manifestsPath, JSON.stringify(emit.manifests, null, 2) + "\n", "utf8");
|
|
720
|
-
writeFileSync(hashesPath, JSON.stringify(emit.hashes, null, 2) + "\n", "utf8");
|
|
721
|
-
writeFileSync(excludedPath, JSON.stringify(emit.excluded, null, 2) + "\n", "utf8");
|
|
722
|
-
return { manifestsPath, hashesPath, excludedPath };
|
|
723
|
-
}
|
|
716
|
+
// writeIdentity (the domain_identity_*.json sidecars) — REMOVED. The admission identity is carried in the
|
|
717
|
+
// package USD IR + the committed nomos.stable-ids.json lockfile; the deploy/runtime contract never read the
|
|
718
|
+
// sidecars (they were local diagnostics that only confused /build consumers). `buildIdentity` stays — its
|
|
719
|
+
// per-domain hash is surfaced in the compile summary.
|
|
724
720
|
|
|
725
721
|
// ─────────────────────────────────────────────────────────────────────────────────
|
|
726
722
|
// The .package.usda envelope + the USD-IR emit
|
package/src/codegen_dart.ts
CHANGED
|
@@ -2454,6 +2454,16 @@ export interface DomainModule {
|
|
|
2454
2454
|
* aggregate `hasInvariant` / a directive `plan`). See `docs/workspace_invariant.md`.
|
|
2455
2455
|
*/
|
|
2456
2456
|
workspaceInvariants?: WorkspaceInvariantDecl[];
|
|
2457
|
+
/**
|
|
2458
|
+
* THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain). When TRUE, this law declares
|
|
2459
|
+
* it REQUIRES a parent-attested-birth: a warranted workspace running this law must travel with a
|
|
2460
|
+
* signed cert chain proving its founder was VERIFIED by its parent at birth, rooting at the pinned
|
|
2461
|
+
* `K_root`. Lowered to the `nomosParentAttestedBirth` USD era key (emitted iff true) — the kernel's
|
|
2462
|
+
* verify doors (author / receive / verify_chain) then verify the travelling cert chain OFFLINE (no
|
|
2463
|
+
* cross-ws read). ADDITIVE + OPTIONAL: a law that declares none omits the key and is byte-identical
|
|
2464
|
+
* to before it existed (the era rule); pre-feature + legacy/non-warranted workspaces replay green.
|
|
2465
|
+
*/
|
|
2466
|
+
parentAttestedBirth?: boolean;
|
|
2457
2467
|
/**
|
|
2458
2468
|
* The domain's NATIVE ZANZIBAR RELATION SCHEMA (`relation_schema.ts` — the userset-rewrite law,
|
|
2459
2469
|
* P1). Pure-data per-type relation rewrites lowered to the `nomosRelations` USD era key (NO JS on
|