@githolon/dsl 0.102.1 → 0.102.3

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.
@@ -48,6 +48,114 @@ import 'node_runner_transport.dart';
48
48
  import 'session_bound.dart' show DomainNotInstalledForSession;
49
49
  import 'webview_bridge.dart' show NomosBridge, NomosSession;
50
50
 
51
+ /// A source checkout is itself a coordinated release candidate: compiler, framework packages and kernels
52
+ /// move together. Generated scenarios must not combine those source-generated laws with a recently-written
53
+ /// but older global runtime cache. Published packages have no release/ directory and keep the normal
54
+ /// release-pinned runtime acquisition path; this branch is only the zero-ceremony dogfood path.
55
+ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
56
+ var cursor = File(nodeEntry).absolute.parent;
57
+ Directory? root;
58
+ for (var depth = 0; depth < 12; depth++) {
59
+ if (File('${cursor.path}/release/kernel-candidate.json').existsSync() &&
60
+ File('${cursor.path}/cloud/dev-server/kernel-testmint.wasm')
61
+ .existsSync()) {
62
+ root = cursor;
63
+ break;
64
+ }
65
+ final parent = cursor.parent;
66
+ if (parent.path == cursor.path) break;
67
+ cursor = parent;
68
+ }
69
+ if (root == null) return null;
70
+
71
+ final descriptor = (jsonDecode(
72
+ await File(
73
+ '${root.path}/release/kernel-candidate.json',
74
+ ).readAsString(),
75
+ ) as Map)
76
+ .cast<String, Object?>();
77
+ String expected(String key) =>
78
+ (((descriptor[key] as Map?)?['sha256'])?.toString() ?? '').trim();
79
+ final productionExpected = expected('production');
80
+ final testMintExpected = expected('testMint');
81
+ final production = File(
82
+ '${root.path}/cloud/cloudflare-shell/vendor/holon_bytes.wasmbin',
83
+ );
84
+ final testMint = File(
85
+ '${root.path}/cloud/dev-server/kernel-testmint.wasm',
86
+ );
87
+ final bootstrap = File(
88
+ '${root.path}/cloud/cloudflare-shell/domains/domain_bootstrap.package.usda',
89
+ );
90
+ final nomos = File(
91
+ '${root.path}/cloud/cloudflare-shell/domains/domain_nomos.package.usda',
92
+ );
93
+ for (final file in [production, testMint, bootstrap, nomos]) {
94
+ if (!file.existsSync()) {
95
+ throw NomosTestHarnessException(
96
+ 'source release candidate is incomplete: ${file.path} is missing',
97
+ );
98
+ }
99
+ }
100
+ final productionBytes = await production.readAsBytes();
101
+ final testMintBytes = await testMint.readAsBytes();
102
+ final productionActual = sha256.convert(productionBytes).toString();
103
+ final testMintActual = sha256.convert(testMintBytes).toString();
104
+ String short(String value) =>
105
+ value.length <= 12 ? value : value.substring(0, 12);
106
+ if (productionActual != productionExpected ||
107
+ testMintActual != testMintExpected) {
108
+ throw NomosTestHarnessException(
109
+ 'source release candidate kernel mismatch: release/kernel-candidate.json '
110
+ 'expects ${short(productionExpected)}/${short(testMintExpected)}, but the staged artifacts are '
111
+ '${short(productionActual)}/${short(testMintActual)} — run the kernel candidate preparation before testing',
112
+ );
113
+ }
114
+ final bootstrapText = await bootstrap.readAsString();
115
+ final nomosText = await nomos.readAsString();
116
+ final bundleIdentity = sha256
117
+ .convert(
118
+ utf8.encode(
119
+ '$productionActual:$testMintActual:'
120
+ '${sha256.convert(utf8.encode(bootstrapText))}:'
121
+ '${sha256.convert(utf8.encode(nomosText))}',
122
+ ),
123
+ )
124
+ .toString();
125
+ final runtime = Directory(
126
+ '${Directory.systemTemp.path}/nomos-source-runtime-${bundleIdentity.substring(0, 20)}',
127
+ );
128
+ await runtime.create(recursive: true);
129
+ Future<void> publish(String name, List<int> bytes) async {
130
+ final target = File('${runtime.path}/$name');
131
+ if (target.existsSync() && target.lengthSync() == bytes.length) return;
132
+ final candidate = File(
133
+ '${target.path}.tmp-${pid}-${DateTime.now().microsecondsSinceEpoch}');
134
+ await candidate.writeAsBytes(bytes, flush: true);
135
+ try {
136
+ await candidate.rename(target.path);
137
+ } on FileSystemException {
138
+ if (!target.existsSync()) rethrow;
139
+ try {
140
+ await candidate.delete();
141
+ } on FileSystemException {
142
+ // Another process won the same content-addressed publication race; its complete target is enough.
143
+ }
144
+ }
145
+ }
146
+
147
+ await publish('holon.wasm', productionBytes);
148
+ await publish('holon-testmint.wasm', testMintBytes);
149
+ await publish(
150
+ 'packages.json',
151
+ utf8.encode(jsonEncode(<String, String>{
152
+ 'bootstrap': bootstrapText,
153
+ 'nomos': nomosText,
154
+ })),
155
+ );
156
+ return runtime.path;
157
+ }
158
+
51
159
  /// Framework-internal classifier for a sync result that can never become converged by retrying. Generated
52
160
  /// scenarios use it through [NomosTestHarness.converge]; it is public only inside `package:nomos_client/src`
53
161
  /// so the false-green contract has a cheap regression test without booting a cloud.
@@ -58,8 +166,11 @@ String? nmsPermanentConvergenceFailure(Map<String, Object?> result) {
58
166
  : const [];
59
167
  final rejected = sessions
60
168
  .whereType<Map>()
61
- .expand((session) =>
62
- session['rejected'] is List ? session['rejected'] as List : const [])
169
+ .expand(
170
+ (session) => session['rejected'] is List
171
+ ? session['rejected'] as List
172
+ : const [],
173
+ )
63
174
  .toList();
64
175
  final deadLetters = admission is Map && admission['deadLetters'] is List
65
176
  ? admission['deadLetters'] as List
@@ -137,9 +248,10 @@ class HarnessPhaseTimeout extends NomosTestHarnessException {
137
248
  required this.awaiting,
138
249
  this.likelyCauses = const [],
139
250
  }) : super(
140
- 'phase "$phase"${workspace != null ? ' (workspace "$workspace")' : ''} '
141
- 'did not complete within ${elapsed.inSeconds}s while awaiting `$awaiting`'
142
- '${likelyCauses.isEmpty ? '' : ' — likely causes: ${likelyCauses.map((c) => '\n • $c').join()}'}');
251
+ 'phase "$phase"${workspace != null ? ' (workspace "$workspace")' : ''} '
252
+ 'did not complete within ${elapsed.inSeconds}s while awaiting `$awaiting`'
253
+ '${likelyCauses.isEmpty ? '' : ' — likely causes: ${likelyCauses.map((c) => '\n • $c').join()}'}',
254
+ );
143
255
  @override
144
256
  String toString() => 'HarnessPhaseTimeout: $message';
145
257
  }
@@ -234,25 +346,32 @@ class NomosTestHarness {
234
346
  final ws = homeWorkspace;
235
347
  if (ws == null) {
236
348
  throw NomosTestHarnessException(
237
- 'this harness has no primary application session');
349
+ 'this harness has no primary application session',
350
+ );
238
351
  }
239
352
  return session(ws);
240
353
  }
241
354
 
242
- NomosTestHarness._(this.bridge, this.workspace, this._transport,
243
- this._workdir, this._onDiagnostic, List<String> sessionNames,
244
- {Map<String, String> devicePublics = const {},
245
- Map<String, String> deviceSecrets = const {},
246
- this.frameworkHash,
247
- Map<String, List<String>> lawHashes = const {},
248
- Map<String, String> snapshots = const {},
249
- _InstallLane? installLane,
250
- Duration? phaseTimeout,
251
- _CloudRestartLane? cloudRestartLane,
252
- this.testCloud,
253
- this.homeWorkspace})
254
- : _sessionNames = List.of(
255
- sessionNames), // GROWABLE — restoreWorkspace may add a fresh session
355
+ NomosTestHarness._(
356
+ this.bridge,
357
+ this.workspace,
358
+ this._transport,
359
+ this._workdir,
360
+ this._onDiagnostic,
361
+ List<String> sessionNames, {
362
+ Map<String, String> devicePublics = const {},
363
+ Map<String, String> deviceSecrets = const {},
364
+ this.frameworkHash,
365
+ Map<String, List<String>> lawHashes = const {},
366
+ Map<String, String> snapshots = const {},
367
+ _InstallLane? installLane,
368
+ Duration? phaseTimeout,
369
+ _CloudRestartLane? cloudRestartLane,
370
+ this.testCloud,
371
+ this.homeWorkspace,
372
+ }) : _sessionNames = List.of(
373
+ sessionNames,
374
+ ), // GROWABLE — restoreWorkspace may add a fresh session
256
375
  _devicePublics = devicePublics,
257
376
  _deviceSecrets = deviceSecrets,
258
377
  _lawHashes = Map.of(lawHashes),
@@ -286,13 +405,16 @@ class NomosTestHarness {
286
405
  final where = workspace != null ? ' "$workspace"' : '';
287
406
  log('phase $phase$where (≤${timeout.inSeconds}s) …');
288
407
  final sw = Stopwatch()..start();
289
- final r = await run().timeout(timeout,
290
- onTimeout: () => throw HarnessPhaseTimeout(
291
- phase: phase,
292
- workspace: workspace,
293
- elapsed: timeout,
294
- awaiting: awaiting,
295
- likelyCauses: likelyCauses));
408
+ final r = await run().timeout(
409
+ timeout,
410
+ onTimeout: () => throw HarnessPhaseTimeout(
411
+ phase: phase,
412
+ workspace: workspace,
413
+ elapsed: timeout,
414
+ awaiting: awaiting,
415
+ likelyCauses: likelyCauses,
416
+ ),
417
+ );
296
418
  log('phase $phase$where done (${sw.elapsedMilliseconds}ms)');
297
419
  return r;
298
420
  }
@@ -322,13 +444,16 @@ class NomosTestHarness {
322
444
  final secret = _deviceSecrets[workspace];
323
445
  if (secret == null || secret.isEmpty) {
324
446
  throw NomosTestHarnessException(
325
- 'the generated scenario cannot establish its business permission because the test device secret is unavailable');
447
+ 'the generated scenario cannot establish its business permission because the test device secret is unavailable',
448
+ );
326
449
  }
327
- final rows = await session(workspace)
328
- .queryById('birth-attestation:self', null, subject);
450
+ final rows = await session(
451
+ workspace,
452
+ ).queryById('birth-attestation:self', null, subject);
329
453
  if (rows.length != 1) {
330
454
  throw NomosTestHarnessException(
331
- 'the generated scenario expected one verified birth attestation before establishing business permission; found ${rows.length}');
455
+ 'the generated scenario expected one verified birth attestation before establishing business permission; found ${rows.length}',
456
+ );
332
457
  }
333
458
  final row = rows.single;
334
459
  final data = row['data'] is Map
@@ -349,7 +474,8 @@ class NomosTestHarness {
349
474
  final certificate = issued['certString']?.toString();
350
475
  if (certificate == null || certificate.isEmpty) {
351
476
  throw NomosTestHarnessException(
352
- 'the generated scenario could not mint the parent-rooted business delegation');
477
+ 'the generated scenario could not mint the parent-rooted business delegation',
478
+ );
353
479
  }
354
480
  await session(workspace).offerOutcome(
355
481
  domain: domain,
@@ -373,7 +499,8 @@ class NomosTestHarness {
373
499
  NomosSession session(String workspace) {
374
500
  if (!_sessionNames.contains(workspace)) {
375
501
  throw NomosTestHarnessException(
376
- 'no session "$workspace" on this harness — connected sessions: $_sessionNames');
502
+ 'no session "$workspace" on this harness — connected sessions: $_sessionNames',
503
+ );
377
504
  }
378
505
  return bridge.session(workspace);
379
506
  }
@@ -415,13 +542,16 @@ class NomosTestHarness {
415
542
  }) async {
416
543
  if (deployJsonPaths.isEmpty) {
417
544
  throw NomosTestHarnessException(
418
- 'no deployJsonPaths — pass the build/<name>.deploy.json `npx githolon compile` emits');
545
+ 'no deployJsonPaths — pass the build/<name>.deploy.json `npx githolon compile` emits',
546
+ );
419
547
  }
