@githolon/dsl 0.55.1 → 0.57.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_headless.dart +16 -1
- package/dart/lib/src/test_harness.dart +184 -6
- package/dart/lib/src/webview_bridge.dart +255 -29
- package/dart/pubspec.yaml +1 -1
- package/dart/test/cluster_attested_test.dart +185 -0
- package/dart/test/co2_acceptance_test.dart +0 -1
- package/dart/test/vo_read_typing_test.dart +0 -1
- package/dart/test/webview_bridge_test.dart +13 -0
- package/package.json +1 -1
- package/src/attested.ts +9 -0
- package/src/attested_core.ts +199 -2
- package/src/build_package.ts +17 -1
- package/src/codegen_dart.ts +7 -0
- package/src/codegen_proof.ts +31 -1
- package/src/directive.ts +55 -11
- package/src/index.ts +6 -0
- package/src/law_check.ts +107 -0
- package/src/read.ts +5 -2
- package/src/shape_compat.ts +10 -6
- package/src/usd.ts +145 -26
- package/src/usd_layers.ts +16 -4
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/// The HEADLESS transport library — `dart:io` only, so it lives outside the main
|
|
2
2
|
/// `package:nomos_client/nomos_client.dart` barrel (which stays web-safe).
|
|
3
3
|
///
|
|
4
|
+
/// MULTI-WORKSPACE BY DESIGN. A [NomosBridge] is a device's LOCAL HOLARCHY PLANE: one bridge / one
|
|
5
|
+
/// transport, and MANY live workspaces on it (the home, each estate, each catalogue, locally-born children).
|
|
6
|
+
/// Each workspace is a LOCAL githolon — cloud sync is an optional per-workspace posture, not a prerequisite.
|
|
7
|
+
/// Open workspaces and hold their [NomosSession] handles:
|
|
8
|
+
///
|
|
4
9
|
/// ```dart
|
|
5
10
|
/// import 'package:nomos_client/nomos_client.dart';
|
|
6
11
|
/// import 'package:nomos_client/nomos_headless.dart';
|
|
@@ -8,9 +13,19 @@
|
|
|
8
13
|
/// final transport = await NodeRunnerTransport.start(resolveFrom: 'path/to/app');
|
|
9
14
|
/// final bridge = NomosBridge(transport);
|
|
10
15
|
/// await bridge.ready;
|
|
11
|
-
///
|
|
16
|
+
/// // open each workspace on the plane (local-only here — no `cloud` needed):
|
|
17
|
+
/// await bridge.connect(workspace: 'home', session: 'home', restoreFrom: homeBytes);
|
|
18
|
+
/// await bridge.connect(workspace: 'estate-42', session: 'estate-42', restoreFrom: estateBytes);
|
|
19
|
+
/// // bind a generated client to a workspace via its session handle:
|
|
20
|
+
/// final estate = EstateClient(bridge.session('estate-42'));
|
|
21
|
+
/// final plane = await bridge.listWorkspaces(); // enumerate + sync posture
|
|
12
22
|
/// ```
|
|
13
23
|
///
|
|
24
|
+
/// The single-workspace `bridge.connect(...)` / `bridge.query(...)` methods are sugar over an inferred
|
|
25
|
+
/// default session (unchanged). For TESTS, [NomosTestHarness] is a thin wrapper: `.ephemeral(...)` installs
|
|
26
|
+
/// one law + opens one session; `.cluster(...)` installs N laws + opens N sessions on ONE plane — it adds
|
|
27
|
+
/// no session logic of its own, it just drives the same production [NomosBridge.session] surface.
|
|
28
|
+
///
|
|
14
29
|
/// The runner child process is the SAME `@githolon/client` every peer runs; the
|
|
15
30
|
/// generated Dart clients drive it through [NomosBridge] exactly as in the app.
|
|
16
31
|
library;
|
|
@@ -34,7 +34,7 @@ import 'dart:convert';
|
|
|
34
34
|
import 'dart:io';
|
|
35
35
|
|
|
36
36
|
import 'node_runner_transport.dart';
|
|
37
|
-
import 'webview_bridge.dart' show NomosBridge;
|
|
37
|
+
import 'webview_bridge.dart' show NomosBridge, NomosSession;
|
|
38
38
|
|
|
39
39
|
class NomosTestHarnessException implements Exception {
|
|
40
40
|
final String message;
|
|
@@ -55,8 +55,50 @@ class NomosTestHarness {
|
|
|
55
55
|
final void Function(String line)? _onDiagnostic;
|
|
56
56
|
bool _disposed = false;
|
|
57
57
|
|
|
58
|
+
/// The workspace names this harness connected. For [ephemeral] this is the one [workspace]; for [cluster]
|
|
59
|
+
/// it is every workspace named in the call. Use [session] to get its handle.
|
|
60
|
+
final List<String> _sessionNames;
|
|
61
|
+
|
|
62
|
+
/// workspace → the base64 device PUBLIC key it was opened with (only for [cluster]'s `deviceKeyed` set).
|
|
63
|
+
final Map<String, String> _devicePublics;
|
|
64
|
+
|
|
65
|
+
/// The nomos framework controller's content hash from the offline install — the `frameworkHash` a
|
|
66
|
+
/// `.births()` genesis installs (not derivable client-side). Same for every workspace on the plane.
|
|
67
|
+
final String? frameworkHash;
|
|
68
|
+
|
|
69
|
+
/// workspace → the tenant law hashes installed into it (in deploy order); `lawHashes[ws].first` is that
|
|
70
|
+
/// package's domainHash — the `lawHash` a `.births()` recipe names for the child's law install.
|
|
71
|
+
final Map<String, List<String>> _lawHashes;
|
|
72
|
+
|
|
73
|
+
/// The tenant law hashes installed into [workspace] (deploy order); empty if unknown.
|
|
74
|
+
List<String> lawHashes(String workspace) =>
|
|
75
|
+
List.unmodifiable(_lawHashes[workspace] ?? const []);
|
|
76
|
+
|
|
58
77
|
NomosTestHarness._(this.bridge, this.workspace, this._transport, this._workdir,
|
|
59
|
-
this._onDiagnostic
|
|
78
|
+
this._onDiagnostic, this._sessionNames,
|
|
79
|
+
{Map<String, String> devicePublics = const {},
|
|
80
|
+
this.frameworkHash,
|
|
81
|
+
Map<String, List<String>> lawHashes = const {}})
|
|
82
|
+
: _devicePublics = devicePublics,
|
|
83
|
+
_lawHashes = lawHashes;
|
|
84
|
+
|
|
85
|
+
/// The base64 device PUBLIC key a `deviceKeyed` [cluster] session was opened with (null otherwise). Its
|
|
86
|
+
/// `sha256(base64decode(...))` is the attestor keyHash a reader enrols to trust this source's signed reads.
|
|
87
|
+
String? devicePublicKey(String workspace) => _devicePublics[workspace];
|
|
88
|
+
|
|
89
|
+
/// The workspace names on this device's local plane (one for [ephemeral], N for [cluster]).
|
|
90
|
+
List<String> get sessions => List.unmodifiable(_sessionNames);
|
|
91
|
+
|
|
92
|
+
/// A [NomosSession] handle pinned to one connected [workspace] — hand it to a generated typed client
|
|
93
|
+
/// (`EstateClient(harness.session('estate'))`). A THIN delegate to the production [NomosBridge.session]:
|
|
94
|
+
/// the harness owns no session logic, it just installs the laws + opens the sessions on the one plane.
|
|
95
|
+
NomosSession session(String workspace) {
|
|
96
|
+
if (!_sessionNames.contains(workspace)) {
|
|
97
|
+
throw NomosTestHarnessException(
|
|
98
|
+
'no session "$workspace" on this harness — connected sessions: $_sessionNames');
|
|
99
|
+
}
|
|
100
|
+
return bridge.session(workspace);
|
|
101
|
+
}
|
|
60
102
|
|
|
61
103
|
/// Install [deployJsonPaths] into a fresh LOCAL workspace and boot a bridge over it — NO cloud contact.
|
|
62
104
|
///
|
|
@@ -114,7 +156,7 @@ class NomosTestHarness {
|
|
|
114
156
|
NodeRunnerTransport? transport;
|
|
115
157
|
try {
|
|
116
158
|
final snapshot = '${workdir.path}/snapshot.bin';
|
|
117
|
-
await _runInstall(
|
|
159
|
+
final info = await _runInstall(
|
|
118
160
|
node: node,
|
|
119
161
|
useInRepoScript: useInRepoScript,
|
|
120
162
|
inRepoScript: resolvedHelper,
|
|
@@ -147,8 +189,8 @@ class NomosTestHarness {
|
|
|
147
189
|
clientId: clientId,
|
|
148
190
|
allowUnborn: true,
|
|
149
191
|
);
|
|
150
|
-
final harness =
|
|
151
|
-
|
|
192
|
+
final harness = NomosTestHarness._(
|
|
193
|
+
bridge, workspace, transport, workdir, onDiagnostic, [workspace]);
|
|
152
194
|
if (autoTearDown && registerTearDown != null) {
|
|
153
195
|
registerTearDown(harness.dispose);
|
|
154
196
|
}
|
|
@@ -167,6 +209,139 @@ class NomosTestHarness {
|
|
|
167
209
|
}
|
|
168
210
|
}
|
|
169
211
|
|
|
212
|
+
/// A COLOCATED TEST CLUSTER — install MANY workspaces' law offline and connect one SESSION per workspace on
|
|
213
|
+
/// ONE engine plane (one runner, one transport, one [bridge]), so a cross-workspace scenario (attested
|
|
214
|
+
/// reads, foreign-source imports) runs in-process with ZERO network. Each workspace's law is installed via
|
|
215
|
+
/// the SAME offline lane [ephemeral] uses (one snapshot per workspace), then `connect`ed as its own session;
|
|
216
|
+
/// the per-connect restore snapshot loads it into the shared plane.
|
|
217
|
+
///
|
|
218
|
+
/// [workspaces] — `{ workspaceName: [deployJsonPaths] }` (installed in order per workspace). Get a
|
|
219
|
+
/// session-scoped view with [session] and bind it to a generated client: `EstateClient(harness.session(
|
|
220
|
+
/// 'estate'))`. All other params mirror [ephemeral]. [clientId] is suffixed per session.
|
|
221
|
+
/// [deviceKeyed] names workspaces to open WITH a fresh in-kernel device key as their signing `authorSecret`
|
|
222
|
+
/// (the warrant default — every write is signed, replay-provably). Required for a source of ATTESTED READS:
|
|
223
|
+
/// the colocation resolver serves+signs a foreign read with the source session's device key, so a reader
|
|
224
|
+
/// enrols `sha256(that device public key)`. Get the public via [devicePublicKey].
|
|
225
|
+
static Future<NomosTestHarness> cluster({
|
|
226
|
+
required Map<String, List<String>> workspaces,
|
|
227
|
+
Set<String> deviceKeyed = const {},
|
|
228
|
+
String clientId = 'test-runner',
|
|
229
|
+
String? runtimeDir,
|
|
230
|
+
String? nodeEntry,
|
|
231
|
+
String? installHelper,
|
|
232
|
+
String? resolveFrom,
|
|
233
|
+
String node = 'node',
|
|
234
|
+
bool autoTearDown = true,
|
|
235
|
+
void Function(Future<void> Function())? registerTearDown,
|
|
236
|
+
void Function(String line)? onDiagnostic,
|
|
237
|
+
Duration readyTimeout = const Duration(seconds: 30),
|
|
238
|
+
}) async {
|
|
239
|
+
if (workspaces.isEmpty) {
|
|
240
|
+
throw NomosTestHarnessException(
|
|
241
|
+
'no workspaces — pass { workspaceName: [build/<name>.deploy.json] } for each colocated workspace');
|
|
242
|
+
}
|
|
243
|
+
for (final e in workspaces.entries) {
|
|
244
|
+
if (e.value.isEmpty) {
|
|
245
|
+
throw NomosTestHarnessException(
|
|
246
|
+
'workspace "${e.key}" has no deployJsonPaths — pass its build/<name>.deploy.json');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
final entry = nodeEntry ??
|
|
250
|
+
await _resolveEntry(node, resolveFrom ?? Directory.current.path);
|
|
251
|
+
final resolvedHelper =
|
|
252
|
+
installHelper ?? '${File(entry).parent.path}/offline-install.mjs';
|
|
253
|
+
final useInRepoScript =
|
|
254
|
+
resolvedHelper.endsWith('.mjs') && File(resolvedHelper).existsSync();
|
|
255
|
+
final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
|
|
256
|
+
|
|
257
|
+
final workdir = await Directory.systemTemp.createTemp('nomos-cluster-');
|
|
258
|
+
NodeRunnerTransport? transport;
|
|
259
|
+
try {
|
|
260
|
+
// 1. install each workspace's law offline → one snapshot per workspace (colocated, still separate law).
|
|
261
|
+
final snapshots = <String, String>{};
|
|
262
|
+
for (final e in workspaces.entries) {
|
|
263
|
+
final snap = '${workdir.path}/${e.key}.bin';
|
|
264
|
+
await _runInstall(
|
|
265
|
+
node: node,
|
|
266
|
+
useInRepoScript: useInRepoScript,
|
|
267
|
+
inRepoScript: resolvedHelper,
|
|
268
|
+
githolonCmd: githolonCmd,
|
|
269
|
+
deployJsonPaths: e.value,
|
|
270
|
+
workspace: e.key,
|
|
271
|
+
snapshot: snap,
|
|
272
|
+
runtimeDir: runtimeDir,
|
|
273
|
+
onDiagnostic: onDiagnostic);
|
|
274
|
+
snapshots[e.key] = snap;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 2. ONE runner / ONE bridge for the whole plane — no env restore default; each connect brings its own.
|
|
278
|
+
transport = await NodeRunnerTransport.start(
|
|
279
|
+
entry: entry,
|
|
280
|
+
resolveFrom: resolveFrom,
|
|
281
|
+
node: node,
|
|
282
|
+
environment: {
|
|
283
|
+
'NOMOS_OFFLINE': '1',
|
|
284
|
+
if (runtimeDir != null) 'NOMOS_OFFLINE_RUNTIME': runtimeDir,
|
|
285
|
+
},
|
|
286
|
+
onDiagnostic: onDiagnostic,
|
|
287
|
+
);
|
|
288
|
+
final bridge = NomosBridge(transport);
|
|
289
|
+
await bridge.ready.timeout(readyTimeout,
|
|
290
|
+
onTimeout: () => throw NomosTestHarnessException(
|
|
291
|
+
'the offline runner never signalled ready within $readyTimeout '
|
|
292
|
+
'(is `node` on PATH and ~/.holon/runtime warm?)'));
|
|
293
|
+
|
|
294
|
+
// 3. connect one SESSION per workspace, each restoring its OWN snapshot — LOCAL-ONLY (no cloud;
|
|
295
|
+
// these are local githolons on the device plane, offline by nature).
|
|
296
|
+
final snapshotB64 = <String, String>{};
|
|
297
|
+
for (final e in workspaces.entries) {
|
|
298
|
+
snapshotB64[e.key] = base64Encode(await File(snapshots[e.key]!).readAsBytes());
|
|
299
|
+
await bridge.connect(
|
|
300
|
+
workspace: e.key,
|
|
301
|
+
session: e.key,
|
|
302
|
+
clientId: '$clientId-${e.key}',
|
|
303
|
+
restoreFrom: snapshotB64[e.key],
|
|
304
|
+
allowUnborn: true,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 3b. DEVICE-KEY the named sessions: mint a fresh in-kernel key on each (now that it is mounted) and
|
|
309
|
+
// RE-OPEN it with that key as its signing authorSecret — the resolver's attested-read signing key.
|
|
310
|
+
final devicePublics = <String, String>{};
|
|
311
|
+
for (final ws in deviceKeyed) {
|
|
312
|
+
final kp = await bridge.session(ws).authorKeygen();
|
|
313
|
+
devicePublics[ws] = (kp['public'] ?? '').toString();
|
|
314
|
+
await bridge.connect(
|
|
315
|
+
workspace: ws,
|
|
316
|
+
session: ws,
|
|
317
|
+
clientId: '$clientId-$ws',
|
|
318
|
+
restoreFrom: snapshotB64[ws],
|
|
319
|
+
authorSecret: (kp['secret'] ?? '').toString(),
|
|
320
|
+
allowUnborn: true,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
final names = workspaces.keys.toList();
|
|
325
|
+
final harness = NomosTestHarness._(
|
|
326
|
+
bridge, names.first, transport, workdir, onDiagnostic, names,
|
|
327
|
+
devicePublics);
|
|
328
|
+
if (autoTearDown && registerTearDown != null) {
|
|
329
|
+
registerTearDown(harness.dispose);
|
|
330
|
+
}
|
|
331
|
+
return harness;
|
|
332
|
+
} catch (_) {
|
|
333
|
+
if (transport != null) {
|
|
334
|
+
try {
|
|
335
|
+
await transport.dispose();
|
|
336
|
+
} catch (_) {}
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
await workdir.delete(recursive: true);
|
|
340
|
+
} catch (_) {}
|
|
341
|
+
rethrow;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
170
345
|
/// Deterministic, idempotent, total-ordered teardown: stop the runner (SIGTERM → 5s deadline → SIGKILL,
|
|
171
346
|
/// via [NodeRunnerTransport.dispose]), then delete the temp dir. Safe to call twice; safe to call after a
|
|
172
347
|
/// failed setup. Nothing left running, nothing left on disk.
|
|
@@ -196,7 +371,7 @@ class NomosTestHarness {
|
|
|
196
371
|
|
|
197
372
|
// ── offline install (a short-lived node child; produces the restore snapshot) ──
|
|
198
373
|
|
|
199
|
-
static Future<
|
|
374
|
+
static Future<Map<String, Object?>> _runInstall({
|
|
200
375
|
required String node,
|
|
201
376
|
required bool useInRepoScript,
|
|
202
377
|
required String inRepoScript,
|
|
@@ -255,6 +430,9 @@ class NomosTestHarness {
|
|
|
255
430
|
throw NomosTestHarnessException(
|
|
256
431
|
'offline law install produced no snapshot (stdout: ${out.isEmpty ? "(empty)" : out})');
|
|
257
432
|
}
|
|
433
|
+
// { ok, snapshot, workspace, lawHashes, frameworkHash } — the caller keeps lawHashes/frameworkHash so a
|
|
434
|
+
// `.births()` directive can be driven (its genesis installs the framework + law by hash).
|
|
435
|
+
return decoded;
|
|
258
436
|
}
|
|
259
437
|
|
|
260
438
|
static Future<String> _resolveEntry(String node, String from) async {
|