@githolon/dsl 0.100.5 → 0.101.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/CHANGELOG.md +12 -0
- package/dart/lib/src/dev_cloud.dart +46 -15
- package/dart/lib/src/test_harness.dart +220 -71
- package/dart/lib/src/webview_bridge.dart +640 -126
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_credentials_test.dart +1 -1
- package/dart/test/adv_realm_interference_test.dart +1 -1
- package/dart/test/bundle_version_pairing_test.dart +11 -22
- package/dart/test/cluster_attested_test.dart +1 -1
- package/dart/test/cluster_births_test.dart +1 -1
- package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
- package/dart/test/cluster_local_autosync_test.dart +1 -1
- package/dart/test/cluster_scale_test.dart +1 -1
- package/dart/test/co2_product_gauntlet_test.dart +228 -0
- package/dart/test/dlq_session_surface_test.dart +1 -1
- package/dart/test/errors_test.dart +63 -12
- package/dart/test/first_app_gauntlet_test.dart +132 -87
- package/dart/test/local_sync_inert_test.dart +10 -5
- package/dart/test/parent_signer_recovery_test.dart +1 -1
- package/dart/test/sealing_client_test.dart +1 -1
- package/dart/test/session_bound_test.dart +1 -1
- package/dart/test/test_harness_test.dart +2 -2
- package/dart/test/vo_read_typing_test.dart +1 -1
- package/dart/test/watch_after_offer_stall_test.dart +1 -1
- package/dart/test/webview_bridge_test.dart +274 -0
- package/dart_types/CHANGELOG.md +6 -0
- package/dart_types/lib/src/errors.dart +136 -30
- package/dart_types/lib/src/surfaced.dart +48 -2
- package/dart_types/lib/src/wire.dart +3 -1
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +19 -0
- package/package.json +1 -1
- package/src/activity.ts +43 -4
- package/src/aggregate.ts +132 -22
- package/src/attested_core.ts +1 -1
- package/src/authoring.ts +119 -6
- package/src/birth_meter.ts +4 -4
- package/src/birth_meter_ensure.ts +165 -29
- package/src/births.ts +53 -22
- package/src/build_package.ts +96 -20
- package/src/business_birth.ts +79 -16
- package/src/codegen_dart.ts +267 -37
- package/src/codegen_dart_client.ts +930 -87
- package/src/codegen_openapi.ts +28 -5
- package/src/codegen_proof.ts +107 -11
- package/src/codegen_ts.ts +961 -135
- package/src/compile_package_main.ts +172 -53
- package/src/conflicts.ts +252 -0
- package/src/describe_model.ts +448 -0
- package/src/directive.ts +5 -38
- package/src/drivers.ts +12 -4
- package/src/engine_entry.ts +4 -0
- package/src/evolve_preview.ts +72 -11
- package/src/fields.ts +201 -55
- package/src/framework/application_home.ts +103 -0
- package/src/framework/application_home_domain.ts +2 -0
- package/src/framework/attested_receipts.ts +2 -2
- package/src/framework/auth_provider_law.ts +51 -24
- package/src/framework/business_corrections.ts +170 -0
- package/src/framework/disclosure.ts +1 -1
- package/src/framework/domain_lifecycle.ts +250 -16
- package/src/framework/encryption_identity.ts +1 -1
- package/src/framework/encryption_scope.ts +21 -13
- package/src/framework/era_certification.ts +1 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +1 -1
- package/src/framework/home.ts +22 -9
- package/src/framework/identity.ts +5 -5
- package/src/framework/impure_capability.ts +2 -2
- package/src/framework/owned_custody.ts +561 -0
- package/src/framework/platform_admin.ts +1 -1
- package/src/framework/relations.ts +9 -1
- package/src/framework/repair.ts +1 -1
- package/src/framework/signing_identity.ts +2 -2
- package/src/framework/source_roster.ts +1 -1
- package/src/framework/workspaces.ts +17 -10
- package/src/generated_client_names.ts +39 -0
- package/src/index.ts +20 -15
- package/src/kernel_capabilities.ts +25 -1
- package/src/law_check.ts +4 -1
- package/src/manifest.ts +356 -63
- package/src/read_projection_manifest.ts +8 -0
- package/src/read_projection_namespace.ts +4 -0
- package/src/relation.ts +1 -1
- package/src/shape_compat.ts +61 -5
- package/src/stable_ids.ts +38 -6
- package/src/stable_ids_types.ts +30 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd.ts +62 -11
- package/src/usd_layers.ts +54 -3
- package/src/value_object.ts +23 -19
- package/src/wire.ts +1 -0
- package/src/wire_encode.ts +48 -4
- package/src/workspace_routing.ts +57 -0
- package/src/workspace_sharding.ts +9 -9
- package/src/certified_read.ts +0 -313
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// snapshot (`restoreFrom`). Every other URL is REFUSED — a stray fetch fails loudly, never reaches the net.
|
|
25
25
|
//
|
|
26
26
|
// PREREQUISITES (documented so a failure is legible): `node` on PATH; a resolvable
|
|
27
|
-
// `@githolon/client/runner/node-entry.ts`; and a warm `~/.holon/runtime` cache (holon.wasm + packages.json —
|
|
27
|
+
// `@githolon/client/runner/node-entry.ts`; and a warm `~/.holon/runtime` cache (holon-testmint.wasm + packages.json —
|
|
28
28
|
// one `githolon` run fills it). All local; no network at test time.
|
|
29
29
|
//
|
|
30
30
|
// PACKAGING NOTE: `offline-install.ts` imports the adapter engine, which today lives beside the in-repo
|
|
@@ -173,8 +173,8 @@ class NomosTestHarness {
|
|
|
173
173
|
return session(ws);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
NomosTestHarness._(this.bridge, this.workspace, this._transport,
|
|
177
|
-
this._onDiagnostic, List<String> sessionNames,
|
|
176
|
+
NomosTestHarness._(this.bridge, this.workspace, this._transport,
|
|
177
|
+
this._workdir, this._onDiagnostic, List<String> sessionNames,
|
|
178
178
|
{Map<String, String> devicePublics = const {},
|
|
179
179
|
Map<String, String> deviceSecrets = const {},
|
|
180
180
|
this.frameworkHash,
|
|
@@ -184,7 +184,8 @@ class NomosTestHarness {
|
|
|
184
184
|
Duration? phaseTimeout,
|
|
185
185
|
this.testCloud,
|
|
186
186
|
this.homeWorkspace})
|
|
187
|
-
: _sessionNames = List.of(
|
|
187
|
+
: _sessionNames = List.of(
|
|
188
|
+
sessionNames), // GROWABLE — restoreWorkspace may add a fresh session
|
|
188
189
|
_devicePublics = devicePublics,
|
|
189
190
|
_deviceSecrets = deviceSecrets,
|
|
190
191
|
_lawHashes = Map.of(lawHashes),
|
|
@@ -256,10 +257,11 @@ class NomosTestHarness {
|
|
|
256
257
|
/// [deployJsonPaths] — the `build/<name>.deploy.json` files `nomos-compile` emits (installed in order;
|
|
257
258
|
/// framework/platform law before a domain that depends on it).
|
|
258
259
|
/// [workspace] — the local workspace name (default `test-ws`; law-only, never leaves the machine).
|
|
259
|
-
/// [runtimeDir] — the runtime cache (default `~/.holon/runtime`); must hold `holon.wasm` +
|
|
260
|
+
/// [runtimeDir] — the runtime cache (default `~/.holon/runtime`); must hold `holon-testmint.wasm` +
|
|
261
|
+
/// `packages.json`. The harness owns a throwaway test root; app code never handles its key material.
|
|
260
262
|
/// [nodeEntry] — path to `node-entry.ts`; when omitted it is resolved from [resolveFrom] via
|
|
261
263
|
/// `require.resolve('@githolon/client/runner/node-entry.ts')`.
|
|
262
|
-
/// [installHelper] — the offline law-install lane. An EXISTING `.mjs` file is run as a node script (the
|
|
264
|
+
/// [installHelper] — the offline law-install lane. An EXISTING `.ts` or `.mjs` file is run as a node script (the
|
|
263
265
|
/// in-repo `offline-install.ts`, the default when it exists beside [nodeEntry]); anything else is a
|
|
264
266
|
/// githolon COMMAND LINE whose `harness-install` subcommand is driven (the default `npx githolon` when the
|
|
265
267
|
/// in-repo helper is absent — a published tenant needs nothing but `githolon`). E.g. force the subcommand
|
|
@@ -291,16 +293,20 @@ class NomosTestHarness {
|
|
|
291
293
|
final entry = nodeEntry ??
|
|
292
294
|
await _resolveEntry(node, resolveFrom ?? Directory.current.path);
|
|
293
295
|
// THE INSTALL LANE. [installHelper] is the one seam (PACKAGING NOTE above): either the in-repo
|
|
294
|
-
// `offline-install.
|
|
296
|
+
// `offline-install.mjs` node script, OR the published `githolon harness-install` subcommand (the CLI
|
|
295
297
|
// bundles the engine + this exact install sequence, so a published tenant needs nothing but `githolon`).
|
|
296
|
-
// • an EXISTING `.mjs` file
|
|
298
|
+
// • an EXISTING `.ts`/`.mjs` file ⇒ run it as a node script (the in-repo default when present);
|
|
297
299
|
// • anything else ⇒ treat it as a githolon COMMAND LINE and drive `harness-install`
|
|
298
300
|
// (default `npx githolon` when the in-repo helper is absent; the test forces this lane by passing
|
|
299
301
|
// e.g. `installHelper: 'node /abs/cli/dist/cli.mjs'`).
|
|
300
|
-
final
|
|
301
|
-
|
|
302
|
+
final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
|
|
303
|
+
final resolvedHelper = installHelper ??
|
|
304
|
+
(File(adjacentInstaller).existsSync()
|
|
305
|
+
? adjacentInstaller
|
|
306
|
+
: '${File(entry).parent.path}/offline-install.ts');
|
|
302
307
|
final useInRepoScript =
|
|
303
|
-
resolvedHelper.endsWith('.
|
|
308
|
+
(resolvedHelper.endsWith('.ts') || resolvedHelper.endsWith('.mjs')) &&
|
|
309
|
+
File(resolvedHelper).existsSync();
|
|
304
310
|
final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
|
|
305
311
|
|
|
306
312
|
// A FRESH temp dir per harness — the snapshot + any transient runner state live here and nowhere else,
|
|
@@ -308,13 +314,18 @@ class NomosTestHarness {
|
|
|
308
314
|
final workdir = await Directory.systemTemp.createTemp('nomos-harness-');
|
|
309
315
|
// Total-ordered teardown even on a mid-setup failure: build the transport lazily and clean up on throw.
|
|
310
316
|
NodeRunnerTransport? transport;
|
|
311
|
-
final phase = (String m) {
|
|
317
|
+
final phase = (String m) {
|
|
318
|
+
if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m');
|
|
319
|
+
};
|
|
312
320
|
try {
|
|
313
321
|
final snapshot = '${workdir.path}/snapshot.bin';
|
|
314
322
|
final info = await _phased(
|
|
315
|
-
phase:
|
|
323
|
+
phase:
|
|
324
|
+
'1/4: offline law install (${deployJsonPaths.length} package(s))',
|
|
316
325
|
workspace: workspace,
|
|
317
|
-
awaiting: useInRepoScript
|
|
326
|
+
awaiting: useInRepoScript
|
|
327
|
+
? 'offline-install.ts'
|
|
328
|
+
: 'githolon harness-install',
|
|
318
329
|
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
319
330
|
onDiagnostic: onDiagnostic,
|
|
320
331
|
likelyCauses: const [
|
|
@@ -344,6 +355,11 @@ class NomosTestHarness {
|
|
|
344
355
|
environment: {
|
|
345
356
|
'NOMOS_OFFLINE': '1',
|
|
346
357
|
'NOMOS_OFFLINE_RESTORE': snapshot,
|
|
358
|
+
'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
|
|
359
|
+
if (info['devicePublic'] != null)
|
|
360
|
+
'NOMOS_TEST_K_ROOT_PUBLIC': info['devicePublic'].toString(),
|
|
361
|
+
if (info['deviceSecret'] != null)
|
|
362
|
+
'NOMOS_TEST_ROOT_SECRET': info['deviceSecret'].toString(),
|
|
347
363
|
if (runtimeDir != null) 'NOMOS_OFFLINE_RUNTIME': runtimeDir,
|
|
348
364
|
},
|
|
349
365
|
onDiagnostic: onDiagnostic,
|
|
@@ -355,7 +371,7 @@ class NomosTestHarness {
|
|
|
355
371
|
timeout: phaseTimeout ?? readyTimeout,
|
|
356
372
|
onDiagnostic: onDiagnostic,
|
|
357
373
|
likelyCauses: const [
|
|
358
|
-
'node is not on PATH, or ~/.holon/runtime is cold (holon.wasm + packages.json)',
|
|
374
|
+
'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + packages.json)',
|
|
359
375
|
'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
|
|
360
376
|
],
|
|
361
377
|
run: () => plane.ready);
|
|
@@ -370,7 +386,8 @@ class NomosTestHarness {
|
|
|
370
386
|
'run with onDiagnostic to see where the runner blocked',
|
|
371
387
|
],
|
|
372
388
|
run: () => plane.connect(
|
|
373
|
-
cloud:
|
|
389
|
+
cloud:
|
|
390
|
+
'https://offline.invalid', // ignored: the offline runner serves everything locally
|
|
374
391
|
workspace: workspace,
|
|
375
392
|
clientId: clientId,
|
|
376
393
|
allowUnborn: true,
|
|
@@ -379,7 +396,8 @@ class NomosTestHarness {
|
|
|
379
396
|
// with the SAME key, so offline `.births()` offers Just Work.
|
|
380
397
|
authorSecret: info['deviceSecret']?.toString(),
|
|
381
398
|
));
|
|
382
|
-
phase(
|
|
399
|
+
phase(
|
|
400
|
+
'ready — session "$workspace" connected (law ${(info['lawHashes'] as List?)?.isNotEmpty == true ? (info['lawHashes'] as List).first.toString().substring(0, 12) : "installed"})');
|
|
383
401
|
// THE DECOY SESSION (multi-workspace by default in tests): sole-session inference is how
|
|
384
402
|
// single-workspace assumptions ship — with one session connected, an op that forgot its session
|
|
385
403
|
// silently works in tests and only refuses in production when the app connects its second
|
|
@@ -403,13 +421,15 @@ class NomosTestHarness {
|
|
|
403
421
|
restoreFrom: base64Encode(await File(snapshot).readAsBytes()),
|
|
404
422
|
));
|
|
405
423
|
}
|
|
406
|
-
final harness = NomosTestHarness._(
|
|
407
|
-
|
|
424
|
+
final harness = NomosTestHarness._(plane.session(workspace), workspace,
|
|
425
|
+
transport, workdir, onDiagnostic, [workspace],
|
|
408
426
|
devicePublics: {
|
|
409
|
-
if (info['devicePublic'] != null)
|
|
427
|
+
if (info['devicePublic'] != null)
|
|
428
|
+
workspace: info['devicePublic'].toString(),
|
|
410
429
|
},
|
|
411
430
|
deviceSecrets: {
|
|
412
|
-
if (info['deviceSecret'] != null)
|
|
431
|
+
if (info['deviceSecret'] != null)
|
|
432
|
+
workspace: info['deviceSecret'].toString(),
|
|
413
433
|
},
|
|
414
434
|
frameworkHash: info['frameworkHash']?.toString(),
|
|
415
435
|
lawHashes: {
|
|
@@ -417,7 +437,9 @@ class NomosTestHarness {
|
|
|
417
437
|
.map((e) => e.toString())
|
|
418
438
|
.toList()
|
|
419
439
|
},
|
|
420
|
-
snapshots: {
|
|
440
|
+
snapshots: {
|
|
441
|
+
workspace: base64Encode(await File(snapshot).readAsBytes())
|
|
442
|
+
},
|
|
421
443
|
installLane: _InstallLane(
|
|
422
444
|
node: node,
|
|
423
445
|
useInRepoScript: useInRepoScript,
|
|
@@ -485,10 +507,14 @@ class NomosTestHarness {
|
|
|
485
507
|
}
|
|
486
508
|
final entry = nodeEntry ??
|
|
487
509
|
await _resolveEntry(node, resolveFrom ?? Directory.current.path);
|
|
488
|
-
final
|
|
489
|
-
|
|
510
|
+
final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
|
|
511
|
+
final resolvedHelper = installHelper ??
|
|
512
|
+
(File(adjacentInstaller).existsSync()
|
|
513
|
+
? adjacentInstaller
|
|
514
|
+
: '${File(entry).parent.path}/offline-install.ts');
|
|
490
515
|
final useInRepoScript =
|
|
491
|
-
resolvedHelper.endsWith('.
|
|
516
|
+
(resolvedHelper.endsWith('.ts') || resolvedHelper.endsWith('.mjs')) &&
|
|
517
|
+
File(resolvedHelper).existsSync();
|
|
492
518
|
final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
|
|
493
519
|
|
|
494
520
|
final workdir = await Directory.systemTemp.createTemp('nomos-cluster-');
|
|
@@ -499,6 +525,8 @@ class NomosTestHarness {
|
|
|
499
525
|
final lawHashesByWs = <String, List<String>>{};
|
|
500
526
|
final installDevicePublics = <String, String>{};
|
|
501
527
|
final installDeviceSecrets = <String, String>{};
|
|
528
|
+
String? planeTestRootPublic;
|
|
529
|
+
String? planeTestRootSecret;
|
|
502
530
|
String? frameworkHash;
|
|
503
531
|
var _i = 0;
|
|
504
532
|
for (final e in workspaces.entries) {
|
|
@@ -506,7 +534,9 @@ class NomosTestHarness {
|
|
|
506
534
|
final info = await _phased(
|
|
507
535
|
phase: '1: offline law install ${++_i}/${workspaces.length}',
|
|
508
536
|
workspace: e.key,
|
|
509
|
-
awaiting: useInRepoScript
|
|
537
|
+
awaiting: useInRepoScript
|
|
538
|
+
? 'offline-install.ts'
|
|
539
|
+
: 'githolon harness-install',
|
|
510
540
|
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
511
541
|
onDiagnostic: onDiagnostic,
|
|
512
542
|
likelyCauses: const [
|
|
@@ -530,12 +560,18 @@ class NomosTestHarness {
|
|
|
530
560
|
frameworkHash ??= info['frameworkHash']?.toString();
|
|
531
561
|
// THE HARNESS DEVICE (era-0): the install lane mints one device key per workspace — sessions sign
|
|
532
562
|
// with it and the client completes the self-serve birth ceremony (enrol + cert) with the SAME key.
|
|
533
|
-
if (info['devicePublic'] != null)
|
|
534
|
-
|
|
563
|
+
if (info['devicePublic'] != null)
|
|
564
|
+
installDevicePublics[e.key] = info['devicePublic'].toString();
|
|
565
|
+
if (info['deviceSecret'] != null)
|
|
566
|
+
installDeviceSecrets[e.key] = info['deviceSecret'].toString();
|
|
567
|
+
planeTestRootPublic ??= info['devicePublic']?.toString();
|
|
568
|
+
planeTestRootSecret ??= info['deviceSecret']?.toString();
|
|
535
569
|
}
|
|
536
570
|
|
|
537
571
|
// 2. ONE runner / ONE bridge for the whole plane — no env restore default; each connect brings its own.
|
|
538
|
-
final phase = (String m) {
|
|
572
|
+
final phase = (String m) {
|
|
573
|
+
if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m');
|
|
574
|
+
};
|
|
539
575
|
transport = await _phased(
|
|
540
576
|
phase: '2: spawn the node runner (${workspaces.length} workspace(s))',
|
|
541
577
|
awaiting: 'Process.start($node)',
|
|
@@ -547,6 +583,11 @@ class NomosTestHarness {
|
|
|
547
583
|
node: node,
|
|
548
584
|
environment: {
|
|
549
585
|
'NOMOS_OFFLINE': '1',
|
|
586
|
+
'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
|
|
587
|
+
if (planeTestRootPublic != null)
|
|
588
|
+
'NOMOS_TEST_K_ROOT_PUBLIC': planeTestRootPublic,
|
|
589
|
+
if (planeTestRootSecret != null)
|
|
590
|
+
'NOMOS_TEST_ROOT_SECRET': planeTestRootSecret,
|
|
550
591
|
if (runtimeDir != null) 'NOMOS_OFFLINE_RUNTIME': runtimeDir,
|
|
551
592
|
},
|
|
552
593
|
onDiagnostic: onDiagnostic,
|
|
@@ -558,7 +599,7 @@ class NomosTestHarness {
|
|
|
558
599
|
timeout: phaseTimeout ?? readyTimeout,
|
|
559
600
|
onDiagnostic: onDiagnostic,
|
|
560
601
|
likelyCauses: const [
|
|
561
|
-
'node is not on PATH, or ~/.holon/runtime is cold (holon.wasm + packages.json)',
|
|
602
|
+
'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + packages.json)',
|
|
562
603
|
'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
|
|
563
604
|
],
|
|
564
605
|
run: () => bridge.ready);
|
|
@@ -568,7 +609,8 @@ class NomosTestHarness {
|
|
|
568
609
|
final snapshotB64 = <String, String>{};
|
|
569
610
|
var _n = 0;
|
|
570
611
|
for (final e in workspaces.entries) {
|
|
571
|
-
snapshotB64[e.key] =
|
|
612
|
+
snapshotB64[e.key] =
|
|
613
|
+
base64Encode(await File(snapshots[e.key]!).readAsBytes());
|
|
572
614
|
await _phased(
|
|
573
615
|
phase: '3: connect session ${++_n}/${workspaces.length}',
|
|
574
616
|
workspace: e.key,
|
|
@@ -633,7 +675,8 @@ class NomosTestHarness {
|
|
|
633
675
|
));
|
|
634
676
|
}
|
|
635
677
|
if (deviceKeyed.isNotEmpty) {
|
|
636
|
-
phase(
|
|
678
|
+
phase(
|
|
679
|
+
'cluster: ${deviceKeyed.length} device-keyed session(s) re-opened');
|
|
637
680
|
}
|
|
638
681
|
|
|
639
682
|
final names = workspaces.keys.toList();
|
|
@@ -714,7 +757,8 @@ class NomosTestHarness {
|
|
|
714
757
|
}
|
|
715
758
|
final entry = nodeEntry ??
|
|
716
759
|
await _resolveEntry(node, resolveFrom ?? Directory.current.path);
|
|
717
|
-
final bareUid =
|
|
760
|
+
final bareUid =
|
|
761
|
+
subject.startsWith('user:') ? subject.substring(5) : subject;
|
|
718
762
|
final http = HttpClient();
|
|
719
763
|
Future<Map<String, Object?>> post(String path, Object? body,
|
|
720
764
|
{Map<String, String> headers = const {}}) async {
|
|
@@ -723,7 +767,8 @@ class NomosTestHarness {
|
|
|
723
767
|
req.headers.set('x-nomos-principal', bareUid);
|
|
724
768
|
if (authToken != null) req.headers.set('x-nomos-auth', authToken);
|
|
725
769
|
headers.forEach(req.headers.set);
|
|
726
|
-
if (body != null)
|
|
770
|
+
if (body != null)
|
|
771
|
+
req.add(utf8.encode(body is String ? body : jsonEncode(body)));
|
|
727
772
|
final resp = await req.close();
|
|
728
773
|
final text = await resp.transform(utf8.decoder).join();
|
|
729
774
|
final decoded = (jsonDecode(text) as Map).cast<String, Object?>();
|
|
@@ -743,6 +788,8 @@ class NomosTestHarness {
|
|
|
743
788
|
// server's in-process operator lane). Booting a born workspace is idempotent; an unborn one
|
|
744
789
|
// refuses 409, surfaced here with the fix.
|
|
745
790
|
String? frameworkHash;
|
|
791
|
+
final frameworkPostRoutes =
|
|
792
|
+
<({String domain, String directive, String path})>[];
|
|
746
793
|
final lawHashes = <String>[];
|
|
747
794
|
await _phased(
|
|
748
795
|
phase: '1/6: boot the born platform + deploy law on the test cloud',
|
|
@@ -769,9 +816,32 @@ class NomosTestHarness {
|
|
|
769
816
|
for (final p in deployJsonPaths) {
|
|
770
817
|
final body = await File(p).readAsString();
|
|
771
818
|
await post('/v2/workspaces/$platformWorkspace/domains', body);
|
|
772
|
-
final
|
|
819
|
+
final deploy = (jsonDecode(body) as Map);
|
|
820
|
+
final summary = (deploy['summary'] as Map?);
|
|
773
821
|
final h = summary?['domainHash']?.toString();
|
|
774
822
|
if (h != null && h.isNotEmpty) lawHashes.add(h);
|
|
823
|
+
final openapi = deploy['openapi'] as Map?;
|
|
824
|
+
final pathSets = <Map>[
|
|
825
|
+
(openapi?['paths'] as Map?) ?? const {},
|
|
826
|
+
(((openapi?['x-nomos-framework-routes'] as Map?)?['paths'])
|
|
827
|
+
as Map?) ??
|
|
828
|
+
const {},
|
|
829
|
+
];
|
|
830
|
+
for (final paths in pathSets) {
|
|
831
|
+
for (final entry in paths.entries) {
|
|
832
|
+
final post = (entry.value as Map?)?['post'] as Map?;
|
|
833
|
+
final route = post?['x-nomos'] as Map?;
|
|
834
|
+
final domain = route?['domain']?.toString() ?? '';
|
|
835
|
+
final directive = route?['directiveId']?.toString() ?? '';
|
|
836
|
+
if (domain.isNotEmpty && directive.isNotEmpty) {
|
|
837
|
+
frameworkPostRoutes.add((
|
|
838
|
+
domain: domain,
|
|
839
|
+
directive: directive,
|
|
840
|
+
path: entry.key.toString(),
|
|
841
|
+
));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
775
845
|
}
|
|
776
846
|
});
|
|
777
847
|
if (frameworkHash == null || frameworkHash!.isEmpty) {
|
|
@@ -782,6 +852,27 @@ class NomosTestHarness {
|
|
|
782
852
|
throw NomosTestHarnessException(
|
|
783
853
|
'cloudBound: no deploy.json carried a summary.domainHash — cannot run the birth ceremony (lawHash unknown)');
|
|
784
854
|
}
|
|
855
|
+
final birthRoutes = frameworkPostRoutes
|
|
856
|
+
.where((route) => route.directive == 'birthHome')
|
|
857
|
+
.toSet()
|
|
858
|
+
.toList();
|
|
859
|
+
if (birthRoutes.length != 1) {
|
|
860
|
+
throw NomosTestHarnessException(
|
|
861
|
+
'cloudBound: the deployed package must expose exactly one semantic birthHome route; found ${birthRoutes.length}');
|
|
862
|
+
}
|
|
863
|
+
final applicationHomeDomain = birthRoutes.single.domain;
|
|
864
|
+
final birthHomePath = birthRoutes.single.path;
|
|
865
|
+
final enrollmentRoutes = frameworkPostRoutes
|
|
866
|
+
.where((route) =>
|
|
867
|
+
route.directive == 'enrollFirstDevice' &&
|
|
868
|
+
route.domain == applicationHomeDomain)
|
|
869
|
+
.toSet()
|
|
870
|
+
.toList();
|
|
871
|
+
if (enrollmentRoutes.length != 1) {
|
|
872
|
+
throw NomosTestHarnessException(
|
|
873
|
+
"cloudBound: application/home domain '$applicationHomeDomain' must expose exactly one semantic enrollFirstDevice route; found ${enrollmentRoutes.length}");
|
|
874
|
+
}
|
|
875
|
+
final enrollFirstDevicePath = enrollmentRoutes.single.path;
|
|
785
876
|
// 2/6 — the LOCAL engine plane, CLOUD-BOUND, spawned BEFORE the birth: the runner's local kernel is
|
|
786
877
|
// what mints the device key + signs the birth cert (era-0: a birth is ATTESTED or not at all).
|
|
787
878
|
transport = await _phased(
|
|
@@ -790,7 +881,16 @@ class NomosTestHarness {
|
|
|
790
881
|
timeout: phaseTimeout ?? _kSpawnDeadline,
|
|
791
882
|
onDiagnostic: onDiagnostic,
|
|
792
883
|
run: () => NodeRunnerTransport.start(
|
|
793
|
-
entry: entry,
|
|
884
|
+
entry: entry,
|
|
885
|
+
resolveFrom: resolveFrom,
|
|
886
|
+
node: node,
|
|
887
|
+
environment: cloud.testKRootPublic == null
|
|
888
|
+
? null
|
|
889
|
+
: {
|
|
890
|
+
...Platform.environment,
|
|
891
|
+
'NOMOS_TEST_K_ROOT_PUBLIC': cloud.testKRootPublic!,
|
|
892
|
+
},
|
|
893
|
+
onDiagnostic: onDiagnostic));
|
|
794
894
|
final plane = NomosBridge(transport!);
|
|
795
895
|
await _phased(
|
|
796
896
|
phase: '3/6: runner ready',
|
|
@@ -804,7 +904,8 @@ class NomosTestHarness {
|
|
|
804
904
|
// on-chain AuthProvider (enrollFirstDevice — no admin in the loop). The platform is WARRANTED, so
|
|
805
905
|
// plain unsigned enrolment refuses by design; this is the one first-device door.
|
|
806
906
|
final device = await _phased(
|
|
807
|
-
phase:
|
|
907
|
+
phase:
|
|
908
|
+
'4/6: device ceremony (keygen → dev-IdP token → enrollFirstDevice)',
|
|
808
909
|
workspace: platformWorkspace,
|
|
809
910
|
awaiting: 'authorKeygen + /dev/mint-token + enrollFirstDevice',
|
|
810
911
|
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
@@ -826,14 +927,17 @@ class NomosTestHarness {
|
|
|
826
927
|
final publicB64 = (kp['public'] ?? '').toString();
|
|
827
928
|
final secret = (kp['secret'] ?? '').toString();
|
|
828
929
|
if (publicB64.isEmpty || secret.isEmpty) {
|
|
829
|
-
throw NomosTestHarnessException(
|
|
930
|
+
throw NomosTestHarnessException(
|
|
931
|
+
'cloudBound: authorKeygen returned no device key');
|
|
830
932
|
}
|
|
831
933
|
final keyHash = sha256.convert(base64Decode(publicB64)).toString();
|
|
832
|
-
final minted = await post(
|
|
934
|
+
final minted = await post(
|
|
935
|
+
'/dev/mint-token', {'sub': bareUid, 'nonce': keyHash});
|
|
833
936
|
final idpToken = (minted['token'] ?? '').toString();
|
|
834
937
|
final issuer = (minted['issuer'] ?? '').toString();
|
|
835
938
|
if (idpToken.isEmpty || issuer.isEmpty) {
|
|
836
|
-
throw NomosTestHarnessException(
|
|
939
|
+
throw NomosTestHarnessException(
|
|
940
|
+
'cloudBound: /dev/mint-token returned no token — is the test cloud started with platform:/deployJsonPath: and a law composing setAuthProvider?');
|
|
837
941
|
}
|
|
838
942
|
// THE KEYLESS ENROLL-ONCE DELEGATION (links[1] of the home cert chain) — device-self-signed,
|
|
839
943
|
// its authority the dev-IdP token. enrollFirstDevice SEEDS it as the user's SignerDelegation, so
|
|
@@ -851,7 +955,8 @@ class NomosTestHarness {
|
|
|
851
955
|
'capturedClock': DateTime.now().toUtc().millisecondsSinceEpoch,
|
|
852
956
|
'epoch': 0,
|
|
853
957
|
}, secret);
|
|
854
|
-
await post(
|
|
958
|
+
await post(
|
|
959
|
+
'/v2/workspaces/$platformWorkspace$enrollFirstDevicePath', {
|
|
855
960
|
'keyHash': keyHash,
|
|
856
961
|
'verifiedVia': issuer,
|
|
857
962
|
'validFromEpoch': 0,
|
|
@@ -860,8 +965,16 @@ class NomosTestHarness {
|
|
|
860
965
|
'actor': subject,
|
|
861
966
|
'authorSecret': secret,
|
|
862
967
|
'delegationCert': jsonEncode(delegationCert),
|
|
863
|
-
},
|
|
864
|
-
|
|
968
|
+
},
|
|
969
|
+
headers: {
|
|
970
|
+
'x-nomos-auth': idpToken
|
|
971
|
+
});
|
|
972
|
+
return (
|
|
973
|
+
publicB64: publicB64,
|
|
974
|
+
secret: secret,
|
|
975
|
+
keyHash: keyHash,
|
|
976
|
+
idpToken: idpToken
|
|
977
|
+
);
|
|
865
978
|
});
|
|
866
979
|
// 5/6 — THE HOME CEREMONY, remotely: the platform's own gateway offer births the home (the ONE
|
|
867
980
|
// control-plane contact; the app lane below never touches the platform again). The offer carries
|
|
@@ -871,7 +984,7 @@ class NomosTestHarness {
|
|
|
871
984
|
await _phased(
|
|
872
985
|
phase: '5/6: remote home-birth ceremony (attested)',
|
|
873
986
|
workspace: platformWorkspace,
|
|
874
|
-
awaiting: 'POST /v2/workspaces/$platformWorkspace
|
|
987
|
+
awaiting: 'POST /v2/workspaces/$platformWorkspace$birthHomePath',
|
|
875
988
|
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
876
989
|
onDiagnostic: onDiagnostic,
|
|
877
990
|
run: () async {
|
|
@@ -886,7 +999,8 @@ class NomosTestHarness {
|
|
|
886
999
|
'parentId': platformWorkspace,
|
|
887
1000
|
'epoch': 0,
|
|
888
1001
|
}, device.secret);
|
|
889
|
-
final out =
|
|
1002
|
+
final out =
|
|
1003
|
+
await post('/v2/workspaces/$platformWorkspace$birthHomePath', {
|
|
890
1004
|
'owner': subject,
|
|
891
1005
|
'ownerKeyHash': device.keyHash,
|
|
892
1006
|
'frameworkHash': frameworkHash,
|
|
@@ -895,9 +1009,15 @@ class NomosTestHarness {
|
|
|
895
1009
|
'homeCert': jsonEncode(homeCert),
|
|
896
1010
|
'actor': subject,
|
|
897
1011
|
'authorSecret': device.secret,
|
|
898
|
-
}, headers: {
|
|
1012
|
+
}, headers: {
|
|
1013
|
+
'x-nomos-auth': device.idpToken
|
|
1014
|
+
});
|
|
899
1015
|
final born = (out['born'] as List?) ?? const [];
|
|
900
|
-
final bornNames = born
|
|
1016
|
+
final bornNames = born
|
|
1017
|
+
.map((b) => (b as Map)['workspace']?.toString())
|
|
1018
|
+
.whereType<String>()
|
|
1019
|
+
.where((w) => w.isNotEmpty)
|
|
1020
|
+
.toList();
|
|
901
1021
|
home = bornNames.isEmpty ? null : bornNames.first;
|
|
902
1022
|
if (home == null) {
|
|
903
1023
|
final outcomes = jsonEncode(out['birthOutcomes'] ?? const []);
|
|
@@ -927,7 +1047,8 @@ class NomosTestHarness {
|
|
|
927
1047
|
// NO DECOY on a cloudBound harness: the app is genuinely multi-workspace (the home plus every
|
|
928
1048
|
// locally-born child), so the plane is never sole-session — the decoy the offline harness
|
|
929
1049
|
// needs (to break single-session inference) would just be a spurious local-only workspace here.
|
|
930
|
-
final _ =
|
|
1050
|
+
final _ =
|
|
1051
|
+
decoySession; // (accepted for signature parity; a cloudBound plane needs no decoy)
|
|
931
1052
|
final harness = NomosTestHarness._(
|
|
932
1053
|
plane.session(home!),
|
|
933
1054
|
home!,
|
|
@@ -951,8 +1072,12 @@ class NomosTestHarness {
|
|
|
951
1072
|
}
|
|
952
1073
|
return harness;
|
|
953
1074
|
} catch (e) {
|
|
954
|
-
try {
|
|
955
|
-
|
|
1075
|
+
try {
|
|
1076
|
+
transport?.dispose();
|
|
1077
|
+
} catch (_) {}
|
|
1078
|
+
try {
|
|
1079
|
+
await workdir.delete(recursive: true);
|
|
1080
|
+
} catch (_) {}
|
|
956
1081
|
rethrow;
|
|
957
1082
|
} finally {
|
|
958
1083
|
http.close(force: true);
|
|
@@ -970,11 +1095,13 @@ class NomosTestHarness {
|
|
|
970
1095
|
/// until every session reports neither `pushFailed` nor `stillAhead`, or [timeout] (then throws with the
|
|
971
1096
|
/// per-workspace last verdicts). Returns the final sync result per workspace.
|
|
972
1097
|
Future<Map<String, Map<String, Object?>>> converge(
|
|
973
|
-
{Duration timeout = const Duration(seconds: 120),
|
|
1098
|
+
{Duration timeout = const Duration(seconds: 120),
|
|
1099
|
+
List<String>? workspaces}) async {
|
|
974
1100
|
// Every LIVE session on the plane (not the harness's construction-time list — a locally-born child
|
|
975
1101
|
// acquired after construction must converge too), PARENT-FIRST: the home syncs before its children, so
|
|
976
1102
|
// its birth offer-effect creates each child's remote custody BEFORE that child pushes its own writes.
|
|
977
|
-
final live =
|
|
1103
|
+
final live =
|
|
1104
|
+
bridge.connectedSessions.where((w) => w != _decoyWorkspace).toList();
|
|
978
1105
|
final hw = homeWorkspace;
|
|
979
1106
|
if (hw != null && live.remove(hw)) live.insert(0, hw); // the parent leads
|
|
980
1107
|
final targets = workspaces ?? live;
|
|
@@ -984,9 +1111,11 @@ class NomosTestHarness {
|
|
|
984
1111
|
while (pending.isNotEmpty) {
|
|
985
1112
|
for (final ws in List.of(pending)) {
|
|
986
1113
|
try {
|
|
987
|
-
final r = (await bridge.session(ws).sync(admit: true))
|
|
1114
|
+
final r = (await bridge.session(ws).sync(admit: true))
|
|
1115
|
+
.cast<String, Object?>();
|
|
988
1116
|
last[ws] = r;
|
|
989
|
-
if (r['pushFailed'] != true && r['stillAhead'] != true)
|
|
1117
|
+
if (r['pushFailed'] != true && r['stillAhead'] != true)
|
|
1118
|
+
pending.remove(ws);
|
|
990
1119
|
} catch (e) {
|
|
991
1120
|
last[ws] = {'error': e.toString()};
|
|
992
1121
|
}
|
|
@@ -1056,7 +1185,9 @@ class NomosTestHarness {
|
|
|
1056
1185
|
final info = await _phased(
|
|
1057
1186
|
phase: 'seed: offline law install',
|
|
1058
1187
|
workspace: workspace,
|
|
1059
|
-
awaiting: lane.useInRepoScript
|
|
1188
|
+
awaiting: lane.useInRepoScript
|
|
1189
|
+
? 'offline-install.ts'
|
|
1190
|
+
: 'githolon harness-install',
|
|
1060
1191
|
timeout: _phaseTimeout ?? _kInstallDeadline,
|
|
1061
1192
|
onDiagnostic: _onDiagnostic,
|
|
1062
1193
|
run: () => _runInstall(
|
|
@@ -1119,7 +1250,8 @@ class NomosTestHarness {
|
|
|
1119
1250
|
/// un-synced writes + the DLQ), with its head + law hashes + capture time. The bytes are the SAME
|
|
1120
1251
|
/// base64 `connect(restoreFrom:)` consumes — [restoreWorkspace] round-trips it. O(1)-ish (a tree serialize).
|
|
1121
1252
|
Future<WorkspaceSnapshot> snapshotWorkspace(String workspace) async {
|
|
1122
|
-
final s = session(
|
|
1253
|
+
final s = session(
|
|
1254
|
+
workspace); // throws NomosTestHarnessException if not a connected session
|
|
1123
1255
|
return _phased(
|
|
1124
1256
|
phase: 'snapshotWorkspace',
|
|
1125
1257
|
workspace: workspace,
|
|
@@ -1147,13 +1279,15 @@ class NomosTestHarness {
|
|
|
1147
1279
|
for (final ws in _sessionNames) {
|
|
1148
1280
|
out[ws] = await snapshotWorkspace(ws);
|
|
1149
1281
|
}
|
|
1150
|
-
return PlaneSnapshot(
|
|
1282
|
+
return PlaneSnapshot(
|
|
1283
|
+
workspaces: Map.unmodifiable(out), capturedAt: DateTime.now().toUtc());
|
|
1151
1284
|
}
|
|
1152
1285
|
|
|
1153
1286
|
/// Restore a workspace FROM a [WorkspaceSnapshot] — re-open its session on this plane from the snapshot's
|
|
1154
1287
|
/// LOCAL CUSTODY (no network clone), replacing the resident holon. The workspace need not currently be a
|
|
1155
1288
|
/// session (a fresh restore is allowed); its name is taken from [snapshot] unless [into] overrides it.
|
|
1156
|
-
Future<NomosSession> restoreWorkspace(WorkspaceSnapshot snapshot,
|
|
1289
|
+
Future<NomosSession> restoreWorkspace(WorkspaceSnapshot snapshot,
|
|
1290
|
+
{String? into}) async {
|
|
1157
1291
|
final ws = into ?? snapshot.workspace;
|
|
1158
1292
|
await _phased(
|
|
1159
1293
|
phase: 'restoreWorkspace',
|
|
@@ -1191,7 +1325,8 @@ class NomosTestHarness {
|
|
|
1191
1325
|
if (_disposed) return;
|
|
1192
1326
|
_disposed = true;
|
|
1193
1327
|
final f = _onDiagnostic;
|
|
1194
|
-
if (f != null)
|
|
1328
|
+
if (f != null)
|
|
1329
|
+
f('[nomos-test-harness] phase dispose (≤${(_phaseTimeout ?? _kOtherDeadline).inSeconds}s) …');
|
|
1195
1330
|
try {
|
|
1196
1331
|
await _transport.dispose().timeout(_phaseTimeout ?? _kOtherDeadline,
|
|
1197
1332
|
onTimeout: () => throw HarnessPhaseTimeout(
|
|
@@ -1200,7 +1335,8 @@ class NomosTestHarness {
|
|
|
1200
1335
|
elapsed: _phaseTimeout ?? _kOtherDeadline,
|
|
1201
1336
|
awaiting: 'NodeRunnerTransport.dispose'));
|
|
1202
1337
|
} catch (e) {
|
|
1203
|
-
_diag(
|
|
1338
|
+
_diag(
|
|
1339
|
+
'[nomos-test-harness] transport dispose error (continuing to temp-dir cleanup): $e');
|
|
1204
1340
|
}
|
|
1205
1341
|
try {
|
|
1206
1342
|
if (await _workdir.exists()) await _workdir.delete(recursive: true);
|
|
@@ -1235,9 +1371,12 @@ class NomosTestHarness {
|
|
|
1235
1371
|
// Both lanes take the SAME flags (--deploy/--workspace/--out/--runtime) and print the SAME one-line JSON
|
|
1236
1372
|
// contract; only the leading command differs (node <script> vs `<githolon...> harness-install`).
|
|
1237
1373
|
final common = <String>[
|
|
1238
|
-
'--deploy',
|
|
1239
|
-
'
|
|
1240
|
-
'--
|
|
1374
|
+
'--deploy',
|
|
1375
|
+
deployJsonPaths.join(','),
|
|
1376
|
+
'--workspace',
|
|
1377
|
+
workspace,
|
|
1378
|
+
'--out',
|
|
1379
|
+
snapshot,
|
|
1241
1380
|
if (runtimeDir != null) ...['--runtime', runtimeDir],
|
|
1242
1381
|
];
|
|
1243
1382
|
final String executable;
|
|
@@ -1247,8 +1386,11 @@ class NomosTestHarness {
|
|
|
1247
1386
|
args = [inRepoScript, ...common];
|
|
1248
1387
|
} else {
|
|
1249
1388
|
// `githolonCmd` is a command line (e.g. `npx githolon` or `node /abs/cli.mjs`) — split into tokens.
|
|
1250
|
-
final tokens =
|
|
1251
|
-
|
|
1389
|
+
final tokens = githolonCmd
|
|
1390
|
+
.trim()
|
|
1391
|
+
.split(RegExp(r'\s+'))
|
|
1392
|
+
.where((t) => t.isNotEmpty)
|
|
1393
|
+
.toList();
|
|
1252
1394
|
if (tokens.isEmpty) {
|
|
1253
1395
|
throw NomosTestHarnessException(
|
|
1254
1396
|
'no install helper resolved — pass installHelper: (an offline-install.ts path, or a githolon command)');
|
|
@@ -1335,12 +1477,15 @@ class WorkspaceSnapshot {
|
|
|
1335
1477
|
'capturedAt': capturedAt.toUtc().toIso8601String(),
|
|
1336
1478
|
};
|
|
1337
1479
|
|
|
1338
|
-
factory WorkspaceSnapshot.fromJson(Map<String, Object?> j) =>
|
|
1480
|
+
factory WorkspaceSnapshot.fromJson(Map<String, Object?> j) =>
|
|
1481
|
+
WorkspaceSnapshot(
|
|
1339
1482
|
workspace: j['workspace'] as String,
|
|
1340
1483
|
snapshotB64: j['snapshotB64'] as String,
|
|
1341
1484
|
head: (j['head'] ?? '') as String,
|
|
1342
|
-
lawHashes:
|
|
1343
|
-
|
|
1485
|
+
lawHashes:
|
|
1486
|
+
((j['lawHashes'] as List?) ?? const []).map((e) => '$e').toList(),
|
|
1487
|
+
capturedAt: DateTime.parse((j['capturedAt'] ??
|
|
1488
|
+
DateTime.now().toUtc().toIso8601String()) as String),
|
|
1344
1489
|
);
|
|
1345
1490
|
|
|
1346
1491
|
@override
|
|
@@ -1363,8 +1508,12 @@ class PlaneSnapshot {
|
|
|
1363
1508
|
};
|
|
1364
1509
|
|
|
1365
1510
|
factory PlaneSnapshot.fromJson(Map<String, Object?> j) => PlaneSnapshot(
|
|
1366
|
-
workspaces: ((j['workspaces'] as Map?) ?? const {}).map(
|
|
1367
|
-
|
|
1368
|
-
|
|
1511
|
+
workspaces: ((j['workspaces'] as Map?) ?? const {}).map((k, v) =>
|
|
1512
|
+
MapEntry(
|
|
1513
|
+
'$k',
|
|
1514
|
+
WorkspaceSnapshot.fromJson(
|
|
1515
|
+
(v as Map).cast<String, Object?>()))),
|
|
1516
|
+
capturedAt: DateTime.parse((j['capturedAt'] ??
|
|
1517
|
+
DateTime.now().toUtc().toIso8601String()) as String),
|
|
1369
1518
|
);
|
|
1370
1519
|
}
|