420
548
  final entry = nodeEntry ??
421
549
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
422
550
  final effectiveRuntimeDir = runtimeDir ??
551
+ Platform.environment['NOMOS_CANDIDATE_RUNTIME'] ??
423
552
  Platform.environment['NOMOS_DART_RUNTIME'] ??
424
- Platform.environment['NOMOS_OFFLINE_RUNTIME'];
553
+ Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
554
+ await _sourceCandidateRuntime(entry);
425
555
  // THE INSTALL LANE. [installHelper] is the one seam (PACKAGING NOTE above): either the in-repo
426
556
  // `offline-install.mjs` node script, OR the published `githolon harness-install` subcommand (the CLI
427
557
  // bundles the engine + this exact install sequence, so a published tenant needs nothing but `githolon`).
@@ -462,28 +592,29 @@ class NomosTestHarness {
462
592
  try {
463
593
  final snapshot = '${workdir.path}/snapshot.bin';
464
594
  final info = await _phased(
465
- phase:
466
- '1/4: offline law install (${deployJsonPaths.length} package(s))',
595
+ phase:
596
+ '1/4: offline law install (${deployJsonPaths.length} package(s))',
597
+ workspace: installWorkspace,
598
+ awaiting:
599
+ useInRepoScript ? 'offline-install.ts' : 'githolon harness-install',
600
+ timeout: phaseTimeout ?? _kInstallDeadline,
601
+ onDiagnostic: onDiagnostic,
602
+ likelyCauses: const [
603
+ 'a cold npx download of githolon (pre-warm with any `githolon` run)',
604
+ 'a very large law package folding through the offline gate — raise phaseTimeout',
605
+ ],
606
+ run: () => _runInstall(
607
+ node: node,
608
+ useInRepoScript: useInRepoScript,
609
+ inRepoScript: resolvedHelper,
610
+ githolonCmd: githolonCmd,
611
+ deployJsonPaths: deployJsonPaths,
467
612
  workspace: installWorkspace,
468
- awaiting: useInRepoScript
469
- ? 'offline-install.ts'
470
- : 'githolon harness-install',
471
- timeout: phaseTimeout ?? _kInstallDeadline,
613
+ snapshot: snapshot,
614
+ runtimeDir: effectiveRuntimeDir,
472
615
  onDiagnostic: onDiagnostic,
473
- likelyCauses: const [
474
- 'a cold npx download of githolon (pre-warm with any `githolon` run)',
475
- 'a very large law package folding through the offline gate — raise phaseTimeout',
476
- ],
477
- run: () => _runInstall(
478
- node: node,
479
- useInRepoScript: useInRepoScript,
480
- inRepoScript: resolvedHelper,
481
- githolonCmd: githolonCmd,
482
- deployJsonPaths: deployJsonPaths,
483
- workspace: installWorkspace,
484
- snapshot: snapshot,
485
- runtimeDir: effectiveRuntimeDir,
486
- onDiagnostic: onDiagnostic));
616
+ ),
617
+ );
487
618
 
488
619
  final runnerEnvironment = <String, String>{
489
620
  'NOMOS_OFFLINE': '1',
@@ -497,50 +628,53 @@ class NomosTestHarness {
497
628
  'NOMOS_OFFLINE_RUNTIME': effectiveRuntimeDir,
498
629
  };
499
630
  transport = await _phased(
500
- phase: '2/4: spawn the node runner',
501
- awaiting: 'Process.start($node)',
502
- timeout: phaseTimeout ?? _kSpawnDeadline,
631
+ phase: '2/4: spawn the node runner',
632
+ awaiting: 'Process.start($node)',
633
+ timeout: phaseTimeout ?? _kSpawnDeadline,
634
+ onDiagnostic: onDiagnostic,
635
+ run: () => NodeRunnerTransport.start(
636
+ entry: entry,
637
+ resolveFrom: resolveFrom,
638
+ node: node,
639
+ environment: runnerEnvironment,
503
640
  onDiagnostic: onDiagnostic,
504
- run: () => NodeRunnerTransport.start(
505
- entry: entry,
506
- resolveFrom: resolveFrom,
507
- node: node,
508
- environment: runnerEnvironment,
509
- onDiagnostic: onDiagnostic,
510
- ));
641
+ ),
642
+ );
511
643
  final plane = NomosBridge(transport!);
512
644
  await _phased(
513
- phase: '3/4: runner ready',
514
- awaiting: "event:'ready'",
515
- timeout: phaseTimeout ?? readyTimeout,
516
- onDiagnostic: onDiagnostic,
517
- likelyCauses: const [
518
- 'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + packages.json)',
519
- 'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
520
- ],
521
- run: () => plane.ready);
645
+ phase: '3/4: runner ready',
646
+ awaiting: "event:'ready'",
647
+ timeout: phaseTimeout ?? readyTimeout,
648
+ onDiagnostic: onDiagnostic,
649
+ likelyCauses: const [
650
+ 'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + packages.json)',
651
+ 'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
652
+ ],
653
+ run: () => plane.ready,
654
+ );
522
655
  await _phased(
523
- phase: '4/4: connect session',
656
+ phase: '4/4: connect session',
657
+ workspace: installWorkspace,
658
+ awaiting: 'connect',
659
+ timeout: phaseTimeout ?? connectTimeout,
660
+ onDiagnostic: onDiagnostic,
661
+ likelyCauses: const [
662
+ 'a large ledger converge-fold can be slow — raise connectTimeout/phaseTimeout',
663
+ 'run with onDiagnostic to see where the runner blocked',
664
+ ],
665
+ run: () => plane.connect(
666
+ cloud:
667
+ 'https://offline.invalid', // ignored: the offline runner serves everything locally
524
668
  workspace: installWorkspace,
525
- awaiting: 'connect',
526
- timeout: phaseTimeout ?? connectTimeout,
527
- onDiagnostic: onDiagnostic,
528
- likelyCauses: const [
529
- 'a large ledger converge-fold can be slow — raise connectTimeout/phaseTimeout',
530
- 'run with onDiagnostic to see where the runner blocked',
531
- ],
532
- run: () => plane.connect(
533
- cloud:
534
- 'https://offline.invalid', // ignored: the offline runner serves everything locally
535
- workspace: installWorkspace,
536
- session: installWorkspace,
537
- clientId: clientId,
538
- allowUnborn: true,
539
- // THE HARNESS DEVICE (era-0): the install lane mints one device key per plane — sessions
540
- // sign with it, and the client completes the self-serve birth ceremony (enrol + cert)
541
- // with the SAME key, so offline `.births()` offers Just Work.
542
- authorSecret: info['deviceSecret']?.toString(),
543
- ));
669
+ session: installWorkspace,
670
+ clientId: clientId,
671
+ allowUnborn: true,
672
+ // THE HARNESS DEVICE (era-0): the install lane mints one device key per plane — sessions
673
+ // sign with it, and the client completes the self-serve birth ceremony (enrol + cert)
674
+ // with the SAME key, so offline `.births()` offers Just Work.
675
+ authorSecret: info['deviceSecret']?.toString(),
676
+ ),
677
+ );
544
678
  if (birthsApplicationHome) {
545
679
  final devicePublic = info['devicePublic']?.toString();
546
680
  final deviceSecret = info['deviceSecret']?.toString();
@@ -557,7 +691,8 @@ class NomosTestHarness {
557
691
  framework.isEmpty ||
558
692
  laws.isEmpty) {
559
693
  throw NomosTestHarnessException(
560
- 'the offline application parent did not return the framework law, application law and test device needed to birth the signed-in application');
694
+ 'the offline application parent did not return the framework law, application law and test device needed to birth the signed-in application',
695
+ );
561
696
  }
562
697
  final keyHash = sha256.convert(base64Decode(devicePublic)).toString();
563
698
  final outcome = await _phased(
@@ -575,7 +710,9 @@ class NomosTestHarness {
575
710
  'owner': owner,
576
711
  'ownerKeyHash': keyHash,
577
712
  'frameworkHash': framework,
578
- 'lawHash': laws.first,
713
+ // Ordered application deployment is reference packages first, deferred child payload last.
714
+ // The parent serves the references; the receiving group is born with only its payload law.
715
+ 'lawHash': laws.last,
579
716
  'bornAt': DateTime.now().toUtc().toIso8601String(),
580
717
  },
581
718
  ),
@@ -591,8 +728,9 @@ class NomosTestHarness {
591
728
  outcome.born.single != workspace ||
592
729
  bornStatus != 'born') {
593
730
  throw NomosTestHarnessException(
594
- 'the offline application parent did not birth "$workspace" cleanly '
595
- '(born=${outcome.born}, birthOutcomes=${outcome.birthOutcomes})');
731
+ 'the offline application parent did not birth "$workspace" cleanly '
732
+ '(born=${outcome.born}, birthOutcomes=${outcome.birthOutcomes})',
733
+ );
596
734
  }
597
735
  await _phased(
598
736
  phase: 'application: mount the signed-in application',
@@ -616,7 +754,8 @@ class NomosTestHarness {
616
754
  deviceSecret == null ||
617
755
  deviceSecret.isEmpty) {
618
756
  throw NomosTestHarnessException(
619
- 'the offline install did not return the test device key needed to make "$owner" an application-ready signer');
757
+ 'the offline install did not return the test device key needed to make "$owner" an application-ready signer',
758
+ );
620
759
  }
621
760
  try {
622
761
  final keyHash = sha256.convert(base64Decode(devicePublic)).toString();
@@ -648,7 +787,8 @@ class NomosTestHarness {
648
787
  // signer route. Preserve their old harness behaviour, while keeping
649
788
  // every refusal from a current package fatal.
650
789
  if (!error.toString().contains(
651
- "no active installed law declares domain '__nomos_application_home_v1'")) {
790
+ "no active installed law declares domain '__nomos_application_home_v1'",
791
+ )) {
652
792
  rethrow;
653
793
  }
654
794
  }
@@ -732,8 +872,19 @@ class NomosTestHarness {
732
872
  // application-ready.
733
873
  }
734
874
  }
735
- phase(
736
- 'ready — session "$workspace" connected (law ${(info['lawHashes'] as List?)?.isNotEmpty == true ? (info['lawHashes'] as List).first.toString().substring(0, 12) : "installed"})');
875
+ final installedHashes =
876
+ ((info['lawHashes'] as List?) ?? const [])
877
+ .map((hash) => hash.toString())
878
+ .where((hash) => hash.isNotEmpty)
879
+ .toList();
880
+ final sessionLaw =
881
+ installedHashes.isEmpty
882
+ ? 'installed'
883
+ : (birthsApplicationHome
884
+ ? installedHashes.last
885
+ : installedHashes.first)
886
+ .substring(0, 12);
887
+ phase('ready — session "$workspace" connected (law $sessionLaw)');
737
888
  // THE DECOY SESSION (multi-workspace by default in tests): sole-session inference is how
738
889
  // single-workspace assumptions ship — with one session connected, an op that forgot its session
739
890
  // silently works in tests and only refuses in production when the app connects its second
@@ -743,58 +894,66 @@ class NomosTestHarness {
743
894
  // single-session trips. Opt out with `decoySession: false` (please don't).
744
895
  if (decoySession && !birthsApplicationHome) {
745
896
  await _phased(
746
- phase: 'decoy: connect the decoy session',
747
- workspace: _decoyWorkspace,
748
- awaiting: 'connect',
749
- timeout: phaseTimeout ?? connectTimeout,
750
- onDiagnostic: onDiagnostic,
751
- run: () async => plane.connect(
752
- cloud: 'https://offline.invalid',
753
- workspace: _decoyWorkspace,
754
- session: _decoyWorkspace,
755
- clientId: '$clientId-decoy',
756
- allowUnborn: true,
757
- restoreFrom: base64Encode(await File(snapshot).readAsBytes()),
758
- ));
759
- }
760
- final harness = NomosTestHarness._(plane.session(workspace), workspace,
761
- transport, workdir, onDiagnostic, [workspace],
762
- devicePublics: {
763
- if (info['devicePublic'] != null)
764
- workspace: info['devicePublic'].toString(),
765
- },
766
- deviceSecrets: {
767
- if (info['deviceSecret'] != null)
768
- workspace: info['deviceSecret'].toString(),
769
- },
770
- frameworkHash: info['frameworkHash']?.toString(),
771
- lawHashes: {
772
- workspace: ((info['lawHashes'] as List?) ?? const [])
773
- .map((e) => e.toString())
774
- .toList()
775
- },
776
- snapshots: {
777
- workspace: base64Encode(await File(snapshot).readAsBytes())
778
- },
779
- installLane: _InstallLane(
780
- node: node,
781
- useInRepoScript: useInRepoScript,
782
- inRepoScript: resolvedHelper,
783
- githolonCmd: githolonCmd,
784
- runtimeDir: effectiveRuntimeDir,
785
- clientId: clientId),
786
- phaseTimeout: phaseTimeout,
787
- cloudRestartLane: _CloudRestartLane(
788
- entry: entry,
789
- node: node,
897
+ phase: 'decoy: connect the decoy session',
898
+ workspace: _decoyWorkspace,
899
+ awaiting: 'connect',
900
+ timeout: phaseTimeout ?? connectTimeout,
901
+ onDiagnostic: onDiagnostic,
902
+ run: () async => plane.connect(
790
903
  cloud: 'https://offline.invalid',
791
- authToken: '',
792
- authorSecret: info['deviceSecret']?.toString() ?? '',
793
- clientId: clientId,
794
- resolveFrom: resolveFrom,
795
- environment: runnerEnvironment,
904
+ workspace: _decoyWorkspace,
905
+ session: _decoyWorkspace,
906
+ clientId: '$clientId-decoy',
907
+ allowUnborn: true,
908
+ restoreFrom: base64Encode(await File(snapshot).readAsBytes()),
796
909
  ),
797
- homeWorkspace: workspace);
910
+ );
911
+ }
912
+ final harness = NomosTestHarness._(
913
+ plane.session(workspace),
914
+ workspace,
915
+ transport,
916
+ workdir,
917
+ onDiagnostic,
918
+ [workspace],
919
+ devicePublics: {
920
+ if (info['devicePublic'] != null)
921
+ workspace: info['devicePublic'].toString(),
922
+ },
923
+ deviceSecrets: {
924
+ if (info['deviceSecret'] != null)
925
+ workspace: info['deviceSecret'].toString(),
926
+ },
927
+ frameworkHash: info['frameworkHash']?.toString(),
928
+ lawHashes: {
929
+ workspace: ((info['lawHashes'] as List?) ?? const [])
930
+ .map((e) => e.toString())
931
+ .toList(),
932
+ },
933
+ snapshots: {
934
+ workspace: base64Encode(await File(snapshot).readAsBytes()),
935
+ },
936
+ installLane: _InstallLane(
937
+ node: node,
938
+ useInRepoScript: useInRepoScript,
939
+ inRepoScript: resolvedHelper,
940
+ githolonCmd: githolonCmd,
941
+ runtimeDir: effectiveRuntimeDir,
942
+ clientId: clientId,
943
+ ),
944
+ phaseTimeout: phaseTimeout,
945
+ cloudRestartLane: _CloudRestartLane(
946
+ entry: entry,
947
+ node: node,
948
+ cloud: 'https://offline.invalid',
949
+ authToken: '',
950
+ authorSecret: info['deviceSecret']?.toString() ?? '',
951
+ clientId: clientId,
952
+ resolveFrom: resolveFrom,
953
+ environment: runnerEnvironment,
954
+ ),
955
+ homeWorkspace: workspace,
956
+ );
798
957
  if (autoTearDown && registerTearDown != null) {
799
958
  registerTearDown(harness.dispose);
800
959
  }
@@ -844,19 +1003,23 @@ class NomosTestHarness {
844
1003
  }) async {
845
1004
  if (workspaces.isEmpty) {
846
1005
  throw NomosTestHarnessException(
847
- 'no workspaces — pass { workspaceName: [build/<name>.deploy.json] } for each colocated workspace');
1006
+ 'no workspaces — pass { workspaceName: [build/<name>.deploy.json] } for each colocated workspace',
1007
+ );
848
1008
  }
849
1009
  for (final e in workspaces.entries) {
850
1010
  if (e.value.isEmpty) {
851
1011
  throw NomosTestHarnessException(
852
- 'workspace "${e.key}" has no deployJsonPaths — pass its build/<name>.deploy.json');
1012
+ 'workspace "${e.key}" has no deployJsonPaths — pass its build/<name>.deploy.json',
1013
+ );
853
1014
  }
854
1015
  }
855
1016
  final entry = nodeEntry ??
856
1017
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
857
1018
  final effectiveRuntimeDir = runtimeDir ??
1019
+ Platform.environment['NOMOS_CANDIDATE_RUNTIME'] ??
858
1020
  Platform.environment['NOMOS_DART_RUNTIME'] ??
859
- Platform.environment['NOMOS_OFFLINE_RUNTIME'];
1021
+ Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
1022
+ await _sourceCandidateRuntime(entry);
860
1023
  final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
861
1024
  final resolvedHelper = installHelper ??
862
1025
  (File(adjacentInstaller).existsSync()
@@ -882,27 +1045,29 @@ class NomosTestHarness {
882
1045
  for (final e in workspaces.entries) {
883
1046
  final snap = '${workdir.path}/${e.key}.bin';
884
1047
  final info = await _phased(
885
- phase: '1: offline law install ${++_i}/${workspaces.length}',
1048
+ phase: '1: offline law install ${++_i}/${workspaces.length}',
1049
+ workspace: e.key,
1050
+ awaiting: useInRepoScript
1051
+ ? 'offline-install.ts'
1052
+ : 'githolon harness-install',
1053
+ timeout: phaseTimeout ?? _kInstallDeadline,
1054
+ onDiagnostic: onDiagnostic,
1055
+ likelyCauses: const [
1056
+ 'a cold npx download of githolon (pre-warm with any `githolon` run)',
1057
+ 'a very large law package folding through the offline gate — raise phaseTimeout',
1058
+ ],
1059
+ run: () => _runInstall(
1060
+ node: node,
1061
+ useInRepoScript: useInRepoScript,
1062
+ inRepoScript: resolvedHelper,
1063
+ githolonCmd: githolonCmd,
1064
+ deployJsonPaths: e.value,
886
1065
  workspace: e.key,
887
- awaiting: useInRepoScript
888
- ? 'offline-install.ts'
889
- : 'githolon harness-install',
890
- timeout: phaseTimeout ?? _kInstallDeadline,
1066
+ snapshot: snap,
1067
+ runtimeDir: effectiveRuntimeDir,
891
1068
  onDiagnostic: onDiagnostic,
892
- likelyCauses: const [
893
- 'a cold npx download of githolon (pre-warm with any `githolon` run)',
894
- 'a very large law package folding through the offline gate — raise phaseTimeout',
895
- ],
896
- run: () => _runInstall(
897
- node: node,
898
- useInRepoScript: useInRepoScript,
899
- inRepoScript: resolvedHelper,
900
- githolonCmd: githolonCmd,
901
- deployJsonPaths: e.value,
902
- workspace: e.key,
903
- snapshot: snap,
904
- runtimeDir: effectiveRuntimeDir,
905
- onDiagnostic: onDiagnostic));
1069
+ ),
1070
+ );
906
1071
  snapshots[e.key] = snap;
907
1072
  lawHashesByWs[e.key] = ((info['lawHashes'] as List?) ?? const [])
908
1073
  .map((x) => x.toString())
@@ -923,64 +1088,68 @@ class NomosTestHarness {
923
1088
  if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m');
924
1089
  };
925
1090
  transport = await _phased(
926
- phase: '2: spawn the node runner (${workspaces.length} workspace(s))',
927
- awaiting: 'Process.start($node)',
928
- timeout: phaseTimeout ?? _kSpawnDeadline,
1091
+ phase: '2: spawn the node runner (${workspaces.length} workspace(s))',
1092
+ awaiting: 'Process.start($node)',
1093
+ timeout: phaseTimeout ?? _kSpawnDeadline,
1094
+ onDiagnostic: onDiagnostic,
1095
+ run: () => NodeRunnerTransport.start(
1096
+ entry: entry,
1097
+ resolveFrom: resolveFrom,
1098
+ node: node,
1099
+ environment: {
1100
+ 'NOMOS_OFFLINE': '1',
1101
+ 'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
1102
+ if (planeTestRootPublic != null)
1103
+ 'NOMOS_TEST_K_ROOT_PUBLIC': planeTestRootPublic,
1104
+ if (planeTestRootSecret != null)
1105
+ 'NOMOS_TEST_ROOT_SECRET': planeTestRootSecret,
1106
+ if (effectiveRuntimeDir != null)
1107
+ 'NOMOS_OFFLINE_RUNTIME': effectiveRuntimeDir,
1108
+ },
929
1109
  onDiagnostic: onDiagnostic,
930
- run: () => NodeRunnerTransport.start(
931
- entry: entry,
932
- resolveFrom: resolveFrom,
933
- node: node,
934
- environment: {
935
- 'NOMOS_OFFLINE': '1',
936
- 'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
937
- if (planeTestRootPublic != null)
938
- 'NOMOS_TEST_K_ROOT_PUBLIC': planeTestRootPublic,
939
- if (planeTestRootSecret != null)
940
- 'NOMOS_TEST_ROOT_SECRET': planeTestRootSecret,
941
- if (effectiveRuntimeDir != null)
942
- 'NOMOS_OFFLINE_RUNTIME': effectiveRuntimeDir,
943
- },
944
- onDiagnostic: onDiagnostic,
945
- ));
1110
+ ),
1111
+ );
946
1112
  final bridge = NomosBridge(transport!);
947
1113
  await _phased(
948
- phase: '2: runner ready',
949
- awaiting: "event:'ready'",
950
- timeout: phaseTimeout ?? readyTimeout,
951
- onDiagnostic: onDiagnostic,
952
- likelyCauses: const [
953
- 'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + packages.json)',
954
- 'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
955
- ],
956
- run: () => bridge.ready);
1114
+ phase: '2: runner ready',
1115
+ awaiting: "event:'ready'",
1116
+ timeout: phaseTimeout ?? readyTimeout,
1117
+ onDiagnostic: onDiagnostic,
1118
+ likelyCauses: const [
1119
+ 'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + packages.json)',
1120
+ 'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
1121
+ ],
1122
+ run: () => bridge.ready,
1123
+ );
957
1124
 
958
1125
  // 3. connect one SESSION per workspace, each restoring its OWN snapshot — LOCAL-ONLY (no cloud;
959
1126
  // these are local githolons on the device plane, offline by nature).
960
1127
  final snapshotB64 = <String, String>{};
961
1128
  var _n = 0;
962
1129
  for (final e in workspaces.entries) {
963
- snapshotB64[e.key] =
964
- base64Encode(await File(snapshots[e.key]!).readAsBytes());
1130
+ snapshotB64[e.key] = base64Encode(
1131
+ await File(snapshots[e.key]!).readAsBytes(),
1132
+ );
965
1133
  await _phased(
966
- phase: '3: connect session ${++_n}/${workspaces.length}',
1134
+ phase: '3: connect session ${++_n}/${workspaces.length}',
1135
+ workspace: e.key,
1136
+ awaiting: 'connect',
1137
+ timeout: phaseTimeout ?? connectTimeout,
1138
+ onDiagnostic: onDiagnostic,
1139
+ likelyCauses: const [
1140
+ 'a large ledger converge-fold can be slow — raise connectTimeout/phaseTimeout',
1141
+ 'run with onDiagnostic to see where the runner blocked',
1142
+ ],
1143
+ run: () => bridge.connect(
967
1144
  workspace: e.key,
968
- awaiting: 'connect',
969
- timeout: phaseTimeout ?? connectTimeout,
970
- onDiagnostic: onDiagnostic,
971
- likelyCauses: const [
972
- 'a large ledger converge-fold can be slow — raise connectTimeout/phaseTimeout',
973
- 'run with onDiagnostic to see where the runner blocked',
974
- ],
975
- run: () => bridge.connect(
976
- workspace: e.key,
977
- session: e.key,
978
- clientId: '$clientId-${e.key}',
979
- restoreFrom: snapshotB64[e.key],
980
- allowUnborn: true,
981
- // signed with the install-lane harness device (era-0 self-serve birth ceremony key)
982
- authorSecret: installDeviceSecrets[e.key],
983
- ));
1145
+ session: e.key,
1146
+ clientId: '$clientId-${e.key}',
1147
+ restoreFrom: snapshotB64[e.key],
1148
+ allowUnborn: true,
1149
+ // signed with the install-lane harness device (era-0 self-serve birth ceremony key)
1150
+ authorSecret: installDeviceSecrets[e.key],
1151
+ ),
1152
+ );
984
1153
  }
985
1154
  phase('cluster: ${workspaces.length} session(s) connected');
986
1155
 
@@ -993,60 +1162,71 @@ class NomosTestHarness {
993
1162
  for (final ws in deviceKeyed) {
994
1163
  if (!workspaces.containsKey(ws)) {
995
1164
  throw NomosTestHarnessException(
996
- 'deviceKeyed names "$ws" but it is not a workspace of this cluster — '
997
- 'workspaces: ${workspaces.keys.toList()}');
1165
+ 'deviceKeyed names "$ws" but it is not a workspace of this cluster — '
1166
+ 'workspaces: ${workspaces.keys.toList()}',
1167
+ );
998
1168
  }
999
1169
  final kp = await _phased(
1000
- phase: '3b: device keygen',
1001
- workspace: ws,
1002
- awaiting: 'authorKeygen',
1003
- timeout: phaseTimeout ?? _kOtherDeadline,
1004
- onDiagnostic: onDiagnostic,
1005
- run: () => bridge.session(ws).authorKeygen());
1170
+ phase: '3b: device keygen',
1171
+ workspace: ws,
1172
+ awaiting: 'authorKeygen',
1173
+ timeout: phaseTimeout ?? _kOtherDeadline,
1174
+ onDiagnostic: onDiagnostic,
1175
+ run: () => bridge.session(ws).authorKeygen(),
1176
+ );
1006
1177
  devicePublics[ws] = (kp['public'] ?? '').toString();
1007
1178
  deviceSecretsKeyed[ws] = (kp['secret'] ?? '').toString();
1008
1179
  await _phased(
1009
- phase: "3b: keyed reconnect of '$ws'",
1180
+ phase: "3b: keyed reconnect of '$ws'",
1181
+ workspace: ws,
1182
+ awaiting: 'connect',
1183
+ timeout: phaseTimeout ?? connectTimeout,
1184
+ onDiagnostic: onDiagnostic,
1185
+ likelyCauses: const [
1186
+ 'the runner never answered the re-connect of an already-open session — '
1187
+ 'update @githolon/client (the session re-open is an atomic replace)',
1188
+ 'a large ledger re-fold on the re-mount — raise connectTimeout/phaseTimeout',
1189
+ ],
1190
+ run: () => bridge.connect(
1010
1191
  workspace: ws,
1011
- awaiting: 'connect',
1012
- timeout: phaseTimeout ?? connectTimeout,
1013
- onDiagnostic: onDiagnostic,
1014
- likelyCauses: const [
1015
- 'the runner never answered the re-connect of an already-open session — '
1016
- 'update @githolon/client (the session re-open is an atomic replace)',
1017
- 'a large ledger re-fold on the re-mount — raise connectTimeout/phaseTimeout',
1018
- ],
1019
- run: () => bridge.connect(
1020
- workspace: ws,
1021
- session: ws,
1022
- clientId: '$clientId-$ws',
1023
- restoreFrom: snapshotB64[ws],
1024
- authorSecret: (kp['secret'] ?? '').toString(),
1025
- allowUnborn: true,
1026
- ));
1192
+ session: ws,
1193
+ clientId: '$clientId-$ws',
1194
+ restoreFrom: snapshotB64[ws],
1195
+ authorSecret: (kp['secret'] ?? '').toString(),
1196
+ allowUnborn: true,
1197
+ ),
1198
+ );
1027
1199
  }
1028
1200
  if (deviceKeyed.isNotEmpty) {
1029
1201
  phase(
1030
- 'cluster: ${deviceKeyed.length} device-keyed session(s) re-opened');
1202
+ 'cluster: ${deviceKeyed.length} device-keyed session(s) re-opened',
1203
+ );
1031
1204
  }
1032
1205
 
1033
1206
  final names = workspaces.keys.toList();
1034
1207
  final harness = NomosTestHarness._(
1035
- bridge, names.first, transport, workdir, onDiagnostic, names,
1036
- // install-lane harness devices under; explicit deviceKeyed re-opens override for their ws
1037
- devicePublics: {...installDevicePublics, ...devicePublics},
1038
- deviceSecrets: {...installDeviceSecrets, ...deviceSecretsKeyed},
1039
- frameworkHash: frameworkHash,
1040
- lawHashes: lawHashesByWs,
1041
- snapshots: snapshotB64,
1042
- installLane: _InstallLane(
1043
- node: node,
1044
- useInRepoScript: useInRepoScript,
1045
- inRepoScript: resolvedHelper,
1046
- githolonCmd: githolonCmd,
1047
- runtimeDir: effectiveRuntimeDir,
1048
- clientId: clientId),
1049
- phaseTimeout: phaseTimeout);
1208
+ bridge,
1209
+ names.first,
1210
+ transport,
1211
+ workdir,
1212
+ onDiagnostic,
1213
+ names,
1214
+ // install-lane harness devices under; explicit deviceKeyed re-opens override for their ws
1215
+ devicePublics: {...installDevicePublics, ...devicePublics},
1216
+ deviceSecrets: {...installDeviceSecrets, ...deviceSecretsKeyed},
1217
+ frameworkHash: frameworkHash,
1218
+ lawHashes: lawHashesByWs,
1219
+ snapshots: snapshotB64,
1220
+ installLane: _InstallLane(
1221
+ node: node,
1222
+ useInRepoScript: useInRepoScript,
1223
+ inRepoScript: resolvedHelper,
1224
+ githolonCmd: githolonCmd,
1225
+ runtimeDir: effectiveRuntimeDir,
1226
+ clientId: clientId,
1227
+ ),
1228
+ phaseTimeout: phaseTimeout,
1229
+ );
1050
1230
  if (autoTearDown && registerTearDown != null) {
1051
1231
  registerTearDown(harness.dispose);
1052
1232
  }
@@ -1104,15 +1284,19 @@ class NomosTestHarness {
1104
1284
  }) async {
1105
1285
  if (deployJsonPaths.isEmpty) {
1106
1286
  throw NomosTestHarnessException(
1107
- 'no deployJsonPaths — pass the build/<name>.deploy.json `npx githolon compile` emits');
1287
+ 'no deployJsonPaths — pass the build/<name>.deploy.json `npx githolon compile` emits',
1288
+ );
1108
1289
  }
1109
1290
  final entry = nodeEntry ??
1110
1291
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
1111
1292
  final bareUid =
1112
1293
  subject.startsWith('user:') ? subject.substring(5) : subject;
1113
1294
  final http = HttpClient();
1114
- Future<Map<String, Object?>> post(String path, Object? body,
1115
- {Map<String, String> headers = const {}}) async {
1295
+ Future<Map<String, Object?>> post(
1296
+ String path,
1297
+ Object? body, {
1298
+ Map<String, String> headers = const {},
1299
+ }) async {
1116
1300
  final req = await http.postUrl(Uri.parse('${cloud.origin}$path'));
1117
1301
  req.headers.set('content-type', 'application/json');
1118
1302
  req.headers.set('x-nomos-principal', bareUid);
@@ -1125,7 +1309,23 @@ class NomosTestHarness {
1125
1309
  final decoded = (jsonDecode(text) as Map).cast<String, Object?>();
1126
1310
  if (resp.statusCode >= 300 || decoded['ok'] == false) {
1127
1311
  throw NomosTestHarnessException(
1128
- 'cloudBound setup: POST $path → ${resp.statusCode}: ${text.substring(0, text.length.clamp(0, 400))}');
1312
+ 'cloudBound setup: POST $path → ${resp.statusCode}: ${text.substring(0, text.length.clamp(0, 400))}',
1313
+ );
1314
+ }
1315
+ return decoded;
1316
+ }
1317
+
1318
+ Future<Map<String, Object?>> get(String path) async {
1319
+ final req = await http.getUrl(Uri.parse('${cloud.origin}$path'));
1320
+ req.headers.set('x-nomos-principal', bareUid);
1321
+ if (authToken != null) req.headers.set('x-nomos-auth', authToken);
1322
+ final resp = await req.close();
1323
+ final text = await resp.transform(utf8.decoder).join();
1324
+ final decoded = (jsonDecode(text) as Map).cast<String, Object?>();
1325
+ if (resp.statusCode >= 300 || decoded['ok'] == false) {
1326
+ throw NomosTestHarnessException(
1327
+ 'cloudBound setup: GET $path → ${resp.statusCode}: ${text.substring(0, text.length.clamp(0, 400))}',
1328
+ );
1129
1329
  }
1130
1330
  return decoded;
1131
1331
  }
@@ -1143,86 +1343,141 @@ class NomosTestHarness {
1143
1343
  <({String domain, String directive, String path})>[];
1144
1344
  final lawHashes = <String>[];
1145
1345
  await _phased(
1146
- phase: '1/6: boot the born platform + deploy law on the test cloud',
1147
- workspace: platformWorkspace,
1148
- awaiting: 'POST /v2/workspaces + /domains',
1149
- timeout: phaseTimeout ?? _kInstallDeadline,
1150
- onDiagnostic: onDiagnostic,
1151
- run: () async {
1152
- Map<String, Object?> created;
1153
- try {
1154
- created = await post('/v2/workspaces/$platformWorkspace', null);
1155
- } on NomosTestHarnessException catch (e) {
1156
- if (e.message.contains('unborn')) {
1157
- throw NomosTestHarnessException(
1158
- "cloudBound: platform '$platformWorkspace' is UNBORN on the test cloud — era-0 has no "
1159
- "host-side create. Start the cloud with it: "
1160
- "NomosTestCloud.start(serverEntry: …, platform: '$platformWorkspace') "
1161
- '(the dev server births it at boot via its operator lane).');
1162
- }
1163
- rethrow;
1346
+ phase: '1/6: boot the born platform + deploy law on the test cloud',
1347
+ workspace: platformWorkspace,
1348
+ awaiting: 'POST /v2/workspaces + /domains',
1349
+ timeout: phaseTimeout ?? _kInstallDeadline,
1350
+ onDiagnostic: onDiagnostic,
1351
+ run: () async {
1352
+ Map<String, Object?> created;
1353
+ try {
1354
+ created = await post('/v2/workspaces/$platformWorkspace', null);
1355
+ } on NomosTestHarnessException catch (e) {
1356
+ if (e.message.contains('unborn')) {
1357
+ throw NomosTestHarnessException(
1358
+ "cloudBound: platform '$platformWorkspace' is UNBORN on the test cloud — era-0 has no "
1359
+ "host-side create. Start the cloud with it: "
1360
+ "NomosTestCloud.start(serverEntry: …, platform: '$platformWorkspace') "
1361
+ '(the dev server births it at boot via its operator lane).',
1362
+ );
1164
1363
  }
1165
- frameworkHash =
1166
- ((created['controller'] as Map?)?['domainHash'])?.toString();
1167
- for (final p in deployJsonPaths) {
1168
- final body = await File(p).readAsString();
1364
+ rethrow;
1365
+ }
1366
+ frameworkHash =
1367
+ ((created['controller'] as Map?)?['domainHash'])?.toString();
1368
+ final existing = await get(
1369
+ '/v2/workspaces/$platformWorkspace/domains',
1370
+ );
1371
+ final heldHashes = <String>{
1372
+ ...((existing['domains'] as List?) ?? const [])
1373
+ .map((row) => (row as Map?)?['domainHash']?.toString())
1374
+ .whereType<String>()
1375
+ .where((hash) => hash.isNotEmpty),
1376
+ ...(((existing['currentLaw'] as Map?) ?? const {})
1377
+ .values
1378
+ .map((hash) => hash?.toString())
1379
+ .whereType<String>()
1380
+ .where((hash) => hash.isNotEmpty)),
1381
+ };
1382
+ for (final p in deployJsonPaths) {
1383
+ final body = await File(p).readAsString();
1384
+ final deploy = (jsonDecode(body) as Map);
1385
+ final summary = (deploy['summary'] as Map?);
1386
+ final h = summary?['domainHash']?.toString();
1387
+ // A composed test application is normally installed atomically by the bootstrap operator.
1388
+ // The signed-in scenario participant must not need installDomain authority merely because the
1389
+ // harness was also handed those artifacts for routes, hashes and child-law selection.
1390
+ if (h == null || h.isEmpty || !heldHashes.contains(h)) {
1169
1391
  await post('/v2/workspaces/$platformWorkspace/domains', body);
1170
- final deploy = (jsonDecode(body) as Map);
1171
- final summary = (deploy['summary'] as Map?);
1172
- final h = summary?['domainHash']?.toString();
1173
- if (h != null && h.isNotEmpty) lawHashes.add(h);
1174
- final openapi = deploy['openapi'] as Map?;
1175
- final pathSets = <Map>[
1176
- (openapi?['paths'] as Map?) ?? const {},
1177
- (((openapi?['x-nomos-framework-routes'] as Map?)?['paths'])
1178
- as Map?) ??
1179
- const {},
1180
- ];
1181
- for (final paths in pathSets) {
1182
- for (final entry in paths.entries) {
1183
- final post = (entry.value as Map?)?['post'] as Map?;
1184
- final route = post?['x-nomos'] as Map?;
1185
- final domain = route?['domain']?.toString() ?? '';
1186
- final directive = route?['directiveId']?.toString() ?? '';
1187
- if (domain.isNotEmpty && directive.isNotEmpty) {
1188
- frameworkPostRoutes.add((
1189
- domain: domain,
1190
- directive: directive,
1191
- path: entry.key.toString(),
1192
- ));
1193
- }
1392
+ if (h != null && h.isNotEmpty) heldHashes.add(h);
1393
+ }
1394
+ if (h != null && h.isNotEmpty) lawHashes.add(h);
1395
+ final openapi = deploy['openapi'] as Map?;
1396
+ final pathSets = <Map>[
1397
+ (openapi?['paths'] as Map?) ?? const {},
1398
+ (((openapi?['x-nomos-framework-routes'] as Map?)?['paths'])
1399
+ as Map?) ??
1400
+ const {},
1401
+ ];
1402
+ for (final paths in pathSets) {
1403
+ for (final entry in paths.entries) {
1404
+ final post = (entry.value as Map?)?['post'] as Map?;
1405
+ final route = post?['x-nomos'] as Map?;
1406
+ final domain = route?['domain']?.toString() ?? '';
1407
+ final directive = route?['directiveId']?.toString() ?? '';
1408
+ if (domain.isNotEmpty && directive.isNotEmpty) {
1409
+ frameworkPostRoutes.add((
1410
+ domain: domain,
1411
+ directive: directive,
1412
+ path: entry.key.toString(),
1413
+ ));
1194
1414
  }
1195
1415
  }
1196
1416
  }
1197
- });
1417
+ }
1418
+ },
1419
+ );
1198
1420
  if (frameworkHash == null || frameworkHash!.isEmpty) {
1199
1421
  throw NomosTestHarnessException(
1200
- 'cloudBound: the test cloud returned no controller.domainHash — cannot run the birth ceremony (frameworkHash unknown)');
1422
+ 'cloudBound: the test cloud returned no controller.domainHash — cannot run the birth ceremony (frameworkHash unknown)',
1423
+ );
1201
1424
  }
1202
1425
  if (lawHashes.isEmpty) {
1203
1426
  throw NomosTestHarnessException(
1204
- 'cloudBound: no deploy.json carried a summary.domainHash — cannot run the birth ceremony (lawHash unknown)');
1427
+ 'cloudBound: no deploy.json carried a summary.domainHash — cannot run the birth ceremony (lawHash unknown)',
1428
+ );
1205
1429
  }
1206
1430
  final birthRoutes = frameworkPostRoutes
1207
- .where((route) =>
1208
- route.directive == 'nomosInternalBirthReceivingGroup7c1f5e')
1431
+ .where(
1432
+ (route) =>
1433
+ route.directive == 'nomosInternalBirthReceivingGroup7c1f5e',
1434
+ )
1209
1435
  .toSet()
1210
1436
  .toList();
1211
1437
  if (birthRoutes.length != 1) {
1212
1438
  throw NomosTestHarnessException(
1213
- 'cloudBound: the deployed package must expose exactly one framework receiving-group route; found ${birthRoutes.length}');
1439
+ 'cloudBound: the deployed package must expose exactly one framework receiving-group route; found ${birthRoutes.length}',
1440
+ );
1214
1441
  }
1215
1442
  final applicationHomeDomain = birthRoutes.single.domain;
1216
1443
  final birthReceivingGroupPath = birthRoutes.single.path;
1444
+ // A composed application has two distinct laws at this seam:
1445
+ // * the parent's CURRENT control law authors enrollFirstDevice + the birth offer;
1446
+ // * the application's final PAYLOAD package is the law installed into the born receiving group.
1447
+ // Treating those as one law clones platform/control state into the user's home. The ordered package
1448
+ // set is the executable form of the application's reference-before-payload arcs: the final artifact is
1449
+ // therefore the explicit child law, even though it is deliberately only staged on the parent.
1450
+ final parentDomains = await get(
1451
+ '/v2/workspaces/$platformWorkspace/domains',
1452
+ );
1453
+ final currentLaw =
1454
+ (parentDomains['currentLaw'] as Map?)?.cast<String, Object?>() ??
1455
+ const <String, Object?>{};
1456
+ final controlHomeLawHash =
1457
+ currentLaw[applicationHomeDomain]?.toString() ?? '';
1458
+ if (controlHomeLawHash.isEmpty) {
1459
+ throw NomosTestHarnessException(
1460
+ "cloudBound: application/home control domain '$applicationHomeDomain' has no current law on '$platformWorkspace'",
1461
+ );
1462
+ }
1463
+ if (!lawHashes.contains(controlHomeLawHash)) {
1464
+ throw NomosTestHarnessException(
1465
+ "cloudBound: current application/home control law '$controlHomeLawHash' was not among the packages this scenario deployed",
1466
+ );
1467
+ }
1468
+ final applicationHomeLawHash = lawHashes.last;
1217
1469
  final enrollmentRoutes = frameworkPostRoutes
1218
- .where((route) =>
1219
- route.directive == 'enrollFirstDevice' &&
1220
- route.domain == applicationHomeDomain)
1470
+ .where(
1471
+ (route) =>
1472
+ route.directive == 'enrollFirstDevice' &&
1473
+ route.domain == applicationHomeDomain,
1474
+ )
1221
1475
  .toSet()
1222
1476
  .toList();
1223
1477
  if (enrollmentRoutes.length != 1) {
1224
1478
  throw NomosTestHarnessException(
1225
- "cloudBound: application/home domain '$applicationHomeDomain' must expose exactly one semantic enrollFirstDevice route; found ${enrollmentRoutes.length}");
1479
+ "cloudBound: application/home domain '$applicationHomeDomain' must expose exactly one semantic enrollFirstDevice route; found ${enrollmentRoutes.length}",
1480
+ );
1226
1481
  }
1227
1482
  final enrollFirstDevicePath = enrollmentRoutes.single.path;
1228
1483
  // 2/6 — the LOCAL engine plane, CLOUD-BOUND, spawned BEFORE the birth: the runner's local kernel is
@@ -1244,15 +1499,19 @@ class NomosTestHarness {
1244
1499
  timeout: phaseTimeout ?? _kInstallDeadline,
1245
1500
  onDiagnostic: onDiagnostic,
1246
1501
  run: () async {
1247
- final request = await http
1248
- .getUrl(Uri.parse('${cloud.origin}/v1/runtime/holon.wasm'));
1502
+ final request = await http.getUrl(
1503
+ Uri.parse('${cloud.origin}/v1/runtime/holon.wasm'),
1504
+ );
1249
1505
  final response = await request.close();
1250
- final bytes = await response
1251
- .fold<List<int>>(<int>[], (all, chunk) => all..addAll(chunk));
1506
+ final bytes = await response.fold<List<int>>(
1507
+ <int>[],
1508
+ (all, chunk) => all..addAll(chunk),
1509
+ );
1252
1510
  if (response.statusCode != HttpStatus.ok || bytes.length < 1024) {
1253
1511
  throw NomosTestHarnessException(
1254
- 'cloudBound: could not pin the client kernel before going offline '
1255
- '(status ${response.statusCode}, ${bytes.length} bytes)');
1512
+ 'cloudBound: could not pin the client kernel before going offline '
1513
+ '(status ${response.statusCode}, ${bytes.length} bytes)',
1514
+ );
1256
1515
  }
1257
1516
  await File(localWasm).writeAsBytes(bytes, flush: true);
1258
1517
  },
@@ -1265,82 +1524,90 @@ class NomosTestHarness {
1265
1524
  'NOMOS_LOCAL_WASM': localWasm,
1266
1525
  };
1267
1526
  transport = await _phased(
1268
- phase: '2/6: spawn the node runner (cloud-bound)',
1269
- awaiting: 'Process.start($node)',
1270
- timeout: phaseTimeout ?? _kSpawnDeadline,
1527
+ phase: '2/6: spawn the node runner (cloud-bound)',
1528
+ awaiting: 'Process.start($node)',
1529
+ timeout: phaseTimeout ?? _kSpawnDeadline,
1530
+ onDiagnostic: onDiagnostic,
1531
+ run: () => NodeRunnerTransport.start(
1532
+ entry: entry,
1533
+ resolveFrom: resolveFrom,
1534
+ node: node,
1535
+ environment: runnerEnvironment,
1271
1536
  onDiagnostic: onDiagnostic,
1272
- run: () => NodeRunnerTransport.start(
1273
- entry: entry,
1274
- resolveFrom: resolveFrom,
1275
- node: node,
1276
- environment: runnerEnvironment,
1277
- onDiagnostic: onDiagnostic));
1537
+ ),
1538
+ );
1278
1539
  final plane = NomosBridge(transport!);
1279
1540
  await _phased(
1280
- phase: '3/6: runner ready',
1281
- awaiting: "event:'ready'",
1282
- timeout: phaseTimeout ?? readyTimeout,
1283
- onDiagnostic: onDiagnostic,
1284
- run: () => plane.ready);
1541
+ phase: '3/6: runner ready',
1542
+ awaiting: "event:'ready'",
1543
+ timeout: phaseTimeout ?? readyTimeout,
1544
+ onDiagnostic: onDiagnostic,
1545
+ run: () => plane.ready,
1546
+ );
1285
1547
  // 4/6 — THE ERA-0 DEVICE CEREMONY (the WARRANTED onboarding lane, exactly production's shape): mint the
1286
1548
  // owner's device key in the LOCAL kernel, get an IdP token from the dev cloud's test IdP
1287
1549
  // (device-bound: nonce == the device keyHash), and enrol the FIRST DEVICE against the platform's
1288
1550
  // on-chain AuthProvider (enrollFirstDevice — no admin in the loop). The platform is WARRANTED, so
1289
1551
  // plain unsigned enrolment refuses by design; this is the one first-device door.
1290
1552
  final device = await _phased(
1291
- phase:
1292
- '4/6: device ceremony (keygen → dev-IdP token → enrollFirstDevice)',
1293
- workspace: platformWorkspace,
1294
- awaiting: 'authorKeygen + /dev/mint-token + enrollFirstDevice',
1295
- timeout: phaseTimeout ?? _kInstallDeadline,
1296
- onDiagnostic: onDiagnostic,
1297
- run: () async {
1298
- // The runner's crypto ops (authorKeygen/signBirthCert) run on a CONNECTED holon — open a
1299
- // SCRATCH session on an unborn name (allowUnborn ⇒ an empty local holon; the pull-miss is
1300
- // tolerated): a PRIVATE platform (declared read caps — co2's shape) refuses an
1301
- // unauthenticated ledger clone, and the ceremony never needs the platform's ledger anyway
1302
- // (its writes go over the HTTP gateway below).
1303
- await plane.connect(
1304
- cloud: cloud.origin,
1305
- workspace: '$clientId-onboard-scratch',
1306
- session: '$clientId-onboard-scratch',
1307
- clientId: '$clientId-onboard',
1308
- allowUnborn: true,
1553
+ phase:
1554
+ '4/6: device ceremony (keygen → dev-IdP token → enrollFirstDevice)',
1555
+ workspace: platformWorkspace,
1556
+ awaiting: 'authorKeygen + /dev/mint-token + enrollFirstDevice',
1557
+ timeout: phaseTimeout ?? _kInstallDeadline,
1558
+ onDiagnostic: onDiagnostic,
1559
+ run: () async {
1560
+ // The runner's crypto ops (authorKeygen/signBirthCert) run on a CONNECTED holon — open a
1561
+ // SCRATCH session on an unborn name (allowUnborn ⇒ an empty local holon; the pull-miss is
1562
+ // tolerated): a PRIVATE platform (declared read caps — co2's shape) refuses an
1563
+ // unauthenticated ledger clone, and the ceremony never needs the platform's ledger anyway
1564
+ // (its writes go over the HTTP gateway below).
1565
+ await plane.connect(
1566
+ cloud: cloud.origin,
1567
+ workspace: '$clientId-onboard-scratch',
1568
+ session: '$clientId-onboard-scratch',
1569
+ clientId: '$clientId-onboard',
1570
+ allowUnborn: true,
1571
+ );
1572
+ final kp = await plane.authorKeygen();
1573
+ final publicB64 = (kp['public'] ?? '').toString();
1574
+ final secret = (kp['secret'] ?? '').toString();
1575
+ if (publicB64.isEmpty || secret.isEmpty) {
1576
+ throw NomosTestHarnessException(
1577
+ 'cloudBound: authorKeygen returned no device key',
1309
1578
  );
1310
- final kp = await plane.authorKeygen();
1311
- final publicB64 = (kp['public'] ?? '').toString();
1312
- final secret = (kp['secret'] ?? '').toString();
1313
- if (publicB64.isEmpty || secret.isEmpty) {
1314
- throw NomosTestHarnessException(
1315
- 'cloudBound: authorKeygen returned no device key');
1316
- }
1317
- final keyHash = sha256.convert(base64Decode(publicB64)).toString();
1318
- final minted = await post(
1319
- '/dev/mint-token', {'sub': bareUid, 'nonce': keyHash});
1320
- final idpToken = (minted['token'] ?? '').toString();
1321
- final issuer = (minted['issuer'] ?? '').toString();
1322
- if (idpToken.isEmpty || issuer.isEmpty) {
1323
- throw NomosTestHarnessException(
1324
- 'cloudBound: /dev/mint-token returned no token — is the test cloud started with platform:/deployJsonPath: and a law composing setAuthProvider?');
1325
- }
1326
- // THE KEYLESS ENROLL-ONCE DELEGATION (links[1] of the home cert chain) — device-self-signed,
1327
- // its authority the dev-IdP token. enrollFirstDevice SEEDS it as the user's SignerDelegation, so
1328
- // birthHome's offer-effect assembles a WELL-FORMED chain [home_cert, u_delegation, …platform…].
1329
- // WITHOUT it the platform holds no delegation for the owner, the assembly falls to the
1330
- // no-delegation branch, the platform_cert lands in the delegation slot, and the home refuses at
1331
- // its OWN verify door ("home cert founder != delegation cert founder").
1332
- final delegationCert = await plane.mintKeylessDelegation({
1333
- 'user': subject,
1334
- 'deviceKey': publicB64,
1335
- 'childName': _receivingGroupNameOf(subject),
1336
- 'parentId': platformWorkspace,
1337
- 'verifiedVia': issuer,
1338
- 'idpToken': idpToken,
1339
- 'capturedClock': DateTime.now().toUtc().millisecondsSinceEpoch,
1340
- 'epoch': 0,
1341
- }, secret);
1342
- await post(
1343
- '/v2/workspaces/$platformWorkspace$enrollFirstDevicePath', {
1579
+ }
1580
+ final keyHash = sha256.convert(base64Decode(publicB64)).toString();
1581
+ final minted = await post('/dev/mint-token', {
1582
+ 'sub': bareUid,
1583
+ 'nonce': keyHash,
1584
+ });
1585
+ final idpToken = (minted['token'] ?? '').toString();
1586
+ final issuer = (minted['issuer'] ?? '').toString();
1587
+ if (idpToken.isEmpty || issuer.isEmpty) {
1588
+ throw NomosTestHarnessException(
1589
+ 'cloudBound: /dev/mint-token returned no token — is the test cloud started with platform:/deployJsonPath: and a law composing setAuthProvider?',
1590
+ );
1591
+ }
1592
+ // THE KEYLESS ENROLL-ONCE DELEGATION (links[1] of the home cert chain) — device-self-signed,
1593
+ // its authority the dev-IdP token. enrollFirstDevice SEEDS it as the user's SignerDelegation, so
1594
+ // birthHome's offer-effect assembles a WELL-FORMED chain [home_cert, u_delegation, …platform…].
1595
+ // WITHOUT it the platform holds no delegation for the owner, the assembly falls to the
1596
+ // no-delegation branch, the platform_cert lands in the delegation slot, and the home refuses at
1597
+ // its OWN verify door ("home cert founder != delegation cert founder").
1598
+ final delegationCert = await plane.mintKeylessDelegation({
1599
+ 'user': subject,
1600
+ 'deviceKey': publicB64,
1601
+ 'childName': _receivingGroupNameOf(subject),
1602
+ 'parentId': platformWorkspace,
1603
+ 'verifiedVia': issuer,
1604
+ 'idpToken': idpToken,
1605
+ 'capturedClock': DateTime.now().toUtc().millisecondsSinceEpoch,
1606
+ 'epoch': 0,
1607
+ }, secret);
1608
+ await post(
1609
+ '/v2/workspaces/$platformWorkspace$enrollFirstDevicePath',
1610
+ {
1344
1611
  'keyHash': keyHash,
1345
1612
  'verifiedVia': issuer,
1346
1613
  'validFromEpoch': 0,
@@ -1350,86 +1617,89 @@ class NomosTestHarness {
1350
1617
  'authorSecret': secret,
1351
1618
  'delegationCert': jsonEncode(delegationCert),
1352
1619
  },
1353
- headers: {
1354
- 'x-nomos-auth': idpToken
1355
- });
1356
- return (
1357
- publicB64: publicB64,
1358
- secret: secret,
1359
- keyHash: keyHash,
1360
- idpToken: idpToken
1361
- );
1362
- });
1620
+ headers: {'x-nomos-auth': idpToken},
1621
+ );
1622
+ return (
1623
+ publicB64: publicB64,
1624
+ secret: secret,
1625
+ keyHash: keyHash,
1626
+ idpToken: idpToken,
1627
+ );
1628
+ },
1629
+ );
1363
1630
  // 5/6 — THE HOME CEREMONY, remotely: the platform's own gateway offer births the home (the ONE
1364
1631
  // control-plane contact; the app lane below never touches the platform again). The offer carries
1365
1632
  // the device-signed homeCert + the owner's device keyHash (the home's genesis ENROLS it, so the
1366
1633
  // born home is SIGNER-READY — it can birth its own estates/catalogues from intent 0).
1367
1634
  String? home;
1368
1635
  await _phased(
1369
- phase: '5/6: remote home-birth ceremony (attested)',
1370
- workspace: platformWorkspace,
1371
- awaiting:
1372
- 'POST /v2/workspaces/$platformWorkspace$birthReceivingGroupPath',
1373
- timeout: phaseTimeout ?? _kInstallDeadline,
1374
- onDiagnostic: onDiagnostic,
1375
- run: () async {
1376
- final homeName = _receivingGroupNameOf(subject);
1377
- final homeCert = await plane.signBirthCert({
1378
- 'child': homeName,
1379
- 'founder': subject,
1380
- // The cert's `verified` must MATCH the gate verdict (CAS-as-law): the offer carries the
1381
- // dev-IdP token, so the gate resolves a VERIFIED founder.
1382
- 'verified': true,
1383
- 'parentKey': device.publicB64,
1384
- 'parentId': platformWorkspace,
1385
- 'epoch': 0,
1386
- }, device.secret);
1387
- final out = await post(
1388
- '/v2/workspaces/$platformWorkspace$birthReceivingGroupPath', {
1636
+ phase: '5/6: remote home-birth ceremony (attested)',
1637
+ workspace: platformWorkspace,
1638
+ awaiting:
1639
+ 'POST /v2/workspaces/$platformWorkspace$birthReceivingGroupPath',
1640
+ timeout: phaseTimeout ?? _kInstallDeadline,
1641
+ onDiagnostic: onDiagnostic,
1642
+ run: () async {
1643
+ final homeName = _receivingGroupNameOf(subject);
1644
+ final homeCert = await plane.signBirthCert({
1645
+ 'child': homeName,
1646
+ 'founder': subject,
1647
+ // The cert's `verified` must MATCH the gate verdict (CAS-as-law): the offer carries the
1648
+ // dev-IdP token, so the gate resolves a VERIFIED founder.
1649
+ 'verified': true,
1650
+ 'parentKey': device.publicB64,
1651
+ 'parentId': platformWorkspace,
1652
+ 'epoch': 0,
1653
+ }, device.secret);
1654
+ final out = await post(
1655
+ '/v2/workspaces/$platformWorkspace$birthReceivingGroupPath',
1656
+ {
1389
1657
  'owner': subject,
1390
1658
  'ownerKeyHash': device.keyHash,
1391
1659
  'frameworkHash': frameworkHash,
1392
- 'lawHash': lawHashes.first,
1660
+ 'lawHash': applicationHomeLawHash,
1393
1661
  'bornAt': DateTime.now().toUtc().toIso8601String(),
1394
1662
  'homeCert': jsonEncode(homeCert),
1395
1663
  'actor': subject,
1396
1664
  'authorSecret': device.secret,
1397
1665
  },
1398
- headers: {
1399
- 'x-nomos-auth': device.idpToken
1400
- });
1401
- final born = (out['born'] as List?) ?? const [];
1402
- final bornNames = born
1403
- .map((b) => (b as Map)['workspace']?.toString())
1404
- .whereType<String>()
1405
- .where((w) => w.isNotEmpty)
1406
- .toList();
1407
- home = bornNames.isEmpty ? null : bornNames.first;
1408
- if (home == null) {
1409
- final outcomes = jsonEncode(out['birthOutcomes'] ?? const []);
1410
- throw NomosTestHarnessException(
1411
- 'cloudBound: birthHome admitted but spawned no home (birthOutcomes=$outcomes)');
1412
- }
1413
- });
1666
+ headers: {'x-nomos-auth': device.idpToken},
1667
+ );
1668
+ final born = (out['born'] as List?) ?? const [];
1669
+ final bornNames = born
1670
+ .map((b) => (b as Map)['workspace']?.toString())
1671
+ .whereType<String>()
1672
+ .where((w) => w.isNotEmpty)
1673
+ .toList();
1674
+ home = bornNames.isEmpty ? null : bornNames.first;
1675
+ if (home == null) {
1676
+ final outcomes = jsonEncode(out['birthOutcomes'] ?? const []);
1677
+ throw NomosTestHarnessException(
1678
+ 'cloudBound: birthHome admitted but spawned no home (birthOutcomes=$outcomes)',
1679
+ );
1680
+ }
1681
+ },
1682
+ );
1414
1683
  // 6/6 — mount the born home as a normal cloud-bound session (clone from the test cloud).
1415
1684
  await _phased(
1416
- phase: '6/6: mount the born home',
1417
- workspace: home,
1418
- awaiting: 'connect(cloud:, workspace: $home)',
1419
- timeout: phaseTimeout ?? connectTimeout,
1420
- onDiagnostic: onDiagnostic,
1421
- run: () => plane.connect(
1422
- cloud: cloud.origin,
1423
- workspace: home!,
1424
- clientId: clientId,
1425
- // A PRIVATE home (declared read caps — co2's shape) serves its ledger only to a VERIFIED
1426
- // principal: ride the caller's token when given, else the dev-IdP token the ceremony minted.
1427
- authToken: authToken ?? device.idpToken,
1428
- // The home was born WARRANTED with THIS device enrolled in its genesis (ownerKeyHash) —
1429
- // connect with the same key so every write is signed and the home's own `.births()` offers
1430
- // (estates/catalogues) are device-attested (the client signs the birth cert automatically).
1431
- authorSecret: device.secret,
1432
- ));
1685
+ phase: '6/6: mount the born home',
1686
+ workspace: home,
1687
+ awaiting: 'connect(cloud:, workspace: $home)',
1688
+ timeout: phaseTimeout ?? connectTimeout,
1689
+ onDiagnostic: onDiagnostic,
1690
+ run: () => plane.connect(
1691
+ cloud: cloud.origin,
1692
+ workspace: home!,
1693
+ clientId: clientId,
1694
+ // A PRIVATE home (declared read caps — co2's shape) serves its ledger only to a VERIFIED
1695
+ // principal: ride the caller's token when given, else the dev-IdP token the ceremony minted.
1696
+ authToken: authToken ?? device.idpToken,
1697
+ // The home was born WARRANTED with THIS device enrolled in its genesis (ownerKeyHash) —
1698
+ // connect with the same key so every write is signed and the home's own `.births()` offers
1699
+ // (estates/catalogues) are device-attested (the client signs the birth cert automatically).
1700
+ authorSecret: device.secret,
1701
+ ),
1702
+ );
1433
1703
  // NO DECOY on a cloudBound harness: the app is genuinely multi-workspace (the home plus every
1434
1704
  // locally-born child), so the plane is never sole-session — the decoy the offline harness
1435
1705
  // needs (to break single-session inference) would just be a spurious local-only workspace here.
@@ -1448,7 +1718,11 @@ class NomosTestHarness {
1448
1718
  devicePublics: {home!: device.publicB64},
1449
1719
  deviceSecrets: {home!: device.secret},
1450
1720
  frameworkHash: frameworkHash,
1451
- lawHashes: {home!: lawHashes},
1721
+ lawHashes: {
1722
+ // The born receiving group contains the payload package, not every reference-only package which
1723
+ // happened to be available on its parent during the ceremony.
1724
+ home!: [applicationHomeLawHash],
1725
+ },
1452
1726
  phaseTimeout: phaseTimeout,
1453
1727
  cloudRestartLane: _CloudRestartLane(
1454
1728
  entry: entry,
@@ -1489,9 +1763,10 @@ class NomosTestHarness {
1489
1763
  /// each locally-born child's remote custody BEFORE that child's own sync converges against it. Repeats
1490
1764
  /// until every session reports neither `pushFailed` nor `stillAhead`, or [timeout] (then throws with the
1491
1765
  /// per-workspace last verdicts). Returns the final sync result per workspace.
1492
- Future<Map<String, Map<String, Object?>>> converge(
1493
- {Duration timeout = const Duration(seconds: 120),
1494
- List<String>? workspaces}) async {
1766
+ Future<Map<String, Map<String, Object?>>> converge({
1767
+ Duration timeout = const Duration(seconds: 120),
1768
+ List<String>? workspaces,
1769
+ }) async {
1495
1770
  // Every LIVE session on the plane (not the harness's construction-time list — a locally-born child
1496
1771
  // acquired after construction must converge too), PARENT-FIRST: the home syncs before its children, so
1497
1772
  // its birth offer-effect creates each child's remote custody BEFORE that child pushes its own writes.
@@ -1528,8 +1803,9 @@ class NomosTestHarness {
1528
1803
  if (pending.isEmpty) break;
1529
1804
  if (DateTime.now().isAfter(deadline)) {
1530
1805
  throw NomosTestHarnessException(
1531
- 'converge: ${pending.length} workspace(s) did not converge within ${timeout.inSeconds}s — '
1532
- '${pending.map((w) => '$w: ${jsonEncode(last[w] ?? {})}').join('; ')}');
1806
+ 'converge: ${pending.length} workspace(s) did not converge within ${timeout.inSeconds}s — '
1807
+ '${pending.map((w) => '$w: ${jsonEncode(last[w] ?? {})}').join('; ')}',
1808
+ );
1533
1809
  }
1534
1810
  await Future<void>.delayed(const Duration(milliseconds: 500));
1535
1811
  }
@@ -1552,22 +1828,24 @@ class NomosTestHarness {
1552
1828
  final snap = _snapshots[workspace];
1553
1829
  if (snap == null) {
1554
1830
  throw NomosTestHarnessException(
1555
- 'resetWorkspaceCustody("$workspace"): no seed snapshot held for this workspace — '
1556
- 'seeded workspaces: ${_snapshots.keys.toList()}');
1831
+ 'resetWorkspaceCustody("$workspace"): no seed snapshot held for this workspace — '
1832
+ 'seeded workspaces: ${_snapshots.keys.toList()}',
1833
+ );
1557
1834
  }
1558
1835
  await _phased(
1559
- phase: 'resetWorkspaceCustody: reconnect from seed snapshot',
1836
+ phase: 'resetWorkspaceCustody: reconnect from seed snapshot',
1837
+ workspace: workspace,
1838
+ awaiting: 'connect',
1839
+ timeout: _phaseTimeout ?? const Duration(seconds: 60),
1840
+ onDiagnostic: _onDiagnostic,
1841
+ run: () => _plane.connect(
1560
1842
  workspace: workspace,
1561
- awaiting: 'connect',
1562
- timeout: _phaseTimeout ?? const Duration(seconds: 60),
1563
- onDiagnostic: _onDiagnostic,
1564
- run: () => _plane.connect(
1565
- workspace: workspace,
1566
- session: workspace,
1567
- clientId: '${_installLane?.clientId ?? 'test-runner'}-$workspace',
1568
- restoreFrom: snap,
1569
- allowUnborn: true,
1570
- ));
1843
+ session: workspace,
1844
+ clientId: '${_installLane?.clientId ?? 'test-runner'}-$workspace',
1845
+ restoreFrom: snap,
1846
+ allowUnborn: true,
1847
+ ),
1848
+ );
1571
1849
  }
1572
1850
 
1573
1851
  /// SEED a NEW workspace on this plane from `build/<name>.deploy.json` law packages — the
@@ -1580,49 +1858,54 @@ class NomosTestHarness {
1580
1858
  final lane = _installLane;
1581
1859
  if (lane == null) {
1582
1860
  throw NomosTestHarnessException(
1583
- 'seedWorkspaceFromDeployJson: this harness holds no install-lane config');
1861
+ 'seedWorkspaceFromDeployJson: this harness holds no install-lane config',
1862
+ );
1584
1863
  }
1585
1864
  if (deployJsonPaths.isEmpty) {
1586
1865
  throw NomosTestHarnessException(
1587
- 'seedWorkspaceFromDeployJson("$workspace"): pass the build/<name>.deploy.json paths');
1866
+ 'seedWorkspaceFromDeployJson("$workspace"): pass the build/<name>.deploy.json paths',
1867
+ );
1588
1868
  }
1589
1869
  final snap = '${_workdir.path}/$workspace.bin';
1590
1870
  final info = await _phased(
1591
- phase: 'seed: offline law install',
1871
+ phase: 'seed: offline law install',
1872
+ workspace: workspace,
1873
+ awaiting: lane.useInRepoScript
1874
+ ? 'offline-install.ts'
1875
+ : 'githolon harness-install',
1876
+ timeout: _phaseTimeout ?? _kInstallDeadline,
1877
+ onDiagnostic: _onDiagnostic,
1878
+ run: () => _runInstall(
1879
+ node: lane.node,
1880
+ useInRepoScript: lane.useInRepoScript,
1881
+ inRepoScript: lane.inRepoScript,
1882
+ githolonCmd: lane.githolonCmd,
1883
+ deployJsonPaths: deployJsonPaths,
1592
1884
  workspace: workspace,
1593
- awaiting: lane.useInRepoScript
1594
- ? 'offline-install.ts'
1595
- : 'githolon harness-install',
1596
- timeout: _phaseTimeout ?? _kInstallDeadline,
1885
+ snapshot: snap,
1886
+ runtimeDir: lane.runtimeDir,
1597
1887
  onDiagnostic: _onDiagnostic,
1598
- run: () => _runInstall(
1599
- node: lane.node,
1600
- useInRepoScript: lane.useInRepoScript,
1601
- inRepoScript: lane.inRepoScript,
1602
- githolonCmd: lane.githolonCmd,
1603
- deployJsonPaths: deployJsonPaths,
1604
- workspace: workspace,
1605
- snapshot: snap,
1606
- runtimeDir: lane.runtimeDir,
1607
- onDiagnostic: _onDiagnostic));
1888
+ ),
1889
+ );
1608
1890
  final b64 = base64Encode(await File(snap).readAsBytes());
1609
1891
  _snapshots[workspace] = b64;
1610
1892
  _lawHashes[workspace] = ((info['lawHashes'] as List?) ?? const [])
1611
1893
  .map((e) => e.toString())
1612
1894
  .toList();
1613
1895
  await _phased(
1614
- phase: 'seed: connect session',
1896
+ phase: 'seed: connect session',
1897
+ workspace: workspace,
1898
+ awaiting: 'connect',
1899
+ timeout: _phaseTimeout ?? const Duration(seconds: 60),
1900
+ onDiagnostic: _onDiagnostic,
1901
+ run: () => _plane.connect(
1615
1902
  workspace: workspace,
1616
- awaiting: 'connect',
1617
- timeout: _phaseTimeout ?? const Duration(seconds: 60),
1618
- onDiagnostic: _onDiagnostic,
1619
- run: () => _plane.connect(
1620
- workspace: workspace,
1621
- session: workspace,
1622
- clientId: '${lane.clientId}-$workspace',
1623
- restoreFrom: b64,
1624
- allowUnborn: true,
1625
- ));
1903
+ session: workspace,
1904
+ clientId: '${lane.clientId}-$workspace',
1905
+ restoreFrom: b64,
1906
+ allowUnborn: true,
1907
+ ),
1908
+ );
1626
1909
  if (!_sessionNames.contains(workspace)) _sessionNames.add(workspace);
1627
1910
  return _plane.session(workspace);
1628
1911
  }
@@ -1631,20 +1914,24 @@ class NomosTestHarness {
1631
1914
  /// for "why does my dispatch refuse". Throws [NomosTestHarnessException] naming the ACTIVE
1632
1915
  /// domain keys when absent. (For the full shape check use
1633
1916
  /// `harness.session(ws).assertCompatible(XClient.interface)`.)
1634
- Future<void> assertDomainInstalled(
1635
- {required String workspace, required String domain}) async {
1917
+ Future<void> assertDomainInstalled({
1918
+ required String workspace,
1919
+ required String domain,
1920
+ }) async {
1636
1921
  final offer = await _phased(
1637
- phase: 'assertDomainInstalled: read the interface offer',
1638
- workspace: workspace,
1639
- awaiting: 'interfaceOffer',
1640
- timeout: _phaseTimeout ?? _kOtherDeadline,
1641
- onDiagnostic: _onDiagnostic,
1642
- run: () => _plane.session(workspace).interfaceOffer());
1922
+ phase: 'assertDomainInstalled: read the interface offer',
1923
+ workspace: workspace,
1924
+ awaiting: 'interfaceOffer',
1925
+ timeout: _phaseTimeout ?? _kOtherDeadline,
1926
+ onDiagnostic: _onDiagnostic,
1927
+ run: () => _plane.session(workspace).interfaceOffer(),
1928
+ );
1643
1929
  final keys = (offer?.keys.toList() ?? const <String>[])..sort();
1644
1930
  if (!keys.contains(domain)) {
1645
1931
  throw NomosTestHarnessException(
1646
- 'domain "$domain" is NOT installed on workspace "$workspace" — active domains: '
1647
- '${keys.isEmpty ? '(none observable — no on-ledger interface yet, or law truly not deployed)' : keys.join(', ')}');
1932
+ 'domain "$domain" is NOT installed on workspace "$workspace" — active domains: '
1933
+ '${keys.isEmpty ? '(none observable — no on-ledger interface yet, or law truly not deployed)' : keys.join(', ')}',
1934
+ );
1648
1935
  }
1649
1936
  }
1650
1937
 
@@ -1658,7 +1945,8 @@ class NomosTestHarness {
1658
1945
  final connected = _plane.connectedSessions;
1659
1946
  if (!connected.contains(workspace)) {
1660
1947
  throw NomosTestHarnessException(
1661
- 'snapshotWorkspace("$workspace"): no connected session; connected workspaces: $connected');
1948
+ 'snapshotWorkspace("$workspace"): no connected session; connected workspaces: $connected',
1949
+ );
1662
1950
  }
1663
1951
  final s = _plane.session(workspace);
1664
1952
  return _phased(
@@ -1686,19 +1974,24 @@ class NomosTestHarness {
1686
1974
  Future<PlaneSnapshot> snapshotPlane() async {
1687
1975
  final out = <String, WorkspaceSnapshot>{};
1688
1976
  final connected = _plane.connectedSessions.where(
1689
- (ws) => ws != _decoyWorkspace && !ws.endsWith('-onboard-scratch'));
1977
+ (ws) => ws != _decoyWorkspace && !ws.endsWith('-onboard-scratch'),
1978
+ );
1690
1979
  for (final ws in connected) {
1691
1980
  out[ws] = await snapshotWorkspace(ws);
1692
1981
  }
1693
1982
  return PlaneSnapshot(
1694
- workspaces: Map.unmodifiable(out), capturedAt: DateTime.now().toUtc());
1983
+ workspaces: Map.unmodifiable(out),
1984
+ capturedAt: DateTime.now().toUtc(),
1985
+ );
1695
1986
  }
1696
1987
 
1697
1988
  /// Restore a workspace FROM a [WorkspaceSnapshot] — re-open its session on this plane from the snapshot's
1698
1989
  /// LOCAL CUSTODY (no network clone), replacing the resident holon. The workspace need not currently be a
1699
1990
  /// session (a fresh restore is allowed); its name is taken from [snapshot] unless [into] overrides it.
1700
- Future<NomosSession> restoreWorkspace(WorkspaceSnapshot snapshot,
1701
- {String? into}) async {
1991
+ Future<NomosSession> restoreWorkspace(
1992
+ WorkspaceSnapshot snapshot, {
1993
+ String? into,
1994
+ }) async {
1702
1995
  final ws = into ?? snapshot.workspace;
1703
1996
  await _phased(
1704
1997
  phase: 'restoreWorkspace',
@@ -1732,19 +2025,22 @@ class NomosTestHarness {
1732
2025
  Future<void> restart() async {
1733
2026
  if (_disposed) {
1734
2027
  throw NomosTestHarnessException(
1735
- 'restart(): this test device has already been disposed');
2028
+ 'restart(): this test device has already been disposed',
2029
+ );
1736
2030
  }
1737
2031
  final lane = _cloudRestartLane;
1738
2032
  final home = homeWorkspace;
1739
2033
  if (lane == null || home == null) {
1740
2034
  throw NomosTestHarnessException(
1741
- 'restart() requires a harness with one primary application session');
2035
+ 'restart() requires a harness with one primary application session',
2036
+ );
1742
2037
  }
1743
2038
 
1744
2039
  final oldPlane = _plane;
1745
2040
  final targets = oldPlane.connectedSessions
1746
2041
  .where(
1747
- (ws) => ws != _decoyWorkspace && !ws.endsWith('-onboard-scratch'))
2042
+ (ws) => ws != _decoyWorkspace && !ws.endsWith('-onboard-scratch'),
2043
+ )
1748
2044
  .toList();
1749
2045
  if (targets.remove(home)) targets.insert(0, home);
1750
2046
  final snapshots = <String, String>{};
@@ -1831,19 +2127,22 @@ class NomosTestHarness {
1831
2127
  Future<void> reinstall() async {
1832
2128
  if (_disposed) {
1833
2129
  throw NomosTestHarnessException(
1834
- 'reinstall(): this test device has already been disposed');
2130
+ 'reinstall(): this test device has already been disposed',
2131
+ );
1835
2132
  }
1836
2133
  final lane = _cloudRestartLane;
1837
2134
  final home = homeWorkspace;
1838
2135
  if (lane == null || home == null || testCloud == null) {
1839
2136
  throw NomosTestHarnessException(
1840
- 'reinstall() requires a connected scenario with remote custody');
2137
+ 'reinstall() requires a connected scenario with remote custody',
2138
+ );
1841
2139
  }
1842
2140
 
1843
2141
  final oldPlane = _plane;
1844
2142
  final targets = oldPlane.connectedSessions
1845
2143
  .where(
1846
- (ws) => ws != _decoyWorkspace && !ws.endsWith('-onboard-scratch'))
2144
+ (ws) => ws != _decoyWorkspace && !ws.endsWith('-onboard-scratch'),
2145
+ )
1847
2146
  .toList();
1848
2147
  if (targets.remove(home)) targets.insert(0, home);
1849
2148
  for (final ws in targets) {
@@ -1890,31 +2189,41 @@ class NomosTestHarness {
1890
2189
  _disposed = true;
1891
2190
  final f = _onDiagnostic;
1892
2191
  if (f != null)
1893
- f('[nomos-test-harness] phase dispose (≤${(_phaseTimeout ?? _kOtherDeadline).inSeconds}s) …');
2192
+ f(
2193
+ '[nomos-test-harness] phase dispose (≤${(_phaseTimeout ?? _kOtherDeadline).inSeconds}s) …',
2194
+ );
1894
2195
  try {
1895
2196
  // The bridge owns the transport's one message subscription and every watch controller. Closing the
1896
2197
  // transport first makes its incoming stream wait on consumers that can no longer receive their
1897
2198
  // unwatch/done messages—a deterministic teardown deadlock in reactive Flutter tests.
1898
- await _plane.dispose().timeout(_phaseTimeout ?? _kOtherDeadline,
1899
- onTimeout: () => throw HarnessPhaseTimeout(
2199
+ await _plane.dispose().timeout(
2200
+ _phaseTimeout ?? _kOtherDeadline,
2201
+ onTimeout: () => throw HarnessPhaseTimeout(
1900
2202
  phase: 'dispose: close bridge watches',
1901
2203
  workspace: workspace,
1902
2204
  elapsed: _phaseTimeout ?? _kOtherDeadline,
1903
- awaiting: 'NomosBridge.dispose'));
2205
+ awaiting: 'NomosBridge.dispose',
2206
+ ),
2207
+ );
1904
2208
  } catch (e) {
1905
2209
  _diag(
1906
- '[nomos-test-harness] bridge dispose error (continuing to runner cleanup): $e');
2210
+ '[nomos-test-harness] bridge dispose error (continuing to runner cleanup): $e',
2211
+ );
1907
2212
  }
1908
2213
  try {
1909
- await _transport.dispose().timeout(_phaseTimeout ?? _kOtherDeadline,
1910
- onTimeout: () => throw HarnessPhaseTimeout(
2214
+ await _transport.dispose().timeout(
2215
+ _phaseTimeout ?? _kOtherDeadline,
2216
+ onTimeout: () => throw HarnessPhaseTimeout(
1911
2217
  phase: 'dispose: stop the node runner',
1912
2218
  workspace: workspace,
1913
2219
  elapsed: _phaseTimeout ?? _kOtherDeadline,
1914
- awaiting: 'NodeRunnerTransport.dispose'));
2220
+ awaiting: 'NodeRunnerTransport.dispose',
2221
+ ),
2222
+ );
1915
2223
  } catch (e) {
1916
2224
  _diag(
1917
- '[nomos-test-harness] transport dispose error (continuing to temp-dir cleanup): $e');
2225
+ '[nomos-test-harness] transport dispose error (continuing to temp-dir cleanup): $e',
2226
+ );
1918
2227
  }
1919
2228
  try {
1920
2229
  if (await _workdir.exists()) await _workdir.delete(recursive: true);
@@ -1971,7 +2280,8 @@ class NomosTestHarness {
1971
2280
  .toList();
1972
2281
  if (tokens.isEmpty) {
1973
2282
  throw NomosTestHarnessException(
1974
- 'no install helper resolved — pass installHelper: (an offline-install.ts path, or a githolon command)');
2283
+ 'no install helper resolved — pass installHelper: (an offline-install.ts path, or a githolon command)',
2284
+ );
1975
2285
  }
1976
2286
  executable = tokens.first;
1977
2287
  args = [...tokens.skip(1), 'harness-install', ...common];
@@ -1986,7 +2296,8 @@ class NomosTestHarness {
1986
2296
  }
1987
2297
  if (r.exitCode != 0) {
1988
2298
  throw NomosTestHarnessException(
1989
- 'offline law install for "$workspace" failed (exit ${r.exitCode}): ${err.isEmpty ? "(no stderr)" : err}');
2299
+ 'offline law install for "$workspace" failed (exit ${r.exitCode}): ${err.isEmpty ? "(no stderr)" : err}',
2300
+ );
1990
2301
  }
1991
2302
  // The helper prints one JSON line on success; verify it before spawning the runner.
1992
2303
  final out = (r.stdout as String).trim();
@@ -1998,7 +2309,8 @@ class NomosTestHarness {
1998
2309
  } catch (_) {}
1999
2310
  if (decoded['ok'] != true || !File(snapshot).existsSync()) {
2000
2311
  throw NomosTestHarnessException(
2001
- 'offline law install produced no snapshot (stdout: ${out.isEmpty ? "(empty)" : out})');
2312
+ 'offline law install produced no snapshot (stdout: ${out.isEmpty ? "(empty)" : out})',
2313
+ );
2002
2314
  }
2003
2315
  // { ok, snapshot, workspace, lawHashes, frameworkHash } — the caller keeps lawHashes/frameworkHash so a
2004
2316
  // `.births()` directive can be driven (its genesis installs the framework + law by hash).
@@ -2042,17 +2354,20 @@ class NomosTestHarness {
2042
2354
  '@githolon/client/runner/node-entry.ts',
2043
2355
  ]) {
2044
2356
  final r = await Process.run(
2045
- node,
2046
- ['-p', "require.resolve('$entry')"],
2047
- workingDirectory: from,
2048
- );
2357
+ node,
2358
+ [
2359
+ '-p',
2360
+ "require.resolve('$entry')",
2361
+ ],
2362
+ workingDirectory: from);
2049
2363
  final out = (r.stdout as String).trim();
2050
2364
  if (r.exitCode == 0 && out.isNotEmpty) return out;
2051
2365
  }
2052
2366
  throw NomosTestHarnessException(
2053
- 'the Nomos release contains no bundled node runner and '
2054
- '@githolon/client/runner/node-entry.{mjs,ts} cannot be resolved from $from. '
2055
- 'Reinstall nomos_client, or pass nodeEntry: only when developing Nomos itself.');
2367
+ 'the Nomos release contains no bundled node runner and '
2368
+ '@githolon/client/runner/node-entry.{mjs,ts} cannot be resolved from $from. '
2369
+ 'Reinstall nomos_client, or pass nodeEntry: only when developing Nomos itself.',
2370
+ );
2056
2371
  }
2057
2372
  }
2058
2373
 
@@ -2091,8 +2406,10 @@ class WorkspaceSnapshot {
2091
2406
  head: (j['head'] ?? '') as String,
2092
2407
  lawHashes:
2093
2408
  ((j['lawHashes'] as List?) ?? const []).map((e) => '$e').toList(),
2094
- capturedAt: DateTime.parse((j['capturedAt'] ??
2095
- DateTime.now().toUtc().toIso8601String()) as String),
2409
+ capturedAt: DateTime.parse(
2410
+ (j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String())
2411
+ as String,
2412
+ ),
2096
2413
  );
2097
2414
 
2098
2415
  @override
@@ -2115,12 +2432,15 @@ class PlaneSnapshot {
2115
2432
  };
2116
2433
 
2117
2434
  factory PlaneSnapshot.fromJson(Map<String, Object?> j) => PlaneSnapshot(
2118
- workspaces: ((j['workspaces'] as Map?) ?? const {}).map((k, v) =>
2119
- MapEntry(
2120
- '$k',
2121
- WorkspaceSnapshot.fromJson(
2122
- (v as Map).cast<String, Object?>()))),
2123
- capturedAt: DateTime.parse((j['capturedAt'] ??
2124
- DateTime.now().toUtc().toIso8601String()) as String),
2435
+ workspaces: ((j['workspaces'] as Map?) ?? const {}).map(
2436
+ (k, v) => MapEntry(
2437
+ '$k',
2438
+ WorkspaceSnapshot.fromJson((v as Map).cast<String, Object?>()),
2439
+ ),
2440
+ ),
2441
+ capturedAt: DateTime.parse(
2442
+ (j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String())
2443
+ as String,
2444
+ ),
2125
2445
  );
2126
2446
  }