@githolon/dsl 0.108.7 → 0.110.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.
Files changed (307) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/check_package.mjs +12 -15
  3. package/compile_package.mjs +17 -18
  4. package/compile_proof.mjs +16 -1
  5. package/dart/CHANGELOG.md +76 -1
  6. package/dart/README.md +25 -2
  7. package/dart/lib/src/admission_decision.dart +41 -0
  8. package/dart/lib/src/application_framework_calls.dart +52 -2
  9. package/dart/lib/src/application_model_adoption.dart +187 -0
  10. package/dart/lib/src/application_status.dart +73 -0
  11. package/dart/lib/src/console_enrolment.dart +271 -0
  12. package/dart/lib/src/console_http_io.dart +26 -0
  13. package/dart/lib/src/console_http_stub.dart +7 -0
  14. package/dart/lib/src/dev_cloud.dart +69 -22
  15. package/dart/lib/src/generated_call.dart +83 -23
  16. package/dart/lib/src/kernel_primitives.dart +119 -0
  17. package/dart/lib/src/nms_conformance_runtime.dart +1 -1
  18. package/dart/lib/src/nms_generated_runtime.dart +32 -10
  19. package/dart/lib/src/nms_test_environment.dart +1 -1
  20. package/dart/lib/src/node_runner_transport.dart +41 -46
  21. package/dart/lib/src/observed_session_directory.dart +70 -0
  22. package/dart/lib/src/operation_receipts.dart +112 -0
  23. package/dart/lib/src/peer_application_streams.dart +103 -0
  24. package/dart/lib/src/reception/dart_reception.dart +4280 -0
  25. package/dart/lib/src/reception/event_desk.dart +211 -0
  26. package/dart/lib/src/reception/extremum_read.dart +46 -0
  27. package/dart/lib/src/reception/local_copies.dart +371 -0
  28. package/dart/lib/src/reception/peer_office_connection.dart +11 -0
  29. package/dart/lib/src/reception/progress_desk.dart +267 -0
  30. package/dart/lib/src/reception/reception_contract.dart +11 -0
  31. package/dart/lib/src/reception/reception_session.dart +284 -0
  32. package/dart/lib/src/reception/sync_diagnostics.dart +26 -0
  33. package/dart/lib/src/reception/workspace_preparation_report.dart +126 -0
  34. package/dart/lib/src/replica_readiness.dart +123 -0
  35. package/dart/lib/src/runner/node-entry.mjs +23905 -15774
  36. package/dart/lib/src/runtime_v2_gateway.dart +56 -3
  37. package/dart/lib/src/runtime_v2_transport.dart +544 -3
  38. package/dart/lib/src/session_bound.dart +200 -311
  39. package/dart/lib/src/signing_identity_kernel.dart +84 -0
  40. package/dart/lib/src/sync_status_projection.dart +548 -0
  41. package/dart/lib/src/sync_status_reducer.dart +170 -0
  42. package/dart/lib/src/test_harness.dart +809 -1153
  43. package/dart/lib/src/testing/test_custody.dart +66 -0
  44. package/dart/lib/src/testing/test_peer.dart +328 -0
  45. package/dart/lib/src/typed_watch_registry.dart +169 -0
  46. package/dart/lib/src/workspace_open_progress.dart +27 -0
  47. package/dart/pubspec.lock +24 -1
  48. package/dart/pubspec.yaml +7 -2
  49. package/dart/pubspec_overrides.yaml +2 -0
  50. package/dart/test/admission_decision_test.dart +48 -0
  51. package/dart/test/background_model_adoption_test.dart +228 -0
  52. package/dart/test/bundle_version_pairing_test.dart +4 -4
  53. package/dart/test/child_authentication_repair_test.dart +359 -0
  54. package/dart/test/cloud_bound_local_shell_test.dart +119 -0
  55. package/dart/test/console_enrolment_refusal_test.dart +52 -0
  56. package/dart/test/console_enrolment_test.dart +227 -0
  57. package/dart/test/errors_test.dart +5 -5
  58. package/dart/test/event_plane_seam_test.dart +50 -164
  59. package/dart/test/extremum_read_test.dart +58 -0
  60. package/dart/test/fixtures/wedge/wedge-entry.mjs +3 -21
  61. package/dart/test/generated_query_test.dart +32 -0
  62. package/dart/test/harness_phase_timeout_test.dart +18 -23
  63. package/dart/test/kernel_author_after_read_test.dart +242 -0
  64. package/dart/test/kernel_fence_test.dart +137 -0
  65. package/dart/test/local_copies_test.dart +222 -0
  66. package/dart/test/model_adoption_test.dart +198 -0
  67. package/dart/test/node_peer_local_custody_test.dart +153 -0
  68. package/dart/test/offer_authentication_proof_test.dart +352 -52
  69. package/dart/test/operation_compatibility_test.dart +151 -0
  70. package/dart/test/platform_credential_effect_test.dart +386 -0
  71. package/dart/test/receiving_group_open_test.dart +213 -0
  72. package/dart/test/reception/workspace_preparation_report_test.dart +103 -0
  73. package/dart/test/reception_span_trace_test.dart +23 -0
  74. package/dart/test/replica_readiness_test.dart +156 -0
  75. package/dart/test/runtime_projections_test.dart +92 -0
  76. package/dart/test/runtime_v2_gateway_test.dart +131 -0
  77. package/dart/test/signing_identity_kernel_test.dart +75 -0
  78. package/dart/test/sync_status_preparation_report_test.dart +29 -0
  79. package/dart/test/sync_status_reducer_test.dart +419 -0
  80. package/dart/test/test_harness_test.dart +71 -95
  81. package/dart/test/typed_query_watch_test.dart +170 -0
  82. package/dart/test/typed_watch_registry_test.dart +249 -0
  83. package/dart_lints/README.md +18 -0
  84. package/dart_lints/lib/nomos_lints.dart +70 -0
  85. package/dart_lints/pubspec.lock +580 -0
  86. package/dart_lints/pubspec.yaml +12 -0
  87. package/dart_lints/test/reference_identity_test.dart +75 -0
  88. package/dart_types/CHANGELOG.md +18 -0
  89. package/dart_types/lib/nomos_kernel_protocol.dart +21 -1
  90. package/dart_types/lib/nomos_types.dart +9 -1
  91. package/dart_types/lib/src/admission_decision.dart +37 -0
  92. package/dart_types/lib/src/aggregate_meta.dart +1 -1
  93. package/dart_types/lib/src/approved_model.dart +65 -0
  94. package/dart_types/lib/src/custody.dart +10 -18
  95. package/dart_types/lib/src/error_info.dart +4 -4
  96. package/dart_types/lib/src/errors.dart +29 -29
  97. package/dart_types/lib/src/foreign_ref.dart +62 -33
  98. package/dart_types/lib/src/local_work_rejected.dart +3 -3
  99. package/dart_types/lib/src/nomos_ref.dart +3 -1
  100. package/dart_types/lib/src/principal.dart +2 -2
  101. package/dart_types/lib/src/proto/google/protobuf/any.pb.dart +163 -0
  102. package/dart_types/lib/src/proto/google/protobuf/any.pbenum.dart +11 -0
  103. package/dart_types/lib/src/proto/nomos/discovery/v1/workspace_placement.pb.dart +137 -0
  104. package/dart_types/lib/src/proto/nomos/discovery/v1/workspace_placement.pbenum.dart +11 -0
  105. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +20721 -10433
  106. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +555 -27
  107. package/dart_types/lib/src/proto/nomos/runtime/v2/application_desk.pb.dart +387 -0
  108. package/dart_types/lib/src/proto/nomos/runtime/v2/application_desk.pbenum.dart +46 -0
  109. package/dart_types/lib/src/proto/nomos/runtime/v2/application_readiness_desk.pb.dart +295 -0
  110. package/dart_types/lib/src/proto/nomos/runtime/v2/application_readiness_desk.pbenum.dart +139 -0
  111. package/dart_types/lib/src/proto/nomos/runtime/v2/authentication_desk.pb.dart +269 -0
  112. package/dart_types/lib/src/proto/nomos/runtime/v2/authentication_desk.pbenum.dart +103 -0
  113. package/dart_types/lib/src/proto/nomos/runtime/v2/compatibility_desk.pb.dart +417 -0
  114. package/dart_types/lib/src/proto/nomos/runtime/v2/compatibility_desk.pbenum.dart +62 -0
  115. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +9968 -0
  116. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pbenum.dart +133 -0
  117. package/dart_types/lib/src/proto/nomos/runtime/v2/credentials_desk.pb.dart +204 -0
  118. package/dart_types/lib/src/proto/nomos/runtime/v2/credentials_desk.pbenum.dart +54 -0
  119. package/dart_types/lib/src/proto/nomos/runtime/v2/custody_desk.pb.dart +1550 -0
  120. package/dart_types/lib/src/proto/nomos/runtime/v2/custody_desk.pbenum.dart +11 -0
  121. package/dart_types/lib/src/proto/nomos/runtime/v2/framework.pb.dart +710 -0
  122. package/dart_types/lib/src/proto/nomos/runtime/v2/framework.pbenum.dart +11 -0
  123. package/dart_types/lib/src/proto/nomos/runtime/v2/identity_desk.pb.dart +845 -0
  124. package/dart_types/lib/src/proto/nomos/runtime/v2/identity_desk.pbenum.dart +11 -0
  125. package/dart_types/lib/src/proto/nomos/runtime/v2/interaction_desk.pb.dart +198 -0
  126. package/dart_types/lib/src/proto/nomos/runtime/v2/interaction_desk.pbenum.dart +11 -0
  127. package/dart_types/lib/src/proto/nomos/runtime/v2/law_desk.pb.dart +369 -0
  128. package/dart_types/lib/src/proto/nomos/runtime/v2/law_desk.pbenum.dart +11 -0
  129. package/dart_types/lib/src/proto/nomos/runtime/v2/maintenance_desk.pb.dart +609 -0
  130. package/dart_types/lib/src/proto/nomos/runtime/v2/maintenance_desk.pbenum.dart +65 -0
  131. package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +358 -0
  132. package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pbenum.dart +100 -0
  133. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pb.dart +2231 -0
  134. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +271 -0
  135. package/dart_types/lib/src/proto/nomos/runtime/v2/placement_desk.pb.dart +1286 -0
  136. package/dart_types/lib/src/proto/nomos/runtime/v2/placement_desk.pbenum.dart +11 -0
  137. package/dart_types/lib/src/proto/nomos/runtime/v2/receiving_group_desk.pb.dart +636 -0
  138. package/dart_types/lib/src/proto/nomos/runtime/v2/receiving_group_desk.pbenum.dart +116 -0
  139. package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pb.dart +1637 -0
  140. package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pbenum.dart +175 -0
  141. package/dart_types/lib/src/proto/nomos/runtime/v2/recovery_desk.pb.dart +1269 -0
  142. package/dart_types/lib/src/proto/nomos/runtime/v2/recovery_desk.pbenum.dart +129 -0
  143. package/dart_types/lib/src/proto/nomos/runtime/v2/residency_desk.pb.dart +2782 -0
  144. package/dart_types/lib/src/proto/nomos/runtime/v2/residency_desk.pbenum.dart +97 -0
  145. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +2232 -8627
  146. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +0 -358
  147. package/dart_types/lib/src/proto/nomos/runtime/v2/sealing_desk.pb.dart +1639 -0
  148. package/dart_types/lib/src/proto/nomos/runtime/v2/sealing_desk.pbenum.dart +46 -0
  149. package/dart_types/lib/src/runtime_outcome.dart +133 -0
  150. package/dart_types/lib/src/subscriptions.dart +116 -23
  151. package/dart_types/pubspec.yaml +1 -1
  152. package/dart_types/test/extremum_subscriptions_test.dart +40 -0
  153. package/dart_types/test/foreign_ref_resolution_test.dart +75 -0
  154. package/dart_types/test/nomos_ref_address_test.dart +25 -0
  155. package/dart_types/test/ordered_subscriptions_test.dart +34 -0
  156. package/operation_contract_check.mjs +24 -0
  157. package/package.json +11 -6
  158. package/src/aggregate.ts +70 -10
  159. package/src/application_topology.ts +18 -11
  160. package/src/attested.ts +3 -0
  161. package/src/attested_core.ts +46 -3
  162. package/src/birth_meter_ensure.ts +19 -7
  163. package/src/births.ts +29 -4
  164. package/src/build_package.ts +140 -13
  165. package/src/business_birth.ts +181 -4
  166. package/src/codegen_call_proto.ts +1274 -0
  167. package/src/codegen_call_usda.ts +49 -6
  168. package/src/codegen_dart.ts +79 -44
  169. package/src/codegen_dart_client.ts +458 -800
  170. package/src/{codegen_dart_application.ts → codegen_dart_facade.ts} +331 -582
  171. package/src/codegen_dart_framework_calls.ts +320 -37
  172. package/src/codegen_dart_indexed_variant.ts +159 -0
  173. package/src/codegen_dart_scenario.ts +42 -13
  174. package/src/codegen_proof.ts +3 -3
  175. package/src/codegen_provider_dart.ts +5 -0
  176. package/src/codegen_query_proto.ts +158 -0
  177. package/src/codegen_refusal.ts +189 -0
  178. package/src/codegen_result_proto.ts +931 -0
  179. package/src/codegen_result_usda.ts +4 -0
  180. package/src/codegen_ts.ts +464 -616
  181. package/src/codegen_workspace_access.ts +105 -0
  182. package/src/compatibility_graph.ts +759 -0
  183. package/src/compatibility_usda.ts +264 -0
  184. package/src/compile_package_main.ts +1994 -1189
  185. package/src/custody_placement.ts +156 -0
  186. package/src/custody_placement_usda.ts +91 -0
  187. package/src/decl_shapes.ts +21 -2
  188. package/src/describe_model.ts +1 -0
  189. package/src/directive.ts +59 -2
  190. package/src/drivers.ts +51 -0
  191. package/src/engine_entry.ts +52 -8
  192. package/src/evolve_preview.ts +11 -4
  193. package/src/exists.ts +2 -0
  194. package/src/fields.ts +75 -11
  195. package/src/flow_gate.ts +2 -1
  196. package/src/framework/application_home.ts +170 -18
  197. package/src/framework/attested_receipts.ts +8 -3
  198. package/src/framework/auth_provider_law.ts +169 -9
  199. package/src/framework/birth_certificate_law.ts +4 -0
  200. package/src/framework/capability.ts +34 -3
  201. package/src/framework/child_authentication.ts +346 -0
  202. package/src/framework/console_governance.ts +295 -0
  203. package/src/framework/delegated_roles.ts +301 -0
  204. package/src/framework/domain_lifecycle.ts +26 -13
  205. package/src/framework/founding_genesis.ts +193 -0
  206. package/src/framework/framework_aggregates.ts +1 -1
  207. package/src/framework/genesis.ts +6 -1
  208. package/src/framework/governance_relations.ts +19 -0
  209. package/src/framework/home.ts +5 -734
  210. package/src/framework/legacy/child_auth_provider_epoch.ts +73 -0
  211. package/src/framework/model_authority_repair.ts +116 -0
  212. package/src/framework/model_authority_seed.ts +185 -0
  213. package/src/framework/model_transition.ts +211 -0
  214. package/src/framework/owned_custody.ts +1 -1
  215. package/src/framework/principal_enrollment.ts +302 -0
  216. package/src/framework/receiving_group.ts +857 -0
  217. package/src/framework/receiving_group_recovery.ts +314 -0
  218. package/src/framework/relations.ts +4 -8
  219. package/src/framework/signing_identity.ts +242 -95
  220. package/src/framework/source_roster.ts +11 -5
  221. package/src/framework/workspace_access.ts +149 -0
  222. package/src/framework/workspace_invariant.ts +73 -52
  223. package/src/framework/workspaces.ts +407 -40
  224. package/src/framework_call_definitions.ts +32 -3
  225. package/src/generated_client_bundle_boundary.ts +97 -0
  226. package/src/generated_client_names.ts +1 -0
  227. package/src/identity_usda.ts +27 -0
  228. package/src/index.ts +183 -12
  229. package/src/indexed_variant.ts +83 -0
  230. package/src/intent.ts +173 -45
  231. package/src/law_check.ts +8 -5
  232. package/src/law_dependency_index.ts +231 -0
  233. package/src/law_graph.ts +508 -71
  234. package/src/law_graph_usda.ts +170 -24
  235. package/src/law_identity.ts +119 -81
  236. package/src/law_source_map.ts +4 -1
  237. package/src/law_units.ts +498 -0
  238. package/src/law_value_type.ts +7 -3
  239. package/src/member_authentication.ts +302 -0
  240. package/src/null_meta.ts +1 -1
  241. package/src/openusd_compiled_layer.ts +132 -0
  242. package/src/operation_contract_check.ts +31 -0
  243. package/src/ordered_read.ts +2 -1
  244. package/src/package_config.ts +33 -12
  245. package/src/package_dependencies.ts +1 -0
  246. package/src/plan_source_slice.ts +30 -1
  247. package/src/protobuf_field_number.ts +36 -0
  248. package/src/provenance_wire.ts +6 -0
  249. package/src/query.ts +2 -1
  250. package/src/read.ts +50 -0
  251. package/src/relation.ts +4 -1
  252. package/src/relation_schema.ts +15 -2
  253. package/src/report.ts +2 -2
  254. package/src/sqlite_computation_bundle.ts +78 -0
  255. package/src/sqlite_computed.ts +102 -0
  256. package/src/sqlite_counts.ts +154 -0
  257. package/src/sqlite_layout.ts +823 -0
  258. package/src/sqlite_reads.ts +275 -0
  259. package/src/sqlite_samples.ts +185 -0
  260. package/src/stable_id_identity_bridge.ts +1 -1
  261. package/src/stable_ids_for_module.ts +16 -4
  262. package/src/test_call_usda.ts +6 -6
  263. package/src/usda_relationship_integrity.ts +20 -3
  264. package/src/usda_text.ts +21 -1
  265. package/src/value_object.ts +81 -11
  266. package/src/wire.ts +22 -0
  267. package/src/wire_encode.ts +227 -20
  268. package/src/workspace_sharding.ts +15 -6
  269. package/src/workspace_sharing.ts +51 -49
  270. package/src/zod_introspection.ts +2 -0
  271. package/compat_package.mjs +0 -46
  272. package/dart/lib/src/home.dart +0 -15
  273. package/dart/lib/src/runner/offline-install.mjs +0 -15367
  274. package/dart/lib/src/source_trust.dart +0 -51
  275. package/dart/lib/src/webview_bridge.dart +0 -6540
  276. package/dart/lib/src/workspace_name.dart +0 -30
  277. package/dart/test/acquire_session_credentials_test.dart +0 -150
  278. package/dart/test/acquire_session_test.dart +0 -534
  279. package/dart/test/adv_realm_interference_test.dart +0 -600
  280. package/dart/test/cloud_bound_topology_test.dart +0 -168
  281. package/dart/test/cluster_births_test.dart +0 -202
  282. package/dart/test/cluster_device_keyed_acceptance_test.dart +0 -105
  283. package/dart/test/cluster_local_autosync_test.dart +0 -123
  284. package/dart/test/cluster_scale_test.dart +0 -122
  285. package/dart/test/compat_test.dart +0 -61
  286. package/dart/test/dlq_session_surface_test.dart +0 -222
  287. package/dart/test/first_app_gauntlet_test.dart +0 -386
  288. package/dart/test/fixtures/wedge/fake-install.mjs +0 -11
  289. package/dart/test/four_layer_nested_topology_test.dart +0 -290
  290. package/dart/test/home_parity_test.dart +0 -13
  291. package/dart/test/local_sync_inert_test.dart +0 -107
  292. package/dart/test/node_runner_transport_test.dart +0 -78
  293. package/dart/test/parent_signer_recovery_test.dart +0 -155
  294. package/dart/test/residency_refusal_test.dart +0 -49
  295. package/dart/test/sealing_client_test.dart +0 -181
  296. package/dart/test/session_bound_test.dart +0 -285
  297. package/dart/test/stale_bundle_test.dart +0 -80
  298. package/dart/test/vo_read_typing_test.dart +0 -131
  299. package/dart/test/watch_after_offer_stall_test.dart +0 -100
  300. package/dart/test/webview_bridge_test.dart +0 -1930
  301. package/dart/test/workspace_name_test.dart +0 -24
  302. package/runtime/generated-application-host.js +0 -22860
  303. package/src/codegen_query_usda.ts +0 -214
  304. package/src/compat_manifest.ts +0 -26
  305. package/src/compat_package_main.ts +0 -224
  306. package/src/shape_compat.ts +0 -352
  307. package/src/usd_state.ts +0 -505
@@ -1,8 +1,8 @@
1
- // NOMOS TEST HARNESS — an EPHEMERAL, FULLY-OFFLINE real-law fixture for headless app tests. One call
1
+ // NOMOS TEST HARNESS — an EPHEMERAL, FULLY-OFFLINE real-law fixture for app tests that run without a UI. One call
2
2
  // installs a tenant's compiled law into a FRESH LOCAL application parent (no cloud contact, ever), performs
3
3
  // the same receiving-group birth a production application performs, boots the REAL githolon (the
4
4
  // byte-identical wasm, the real ledger, the real gate) over [NodeRunnerTransport], and hands
5
- // out a [NomosBridge] the generated Dart clients drive — the SAME surface the app UI uses — under
5
+ // out a [DartReception] the generated Dart clients drive — the SAME surface the app UI uses — under
6
6
  // `flutter test` / `dart test` / CI.
7
7
  //
8
8
  // ```dart
@@ -10,28 +10,20 @@
10
10
  // lawUsdzPaths: ['build/guestbook.law.usdz'],
11
11
  // registerTearDown: addTearDown, // auto-disposes when the test ends (see below)
12
12
  // );
13
- // final app = guestbookClient(harness.bridge); // the generated client — same one the app uses
13
+ // final app = guestbookClient(harness.reception); // the generated client — same one the app uses
14
14
  // await app.signGuestbook(author: 'ada', message: 'hi', mood: 'happy', signedAt: ...);
15
15
  // final rows = await app.entriesByAuthor(author: 'ada');
16
16
  // await harness.dispose(); // DETERMINISTIC teardown (also auto-fired via registerTearDown)
17
17
  // ```
18
18
  //
19
- // HOW IT STAYS OFFLINE. Two node children, both talking only to the local disk:
20
- // 1. `offline-install.ts` runs the PROVEN offline install lane (`githolon proof`'s adapter engine) on a
21
- // throwaway application parent — genesis → framework controller → each tenant package's
22
- // `installDomain`, folded through the ONE gate — and serializes its tree to a fresh temp dir. Generated
23
- // scenarios then birth their deterministic receiving group locally through the real declared birth.
24
- // 2. the node runner (`node-entry.ts`) boots in `NOMOS_OFFLINE=1` mode: it serves the runtime wasm from the
25
- // local cache (`~/.holon/runtime`, filled by any warm `githolon` run) and boots the holon from that
26
- // snapshot (`restoreFrom`). Every other URL is REFUSED — a stray fetch fails loudly, never reaches the net.
19
+ // HOW IT STAYS OFFLINE. One Node test Platform Shell opens the canonical Peer Office over a temporary
20
+ // filesystem Git custody directory. Workspace genesis and every law install cross Runtime v2 and the
21
+ // kernel's generated Protobuf ABI. There is no private installer, serialized repository snapshot, alternate
22
+ // engine, USDA parser, or generic command bridge in this harness.
27
23
  //
28
24
  // PREREQUISITES (documented so a failure is legible): `node` on PATH and the release-pinned runtime cache.
29
- // The self-contained runner + installer ship inside nomos_client; the kernel executable does not. A cold
25
+ // The self-contained runner ships inside nomos_client; the kernel executable does not. A cold
30
26
  // runtime is acquired and content-addressed by the release tooling, then tests remain fully offline.
31
- //
32
- // PACKAGING NOTE: `offline-install.ts` imports the adapter engine, which today lives beside the in-repo
33
- // runner. For a PUBLISHED harness this install belongs behind a `githolon` subcommand (the CLI already
34
- // bundles the engine + this exact sequence in `proof_offline`); [installHelper] is the one seam to repoint.
35
27
  @internal
36
28
  library;
37
29
 
@@ -40,18 +32,28 @@ import 'package:meta/meta.dart' show internal;
40
32
  import 'dart:convert';
41
33
  import 'dart:io';
42
34
 
35
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as protocol;
36
+
43
37
  import 'package:crypto/crypto.dart' show sha256;
44
38
  import 'package:fixnum/fixnum.dart' show Int64;
45
39
  import 'package:nomos_types/nomos_kernel_protocol.dart'
46
40
  show BirthCertificate, SyncWorkspaceResponse;
47
- import 'package:nomos_types/nomos_types.dart' show Sha256Digest;
41
+ import 'package:nomos_types/nomos_types.dart'
42
+ show
43
+ Sha256Digest,
44
+ NomosRuntimeFailure,
45
+ NomosUnavailable,
46
+ NomosUnavailableReason;
48
47
  import 'package:package_config/package_config.dart' show loadPackageConfigUri;
49
48
 
50
49
  import 'dev_cloud.dart';
51
50
  import 'application_framework_calls.dart';
52
51
  import 'node_runner_transport.dart';
52
+ import 'runtime_v2_transport.dart';
53
53
  import 'session_bound.dart' show DomainNotInstalledForSession;
54
- import 'webview_bridge.dart' show NomosBridge, NomosSession;
54
+ import 'reception/dart_reception.dart'
55
+ show DartReception, NomosBoundInteraction, NomosSession;
56
+ import 'testing/test_peer.dart' show NmsTestPeer;
55
57
 
56
58
  enum _KernelRuntimeArtifact {
57
59
  executable('holon.wasm'),
@@ -64,38 +66,17 @@ enum _KernelRuntimeArtifact {
64
66
  final String? providerFileName;
65
67
  }
66
68
 
67
- enum _CandidateKernelBinary {
68
- production('holon.wasm'),
69
- testMint('holon-testmint.wasm');
70
-
71
- const _CandidateKernelBinary(this.fileName);
72
- final String fileName;
69
+ /// Opaque authenticated Git point captured from one isolated local test peer.
70
+ /// It carries no credentials and cannot be used with a production reception.
71
+ final class NomosTestFrontier {
72
+ const NomosTestFrontier._(
73
+ this._owner, this.workspace, this.head, this._offers);
74
+ final Object _owner;
75
+ final String workspace;
76
+ final String head;
77
+ final protocol.CustodyOfferBatchRequest _offers;
73
78
  }
74
79
 
75
- /// Lower the generated protobuf certificate into the framework law's USDA-authored value shape. This is
76
- /// deliberately not a second certificate type: callers cannot construct it independently, and every field
77
- /// is read from the compiler-generated message.
78
- Map<String, Object?> _birthCertificateLawValue(BirthCertificate certificate) =>
79
- <String, Object?>{
80
- 'child': certificate.child,
81
- 'founder': certificate.founder,
82
- 'verified': certificate.verified,
83
- 'parentKey': base64Encode(certificate.parentKey),
84
- 'parentId': certificate.parentId,
85
- 'epoch': certificate.epoch.toInt(),
86
- 'signature': base64Encode(certificate.signature),
87
- if (certificate.hasVerifiedVia()) 'verifiedVia': certificate.verifiedVia,
88
- if (certificate.hasIdpToken()) 'idpToken': certificate.idpToken,
89
- if (certificate.hasCapturedClock())
90
- 'capturedClock': certificate.capturedClock.toInt(),
91
- if (certificate.hasAuthProviderIssuer())
92
- 'authProviderIssuer': certificate.authProviderIssuer,
93
- if (certificate.hasAuthProviderJwks())
94
- 'authProviderJwks': certificate.authProviderJwks,
95
- if (certificate.hasAuthProviderAudience())
96
- 'authProviderAudience': certificate.authProviderAudience,
97
- };
98
-
99
80
  /// A source checkout is itself a coordinated release candidate: compiler, framework packages and kernels
100
81
  /// move together. Generated scenarios must not combine those source-generated laws with a recently-written
101
82
  /// but older global runtime cache. Published packages have no release/ directory and keep the normal
@@ -105,9 +86,8 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
105
86
  Directory? root;
106
87
  for (var depth = 0; depth < 12; depth++) {
107
88
  if (File('${cursor.path}/release/kernel-candidate.usda').existsSync() &&
108
- File(
109
- '${cursor.path}/cloud/dev-server/kernel-testmint.wasm',
110
- ).existsSync()) {
89
+ File('${cursor.path}/cloud/cloudflare-shell/vendor/holon_bytes.wasmbin')
90
+ .existsSync()) {
111
91
  root = cursor;
112
92
  break;
113
93
  }
@@ -156,19 +136,12 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
156
136
  }
157
137
 
158
138
  final productionExpected = await candidateField('production-sha256');
159
- final testMintExpected = await candidateField('test-mint-sha256');
160
139
  final production = File(
161
140
  '${sourceRoot.path}/cloud/cloudflare-shell/vendor/holon_bytes.wasmbin',
162
141
  );
163
- final testMint = File(
164
- '${sourceRoot.path}/cloud/dev-server/kernel-testmint.wasm',
165
- );
166
142
  final productionRepository = File(
167
143
  '${sourceRoot.path}/cloud/cloudflare-shell/runtime/kernel.repository.xml',
168
144
  );
169
- final testMintRepository = File(
170
- '${sourceRoot.path}/cloud/cloudflare-shell/runtime/kernel-testmint.repository.xml',
171
- );
172
145
  final bootstrap = File(
173
146
  '${sourceRoot.path}/cloud/cloudflare-shell/domains/build/bootstrap/domain_bootstrap.law.usdz',
174
147
  );
@@ -177,9 +150,7 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
177
150
  );
178
151
  for (final file in [
179
152
  production,
180
- testMint,
181
153
  productionRepository,
182
- testMintRepository,
183
154
  bootstrap,
184
155
  nomos,
185
156
  ]) {
@@ -190,19 +161,15 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
190
161
  }
191
162
  }
192
163
  final productionBytes = await production.readAsBytes();
193
- final testMintBytes = await testMint.readAsBytes();
194
164
  final productionRepositoryBytes = await productionRepository.readAsBytes();
195
- final testMintRepositoryBytes = await testMintRepository.readAsBytes();
196
165
  final productionActual = sha256.convert(productionBytes).toString();
197
- final testMintActual = sha256.convert(testMintBytes).toString();
198
166
  String short(String value) =>
199
167
  value.length <= 12 ? value : value.substring(0, 12);
200
- if (productionActual != productionExpected ||
201
- testMintActual != testMintExpected) {
168
+ if (productionActual != productionExpected) {
202
169
  throw NomosTestHarnessException(
203
170
  'source release candidate kernel mismatch: release/kernel-candidate.usda '
204
- 'expects ${short(productionExpected)}/${short(testMintExpected)}, but the staged artifacts are '
205
- '${short(productionActual)}/${short(testMintActual)} — run the kernel candidate preparation before testing',
171
+ 'expects ${short(productionExpected)}, but the staged artifact is '
172
+ '${short(productionActual)} — run the kernel candidate preparation before testing',
206
173
  );
207
174
  }
208
175
  final bootstrapBytes = await bootstrap.readAsBytes();
@@ -210,9 +177,8 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
210
177
  final bundleIdentity = sha256
211
178
  .convert(
212
179
  utf8.encode(
213
- '$productionActual:$testMintActual:'
180
+ '$productionActual:'
214
181
  '${sha256.convert(productionRepositoryBytes)}:'
215
- '${sha256.convert(testMintRepositoryBytes)}:'
216
182
  '${sha256.convert(bootstrapBytes)}:'
217
183
  '${sha256.convert(nomosBytes)}',
218
184
  ),
@@ -242,9 +208,7 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
242
208
  }
243
209
 
244
210
  await publish('holon.wasm', productionBytes);
245
- await publish('holon-testmint.wasm', testMintBytes);
246
211
  await publish('kernel.repository.xml', productionRepositoryBytes);
247
- await publish('kernel-testmint.repository.xml', testMintRepositoryBytes);
248
212
  await publish('bootstrap.law.usdz', bootstrapBytes);
249
213
  await publish('nomos.law.usdz', nomosBytes);
250
214
  return runtime.path;
@@ -262,25 +226,21 @@ String? nmsPermanentConvergenceFailure(SyncWorkspaceResponse result) {
262
226
  'push=${result.hasPushError() ? result.pushError.message : "none"}';
263
227
  }
264
228
 
265
- /// The offline-install lane configuration a harness keeps so the CUSTODY DIAGNOSTIC helpers
266
- /// (ask 9 of architecture/session_bound_clients.md) can re-run installs after construction:
267
- /// [seedWorkspaceFromLawUsdz] / [resetWorkspaceCustody].
268
- class _InstallLane {
269
- final String node;
270
- final bool useInRepoScript;
271
- final String inRepoScript;
272
- final String githolonCmd;
273
- final String? runtimeDir;
274
- final String clientId;
275
- const _InstallLane({
276
- required this.node,
277
- required this.useInRepoScript,
278
- required this.inRepoScript,
279
- required this.githolonCmd,
280
- required this.runtimeDir,
281
- required this.clientId,
282
- });
283
- }
229
+ /// A missing frontier or retryable transport/discovery failure is not a
230
+ /// successful remote convergence merely because its counters default to zero.
231
+ bool nmsRemoteConvergenceSettled(SyncWorkspaceResponse result) =>
232
+ result.hasCurrentHead() &&
233
+ result.currentHead.isNotEmpty &&
234
+ !result.localOnly &&
235
+ !result.pushFailed &&
236
+ !result.stillAhead &&
237
+ !result.offline &&
238
+ !result.reconnectRequired &&
239
+ !result.directOfferDeferred &&
240
+ !result.hasPushError() &&
241
+ !result.hasDiscoveryError() &&
242
+ result.deferred == 0 &&
243
+ nmsPermanentConvergenceFailure(result) == null;
284
244
 
285
245
  class NomosTestHarnessException implements Exception {
286
246
  final String message;
@@ -292,7 +252,7 @@ class NomosTestHarnessException implements Exception {
292
252
  /// A TYPED per-phase deadline breach — the "NO SILENT HANGS" guarantee: every lifecycle await in
293
253
  /// [NomosTestHarness] (install, runner spawn, ready, connect, keygen, keyed reconnect, seed, dispose)
294
254
  /// runs under a generous deadline and, instead of wedging forever with no output, throws THIS —
295
- /// naming the phase, the workspace, how long it waited, the bridge op it was awaiting, and the
255
+ /// naming the phase, the workspace, how long it waited, the reception op it was awaiting, and the
296
256
  /// likely causes where they are known. Subclasses [NomosTestHarnessException] so existing
297
257
  /// `on NomosTestHarnessException` handling keeps working. Tune deadlines with the harness's
298
258
  /// `phaseTimeout` parameter (it overrides EVERY phase's default when set).
@@ -306,7 +266,7 @@ class HarnessPhaseTimeout extends NomosTestHarnessException {
306
266
  /// How long the harness waited before giving up.
307
267
  final Duration elapsed;
308
268
 
309
- /// What the harness was awaiting (the bridge op / subprocess), e.g. `connect`, `authorKeygen`.
269
+ /// What the harness was awaiting (the reception op / subprocess), e.g. `connect`, `authorKeygen`.
310
270
  final String awaiting;
311
271
 
312
272
  /// Likely causes, where the harness knows them (mirrors DomainNotInstalledForSession.possibleCauses).
@@ -332,15 +292,16 @@ class HarnessPhaseTimeout extends NomosTestHarnessException {
332
292
  const String _decoyWorkspace = '__nomos-decoy';
333
293
 
334
294
  class _CloudRestartLane {
335
- const _CloudRestartLane({
295
+ _CloudRestartLane({
336
296
  required this.entry,
337
297
  required this.node,
338
298
  required this.cloud,
339
299
  required this.authToken,
340
300
  required this.authorSecret,
341
301
  required this.clientId,
302
+ required this.custodyDirectory,
342
303
  this.resolveFrom,
343
- this.environment,
304
+ this.arguments = const <String>[],
344
305
  });
345
306
 
346
307
  final String entry;
@@ -350,15 +311,16 @@ class _CloudRestartLane {
350
311
  final String authorSecret;
351
312
  final String clientId;
352
313
  final String? resolveFrom;
353
- final Map<String, String>? environment;
314
+ final List<String> arguments;
315
+ String custodyDirectory;
354
316
  }
355
317
 
356
318
  class NomosTestHarness {
357
- /// The connected bridge — hand it to the generated typed clients (the same surface the app UI uses).
319
+ /// The connected reception — hand it to the generated typed clients (the same surface the app UI uses).
358
320
  /// For [ephemeral] this is a [NomosSession] HANDLE pinned to [workspace] (the plane underneath also
359
321
  /// holds a decoy session, so unpinned plane-level calls refuse the way they would in a real
360
322
  /// multi-workspace app); for [cluster] it is the shared plane — name sessions via [session].
361
- NomosBridge bridge;
323
+ DartReception reception;
362
324
 
363
325
  /// The local workspace name the law was installed into.
364
326
  final String workspace;
@@ -387,15 +349,21 @@ class NomosTestHarness {
387
349
  /// package's domainHash — the `lawHash` a `.births()` recipe names for the child's law install.
388
350
  final Map<String, List<String>> _lawHashes;
389
351
 
352
+ /// Domain keys reported by the kernel while inspecting each installed law package.
353
+ final Map<String, Set<String>> _domainKeys;
354
+
390
355
  /// The tenant law hashes installed into [workspace] (deploy order); empty if unknown.
391
356
  List<String> lawHashes(String workspace) =>
392
357
  List.unmodifiable(_lawHashes[workspace] ?? const []);
393
358
 
394
- /// workspace → the base64 restore snapshot it was seeded from (for [resetWorkspaceCustody]).
395
- final Map<String, String> _snapshots;
359
+ /// Present for local harnesses so a later scenario workspace uses the same
360
+ /// canonical Peer Office and real Git custody, rather than another installer.
361
+ final NmsTestPeer? _testPeer;
396
362
 
397
- /// The offline-install lane config (for [seedWorkspaceFromLawUsdz]); null when unknown.
398
- final _InstallLane? _installLane;
363
+ /// Sessions used only to establish the test application. They share the
364
+ /// kernel plane while setup runs, but are not part of the application's
365
+ /// durable working set and must never be reopened as though they were.
366
+ final Set<String> _setupSessions;
399
367
 
400
368
  /// When set, overrides EVERY phase's default deadline (see [HarnessPhaseTimeout]).
401
369
  final Duration? _phaseTimeout;
@@ -424,7 +392,7 @@ class NomosTestHarness {
424
392
  }
425
393
 
426
394
  NomosTestHarness._(
427
- this.bridge,
395
+ this.reception,
428
396
  this.workspace,
429
397
  this._transport,
430
398
  this._workdir,
@@ -434,8 +402,9 @@ class NomosTestHarness {
434
402
  Map<String, String> deviceSecrets = const {},
435
403
  this.frameworkHash,
436
404
  Map<String, List<String>> lawHashes = const {},
437
- Map<String, String> snapshots = const {},
438
- _InstallLane? installLane,
405
+ Map<String, Set<String>> domainKeys = const {},
406
+ NmsTestPeer? testPeer,
407
+ Set<String> setupSessions = const {},
439
408
  Duration? phaseTimeout,
440
409
  _CloudRestartLane? cloudRestartLane,
441
410
  this.testCloud,
@@ -446,8 +415,9 @@ class NomosTestHarness {
446
415
  _devicePublics = devicePublics,
447
416
  _deviceSecrets = deviceSecrets,
448
417
  _lawHashes = Map.of(lawHashes),
449
- _snapshots = Map.of(snapshots),
450
- _installLane = installLane,
418
+ _domainKeys = Map.of(domainKeys),
419
+ _testPeer = testPeer,
420
+ _setupSessions = Set.unmodifiable(setupSessions),
451
421
  _phaseTimeout = phaseTimeout,
452
422
  _cloudRestartLane = cloudRestartLane;
453
423
 
@@ -476,18 +446,26 @@ class NomosTestHarness {
476
446
  final where = workspace != null ? ' "$workspace"' : '';
477
447
  log('phase $phase$where (≤${timeout.inSeconds}s) …');
478
448
  final sw = Stopwatch()..start();
479
- final r = await run().timeout(
480
- timeout,
481
- onTimeout: () => throw HarnessPhaseTimeout(
482
- phase: phase,
483
- workspace: workspace,
484
- elapsed: timeout,
485
- awaiting: awaiting,
486
- likelyCauses: likelyCauses,
487
- ),
488
- );
489
- log('phase $phase$where done (${sw.elapsedMilliseconds}ms)');
490
- return r;
449
+ try {
450
+ final r = await run().timeout(
451
+ timeout,
452
+ onTimeout: () => throw HarnessPhaseTimeout(
453
+ phase: phase,
454
+ workspace: workspace,
455
+ elapsed: timeout,
456
+ awaiting: awaiting,
457
+ likelyCauses: likelyCauses,
458
+ ),
459
+ );
460
+ log('phase $phase$where done (${sw.elapsedMilliseconds}ms)');
461
+ return r;
462
+ } on NomosRuntimeFailure catch (failure) {
463
+ // The presentation-safe exception deliberately omits diagnostic prose. Test fixtures
464
+ // still need the runtime's actual reason, without changing production UI rendering.
465
+ log('phase $phase$where failed (${sw.elapsedMilliseconds}ms): '
466
+ '${failure.code}: ${failure.message}');
467
+ rethrow;
468
+ }
491
469
  }
492
470
 
493
471
  /// The base64 device PUBLIC key a `deviceKeyed` [cluster] session was opened with (null otherwise). Its
@@ -504,76 +482,14 @@ class NomosTestHarness {
504
482
 
505
483
  /// The base64 device SECRET a session was opened with (TEST HARNESS ONLY — the harness IS the device;
506
484
  /// a real app keeps this in secure storage). Sign writes to locally-born children with it, e.g.
507
- /// `bridge.acquireSession(childWs, authorSecret: harness.deviceSecret(home))`.
485
+ /// `reception.acquireSession(childWs, authorSecret: harness.deviceSecret(home))`.
508
486
  String? deviceSecret(String workspace) => _deviceSecrets[workspace];
509
487
 
510
- /// Framework-private scenario setup for a business intent guarded by a delegated role.
511
- /// Generated `scenario.given(person).mayX()` methods are the only caller: they name the
512
- /// business permission while this harness mints and admits the real parent-rooted delegation.
513
- /// Tenant tests never handle relation names, certificate bytes, signer secrets, or raw offers.
514
- Future<void> nomosInternalAllowDelegatedScenarioAction({
515
- required String domain,
516
- required String acceptanceIntent,
517
- required String relation,
518
- required String subject,
519
- required String subjectField,
520
- required String certificateField,
521
- Map<String, Object?> facts = const {},
522
- }) async {
523
- final secret = _deviceSecrets[workspace];
524
- if (secret == null || secret.isEmpty) {
525
- throw NomosTestHarnessException(
526
- 'the generated scenario cannot establish its business permission because the test device secret is unavailable',
527
- );
528
- }
529
- final rows = await session(
530
- workspace,
531
- ).queryById('birth-attestation:self', null, subject);
532
- if (rows.length != 1) {
533
- throw NomosTestHarnessException(
534
- 'the generated scenario expected one verified birth attestation before establishing business permission; found ${rows.length}',
535
- );
536
- }
537
- final row = rows.single;
538
- final data = row['data'] is Map
539
- ? (row['data'] as Map).cast<String, Object?>()
540
- : row.cast<String, Object?>();
541
- final rawChain = data['certChain'];
542
- final decodedChain = rawChain is String ? jsonDecode(rawChain) : rawChain;
543
- final links = decodedChain is Map && decodedChain['links'] is List
544
- ? List<Object?>.from(decodedChain['links'] as List)
545
- : const <Object?>[];
546
- final issued = await session(workspace).issueDelegationCert(
547
- signerSecret: secret,
548
- grantorId: (data['parentId'] ?? workspace).toString(),
549
- relation: relation,
550
- subject: subject,
551
- certChain: links,
552
- );
553
- final certificate = issued['certString']?.toString();
554
- if (certificate == null || certificate.isEmpty) {
555
- throw NomosTestHarnessException(
556
- 'the generated scenario could not mint the parent-rooted business delegation',
557
- );
558
- }
559
- await session(workspace).offerOutcome(
560
- domain: domain,
561
- directiveId: acceptanceIntent,
562
- actor: subject,
563
- authorSecret: secret,
564
- payload: <String, Object?>{
565
- ...facts,
566
- subjectField: subject,
567
- certificateField: certificate,
568
- },
569
- );
570
- }
571
-
572
488
  /// The workspace names on this device's local plane (one for [ephemeral], N for [cluster]).
573
489
  List<String> get sessions => List.unmodifiable(_sessionNames);
574
490
 
575
491
  /// A [NomosSession] handle pinned to one connected [workspace] — hand it to a generated typed client
576
- /// (`EstateClient(harness.session('estate'))`). A THIN delegate to the production [NomosBridge.session]:
492
+ /// (`EstateClient(harness.session('estate'))`). A THIN delegate to the production [DartReception.session]:
577
493
  /// the harness owns no session logic, it just installs the laws + opens the sessions on the one plane.
578
494
  NomosSession session(String workspace) {
579
495
  if (!_sessionNames.contains(workspace)) {
@@ -581,23 +497,18 @@ class NomosTestHarness {
581
497
  'no session "$workspace" on this harness — connected sessions: $_sessionNames',
582
498
  );
583
499
  }
584
- return bridge.session(workspace);
500
+ return reception.session(workspace);
585
501
  }
586
502
 
587
- /// Install [lawUsdzPaths] into a fresh LOCAL workspace and boot a bridge over it — NO cloud contact.
503
+ /// Install [lawUsdzPaths] into a fresh LOCAL workspace and boot a reception over it — NO cloud contact.
588
504
  ///
589
505
  /// [lawUsdzPaths] — the `build/<name>.law.usdz` files `nomos-compile` emits (installed in order;
590
506
  /// framework/platform law before a domain that depends on it).
591
507
  /// [workspace] — the local workspace name (default `test-ws`; law-only, never leaves the machine).
592
- /// [runtimeDir] — the runtime cache (default `~/.holon/runtime`); must hold `holon-testmint.wasm` +
508
+ /// [runtimeDir] — the runtime cache (default `~/.holon/runtime`); must hold `holon.wasm` +
593
509
  /// `bootstrap.law.usdz` + `nomos.law.usdz`. The harness owns a throwaway test root; app code never handles its key material.
594
- /// [nodeEntry] — path to `node-entry.ts`; when omitted it is resolved from [resolveFrom] via
595
- /// `require.resolve('@githolon/client/runner/node-entry.ts')`.
596
- /// [installHelper] — the offline law-install lane. An EXISTING `.ts` or `.mjs` file is run as a node script (the
597
- /// in-repo `offline-install.ts`, the default when it exists beside [nodeEntry]); anything else is a
598
- /// githolon COMMAND LINE whose `harness-install` subcommand is driven (the default `npx githolon` when the
599
- /// in-repo helper is absent — a published tenant needs nothing but `githolon`). E.g. force the subcommand
600
- /// with `installHelper: 'node /abs/cli/dist/cli.mjs'`.
510
+ /// [nodeEntry] — path to the bundled test Platform Shell; normally resolved
511
+ /// from `package:nomos_client/src/runner/node-entry.mjs` (the stdio node peer, given --custody).
601
512
  /// [registerTearDown] — pass the test framework's `addTearDown` to auto-[dispose] when the test ends
602
513
  /// (feature-detecting `package:test` from a library is not clean in Dart, so this is explicit and safe).
603
514
  /// [autoTearDown] — when true (default) and [registerTearDown] is given, registers [dispose] as a teardown.
@@ -606,11 +517,15 @@ class NomosTestHarness {
606
517
  NmsApplicationFrameworkCalls? applicationFrameworkCalls,
607
518
  int? applicationMaxChildWorkspacesPerUser,
608
519
  String? ownerSubject,
609
- String workspace = 'test-ws',
520
+
521
+ /// The raw law-focused lane's workspace. It is REFUSED alongside
522
+ /// [ownerSubject]: naming an owner declares the application-home lane, whose
523
+ /// workspace is the office's receiving-group address, not a name a caller
524
+ /// (or this harness) derives.
525
+ String? workspace,
610
526
  String clientId = 'test-runner',
611
527
  String? runtimeDir,
612
528
  String? nodeEntry,
613
- String? installHelper,
614
529
  String? resolveFrom,
615
530
  String node = 'node',
616
531
  bool autoTearDown = true,
@@ -627,33 +542,24 @@ class NomosTestHarness {
627
542
  );
628
543
  }
629
544
  final entry = nodeEntry ??
630
- await _resolveEntry(node, resolveFrom ?? Directory.current.path);
545
+ await _resolveTestEntry(node, resolveFrom ?? Directory.current.path);
631
546
  final effectiveRuntimeDir = runtimeDir ??
632
547
  Platform.environment['NOMOS_CANDIDATE_RUNTIME'] ??
633
548
  Platform.environment['NOMOS_DART_RUNTIME'] ??
634
549
  Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
635
- await _sourceCandidateRuntime(entry);
636
- // THE INSTALL LANE. [installHelper] is the one seam (PACKAGING NOTE above): either the in-repo
637
- // `offline-install.mjs` node script, OR the published `githolon harness-install` subcommand (the CLI
638
- // bundles the engine + this exact install sequence, so a published tenant needs nothing but `githolon`).
639
- // • an EXISTING `.ts`/`.mjs` file ⇒ run it as a node script (the in-repo default when present);
640
- // • anything else ⇒ treat it as a githolon COMMAND LINE and drive `harness-install`
641
- // (default `npx githolon` when the in-repo helper is absent; the test forces this lane by passing
642
- // e.g. `installHelper: 'node /abs/cli/dist/cli.mjs'`).
643
- final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
644
- final resolvedHelper = installHelper ??
645
- (File(adjacentInstaller).existsSync()
646
- ? adjacentInstaller
647
- : '${File(entry).parent.path}/offline-install.ts');
648
- final useInRepoScript =
649
- (resolvedHelper.endsWith('.ts') || resolvedHelper.endsWith('.mjs')) &&
650
- File(resolvedHelper).existsSync();
651
- final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
652
-
550
+ await _sourceCandidateRuntime(entry) ??
551
+ '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
653
552
  final owner = ownerSubject?.trim();
654
- final birthsApplicationHome = owner != null &&
655
- owner.isNotEmpty &&
656
- workspace == _receivingGroupNameOf(owner);
553
+ // THE CALLER DECLARES ITS LANE. Naming an owner IS the application-home lane;
554
+ // the harness no longer re-derives a receiving-group name to recognise one by
555
+ // comparison. The office answers that name once the peer is up.
556
+ final birthsApplicationHome = owner != null && owner.isNotEmpty;
557
+ if (birthsApplicationHome && workspace != null) {
558
+ throw NomosTestHarnessException(
559
+ 'ownerSubject declares the application-home lane, whose workspace is the '
560
+ 'office\'s receiving-group address for that owner — do not name `workspace` too',
561
+ );
562
+ }
657
563
  if (birthsApplicationHome && applicationFrameworkCalls == null) {
658
564
  throw NomosTestHarnessException(
659
565
  'a generated application scenario must pass its compiler-generated framework calls; '
@@ -666,101 +572,71 @@ class NomosTestHarness {
666
572
  // birth machinery production uses. Raw/law-focused harness callers keep their explicitly named workspace.
667
573
  final installWorkspace = birthsApplicationHome
668
574
  ? 'nms-app-${DateTime.now().microsecondsSinceEpoch.toRadixString(36)}'
669
- : workspace;
575
+ : (workspace ?? 'test-ws');
670
576
 
671
- // A FRESH temp dir per harness — the snapshot + any transient runner state live here and nowhere else,
672
- // so dispose() leaves nothing behind.
673
- final workdir = await Directory.systemTemp.createTemp('nomos-harness-');
674
- // Total-ordered teardown even on a mid-setup failure: build the transport lazily and clean up on throw.
577
+ Directory? workdir;
675
578
  NodeRunnerTransport? transport;
676
579
  final phase = (String m) {
677
580
  if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m');
678
581
  };
679
582
  try {
680
- final snapshot = '${workdir.path}/snapshot.bin';
681
- final info = await _phased(
682
- phase: '1/4: offline law install (${lawUsdzPaths.length} package(s))',
683
- workspace: installWorkspace,
684
- awaiting:
685
- useInRepoScript ? 'offline-install.ts' : 'githolon harness-install',
686
- timeout: phaseTimeout ?? _kInstallDeadline,
687
- onDiagnostic: onDiagnostic,
688
- likelyCauses: const [
689
- 'a cold npx download of githolon (pre-warm with any `githolon` run)',
690
- 'a very large law package folding through the offline gate — raise phaseTimeout',
691
- ],
692
- run: () => _runInstall(
693
- node: node,
694
- useInRepoScript: useInRepoScript,
695
- inRepoScript: resolvedHelper,
696
- githolonCmd: githolonCmd,
697
- lawUsdzPaths: lawUsdzPaths,
698
- workspace: installWorkspace,
699
- snapshot: snapshot,
700
- runtimeDir: effectiveRuntimeDir,
701
- onDiagnostic: onDiagnostic,
702
- ),
703
- );
704
-
705
- final runnerEnvironment = <String, String>{
706
- 'NOMOS_OFFLINE': '1',
707
- 'NOMOS_OFFLINE_RESTORE': snapshot,
708
- 'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
709
- if (info['devicePublic'] != null)
710
- 'NOMOS_TEST_K_ROOT_PUBLIC': info['devicePublic'].toString(),
711
- if (info['deviceSecret'] != null)
712
- 'NOMOS_TEST_ROOT_SECRET': info['deviceSecret'].toString(),
713
- if (effectiveRuntimeDir != null)
714
- 'NOMOS_OFFLINE_RUNTIME': effectiveRuntimeDir,
715
- };
716
- transport = await _phased(
717
- phase: '2/4: spawn the node runner',
718
- awaiting: 'Process.start($node)',
583
+ final peer = await _phased(
584
+ phase: '1/2: open canonical test peer',
585
+ awaiting: 'Peer Office + filesystem Git custody',
719
586
  timeout: phaseTimeout ?? _kSpawnDeadline,
720
587
  onDiagnostic: onDiagnostic,
721
- run: () => NodeRunnerTransport.start(
588
+ run: () => NmsTestPeer.start(
722
589
  entry: entry,
590
+ runtimeDirectory: effectiveRuntimeDir,
723
591
  resolveFrom: resolveFrom,
724
592
  node: node,
725
- environment: runnerEnvironment,
726
593
  onDiagnostic: onDiagnostic,
727
594
  ),
728
595
  );
729
- final plane = NomosBridge(transport!);
730
- await _phased(
731
- phase: '3/4: runner ready',
732
- awaiting: "event:'ready'",
733
- timeout: phaseTimeout ?? readyTimeout,
734
- onDiagnostic: onDiagnostic,
735
- likelyCauses: const [
736
- 'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + framework law archives)',
737
- 'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
738
- ],
739
- run: () => plane.ready,
740
- );
741
- await _phased(
742
- phase: '4/4: connect session',
596
+ transport = peer.transport;
597
+ workdir = peer.custody.directory;
598
+ var plane = peer.reception;
599
+ // An application home is warranted by certificates the founding device signs, so that key is
600
+ // minted FIRST and the peer is booted to trust it (typed boot configuration) before genesis.
601
+ protocol.SigningKeyPair? founder;
602
+ if (birthsApplicationHome) {
603
+ founder = await _phased(
604
+ phase: '1b/2: mint the founding device and boot the peer to trust it',
605
+ awaiting: 'authorKeygen + peer reboot with --trust-root-public-key',
606
+ timeout: phaseTimeout ?? _kSpawnDeadline,
607
+ onDiagnostic: onDiagnostic,
608
+ run: () async {
609
+ final key = await peer.mintFoundingDevice();
610
+ await peer.reboot(trustRootPublicKey: key.publicKey);
611
+ return key;
612
+ },
613
+ );
614
+ transport = peer.transport;
615
+ plane = peer.reception;
616
+ }
617
+ // THE OFFICE NAMES THE APPLICATION HOME. The harness holds no copy of the
618
+ // receiving-group derivation; it asks the peer it just started.
619
+ final openWorkspace = birthsApplicationHome
620
+ ? await plane.nmsReceivingGroupWorkspace(owner)
621
+ : installWorkspace;
622
+ final installed = await _phased(
623
+ phase: '2/2: create Git custody and install law',
743
624
  workspace: installWorkspace,
744
- awaiting: 'connect',
625
+ awaiting: 'genesis offer + ordinary lifecycle offers',
745
626
  timeout: phaseTimeout ?? connectTimeout,
746
627
  onDiagnostic: onDiagnostic,
747
- likelyCauses: const [
748
- 'a large ledger converge-fold can be slow — raise connectTimeout/phaseTimeout',
749
- 'run with onDiagnostic to see where the runner blocked',
750
- ],
751
- run: () => plane.connect(
752
- cloud:
753
- 'https://offline.invalid', // ignored: the offline runner serves everything locally
628
+ run: () => peer.installWorkspace(
754
629
  workspace: installWorkspace,
755
- session: installWorkspace,
756
- clientId: clientId,
757
- allowUnborn: true,
758
- // THE HARNESS DEVICE (era-0): the install lane mints one device key per plane — sessions
759
- // sign with it, and the client completes the self-serve birth ceremony (enrol + cert)
760
- // with the SAME key, so offline `.births()` offers Just Work.
761
- authorSecret: info['deviceSecret']?.toString(),
630
+ lawUsdzPaths: lawUsdzPaths,
631
+ deviceKey: founder,
762
632
  ),
763
633
  );
634
+ final info = <String, Object?>{
635
+ 'devicePublic': base64Encode(installed.deviceKey.publicKey),
636
+ 'deviceSecret': base64Encode(installed.deviceKey.secret),
637
+ 'frameworkHash': installed.controllerHash,
638
+ 'lawHashes': installed.lawHashes,
639
+ };
764
640
  if (birthsApplicationHome) {
765
641
  final maxChildWorkspaces = applicationMaxChildWorkspacesPerUser;
766
642
  if (maxChildWorkspaces == null || maxChildWorkspaces < 0) {
@@ -799,7 +675,7 @@ class NomosTestHarness {
799
675
  // scenario carries the value from the same package declaration that compiled the
800
676
  // receiving-group plan; the harness does not choose or default it. The ordinary
801
677
  // seed offer crosses the kernel author gate before the first birth is attempted.
802
- run: () => plane.session(installWorkspace).offerCallUsdaOutcome(
678
+ run: () => plane.session(installWorkspace).offerGeneratedCallOutcome(
803
679
  call: applicationFrameworkCalls!.seedBirthPolicy(
804
680
  domain: '__nomos_application_home_v1',
805
681
  workspace: installWorkspace,
@@ -808,6 +684,7 @@ class NomosTestHarness {
808
684
  setAt: bornAt,
809
685
  ),
810
686
  actor: owner,
687
+ authorSecret: deviceSecret,
811
688
  ),
812
689
  );
813
690
  final keyHash = sha256.convert(base64Decode(devicePublic)).toString();
@@ -817,29 +694,30 @@ class NomosTestHarness {
817
694
  awaiting: 'law-declared enrollSigner offer',
818
695
  timeout: phaseTimeout ?? _kOtherDeadline,
819
696
  onDiagnostic: onDiagnostic,
820
- run: () => plane.session(installWorkspace).offerCallUsdaOutcome(
697
+ run: () => plane.session(installWorkspace).offerGeneratedCallOutcome(
821
698
  call: applicationFrameworkCalls!.enrollSigner(
822
699
  domain: '__nomos_application_home_v1',
823
700
  workspace: installWorkspace,
824
701
  keyHash: keyHash,
825
702
  grantedBy: owner,
826
703
  grantedAt: 1,
827
- verifiedVia: 'nomos:test-mint',
704
+ verifiedVia: 'nomos:harness-root',
828
705
  ),
829
706
  actor: owner,
707
+ authorSecret: deviceSecret,
830
708
  ),
831
709
  );
832
710
  final delegationCert =
833
711
  await plane.session(installWorkspace).signBirthCert(
834
712
  BirthCertificate(
835
- child: workspace,
713
+ child: openWorkspace,
836
714
  founder: owner,
837
715
  verified: true,
838
716
  parentKey: base64Decode(devicePublic),
839
717
  parentId: 'root',
840
718
  epoch: Int64.ZERO,
841
719
  ),
842
- deviceSecret,
720
+ base64Decode(deviceSecret),
843
721
  );
844
722
  await _phased(
845
723
  phase: 'application: record the parent-signed device delegation',
@@ -847,16 +725,17 @@ class NomosTestHarness {
847
725
  awaiting: 'law-declared enrollDelegation offer',
848
726
  timeout: phaseTimeout ?? _kOtherDeadline,
849
727
  onDiagnostic: onDiagnostic,
850
- run: () => plane.session(installWorkspace).offerCallUsdaOutcome(
728
+ run: () => plane.session(installWorkspace).offerGeneratedCallOutcome(
851
729
  call: applicationFrameworkCalls!.enrollDelegation(
852
730
  domain: '__nomos_application_home_v1',
853
731
  workspace: installWorkspace,
854
732
  user: owner,
855
- deviceKey: devicePublic,
733
+ deviceKey: 'key:$keyHash',
856
734
  certificate: delegationCert,
857
735
  enrolledAt: '1970-01-01T00:00:00.000Z',
858
736
  ),
859
737
  actor: owner,
738
+ authorSecret: deviceSecret,
860
739
  ),
861
740
  );
862
741
  final parentCert = await plane.session(installWorkspace).signBirthCert(
@@ -868,7 +747,7 @@ class NomosTestHarness {
868
747
  parentId: 'root',
869
748
  epoch: Int64.ZERO,
870
749
  ),
871
- deviceSecret,
750
+ base64Decode(deviceSecret),
872
751
  );
873
752
  await _phased(
874
753
  phase: 'application: attest and warrant the application parent',
@@ -877,7 +756,7 @@ class NomosTestHarness {
877
756
  timeout: phaseTimeout ?? _kOtherDeadline,
878
757
  onDiagnostic: onDiagnostic,
879
758
  run: () async {
880
- await plane.session(installWorkspace).offerCallUsdaOutcome(
759
+ await plane.session(installWorkspace).offerGeneratedCallOutcome(
881
760
  call: applicationFrameworkCalls!.seedBirthAttestation(
882
761
  domain: '__nomos_application_home_v1',
883
762
  workspace: installWorkspace,
@@ -890,8 +769,9 @@ class NomosTestHarness {
890
769
  links: <BirthCertificate>[parentCert],
891
770
  ),
892
771
  actor: owner,
772
+ authorSecret: deviceSecret,
893
773
  );
894
- await plane.session(installWorkspace).offerCallUsdaOutcome(
774
+ await plane.session(installWorkspace).offerGeneratedCallOutcome(
895
775
  call: applicationFrameworkCalls.seedGenesis(
896
776
  domain: '__nomos_application_home_v1',
897
777
  workspace: installWorkspace,
@@ -900,27 +780,28 @@ class NomosTestHarness {
900
780
  bornAt: '1970-01-01T00:00:00.000Z',
901
781
  ),
902
782
  actor: owner,
783
+ authorSecret: deviceSecret,
903
784
  );
904
785
  },
905
786
  );
906
787
  final homeCert = await plane.session(installWorkspace).signBirthCert(
907
788
  BirthCertificate(
908
- child: workspace,
789
+ child: openWorkspace,
909
790
  founder: owner,
910
791
  verified: true,
911
792
  parentKey: base64Decode(devicePublic),
912
793
  parentId: installWorkspace,
913
794
  epoch: Int64.ZERO,
914
795
  ),
915
- deviceSecret,
796
+ base64Decode(deviceSecret),
916
797
  );
917
798
  final outcome = await _phased(
918
799
  phase: 'application: birth the signed-in application',
919
- workspace: workspace,
800
+ workspace: openWorkspace,
920
801
  awaiting: 'attested receiving-group birth',
921
802
  timeout: phaseTimeout ?? _kOtherDeadline,
922
803
  onDiagnostic: onDiagnostic,
923
- run: () => plane.session(installWorkspace).offerCallUsdaOutcome(
804
+ run: () => plane.session(installWorkspace).offerGeneratedCallOutcome(
924
805
  call: applicationFrameworkCalls!.birthReceivingGroup(
925
806
  domain: '__nomos_application_home_v1',
926
807
  workspace: installWorkspace,
@@ -936,32 +817,34 @@ class NomosTestHarness {
936
817
  );
937
818
  String? bornStatus;
938
819
  for (final entry in outcome.birthOutcomes) {
939
- if (entry.workspace == workspace) {
820
+ if (entry.workspace == openWorkspace) {
940
821
  bornStatus = entry.status.name;
941
822
  break;
942
823
  }
943
824
  }
944
825
  if (outcome.born.length != 1 ||
945
- outcome.born.single != workspace ||
826
+ outcome.born.single != openWorkspace ||
946
827
  bornStatus != 'BIRTH_OUTCOME_STATUS_BORN') {
947
828
  throw NomosTestHarnessException(
948
- 'the offline application parent did not birth "$workspace" cleanly '
829
+ 'the offline application parent did not birth "$openWorkspace" cleanly '
949
830
  '(born=${outcome.born}, birthOutcomes=${outcome.birthOutcomes})',
950
831
  );
951
832
  }
952
833
  await _phased(
953
834
  phase: 'application: mount the signed-in application',
954
- workspace: workspace,
835
+ workspace: openWorkspace,
955
836
  awaiting: 'connect born receiving group',
956
837
  timeout: phaseTimeout ?? connectTimeout,
957
838
  onDiagnostic: onDiagnostic,
958
- run: () => plane.connect(
959
- cloud: 'https://offline.invalid',
960
- workspace: workspace,
961
- session: workspace,
962
- clientId: '$clientId-home',
963
- authorSecret: deviceSecret,
964
- ),
839
+ run: () async {
840
+ await plane.bindSession(
841
+ workspace: openWorkspace,
842
+ session: openWorkspace,
843
+ );
844
+ await plane.session(openWorkspace).refreshCredentials(
845
+ authorSecret: deviceSecret,
846
+ );
847
+ },
965
848
  );
966
849
  } else if (owner != null && owner.isNotEmpty) {
967
850
  final devicePublic = info['devicePublic']?.toString();
@@ -978,22 +861,22 @@ class NomosTestHarness {
978
861
  final keyHash = sha256.convert(base64Decode(devicePublic)).toString();
979
862
  await _phased(
980
863
  phase: 'owner: seat the test device signer',
981
- workspace: workspace,
864
+ workspace: openWorkspace,
982
865
  awaiting: 'verified test-root signer enrollment',
983
866
  timeout: phaseTimeout ?? _kOtherDeadline,
984
867
  onDiagnostic: onDiagnostic,
985
868
  // A generated TestPerson stands in for a real signed-in person. Seat the throwaway
986
869
  // harness device through the installed framework law with the same verified provenance
987
870
  // that the private test root later attests. Production clients never manufacture this
988
- // marker; it exists only inside the offline test-mint ceremony.
989
- run: () => plane.session(workspace).offerCallUsdaOutcome(
871
+ // marker; it exists only inside the offline harness ceremony.
872
+ run: () => plane.session(openWorkspace).offerGeneratedCallOutcome(
990
873
  call: applicationFrameworkCalls!.enrollSigner(
991
874
  domain: '__nomos_application_home_v1',
992
- workspace: workspace,
875
+ workspace: openWorkspace,
993
876
  keyHash: keyHash,
994
877
  grantedBy: owner,
995
878
  grantedAt: 1,
996
- verifiedVia: 'nomos:test-mint',
879
+ verifiedVia: 'nomos:harness-root',
997
880
  ),
998
881
  actor: owner,
999
882
  ),
@@ -1012,25 +895,25 @@ class NomosTestHarness {
1012
895
  final bornAt = DateTime.now().toUtc().toIso8601String();
1013
896
  await _phased(
1014
897
  phase: 'owner: make the test application ready',
1015
- workspace: workspace,
898
+ workspace: openWorkspace,
1016
899
  awaiting: 'framework owner birth',
1017
900
  timeout: phaseTimeout ?? _kOtherDeadline,
1018
901
  onDiagnostic: onDiagnostic,
1019
902
  run: () async {
1020
- await plane.session(workspace).offerCallUsdaOutcome(
903
+ await plane.session(openWorkspace).offerGeneratedCallOutcome(
1021
904
  call: applicationFrameworkCalls!.seedReceivingGroupPolicy(
1022
905
  domain: '__nomos_application_home_v1',
1023
- workspace: workspace,
906
+ workspace: openWorkspace,
1024
907
  setAt: bornAt,
1025
908
  ),
1026
909
  actor: 'host-birth',
1027
910
  );
1028
- await plane.session(workspace).offerCallUsdaOutcome(
911
+ await plane.session(openWorkspace).offerGeneratedCallOutcome(
1029
912
  call: applicationFrameworkCalls.seedReceivingGroup(
1030
913
  domain: '__nomos_application_home_v1',
1031
- workspace: workspace,
914
+ workspace: openWorkspace,
1032
915
  principal: owner,
1033
- workspaceName: _receivingGroupNameOf(owner),
916
+ workspaceName: openWorkspace,
1034
917
  bornAt: bornAt,
1035
918
  ),
1036
919
  actor: 'host-birth',
@@ -1039,52 +922,48 @@ class NomosTestHarness {
1039
922
  );
1040
923
  final parentCert = await plane.signBirthCert(
1041
924
  BirthCertificate(
1042
- child: workspace,
925
+ child: openWorkspace,
1043
926
  founder: owner,
1044
927
  verified: true,
1045
928
  parentKey: base64Decode(devicePublic),
1046
929
  parentId: 'root',
1047
930
  epoch: Int64.ZERO,
1048
931
  ),
1049
- deviceSecret,
932
+ base64Decode(deviceSecret),
1050
933
  );
1051
934
  await _phased(
1052
935
  phase: 'owner: establish the verified test application root',
1053
- workspace: workspace,
936
+ workspace: openWorkspace,
1054
937
  awaiting: 'birth attestation then genesis warrant',
1055
938
  timeout: phaseTimeout ?? _kOtherDeadline,
1056
939
  onDiagnostic: onDiagnostic,
1057
940
  run: () async {
1058
- await plane.session(workspace).offerOutcome(
1059
- domain: '__nomos_application_home_v1',
1060
- directiveId: 'seedBirthAttestation',
1061
- actor: owner,
1062
- authorSecret: deviceSecret,
1063
- payload: <String, Object?>{
1064
- 'child': workspace,
1065
- 'founder': owner,
1066
- 'verified': 'true',
1067
- 'parentId': 'root',
1068
- 'parentKey': devicePublic,
1069
- 'epoch': 0,
1070
- 'certChain': jsonEncode(<String, Object?>{
1071
- 'links': <Object?>[
1072
- _birthCertificateLawValue(parentCert),
1073
- ],
1074
- }),
1075
- },
1076
- );
1077
- await plane.session(workspace).offerOutcome(
1078
- domain: '__nomos_application_home_v1',
1079
- directiveId: 'seedGenesis',
1080
- actor: owner,
1081
- authorSecret: deviceSecret,
1082
- payload: <String, Object?>{
1083
- 'principal': owner,
1084
- 'workspaceId': workspace,
1085
- 'bornAt': '2000-01-01T00:00:00.000Z',
1086
- },
1087
- );
941
+ await plane.session(openWorkspace).offerGeneratedCallOutcome(
942
+ call: applicationFrameworkCalls!.seedBirthAttestation(
943
+ domain: '__nomos_application_home_v1',
944
+ workspace: openWorkspace,
945
+ child: openWorkspace,
946
+ founder: owner,
947
+ verified: 'true',
948
+ parentId: 'root',
949
+ parentKey: devicePublic,
950
+ epoch: 0,
951
+ links: <BirthCertificate>[parentCert],
952
+ ),
953
+ actor: owner,
954
+ authorSecret: deviceSecret,
955
+ );
956
+ await plane.session(openWorkspace).offerGeneratedCallOutcome(
957
+ call: applicationFrameworkCalls.seedGenesis(
958
+ domain: '__nomos_application_home_v1',
959
+ workspace: openWorkspace,
960
+ principal: owner,
961
+ workspaceId: openWorkspace,
962
+ bornAt: '2000-01-01T00:00:00.000Z',
963
+ ),
964
+ actor: owner,
965
+ authorSecret: deviceSecret,
966
+ );
1088
967
  },
1089
968
  );
1090
969
  } on DomainNotInstalledForSession {
@@ -1105,12 +984,12 @@ class NomosTestHarness {
1105
984
  ? installedHashes.last
1106
985
  : installedHashes.first)
1107
986
  .substring(0, 12);
1108
- phase('ready — session "$workspace" connected (law $sessionLaw)');
987
+ phase('ready — session "$openWorkspace" connected (law $sessionLaw)');
1109
988
  // THE DECOY SESSION (multi-workspace by default in tests): sole-session inference is how
1110
989
  // single-workspace assumptions ship — with one session connected, an op that forgot its session
1111
990
  // silently works in tests and only refuses in production when the app connects its second
1112
991
  // workspace. The decoy keeps the plane at ≥2 sessions so any unpinned op refuses HERE, in the
1113
- // dev's first test. [bridge] is a SESSION HANDLE pinned to [workspace], so tenant code written
992
+ // dev's first test. [reception] is a SESSION HANDLE pinned to the opened workspace, so tenant code written
1114
993
  // against the harness keeps working unchanged; only code that grabs the raw plane and assumes
1115
994
  // single-session trips. Opt out with `decoySession: false` (please don't).
1116
995
  if (decoySession && !birthsApplicationHome) {
@@ -1120,60 +999,42 @@ class NomosTestHarness {
1120
999
  awaiting: 'connect',
1121
1000
  timeout: phaseTimeout ?? connectTimeout,
1122
1001
  onDiagnostic: onDiagnostic,
1123
- run: () async => plane.connect(
1124
- cloud: 'https://offline.invalid',
1002
+ run: () async => plane.bindSession(
1125
1003
  workspace: _decoyWorkspace,
1126
1004
  session: _decoyWorkspace,
1127
- clientId: '$clientId-decoy',
1128
- allowUnborn: true,
1129
- restoreFrom: base64Encode(await File(snapshot).readAsBytes()),
1130
1005
  ),
1131
1006
  );
1132
1007
  }
1133
1008
  final harness = NomosTestHarness._(
1134
- plane.session(workspace),
1135
- workspace,
1009
+ plane.session(openWorkspace),
1010
+ openWorkspace,
1136
1011
  transport,
1137
1012
  workdir,
1138
1013
  onDiagnostic,
1139
- [workspace],
1014
+ [openWorkspace],
1140
1015
  devicePublics: {
1141
1016
  if (info['devicePublic'] != null)
1142
- workspace: info['devicePublic'].toString(),
1017
+ openWorkspace: info['devicePublic'].toString(),
1143
1018
  },
1144
1019
  deviceSecrets: {
1145
1020
  if (info['deviceSecret'] != null)
1146
- workspace: info['deviceSecret'].toString(),
1021
+ openWorkspace: info['deviceSecret'].toString(),
1147
1022
  },
1148
1023
  frameworkHash: info['frameworkHash']?.toString(),
1149
1024
  lawHashes: {
1150
- workspace: ((info['lawHashes'] as List?) ?? const [])
1025
+ openWorkspace: ((info['lawHashes'] as List?) ?? const [])
1151
1026
  .map((e) => e.toString())
1152
1027
  .toList(),
1153
1028
  },
1154
- snapshots: {
1155
- workspace: base64Encode(await File(snapshot).readAsBytes()),
1029
+ domainKeys: {
1030
+ openWorkspace: installed.domainKeys,
1031
+ },
1032
+ testPeer: peer,
1033
+ setupSessions: {
1034
+ if (birthsApplicationHome) installWorkspace,
1156
1035
  },
1157
- installLane: _InstallLane(
1158
- node: node,
1159
- useInRepoScript: useInRepoScript,
1160
- inRepoScript: resolvedHelper,
1161
- githolonCmd: githolonCmd,
1162
- runtimeDir: effectiveRuntimeDir,
1163
- clientId: clientId,
1164
- ),
1165
1036
  phaseTimeout: phaseTimeout,
1166
- cloudRestartLane: _CloudRestartLane(
1167
- entry: entry,
1168
- node: node,
1169
- cloud: 'https://offline.invalid',
1170
- authToken: '',
1171
- authorSecret: info['deviceSecret']?.toString() ?? '',
1172
- clientId: clientId,
1173
- resolveFrom: resolveFrom,
1174
- environment: runnerEnvironment,
1175
- ),
1176
- homeWorkspace: workspace,
1037
+ homeWorkspace: openWorkspace,
1177
1038
  );
1178
1039
  if (autoTearDown && registerTearDown != null) {
1179
1040
  registerTearDown(harness.dispose);
@@ -1187,17 +1048,16 @@ class NomosTestHarness {
1187
1048
  } catch (_) {}
1188
1049
  }
1189
1050
  try {
1190
- await workdir.delete(recursive: true);
1051
+ await workdir?.delete(recursive: true);
1191
1052
  } catch (_) {}
1192
1053
  rethrow;
1193
1054
  }
1194
1055
  }
1195
1056
 
1196
1057
  /// A COLOCATED TEST CLUSTER — install MANY workspaces' law offline and connect one SESSION per workspace on
1197
- /// ONE engine plane (one runner, one transport, one [bridge]), so a cross-workspace scenario (attested
1198
- /// reads, foreign-source imports) runs in-process with ZERO network. Each workspace's law is installed via
1199
- /// the SAME offline lane [ephemeral] uses (one snapshot per workspace), then `connect`ed as its own session;
1200
- /// the per-connect restore snapshot loads it into the shared plane.
1058
+ /// ONE kernel host (one runner, one transport, one [reception]), so a cross-workspace scenario (attested
1059
+ /// reads, foreign-source imports) runs in-process with ZERO network. Each workspace has separate real Git
1060
+ /// custody, while every operation enters the same Peer Office and kernel.
1201
1061
  ///
1202
1062
  /// [workspaces] — `{ workspaceName: [lawUsdzPaths] }` (installed in order per workspace). Get a
1203
1063
  /// session-scoped view with [session] and bind it to a generated client: `EstateClient(harness.session(
@@ -1212,7 +1072,6 @@ class NomosTestHarness {
1212
1072
  String clientId = 'test-runner',
1213
1073
  String? runtimeDir,
1214
1074
  String? nodeEntry,
1215
- String? installHelper,
1216
1075
  String? resolveFrom,
1217
1076
  String node = 'node',
1218
1077
  bool autoTearDown = true,
@@ -1234,218 +1093,75 @@ class NomosTestHarness {
1234
1093
  );
1235
1094
  }
1236
1095
  }
1096
+ for (final workspace in deviceKeyed) {
1097
+ if (!workspaces.containsKey(workspace)) {
1098
+ throw NomosTestHarnessException(
1099
+ 'deviceKeyed names "$workspace" but it is not a workspace of this cluster — '
1100
+ 'workspaces: ${workspaces.keys.toList()}',
1101
+ );
1102
+ }
1103
+ }
1237
1104
  final entry = nodeEntry ??
1238
- await _resolveEntry(node, resolveFrom ?? Directory.current.path);
1105
+ await _resolveTestEntry(node, resolveFrom ?? Directory.current.path);
1239
1106
  final effectiveRuntimeDir = runtimeDir ??
1240
1107
  Platform.environment['NOMOS_CANDIDATE_RUNTIME'] ??
1241
1108
  Platform.environment['NOMOS_DART_RUNTIME'] ??
1242
1109
  Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
1243
- await _sourceCandidateRuntime(entry);
1244
- final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
1245
- final resolvedHelper = installHelper ??
1246
- (File(adjacentInstaller).existsSync()
1247
- ? adjacentInstaller
1248
- : '${File(entry).parent.path}/offline-install.ts');
1249
- final useInRepoScript =
1250
- (resolvedHelper.endsWith('.ts') || resolvedHelper.endsWith('.mjs')) &&
1251
- File(resolvedHelper).existsSync();
1252
- final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
1253
-
1254
- final workdir = await Directory.systemTemp.createTemp('nomos-cluster-');
1255
- NodeRunnerTransport? transport;
1110
+ await _sourceCandidateRuntime(entry) ??
1111
+ '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
1112
+ NmsTestPeer? peer;
1256
1113
  try {
1257
- // 1. install each workspace's law offline → one snapshot per workspace (colocated, still separate law).
1258
- final snapshots = <String, String>{};
1259
- final lawHashesByWs = <String, List<String>>{};
1260
- final installDevicePublics = <String, String>{};
1261
- final installDeviceSecrets = <String, String>{};
1262
- String? planeTestRootPublic;
1263
- String? planeTestRootSecret;
1264
- String? frameworkHash;
1265
- var _i = 0;
1266
- for (final e in workspaces.entries) {
1267
- final snap = '${workdir.path}/${e.key}.bin';
1268
- final info = await _phased(
1269
- phase: '1: offline law install ${++_i}/${workspaces.length}',
1270
- workspace: e.key,
1271
- awaiting: useInRepoScript
1272
- ? 'offline-install.ts'
1273
- : 'githolon harness-install',
1274
- timeout: phaseTimeout ?? _kInstallDeadline,
1275
- onDiagnostic: onDiagnostic,
1276
- likelyCauses: const [
1277
- 'a cold npx download of githolon (pre-warm with any `githolon` run)',
1278
- 'a very large law package folding through the offline gate — raise phaseTimeout',
1279
- ],
1280
- run: () => _runInstall(
1281
- node: node,
1282
- useInRepoScript: useInRepoScript,
1283
- inRepoScript: resolvedHelper,
1284
- githolonCmd: githolonCmd,
1285
- lawUsdzPaths: e.value,
1286
- workspace: e.key,
1287
- snapshot: snap,
1288
- runtimeDir: effectiveRuntimeDir,
1289
- onDiagnostic: onDiagnostic,
1290
- ),
1291
- );
1292
- snapshots[e.key] = snap;
1293
- lawHashesByWs[e.key] = ((info['lawHashes'] as List?) ?? const [])
1294
- .map((x) => x.toString())
1295
- .toList();
1296
- frameworkHash ??= info['frameworkHash']?.toString();
1297
- // THE HARNESS DEVICE (era-0): the install lane mints one device key per workspace — sessions sign
1298
- // with it and the client completes the self-serve birth ceremony (enrol + cert) with the SAME key.
1299
- if (info['devicePublic'] != null)
1300
- installDevicePublics[e.key] = info['devicePublic'].toString();
1301
- if (info['deviceSecret'] != null)
1302
- installDeviceSecrets[e.key] = info['deviceSecret'].toString();
1303
- planeTestRootPublic ??= info['devicePublic']?.toString();
1304
- planeTestRootSecret ??= info['deviceSecret']?.toString();
1305
- }
1306
-
1307
- // 2. ONE runner / ONE bridge for the whole plane — no env restore default; each connect brings its own.
1308
- final phase = (String m) {
1309
- if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m');
1310
- };
1311
- transport = await _phased(
1312
- phase: '2: spawn the node runner (${workspaces.length} workspace(s))',
1313
- awaiting: 'Process.start($node)',
1114
+ peer = await _phased(
1115
+ phase: '1/2: open canonical test peer',
1116
+ awaiting: 'Peer Office + filesystem Git custody',
1314
1117
  timeout: phaseTimeout ?? _kSpawnDeadline,
1315
1118
  onDiagnostic: onDiagnostic,
1316
- run: () => NodeRunnerTransport.start(
1119
+ run: () => NmsTestPeer.start(
1317
1120
  entry: entry,
1121
+ runtimeDirectory: effectiveRuntimeDir,
1318
1122
  resolveFrom: resolveFrom,
1319
1123
  node: node,
1320
- environment: {
1321
- 'NOMOS_OFFLINE': '1',
1322
- 'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
1323
- if (planeTestRootPublic != null)
1324
- 'NOMOS_TEST_K_ROOT_PUBLIC': planeTestRootPublic,
1325
- if (planeTestRootSecret != null)
1326
- 'NOMOS_TEST_ROOT_SECRET': planeTestRootSecret,
1327
- if (effectiveRuntimeDir != null)
1328
- 'NOMOS_OFFLINE_RUNTIME': effectiveRuntimeDir,
1329
- },
1330
1124
  onDiagnostic: onDiagnostic,
1331
1125
  ),
1332
1126
  );
1333
- final bridge = NomosBridge(transport!);
1334
- await _phased(
1335
- phase: '2: runner ready',
1336
- awaiting: "event:'ready'",
1337
- timeout: phaseTimeout ?? readyTimeout,
1338
- onDiagnostic: onDiagnostic,
1339
- likelyCauses: const [
1340
- 'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + framework law archives)',
1341
- 'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
1342
- ],
1343
- run: () => bridge.ready,
1344
- );
1345
-
1346
- // 3. connect one SESSION per workspace, each restoring its OWN snapshot — LOCAL-ONLY (no cloud;
1347
- // these are local githolons on the device plane, offline by nature).
1348
- final snapshotB64 = <String, String>{};
1349
- var _n = 0;
1127
+ final lawHashesByWs = <String, List<String>>{};
1128
+ final domainKeysByWs = <String, Set<String>>{};
1129
+ final devicePublics = <String, String>{};
1130
+ final deviceSecrets = <String, String>{};
1131
+ String? frameworkHash;
1132
+ var index = 0;
1350
1133
  for (final e in workspaces.entries) {
1351
- snapshotB64[e.key] = base64Encode(
1352
- await File(snapshots[e.key]!).readAsBytes(),
1353
- );
1354
- await _phased(
1355
- phase: '3: connect session ${++_n}/${workspaces.length}',
1134
+ final installed = await _phased(
1135
+ phase: '2/2: install workspace ${++index}/${workspaces.length}',
1356
1136
  workspace: e.key,
1357
- awaiting: 'connect',
1358
- timeout: phaseTimeout ?? connectTimeout,
1137
+ awaiting: 'genesis offer + ordinary lifecycle offers',
1138
+ timeout: phaseTimeout ?? _kInstallDeadline,
1359
1139
  onDiagnostic: onDiagnostic,
1360
- likelyCauses: const [
1361
- 'a large ledger converge-fold can be slow — raise connectTimeout/phaseTimeout',
1362
- 'run with onDiagnostic to see where the runner blocked',
1363
- ],
1364
- run: () => bridge.connect(
1140
+ run: () => peer!.installWorkspace(
1365
1141
  workspace: e.key,
1366
- session: e.key,
1367
- clientId: '$clientId-${e.key}',
1368
- restoreFrom: snapshotB64[e.key],
1369
- allowUnborn: true,
1370
- // signed with the install-lane harness device (era-0 self-serve birth ceremony key)
1371
- authorSecret: installDeviceSecrets[e.key],
1372
- ),
1373
- );
1374
- }
1375
- phase('cluster: ${workspaces.length} session(s) connected');
1376
-
1377
- // 3b. DEVICE-KEY the named sessions: mint a fresh in-kernel key on each (now that it is mounted) and
1378
- // RE-OPEN it with that key as its signing authorSecret — the resolver's attested-read signing key.
1379
- // The re-open is the runner's ATOMIC session replace (dispose-then-connect under the hood); each
1380
- // leg runs as a named, deadlined phase so a wedge is a typed HarnessPhaseTimeout, never a hang.
1381
- final devicePublics = <String, String>{};
1382
- final deviceSecretsKeyed = <String, String>{};
1383
- for (final ws in deviceKeyed) {
1384
- if (!workspaces.containsKey(ws)) {
1385
- throw NomosTestHarnessException(
1386
- 'deviceKeyed names "$ws" but it is not a workspace of this cluster — '
1387
- 'workspaces: ${workspaces.keys.toList()}',
1388
- );
1389
- }
1390
- final kp = await _phased(
1391
- phase: '3b: device keygen',
1392
- workspace: ws,
1393
- awaiting: 'authorKeygen',
1394
- timeout: phaseTimeout ?? _kOtherDeadline,
1395
- onDiagnostic: onDiagnostic,
1396
- run: () => bridge.session(ws).authorKeygen(),
1397
- );
1398
- devicePublics[ws] = (kp['public'] ?? '').toString();
1399
- deviceSecretsKeyed[ws] = (kp['secret'] ?? '').toString();
1400
- await _phased(
1401
- phase: "3b: keyed reconnect of '$ws'",
1402
- workspace: ws,
1403
- awaiting: 'connect',
1404
- timeout: phaseTimeout ?? connectTimeout,
1405
- onDiagnostic: onDiagnostic,
1406
- likelyCauses: const [
1407
- 'the runner never answered the re-connect of an already-open session — '
1408
- 'update @githolon/client (the session re-open is an atomic replace)',
1409
- 'a large ledger re-fold on the re-mount — raise connectTimeout/phaseTimeout',
1410
- ],
1411
- run: () => bridge.connect(
1412
- workspace: ws,
1413
- session: ws,
1414
- clientId: '$clientId-$ws',
1415
- restoreFrom: snapshotB64[ws],
1416
- authorSecret: (kp['secret'] ?? '').toString(),
1417
- allowUnborn: true,
1142
+ lawUsdzPaths: e.value,
1418
1143
  ),
1419
1144
  );
1145
+ lawHashesByWs[e.key] = installed.lawHashes;
1146
+ domainKeysByWs[e.key] = installed.domainKeys;
1147
+ frameworkHash ??= installed.controllerHash;
1148
+ devicePublics[e.key] = base64Encode(installed.deviceKey.publicKey);
1149
+ deviceSecrets[e.key] = base64Encode(installed.deviceKey.secret);
1420
1150
  }
1421
- if (deviceKeyed.isNotEmpty) {
1422
- phase(
1423
- 'cluster: ${deviceKeyed.length} device-keyed session(s) re-opened',
1424
- );
1425
- }
1426
-
1427
1151
  final names = workspaces.keys.toList();
1428
1152
  final harness = NomosTestHarness._(
1429
- bridge,
1153
+ peer!.reception,
1430
1154
  names.first,
1431
- transport,
1432
- workdir,
1155
+ peer.transport,
1156
+ peer.custody.directory,
1433
1157
  onDiagnostic,
1434
1158
  names,
1435
- // install-lane harness devices under; explicit deviceKeyed re-opens override for their ws
1436
- devicePublics: {...installDevicePublics, ...devicePublics},
1437
- deviceSecrets: {...installDeviceSecrets, ...deviceSecretsKeyed},
1159
+ devicePublics: devicePublics,
1160
+ deviceSecrets: deviceSecrets,
1438
1161
  frameworkHash: frameworkHash,
1439
1162
  lawHashes: lawHashesByWs,
1440
- snapshots: snapshotB64,
1441
- installLane: _InstallLane(
1442
- node: node,
1443
- useInRepoScript: useInRepoScript,
1444
- inRepoScript: resolvedHelper,
1445
- githolonCmd: githolonCmd,
1446
- runtimeDir: effectiveRuntimeDir,
1447
- clientId: clientId,
1448
- ),
1163
+ domainKeys: domainKeysByWs,
1164
+ testPeer: peer,
1449
1165
  phaseTimeout: phaseTimeout,
1450
1166
  );
1451
1167
  if (autoTearDown && registerTearDown != null) {
@@ -1453,23 +1169,16 @@ class NomosTestHarness {
1453
1169
  }
1454
1170
  return harness;
1455
1171
  } catch (_) {
1456
- if (transport != null) {
1457
- try {
1458
- await transport.dispose();
1459
- } catch (_) {}
1460
- }
1461
- try {
1462
- await workdir.delete(recursive: true);
1463
- } catch (_) {}
1172
+ await peer?.dispose();
1464
1173
  rethrow;
1465
1174
  }
1466
1175
  }
1467
1176
 
1468
1177
  /// CLOUD-BOUND HARNESS (co2's real-app topology): a [NomosTestCloud] plays the remote control plane, the
1469
1178
  /// REAL authenticated home-birth ceremony runs REMOTELY on [platformWorkspace], and the born home is
1470
- /// mounted into ONE local engine plane as a normal cloud-bound [NomosSession] ([homeSession]). From there
1179
+ /// mounted into ONE local kernel host as a normal cloud-bound [NomosSession] ([homeSession]). From there
1471
1180
  /// the app shape holds exactly: generated `.births()` directives run LOCALLY (the app never clones the
1472
- /// platform), children open as sibling sessions (`outcome.bornRefs.single.acquire(harness.bridge)`),
1181
+ /// platform), children open as sibling sessions (`outcome.bornRefs.single.acquire(harness.reception)`),
1473
1182
  /// writes/projections work offline, and [converge] establishes remote custody parent-first when
1474
1183
  /// connectivity returns ([NomosTestCloud.setOnline]). Entirely inside the test cloud's trust universe.
1475
1184
  ///
@@ -1503,6 +1212,11 @@ class NomosTestHarness {
1503
1212
  Duration connectTimeout = const Duration(seconds: 120),
1504
1213
  Duration? phaseTimeout,
1505
1214
  bool decoySession = true,
1215
+
1216
+ /// Where the spawned node peer keeps its Git custody. A directory here makes that custody durable
1217
+ /// across the peer's restarts (the local-first restart lane); absent, the peer uses a temporary
1218
+ /// directory it removes on exit. NOMOS_NODE_PEER_CUSTODY names one for a whole test run.
1219
+ String? nodePeerCustody,
1506
1220
  }) async {
1507
1221
  if (lawUsdzPaths.isEmpty) {
1508
1222
  throw NomosTestHarnessException(
@@ -1520,6 +1234,26 @@ class NomosTestHarness {
1520
1234
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
1521
1235
  final bareUid =
1522
1236
  subject.startsWith('user:') ? subject.substring(5) : subject;
1237
+ // THE OPERATOR SEAM, AND THE ONLY RAW HOST LANE LEFT IN THIS FILE.
1238
+ //
1239
+ // `cloudBound` stands a REMOTE test cloud up before any custody exists on
1240
+ // this device. In that moment the harness is acting as an OPERATOR, not as a
1241
+ // client: there is no session, no mounted workspace, and therefore no typed
1242
+ // office paper that could carry these — every Runtime v2 read answers from
1243
+ // local custody the harness does not yet have. Exactly three calls remain,
1244
+ // and each is a host control-plane route with no client equivalent:
1245
+ //
1246
+ // POST /v2/workspaces/<platform> boot the born platform (idempotent)
1247
+ // GET /v2/workspaces/<platform>/domains its installed law, before custody
1248
+ // POST /dev/mint-token the LOCAL dev server's test identity
1249
+ // provider — a fixture route, not a
1250
+ // Nomos host route at all
1251
+ //
1252
+ // What was here and is gone: a `GET /openapi.json` scrape whose only product
1253
+ // was a domain name the compiled framework calls already carry. Everything
1254
+ // after this phase — every offer, every read, the whole birth ceremony —
1255
+ // goes through the office. Nothing below may grow a fourth probe: if a
1256
+ // client would need it, it belongs on a desk.
1523
1257
  final http = HttpClient();
1524
1258
  Future<Map<String, Object?>> post(
1525
1259
  String path,
@@ -1569,13 +1303,12 @@ class NomosTestHarness {
1569
1303
  // server's in-process operator lane). Booting a born workspace is idempotent; an unborn one
1570
1304
  // refuses 409, surfaced here with the fix.
1571
1305
  String? frameworkHash;
1572
- final frameworkPostRoutes =
1573
- <({String domain, String directive, String path})>[];
1574
1306
  final lawHashes = <String>[];
1307
+ late NomosBoundInteraction applicationInteraction;
1575
1308
  await _phased(
1576
1309
  phase: '1/6: boot the born platform + inspect installed law',
1577
1310
  workspace: platformWorkspace,
1578
- awaiting: 'POST /v2/workspaces + GET /domains + /openapi.json',
1311
+ awaiting: 'boot the born platform + read its installed law',
1579
1312
  timeout: phaseTimeout ?? _kInstallDeadline,
1580
1313
  onDiagnostic: onDiagnostic,
1581
1314
  run: () async {
@@ -1623,30 +1356,6 @@ class NomosTestHarness {
1623
1356
  in (((existing['currentLaw'] as Map?) ?? const {}).values)) {
1624
1357
  rememberLaw(hash);
1625
1358
  }
1626
- final openapi = await get(
1627
- '/v2/workspaces/$platformWorkspace/openapi.json',
1628
- );
1629
- final pathSets = <Map>[
1630
- (openapi['paths'] as Map?) ?? const {},
1631
- (((openapi['x-nomos-framework-routes'] as Map?)?['paths'])
1632
- as Map?) ??
1633
- const {},
1634
- ];
1635
- for (final paths in pathSets) {
1636
- for (final entry in paths.entries) {
1637
- final operation = (entry.value as Map?)?['post'] as Map?;
1638
- final route = operation?['x-nomos'] as Map?;
1639
- final domain = route?['domain']?.toString() ?? '';
1640
- final directive = route?['directiveId']?.toString() ?? '';
1641
- if (domain.isNotEmpty && directive.isNotEmpty) {
1642
- frameworkPostRoutes.add((
1643
- domain: domain,
1644
- directive: directive,
1645
- path: entry.key.toString(),
1646
- ));
1647
- }
1648
- }
1649
- }
1650
1359
  },
1651
1360
  );
1652
1361
  if (frameworkHash == null || frameworkHash!.isEmpty) {
@@ -1659,19 +1368,13 @@ class NomosTestHarness {
1659
1368
  'cloudBound: the born test platform reported no installed application law — cannot run the birth ceremony',
1660
1369
  );
1661
1370
  }
1662
- final birthRoutes = frameworkPostRoutes
1663
- .where(
1664
- (route) =>
1665
- route.directive == 'nomosInternalBirthReceivingGroup7c1f5e',
1666
- )
1667
- .toSet()
1668
- .toList();
1669
- if (birthRoutes.length != 1) {
1670
- throw NomosTestHarnessException(
1671
- 'cloudBound: the deployed package must expose exactly one framework receiving-group route; found ${birthRoutes.length}',
1672
- );
1673
- }
1674
- final applicationHomeDomain = birthRoutes.single.domain;
1371
+ // THE COMPILED DESCRIPTOR NAMES ITS OWN DOMAIN. This used to scrape the
1372
+ // platform's `/openapi.json` for the framework birth route and read the
1373
+ // domain off it — a raw JSON probe of the host to learn a fact the
1374
+ // application's generated framework calls already carry, and whose scraped
1375
+ // paths were never sent anything: remote offers go through
1376
+ // `nomosInternalOfferRemoteGeneratedCall`, which routes by generated call.
1377
+ final applicationHomeDomain = frameworkCalls.applicationHomeDomain;
1675
1378
  // A composed application has two distinct laws at this seam:
1676
1379
  // * the parent's CURRENT control law authors enrollFirstDevice + the birth offer;
1677
1380
  // * the application's final PAYLOAD package is the law installed into the born receiving group.
@@ -1697,25 +1400,7 @@ class NomosTestHarness {
1697
1400
  );
1698
1401
  }
1699
1402
  final applicationHomeLawHash = lawHashes.last;
1700
- final enrollmentRoutes = frameworkPostRoutes
1701
- .where(
1702
- (route) =>
1703
- route.directive == 'enrollFirstDevice' &&
1704
- route.domain == applicationHomeDomain,
1705
- )
1706
- .toSet()
1707
- .toList();
1708
- if (enrollmentRoutes.length != 1) {
1709
- throw NomosTestHarnessException(
1710
- "cloudBound: application/home domain '$applicationHomeDomain' must expose exactly one semantic enrollFirstDevice route; found ${enrollmentRoutes.length}",
1711
- );
1712
- }
1713
- if (enrollmentRoutes.single.path.isEmpty) {
1714
- throw NomosTestHarnessException(
1715
- 'cloudBound: enrollFirstDevice generated an empty relay path',
1716
- );
1717
- }
1718
- // 2/6 — the LOCAL engine plane, CLOUD-BOUND, spawned BEFORE the birth: the runner's local kernel is
1403
+ // 2/6 — the LOCAL kernel host, CLOUD-BOUND, spawned BEFORE the birth: the runner's local kernel is
1719
1404
  // what mints the device key + signs the birth cert (era-0: a birth is ATTESTED or not at all).
1720
1405
  // The executable AND its framework-law packages are one runtime bundle in device custody. A process
1721
1406
  // restart while offline must not fetch any member before it can replay the persisted ledger. Release CI
@@ -1724,18 +1409,15 @@ class NomosTestHarness {
1724
1409
  final candidateRuntimeDir = Platform.environment['NOMOS_DART_RUNTIME'] ??
1725
1410
  Platform.environment['NOMOS_OFFLINE_RUNTIME'];
1726
1411
  final localRuntime = await Directory(
1727
- '${workdir.path}/client-runtime',
1412
+ '${workdir.path}/peer-office-runtime',
1728
1413
  ).create(recursive: true);
1729
- final selectedKernel = cloud.testKRootPublic == null
1730
- ? _CandidateKernelBinary.production
1731
- : _CandidateKernelBinary.testMint;
1732
1414
  final candidateArtifacts = candidateRuntimeDir == null
1733
1415
  ? const <_KernelRuntimeArtifact, String>{}
1734
1416
  : <_KernelRuntimeArtifact, String>{
1735
1417
  _KernelRuntimeArtifact.executable:
1736
- '$candidateRuntimeDir/${selectedKernel.fileName}',
1418
+ '$candidateRuntimeDir/holon.wasm',
1737
1419
  _KernelRuntimeArtifact.repository:
1738
- '$candidateRuntimeDir/${selectedKernel == _CandidateKernelBinary.production ? 'kernel.repository.xml' : 'kernel-testmint.repository.xml'}',
1420
+ '$candidateRuntimeDir/kernel.repository.xml',
1739
1421
  _KernelRuntimeArtifact.stageZeroLaw:
1740
1422
  '$candidateRuntimeDir/${_KernelRuntimeArtifact.stageZeroLaw.fileName}',
1741
1423
  _KernelRuntimeArtifact.controllerLaw:
@@ -1781,12 +1463,22 @@ class NomosTestHarness {
1781
1463
  },
1782
1464
  );
1783
1465
  }
1784
- final runnerEnvironment = <String, String>{
1785
- ...Platform.environment,
1466
+ final custodyDirectory = nodePeerCustody ??
1467
+ Platform.environment['NOMOS_NODE_PEER_CUSTODY'] ??
1468
+ '${workdir.path}/device';
1469
+ final runnerArguments = <String>[
1470
+ '--runtime-bundle=${localRuntime.path}',
1471
+ '--discovery-provider=${cloud.origin}',
1786
1472
  if (cloud.testKRootPublic != null)
1787
- 'NOMOS_TEST_K_ROOT_PUBLIC': cloud.testKRootPublic!,
1788
- 'NOMOS_LOCAL_RUNTIME_DIR': localRuntime.path,
1789
- };
1473
+ '--trust-root-public-key=${cloud.testKRootPublic}',
1474
+ // Under --diagnostics the spawned peer narrates every frame (command → outcome) to its stderr,
1475
+ // which this harness relays as `Shell:` lines.
1476
+ if (Platform.environment['NOMOS_TEST_DIAGNOSTICS'] == '1')
1477
+ '--trace=frames',
1478
+ // Process memory is disposable; restart must reuse this device's disk.
1479
+ // Keep default custody under the harness-owned root until dispose.
1480
+ '--custody=$custodyDirectory',
1481
+ ];
1790
1482
  transport = await _phased(
1791
1483
  phase: '2/6: spawn the node runner (cloud-bound)',
1792
1484
  awaiting: 'Process.start($node)',
@@ -1796,11 +1488,11 @@ class NomosTestHarness {
1796
1488
  entry: entry,
1797
1489
  resolveFrom: resolveFrom,
1798
1490
  node: node,
1799
- environment: runnerEnvironment,
1491
+ arguments: runnerArguments,
1800
1492
  onDiagnostic: onDiagnostic,
1801
1493
  ),
1802
1494
  );
1803
- final plane = NomosBridge(transport!);
1495
+ final plane = DartReception(transport!);
1804
1496
  await _phased(
1805
1497
  phase: '3/6: runner ready',
1806
1498
  awaiting: "event:'ready'",
@@ -1826,21 +1518,18 @@ class NomosTestHarness {
1826
1518
  // tolerated): a PRIVATE platform (declared read caps — co2's shape) refuses an
1827
1519
  // unauthenticated ledger clone, and the ceremony never needs the platform's ledger anyway
1828
1520
  // (its writes go over the HTTP gateway below).
1829
- await plane.connect(
1830
- cloud: cloud.origin,
1521
+ await plane.bindSession(
1831
1522
  workspace: '$clientId-onboard-scratch',
1832
1523
  session: '$clientId-onboard-scratch',
1833
- clientId: '$clientId-onboard',
1834
- allowUnborn: true,
1835
1524
  );
1836
1525
  final kp = await plane.authorKeygen();
1837
- final publicB64 = (kp['public'] ?? '').toString();
1838
- final secret = (kp['secret'] ?? '').toString();
1839
- if (publicB64.isEmpty || secret.isEmpty) {
1526
+ if (kp.publicKey.length != 32 || kp.secret.length != 32) {
1840
1527
  throw NomosTestHarnessException(
1841
1528
  'cloudBound: authorKeygen returned no device key',
1842
1529
  );
1843
1530
  }
1531
+ final publicB64 = base64Encode(kp.publicKey);
1532
+ final secret = base64Encode(kp.secret);
1844
1533
  final keyHash = sha256.convert(base64Decode(publicB64)).toString();
1845
1534
  final minted = await post('/dev/mint-token', {
1846
1535
  'sub': bareUid,
@@ -1859,16 +1548,16 @@ class NomosTestHarness {
1859
1548
  // WITHOUT it the platform holds no delegation for the owner, the assembly falls to the
1860
1549
  // no-delegation branch, the platform_cert lands in the delegation slot, and the home refuses at
1861
1550
  // its OWN verify door ("home cert founder != delegation cert founder").
1862
- final delegationCert = await plane.mintKeylessDelegation({
1863
- 'user': subject,
1864
- 'deviceKey': publicB64,
1865
- 'childName': _receivingGroupNameOf(subject),
1866
- 'parentId': platformWorkspace,
1867
- 'verifiedVia': issuer,
1868
- 'idpToken': idpToken,
1869
- 'capturedClock': DateTime.now().toUtc().millisecondsSinceEpoch,
1870
- 'epoch': 0,
1871
- }, secret);
1551
+ final delegationCert = await plane.mintKeylessDelegation(
1552
+ user: subject,
1553
+ devicePublic: kp.publicKey,
1554
+ childName: await plane.nmsReceivingGroupWorkspace(subject),
1555
+ parentId: platformWorkspace,
1556
+ verifiedVia: issuer,
1557
+ idpToken: idpToken,
1558
+ capturedClock: DateTime.now().toUtc().millisecondsSinceEpoch,
1559
+ deviceSecret: kp.secret,
1560
+ );
1872
1561
  return (
1873
1562
  publicB64: publicB64,
1874
1563
  secret: secret,
@@ -1889,85 +1578,109 @@ class NomosTestHarness {
1889
1578
  timeout: phaseTimeout ?? _kInstallDeadline,
1890
1579
  onDiagnostic: onDiagnostic,
1891
1580
  run: () async {
1892
- final homeName = _receivingGroupNameOf(subject);
1581
+ final homeName = await plane.nmsReceivingGroupWorkspace(subject);
1893
1582
  final bornAt = DateTime.now().toUtc().toIso8601String();
1894
- final result = await plane
1895
- .session('$clientId-onboard-scratch')
1896
- .nomosInternalBirthReceivingGroup(
1897
- parent: platformWorkspace,
1898
- principal: bareUid,
1899
- subject: subject,
1900
- device: {
1901
- 'public': device.publicB64,
1902
- 'secret': device.secret,
1903
- },
1904
- authToken: device.idpToken,
1905
- idToken: device.idpToken,
1906
- verifiedVia: device.issuer,
1907
- bornAt: bornAt,
1908
- enrollFirstDeviceCall: frameworkCalls.enrollFirstDevice(
1909
- domain: applicationHomeDomain,
1910
- workspace: platformWorkspace,
1911
- keyHash: device.keyHash,
1912
- verifiedVia: device.issuer,
1913
- validFromEpoch: 0,
1914
- grantedAt: bornAt,
1915
- idpToken: device.idpToken,
1916
- delegationCertificate: device.delegationCert,
1917
- ),
1918
- birthReceivingGroupCall: frameworkCalls.birthReceivingGroup(
1919
- domain: applicationHomeDomain,
1920
- workspace: platformWorkspace,
1921
- owner: subject,
1922
- frameworkHash: Sha256Digest(frameworkHash!),
1923
- lawHash: Sha256Digest(applicationHomeLawHash),
1924
- bornAt: bornAt,
1925
- ),
1926
- confirm: true,
1927
- );
1928
- if (result.home != homeName) {
1929
- throw NomosTestHarnessException(
1930
- 'cloudBound: production custody ceremony returned ${result.home} instead of "$homeName"',
1931
- );
1932
- }
1933
- if (!result.verified) {
1934
- throw NomosTestHarnessException(
1935
- 'cloudBound: born Home failed kernel verification: ${result.verificationError}',
1936
- );
1937
- }
1583
+ final scratch = plane.session('$clientId-onboard-scratch');
1584
+ final enrolled = await scratch.nomosInternalOfferRemoteGeneratedCall(
1585
+ targetWorkspace: platformWorkspace,
1586
+ actor: subject,
1587
+ authorSecret: device.secret,
1588
+ authToken: device.idpToken,
1589
+ call: frameworkCalls.enrollFirstDevice(
1590
+ domain: applicationHomeDomain,
1591
+ workspace: platformWorkspace,
1592
+ keyHash: device.keyHash,
1593
+ verifiedVia: device.issuer,
1594
+ validFromEpoch: 0,
1595
+ grantedAt: bornAt,
1596
+ idpToken: device.idpToken,
1597
+ delegationCertificate: device.delegationCert,
1598
+ ),
1599
+ );
1600
+ // The Home's OWN certificate: plain, device-signed, no keyless fields — the delegation
1601
+ // enrolled above rides the chain as its own link (see mintHomeBirthCertificate).
1602
+ final homeCert = await plane.mintHomeBirthCertificate(
1603
+ user: subject,
1604
+ devicePublic: base64Decode(device.publicB64),
1605
+ childName: homeName,
1606
+ parentId: platformWorkspace,
1607
+ deviceSecret: base64Decode(device.secret),
1608
+ );
1609
+ await scratch.nomosInternalOfferRemoteGeneratedCall(
1610
+ targetWorkspace: platformWorkspace,
1611
+ actor: subject,
1612
+ authorSecret: device.secret,
1613
+ authToken: device.idpToken,
1614
+ call: frameworkCalls.birthReceivingGroup(
1615
+ domain: applicationHomeDomain,
1616
+ workspace: platformWorkspace,
1617
+ owner: subject,
1618
+ frameworkHash: Sha256Digest(frameworkHash!),
1619
+ lawHash: Sha256Digest(applicationHomeLawHash),
1620
+ bornAt: bornAt,
1621
+ ),
1622
+ birthCertificate: homeCert,
1623
+ requiredTargetHead: enrolled.head,
1624
+ );
1938
1625
  home = homeName;
1939
1626
  },
1940
1627
  );
1941
- // 6/6 — mount the born home as a normal cloud-bound session (clone from the test cloud).
1628
+ // 6/6 — bind the interaction, then observe the local verification postcondition.
1629
+ // Binding owns no custody. The cloud's published retained placement independently
1630
+ // drives Replica Follow; this test barrier observes readiness and never starts sync.
1942
1631
  await _phased(
1943
1632
  phase: '6/6: mount the born home',
1944
1633
  workspace: home,
1945
- awaiting: 'connect(cloud:, workspace: $home)',
1634
+ awaiting: 'retained Home custody and local chain verification',
1946
1635
  timeout: phaseTimeout ?? connectTimeout,
1947
1636
  onDiagnostic: onDiagnostic,
1948
- run: () => plane.connect(
1949
- cloud: cloud.origin,
1950
- workspace: home!,
1951
- clientId: clientId,
1637
+ run: () async {
1952
1638
  // A PRIVATE home (declared read caps — co2's shape) serves its ledger only to a VERIFIED
1953
1639
  // principal: ride the caller's token when given, else the dev-IdP token the ceremony minted.
1954
- authToken: authToken ?? device.idpToken,
1955
- // The home was born WARRANTED with THIS device enrolled by the kernel-owned attestation step —
1956
- // connect with the same key so every write is signed and the home's own `.births()` offers
1957
- // (estates/catalogues) are device-attested (the client signs the birth cert automatically).
1958
- authorSecret: device.secret,
1959
- ),
1960
- );
1961
- // A cloud-bound generated application owns continuous convergence from the moment its home is
1962
- // mounted. Starting it here makes the harness exercise the product lifecycle: local writes flush,
1963
- // transport debt survives a partition, and reconnect settles exact operation receipts without tenant
1964
- // code calling sync(). Child sessions take the same path in acquireSession().
1965
- final applicationSession = plane.session(home!);
1966
- applicationSession.nomosInternalRetainCloudAccess(
1967
- cloud: cloud.origin,
1968
- signIn: () async => authToken ?? device.idpToken,
1640
+ // The home was born WARRANTED with THIS device enrolled by the kernel-owned attestation step,
1641
+ // so the same key signs every later write and its own `.births()` offers.
1642
+ await plane.session('$clientId-onboard-scratch').refreshCredentials(
1643
+ authToken: authToken ?? device.idpToken,
1644
+ authorSecret: device.secret,
1645
+ );
1646
+ applicationInteraction = await plane.bindSession(
1647
+ workspace: home!,
1648
+ session: home!,
1649
+ credentialSourceSession: '$clientId-onboard-scratch',
1650
+ );
1651
+ plane.nomosInternalRegisterCredentialProvider(
1652
+ interaction: applicationInteraction,
1653
+ cloud: cloud.origin,
1654
+ signIn: ({deviceChallenge}) async => authToken ?? device.idpToken,
1655
+ );
1656
+ final deadline = DateTime.now().add(phaseTimeout ?? connectTimeout);
1657
+ while (true) {
1658
+ try {
1659
+ final verdict = await plane.session(home!).verifyChain();
1660
+ if (!verdict.valid) {
1661
+ throw NomosTestHarnessException(
1662
+ 'cloudBound: born Home failed kernel verification: '
1663
+ '${verdict.hasError() ? verdict.error : 'unknown refusal'}',
1664
+ );
1665
+ }
1666
+ break;
1667
+ } on NomosUnavailable catch (unavailable) {
1668
+ if (!unavailable.retryable ||
1669
+ !{
1670
+ NomosUnavailableReason.localReplicaNotReady,
1671
+ NomosUnavailableReason.workspaceOpening,
1672
+ }.contains(unavailable.reason) ||
1673
+ DateTime.now().isAfter(deadline)) rethrow;
1674
+ onDiagnostic?.call(
1675
+ '[nomos-test-harness] waiting for retained Home: ${unavailable.reason.name}',
1676
+ );
1677
+ await Future<void>.delayed(
1678
+ unavailable.retryAfter ?? const Duration(milliseconds: 100),
1679
+ );
1680
+ }
1681
+ }
1682
+ },
1969
1683
  );
1970
- await applicationSession.autoSync(fallbackMs: 60000);
1971
1684
  // NO DECOY on a cloudBound harness: the app is genuinely multi-workspace (the home plus every
1972
1685
  // locally-born child), so the plane is never sole-session — the decoy the offline harness
1973
1686
  // needs (to break single-session inference) would just be a spurious local-only workspace here.
@@ -1994,7 +1707,10 @@ class NomosTestHarness {
1994
1707
  entry: entry,
1995
1708
  resolveFrom: resolveFrom,
1996
1709
  node: node,
1997
- environment: runnerEnvironment,
1710
+ arguments: runnerArguments
1711
+ .where((arg) => !arg.startsWith('--custody='))
1712
+ .toList(),
1713
+ custodyDirectory: custodyDirectory,
1998
1714
  cloud: cloud.origin,
1999
1715
  authToken: authToken ?? device.idpToken,
2000
1716
  authorSecret: device.secret,
@@ -2020,10 +1736,6 @@ class NomosTestHarness {
2020
1736
  }
2021
1737
  }
2022
1738
 
2023
- /// Compiler/runtime-only receiving-group address. The law independently re-derives the same value.
2024
- static String _receivingGroupNameOf(String principal) =>
2025
- 'nomos-rg-${sha256.convert(utf8.encode(principal)).toString().substring(0, 40)}';
2026
-
2027
1739
  /// CONVERGE local custody with the test cloud (co2 topology step 8): sync every connected session —
2028
1740
  /// PARENT-FIRST by connect order, so the home's chain lands remotely and its birth offer-effects create
2029
1741
  /// each locally-born child's remote custody BEFORE that child's own sync converges against it. Repeats
@@ -2047,7 +1759,7 @@ class NomosTestHarness {
2047
1759
  while (pending.isNotEmpty) {
2048
1760
  for (final ws in List.of(pending)) {
2049
1761
  try {
2050
- final r = await bridge.session(ws).sync(admit: true);
1762
+ final r = await _syncForTest(ws);
2051
1763
  last[ws] = r;
2052
1764
  lastErrors.remove(ws);
2053
1765
  final permanentFailure = nmsPermanentConvergenceFailure(r);
@@ -2056,8 +1768,7 @@ class NomosTestHarness {
2056
1768
  'converge: $ws rejected or dead-lettered business work — $permanentFailure',
2057
1769
  );
2058
1770
  }
2059
- if (!r.pushFailed && !r.stillAhead && !r.offline && r.deferred == 0)
2060
- pending.remove(ws);
1771
+ if (nmsRemoteConvergenceSettled(r)) pending.remove(ws);
2061
1772
  } on NomosTestHarnessException {
2062
1773
  rethrow;
2063
1774
  } catch (e) {
@@ -2076,12 +1787,119 @@ class NomosTestHarness {
2076
1787
  return last;
2077
1788
  }
2078
1789
 
1790
+ /// Test-only convergence control. Production application sessions cannot
1791
+ /// drive Replica Follow; cloud fixtures need an explicit completion barrier.
1792
+ Future<SyncWorkspaceResponse> _syncForTest(String workspace) async {
1793
+ final runtime = _transport as NomosRuntimeV2Transport;
1794
+ final response = await runtime.invokeRuntime(protocol.RuntimeRequest(
1795
+ protocolVersion: 2,
1796
+ requestId: Int64(runtime.nextRuntimeRequestId()),
1797
+ syncWorkspace: protocol.SyncWorkspaceRequest(
1798
+ workspace: workspace,
1799
+ admit: true,
1800
+ ),
1801
+ ));
1802
+ if (!response.hasSyncWorkspace()) {
1803
+ throw StateError(
1804
+ 'Test convergence returned ${response.whichOutcome().name}'
1805
+ '${response.hasFailure() ? ': ${response.failure.code}: ${response.failure.message}' : ''}',
1806
+ );
1807
+ }
1808
+ return response.syncWorkspace;
1809
+ }
1810
+
2079
1811
  // ── CUSTODY DIAGNOSTIC HELPERS (architecture/session_bound_clients.md ask 9) ──
2080
1812
 
2081
- /// The raw multi-workspace plane under [bridge] (the session handle a single-workspace harness
1813
+ /// Capture a point for a retained-upgrade fixture in this throwaway peer.
1814
+ Future<NomosTestFrontier> captureLocalFrontier(String workspace,
1815
+ {NomosTestFrontier? after}) async {
1816
+ if (_testPeer == null || testCloud != null) {
1817
+ throw StateError(
1818
+ 'Retained frontier fixtures require an isolated local test peer.');
1819
+ }
1820
+ if (!_plane.boundSessions.contains(workspace)) {
1821
+ throw StateError('Capture requires an already-open local workspace.');
1822
+ }
1823
+ if (after != null) {
1824
+ _requireLocalFrontier(after);
1825
+ if (after.workspace != workspace)
1826
+ throw StateError('Fixture base belongs to another workspace.');
1827
+ }
1828
+ final runtime = _transport as NomosRuntimeV2Transport;
1829
+ final exported = await runtime.invokeRuntime(protocol.RuntimeRequest(
1830
+ protocolVersion: 2,
1831
+ requestId: Int64(runtime.nextRuntimeRequestId()),
1832
+ exportLiveOffers: protocol.ExportLiveOffersRequest(
1833
+ session: workspace, baseHead: after?.head ?? ''),
1834
+ ));
1835
+ if (!exported.hasLiveOffers()) {
1836
+ // The office answers a refusal as a typed failure, not as an absent field. Reporting only
1837
+ // "not exported" threw that away and every caller — the bench bridge included — saw a
1838
+ // sentence that named neither the workspace nor the reason.
1839
+ final because = exported.hasFailure()
1840
+ ? '${exported.failure.code}: ${exported.failure.message}'
1841
+ : 'the office answered ${exported.whichOutcome().name}';
1842
+ throw StateError(
1843
+ "Test frontier offers were not exported for '$workspace' — $because");
1844
+ }
1845
+ return NomosTestFrontier._(this, workspace,
1846
+ await _plane.session(workspace).head(), exported.liveOffers);
1847
+ }
1848
+
1849
+ /// Arrange a stale retained target before opening the presentation under test.
1850
+ /// The test process stops before selecting an earlier already-authenticated
1851
+ /// Git point. Restart uses ordinary authenticated custody open; production
1852
+ /// and connected peers are ineligible. This is fixture arrangement, not sync.
1853
+ Future<void> arrangeRetainedFrontier(NomosTestFrontier frontier) async {
1854
+ _requireLocalFrontier(frontier);
1855
+ await restart(retainedFixture: frontier);
1856
+ }
1857
+
1858
+ /// Deliver already retained Git history through ordinary offer-gated sync.
1859
+ /// Reception owns subscription invalidation; the test cannot fabricate rows.
1860
+ Future<String> admitRetainedAdvance(NomosTestFrontier frontier) async {
1861
+ _requireLocalFrontier(frontier);
1862
+ if (!frontier._offers.hasInline() ||
1863
+ frontier._offers.inline.offers.isEmpty) {
1864
+ throw StateError(
1865
+ 'Capture the advancing frontier with its earlier fixture base.');
1866
+ }
1867
+ final runtime = _transport as NomosRuntimeV2Transport;
1868
+ final response = await runtime.invokeRuntime(protocol.RuntimeRequest(
1869
+ protocolVersion: 2,
1870
+ requestId: Int64(runtime.nextRuntimeRequestId()),
1871
+ target: protocol.InvocationTarget(
1872
+ workspace: frontier.workspace, session: frontier.workspace),
1873
+ admitCustodyOffers: frontier._offers,
1874
+ ));
1875
+ if (!response.hasCustodyOffersAdmitted() ||
1876
+ response.custodyOffersAdmitted.sessions
1877
+ .expand((session) => session.intents)
1878
+ .length !=
1879
+ frontier._offers.inline.offers.length ||
1880
+ response.custodyOffersAdmitted.sessions
1881
+ .expand((session) => session.intents)
1882
+ .any((intent) => !intent.hasAdmitted() && !intent.hasSkipped()) ||
1883
+ await _plane.session(frontier.workspace).head() !=
1884
+ response.custodyOffersAdmitted.main) {
1885
+ throw StateError('Retained fixture advance was not admitted: $response');
1886
+ }
1887
+ return response.custodyOffersAdmitted.main;
1888
+ }
1889
+
1890
+ void _requireLocalFrontier(NomosTestFrontier frontier) {
1891
+ if (_testPeer == null ||
1892
+ testCloud != null ||
1893
+ !identical(frontier._owner, this)) {
1894
+ throw StateError(
1895
+ 'Frontier belongs to another or non-isolated test peer.');
1896
+ }
1897
+ }
1898
+
1899
+ /// The raw multi-workspace plane under [reception] (the session handle a single-workspace harness
2082
1900
  /// hands out is a view on it).
2083
- NomosBridge get _plane {
2084
- final b = bridge;
1901
+ DartReception get _plane {
1902
+ final b = reception;
2085
1903
  return b is NomosSession ? b.parent : b;
2086
1904
  }
2087
1905
 
@@ -2094,52 +1912,56 @@ class NomosTestHarness {
2094
1912
  final lane = _cloudRestartLane;
2095
1913
  final ceremonySessions = <String>{
2096
1914
  _decoyWorkspace,
1915
+ ..._setupSessions,
2097
1916
  if (lane != null) '${lane.clientId}-onboard-scratch',
2098
1917
  if (lane != null) '${lane.clientId}-platform-author',
2099
1918
  };
2100
- return _plane.connectedSessions
1919
+ return _plane.boundSessions
2101
1920
  .where((session) => !ceremonySessions.contains(session))
2102
1921
  .toList();
2103
1922
  }
2104
1923
 
2105
- /// RESET a workspace's LOCAL custody to the snapshot it was seeded from — the "stale/corrupt
2106
- /// local custody" repair move in tests. Reconnects the session in place (watches on the old
2107
- /// holon are stopped by the runner's re-connect semantics).
2108
- Future<void> resetWorkspaceCustody(String workspace) async {
2109
- final snap = _snapshots[workspace];
2110
- if (snap == null) {
1924
+ /// Cold-reopen a workspace from the same real Git custody directory.
1925
+ ///
1926
+ /// This deliberately does not reset or deserialize custody. Git is the
1927
+ /// durable test state; reopening proves the Peer Office can remount it.
1928
+ Future<void> reopenWorkspaceCustody(String workspace) async {
1929
+ if (!_sessionNames.contains(workspace)) {
2111
1930
  throw NomosTestHarnessException(
2112
- 'resetWorkspaceCustody("$workspace"): no seed snapshot held for this workspace — '
2113
- 'seeded workspaces: ${_snapshots.keys.toList()}',
1931
+ 'reopenWorkspaceCustody("$workspace"): workspace is not mounted',
2114
1932
  );
2115
1933
  }
2116
1934
  await _phased(
2117
- phase: 'resetWorkspaceCustody: reconnect from seed snapshot',
1935
+ phase: 'reopenWorkspaceCustody: remount Git custody',
2118
1936
  workspace: workspace,
2119
1937
  awaiting: 'connect',
2120
1938
  timeout: _phaseTimeout ?? const Duration(seconds: 60),
2121
1939
  onDiagnostic: _onDiagnostic,
2122
- run: () => _plane.connect(
2123
- workspace: workspace,
2124
- session: workspace,
2125
- clientId: '${_installLane?.clientId ?? 'test-runner'}-$workspace',
2126
- restoreFrom: snap,
2127
- allowUnborn: true,
2128
- ),
1940
+ run: () async {
1941
+ await _plane.bindSession(
1942
+ workspace: workspace,
1943
+ session: workspace,
1944
+ );
1945
+ final authorSecret = _deviceSecrets[workspace];
1946
+ if (authorSecret != null) {
1947
+ await _plane.session(workspace).refreshCredentials(
1948
+ authorSecret: authorSecret,
1949
+ );
1950
+ }
1951
+ },
2129
1952
  );
2130
1953
  }
2131
1954
 
2132
- /// SEED a NEW workspace on this plane from `build/<name>.law.usdz` law packages — the
2133
- /// mid-test twin of [cluster]'s per-workspace install: runs the SAME offline install lane,
2134
- /// then connects the workspace as its own session. Returns its [NomosSession].
1955
+ /// Seed a new workspace through the same Peer Office and kernel used by the
1956
+ /// rest of this test. The resulting state is ordinary Git custody.
2135
1957
  Future<NomosSession> seedWorkspaceFromLawUsdz({
2136
1958
  required String workspace,
2137
1959
  required List<String> lawUsdzPaths,
2138
1960
  }) async {
2139
- final lane = _installLane;
2140
- if (lane == null) {
1961
+ final peer = _testPeer;
1962
+ if (peer == null) {
2141
1963
  throw NomosTestHarnessException(
2142
- 'seedWorkspaceFromLawUsdz: this harness holds no install-lane config',
1964
+ 'seedWorkspaceFromLawUsdz requires a local canonical test peer',
2143
1965
  );
2144
1966
  }
2145
1967
  if (lawUsdzPaths.isEmpty) {
@@ -2147,46 +1969,21 @@ class NomosTestHarness {
2147
1969
  'seedWorkspaceFromLawUsdz("$workspace"): pass the build/<name>.law.usdz paths',
2148
1970
  );
2149
1971
  }
2150
- final snap = '${_workdir.path}/$workspace.bin';
2151
- final info = await _phased(
2152
- phase: 'seed: offline law install',
1972
+ final installed = await _phased(
1973
+ phase: 'seed: initialize Git custody and install law',
2153
1974
  workspace: workspace,
2154
- awaiting: lane.useInRepoScript
2155
- ? 'offline-install.ts'
2156
- : 'githolon harness-install',
1975
+ awaiting: 'genesis offer + ordinary lifecycle offers',
2157
1976
  timeout: _phaseTimeout ?? _kInstallDeadline,
2158
1977
  onDiagnostic: _onDiagnostic,
2159
- run: () => _runInstall(
2160
- node: lane.node,
2161
- useInRepoScript: lane.useInRepoScript,
2162
- inRepoScript: lane.inRepoScript,
2163
- githolonCmd: lane.githolonCmd,
2164
- lawUsdzPaths: lawUsdzPaths,
2165
- workspace: workspace,
2166
- snapshot: snap,
2167
- runtimeDir: lane.runtimeDir,
2168
- onDiagnostic: _onDiagnostic,
2169
- ),
2170
- );
2171
- final b64 = base64Encode(await File(snap).readAsBytes());
2172
- _snapshots[workspace] = b64;
2173
- _lawHashes[workspace] = ((info['lawHashes'] as List?) ?? const [])
2174
- .map((e) => e.toString())
2175
- .toList();
2176
- await _phased(
2177
- phase: 'seed: connect session',
2178
- workspace: workspace,
2179
- awaiting: 'connect',
2180
- timeout: _phaseTimeout ?? const Duration(seconds: 60),
2181
- onDiagnostic: _onDiagnostic,
2182
- run: () => _plane.connect(
1978
+ run: () => peer.installWorkspace(
2183
1979
  workspace: workspace,
2184
- session: workspace,
2185
- clientId: '${lane.clientId}-$workspace',
2186
- restoreFrom: b64,
2187
- allowUnborn: true,
1980
+ lawUsdzPaths: lawUsdzPaths,
2188
1981
  ),
2189
1982
  );
1983
+ _lawHashes[workspace] = installed.lawHashes;
1984
+ _domainKeys[workspace] = installed.domainKeys;
1985
+ _devicePublics[workspace] = base64Encode(installed.deviceKey.publicKey);
1986
+ _deviceSecrets[workspace] = base64Encode(installed.deviceKey.secret);
2190
1987
  if (!_sessionNames.contains(workspace)) _sessionNames.add(workspace);
2191
1988
  return _plane.session(workspace);
2192
1989
  }
@@ -2199,15 +1996,7 @@ class NomosTestHarness {
2199
1996
  required String workspace,
2200
1997
  required String domain,
2201
1998
  }) async {
2202
- final offer = await _phased(
2203
- phase: 'assertDomainInstalled: read the interface offer',
2204
- workspace: workspace,
2205
- awaiting: 'interfaceOffer',
2206
- timeout: _phaseTimeout ?? _kOtherDeadline,
2207
- onDiagnostic: _onDiagnostic,
2208
- run: () => _plane.session(workspace).interfaceOffer(),
2209
- );
2210
- final keys = (offer?.keys.toList() ?? const <String>[])..sort();
1999
+ final keys = (_domainKeys[workspace]?.toList() ?? const <String>[])..sort();
2211
2000
  if (!keys.contains(domain)) {
2212
2001
  throw NomosTestHarnessException(
2213
2002
  'domain "$domain" is NOT installed on workspace "$workspace" — active domains: '
@@ -2216,189 +2005,137 @@ class NomosTestHarness {
2216
2005
  }
2217
2006
  }
2218
2007
 
2219
- // ── SNAPSHOT LIFECYCLE (co2 #5) — a typed async snapshot API over the per-workspace export()/restoreFrom
2220
- // primitives, so staged AppSim snapshots migrate mechanically without reaching into custody internals. ──
2221
-
2222
- /// Capture a typed [WorkspaceSnapshot] of one workspace's whole local holon (the bare nomos.git incl.
2223
- /// un-synced writes + the DLQ), with its head + law hashes + capture time. The bytes are the SAME
2224
- /// base64 `connect(restoreFrom:)` consumes — [restoreWorkspace] round-trips it. O(1)-ish (a tree serialize).
2225
- Future<WorkspaceSnapshot> snapshotWorkspace(String workspace) async {
2226
- final connected = _plane.connectedSessions;
2227
- if (!connected.contains(workspace)) {
2008
+ /// Reopen the application from durable LOCAL custody. The canonical runner spawned for a
2009
+ /// local scenario owns a filesystem Git directory independently of the Node process. Restarting
2010
+ /// replaces Reception, Peer Office and kernel memory, then reconnects the same workspace names
2011
+ /// against that directory. No snapshot or remote fetch participates.
2012
+ Future<void> restart({NomosTestFrontier? retainedFixture}) async {
2013
+ if (_disposed) {
2228
2014
  throw NomosTestHarnessException(
2229
- 'snapshotWorkspace("$workspace"): no connected session; connected workspaces: $connected',
2015
+ 'restart(): this test device has already been disposed',
2230
2016
  );
2231
2017
  }
2232
- final s = _plane.session(workspace);
2233
- return _phased(
2234
- phase: 'snapshotWorkspace',
2235
- workspace: workspace,
2236
- awaiting: 'export',
2237
- timeout: _phaseTimeout ?? _kOtherDeadline,
2238
- onDiagnostic: _onDiagnostic,
2239
- run: () async {
2240
- final b64 = await s.export();
2241
- final head = await s.head();
2242
- return WorkspaceSnapshot(
2243
- workspace: workspace,
2244
- snapshotB64: b64,
2245
- head: head,
2246
- lawHashes: lawHashes(workspace),
2247
- capturedAt: DateTime.now().toUtc(),
2018
+ final peer = _testPeer;
2019
+ final home = homeWorkspace;
2020
+ final cloudLane = _cloudRestartLane;
2021
+ if (cloudLane != null && home != null) {
2022
+ if (retainedFixture != null) {
2023
+ throw NomosTestHarnessException(
2024
+ 'restart(): retained frontier fixtures require an isolated local test peer',
2248
2025
  );
2249
- },
2250
- );
2251
- }
2252
-
2253
- /// Snapshot EVERY connected workspace on this plane into a typed [PlaneSnapshot] (session/workspace metadata
2254
- /// carried per entry). The plane-level twin of [snapshotWorkspace] — migrate a whole staged AppSim plane.
2255
- Future<PlaneSnapshot> snapshotPlane() async {
2256
- final out = <String, WorkspaceSnapshot>{};
2257
- final connected = _applicationConnectedSessions();
2258
- for (final ws in connected) {
2259
- out[ws] = await snapshotWorkspace(ws);
2260
- }
2261
- return PlaneSnapshot(
2262
- workspaces: Map.unmodifiable(out),
2263
- capturedAt: DateTime.now().toUtc(),
2264
- );
2265
- }
2266
-
2267
- /// Restore a workspace FROM a [WorkspaceSnapshot] — re-open its session on this plane from the snapshot's
2268
- /// LOCAL CUSTODY (no network clone), replacing the resident holon. The workspace need not currently be a
2269
- /// session (a fresh restore is allowed); its name is taken from [snapshot] unless [into] overrides it.
2270
- Future<NomosSession> restoreWorkspace(
2271
- WorkspaceSnapshot snapshot, {
2272
- String? into,
2273
- }) async {
2274
- final ws = into ?? snapshot.workspace;
2275
- await _phased(
2276
- phase: 'restoreWorkspace',
2277
- workspace: ws,
2278
- awaiting: 'connect(restoreFrom:)',
2279
- timeout: _phaseTimeout ?? const Duration(seconds: 60),
2280
- onDiagnostic: _onDiagnostic,
2281
- run: () => _plane.connect(
2282
- workspace: ws,
2283
- session: ws,
2284
- clientId: '${_installLane?.clientId ?? 'test-runner'}-$ws',
2285
- restoreFrom: snapshot.snapshotB64,
2286
- allowUnborn: true,
2287
- ),
2288
- );
2289
- if (!_sessionNames.contains(ws)) _sessionNames.add(ws);
2290
- return bridge.session(ws);
2291
- }
2292
-
2293
- /// Restore an entire [PlaneSnapshot] — every workspace re-opened from its snapshot. Migrates a staged plane
2294
- /// wholesale (the mechanical AppSim-snapshot migration co2 #5 asks for).
2295
- Future<void> restorePlane(PlaneSnapshot plane) async {
2296
- for (final entry in plane.workspaces.values) {
2297
- await restoreWorkspace(entry);
2026
+ }
2027
+ await _reopenCloudDevice(cloudLane, home);
2028
+ return;
2298
2029
  }
2299
- }
2300
-
2301
- /// Simulate an app process restart from its durably persisted local custody. This is deliberately one
2302
- /// harness verb: snapshots, runner replacement, credentials, workspace ordering and auto-sync registration
2303
- /// remain Nomos concerns. The caller reacquires generated clients from [homeSession]/[bridge] afterward.
2304
- Future<void> restart() async {
2305
- if (_disposed) {
2030
+ if (peer == null || home == null) {
2306
2031
  throw NomosTestHarnessException(
2307
- 'restart(): this test device has already been disposed',
2032
+ 'restart() requires a local canonical test peer and primary application workspace',
2308
2033
  );
2309
2034
  }
2310
- final lane = _cloudRestartLane;
2311
- final home = homeWorkspace;
2312
- if (lane == null || home == null) {
2035
+ final publicKey = _devicePublics[home];
2036
+ final homeSecret = _deviceSecrets[home];
2037
+ if (publicKey == null ||
2038
+ publicKey.isEmpty ||
2039
+ homeSecret == null ||
2040
+ homeSecret.isEmpty) {
2313
2041
  throw NomosTestHarnessException(
2314
- 'restart() requires a harness with one primary application session',
2042
+ 'restart(): the local device has no retained signing identity for "$home"',
2315
2043
  );
2316
2044
  }
2317
-
2318
- final oldPlane = _plane;
2319
2045
  final targets = _applicationConnectedSessions();
2320
2046
  if (targets.remove(home)) targets.insert(0, home);
2321
- final snapshots = <String, String>{};
2322
- for (final ws in targets) {
2323
- final session = oldPlane.session(ws);
2324
- await _phased(
2325
- phase: 'restart: stop automatic sync',
2326
- workspace: ws,
2327
- awaiting: 'stopAutoSync',
2328
- timeout: _phaseTimeout ?? _kOtherDeadline,
2329
- onDiagnostic: _onDiagnostic,
2330
- run: session.stopAutoSync,
2331
- );
2332
- snapshots[ws] = await _phased(
2333
- phase: 'restart: persist local custody',
2334
- workspace: ws,
2335
- awaiting: 'export',
2336
- timeout: _phaseTimeout ?? _kOtherDeadline,
2337
- onDiagnostic: _onDiagnostic,
2338
- run: session.export,
2339
- );
2340
- }
2341
-
2047
+ final secrets = <String, String>{
2048
+ for (final workspace in targets)
2049
+ workspace: _deviceSecrets[workspace] ?? homeSecret,
2050
+ };
2051
+ final prior = retainedFixture == null
2052
+ ? null
2053
+ : await _plane.session(retainedFixture.workspace).head();
2054
+ if (retainedFixture != null) _requireLocalFrontier(retainedFixture);
2342
2055
  await _phased(
2343
- phase: 'restart: terminate old runner',
2344
- awaiting: 'NodeRunnerTransport.dispose',
2345
- timeout: _phaseTimeout ?? _kOtherDeadline,
2346
- onDiagnostic: _onDiagnostic,
2347
- run: _transport.dispose,
2348
- );
2349
- _transport = await _phased(
2350
- phase: 'restart: spawn runner',
2351
- awaiting: 'Process.start(${lane.node})',
2352
- timeout: _phaseTimeout ?? _kSpawnDeadline,
2056
+ phase: 'restart: reopen Peer Office from Git custody',
2057
+ workspace: home,
2058
+ awaiting: 'process reboot + custody remount',
2059
+ timeout: _phaseTimeout ?? _kInstallDeadline,
2353
2060
  onDiagnostic: _onDiagnostic,
2354
- run: () => NodeRunnerTransport.start(
2355
- entry: lane.entry,
2356
- resolveFrom: lane.resolveFrom,
2357
- node: lane.node,
2358
- environment: lane.environment,
2359
- onDiagnostic: _onDiagnostic,
2061
+ run: () => peer.reboot(
2062
+ trustRootPublicKey: base64Decode(publicKey),
2063
+ authorSecrets: secrets,
2064
+ arrangeStoppedCustody: retainedFixture == null
2065
+ ? null
2066
+ : () async {
2067
+ final fixture = retainedFixture;
2068
+ // Exact existing workspace beneath this peer-owned temporary root.
2069
+ if (!RegExp(r'^[A-Za-z0-9_-]+$').hasMatch(fixture.workspace)) {
2070
+ throw StateError('Invalid fixture workspace');
2071
+ }
2072
+ final repo =
2073
+ '${peer.custody.directory.path}/custody/holon-${fixture.workspace}.git';
2074
+ // `update-ref <new> <old>` is a compare-and-swap, and git reads an
2075
+ // EMPTY <old> as "the ref must not exist". `prior` comes from the
2076
+ // plane's workspace status, which answers '' whenever the workspace
2077
+ // is not resident at that moment — after a page crawl has released
2078
+ // it, for instance — so the rewind failed with "cannot lock ref
2079
+ // 'refs/heads/main': reference already exists". The peer is stopped
2080
+ // and this repo is its own temporary custody, so what the repo holds
2081
+ // IS the truth: use it whenever the plane had no answer.
2082
+ var expected = prior ?? '';
2083
+ if (expected.isEmpty) {
2084
+ final current = await Process.run('git', [
2085
+ '--git-dir=$repo',
2086
+ 'rev-parse',
2087
+ '--verify',
2088
+ 'refs/heads/main',
2089
+ ]);
2090
+ if (current.exitCode != 0) {
2091
+ throw StateError(
2092
+ 'Could not read the retained Git point to rewind from: ${current.stderr}');
2093
+ }
2094
+ expected = (current.stdout as String).trim();
2095
+ }
2096
+ final selected = await Process.run('git', [
2097
+ '--git-dir=$repo',
2098
+ 'update-ref',
2099
+ 'refs/heads/main',
2100
+ fixture.head,
2101
+ expected,
2102
+ ]);
2103
+ if (selected.exitCode != 0)
2104
+ throw StateError(
2105
+ 'Could not arrange isolated retained Git point: ${selected.stderr}');
2106
+ },
2360
2107
  ),
2361
2108
  );
2362
- final restarted = NomosBridge(_transport);
2363
- await _phased(
2364
- phase: 'restart: runner ready',
2365
- awaiting: "event:'ready'",
2366
- timeout: _phaseTimeout ?? _kOtherDeadline,
2367
- onDiagnostic: _onDiagnostic,
2368
- run: () => restarted.ready,
2369
- );
2370
- for (final ws in targets) {
2371
- await _phased(
2372
- phase: 'restart: reopen local custody',
2373
- workspace: ws,
2374
- awaiting: 'connect(restoreFrom:)',
2375
- timeout: _phaseTimeout ?? const Duration(seconds: 60),
2376
- onDiagnostic: _onDiagnostic,
2377
- run: () => restarted.connect(
2378
- cloud: lane.cloud,
2379
- workspace: ws,
2380
- session: ws,
2381
- clientId: '${lane.clientId}-$ws',
2382
- authToken: lane.authToken,
2383
- authorSecret: lane.authorSecret,
2384
- restoreFrom: snapshots[ws],
2385
- allowUnborn: true,
2386
- ),
2387
- );
2388
- }
2389
- bridge = restarted.session(home);
2109
+ _transport = peer.transport;
2110
+ reception = peer.reception.session(home);
2390
2111
  _sessionNames
2391
2112
  ..clear()
2392
2113
  ..addAll(targets);
2393
- _snapshots.addAll(snapshots);
2394
- for (final ws in targets) {
2395
- await restarted.session(ws).autoSync(fallbackMs: 60000);
2396
- }
2397
2114
  }
2398
2115
 
2399
- /// Simulate reinstalling the application with no local ledger custody. Every application workspace must
2400
- /// reopen from remote custody; a successful business read afterward is therefore a replay proof, not a
2401
- /// read from the old process snapshot.
2116
+ /// The scenario-side delegated-permission lane (a test device minting a delegation cert and
2117
+ /// authoring the acceptance intent on the person's behalf) was retired with the local-tooling
2118
+ /// cutover (2026-09-03): it was host-side orchestration outside the one Peer Office. Generated
2119
+ /// `may…()` helpers still call here; the honest answer is a typed refusal naming the intent, so
2120
+ /// the scenario establishes authority through the generated business API instead.
2121
+ Future<void> nomosInternalAllowDelegatedScenarioAction({
2122
+ required String domain,
2123
+ required String acceptanceIntent,
2124
+ required String relation,
2125
+ required String subject,
2126
+ required String subjectField,
2127
+ required String certificateField,
2128
+ Map<String, Object?> facts = const {},
2129
+ }) async {
2130
+ throw NomosTestHarnessException(
2131
+ 'delegated scenario permissions are not available in the canonical harness: '
2132
+ 'establish "$relation" for $subject through the generated business API '
2133
+ '(domain $domain, acceptance intent $acceptanceIntent)',
2134
+ );
2135
+ }
2136
+
2137
+ /// Reinstall with empty local custody; subsequent reads must reacquire the
2138
+ /// remotely retained frontier. Restart, in contrast, preserves device disk.
2402
2139
  Future<void> reinstall() async {
2403
2140
  if (_disposed) {
2404
2141
  throw NomosTestHarnessException(
@@ -2413,43 +2150,69 @@ class NomosTestHarness {
2413
2150
  );
2414
2151
  }
2415
2152
 
2416
- final oldPlane = _plane;
2153
+ // LOCAL-FIRST REINSTALL: a fresh install restores from the cloud, so what survives is exactly what
2154
+ // this peer had SYNCED — the Home's births are re-admitted by the cloud through its own gate (which
2155
+ // births the children there), then each child's own work follows. Converge first, parent leading;
2156
+ // an unsynced push failure surfaces here instead of as a silent loss after the restart.
2157
+ await converge();
2158
+ // createTemp's argument is the PREFIX; the receiver is the parent directory.
2159
+ final freshCustody = await _workdir.createTemp('reinstall-');
2160
+ await _reopenCloudDevice(lane, home, custody: freshCustody.path);
2161
+ }
2162
+
2163
+ /// Replace only the process and its bindings. Placement/follow scheduling
2164
+ /// stays in Peer Office; the harness supplies credentials, never sync flags.
2165
+ Future<void> _reopenCloudDevice(
2166
+ _CloudRestartLane lane,
2167
+ String home, {
2168
+ String? custody,
2169
+ }) async {
2417
2170
  final targets = _applicationConnectedSessions();
2418
2171
  if (targets.remove(home)) targets.insert(0, home);
2419
- for (final ws in targets) {
2420
- await oldPlane.session(ws).stopAutoSync();
2421
- }
2172
+ await _plane.dispose();
2422
2173
  await _transport.dispose();
2423
2174
 
2424
2175
  _transport = await NodeRunnerTransport.start(
2425
2176
  entry: lane.entry,
2426
2177
  resolveFrom: lane.resolveFrom,
2427
2178
  node: lane.node,
2428
- environment: lane.environment,
2179
+ arguments: [
2180
+ ...lane.arguments,
2181
+ '--custody=${custody ?? lane.custodyDirectory}'
2182
+ ],
2429
2183
  onDiagnostic: _onDiagnostic,
2430
2184
  );
2431
- final reinstalled = NomosBridge(_transport);
2185
+ if (custody != null) lane.custodyDirectory = custody;
2186
+ final reinstalled = DartReception(_transport);
2432
2187
  await reinstalled.ready;
2188
+ final scratch = '${lane.clientId}-onboard-scratch';
2189
+ await reinstalled.bindSession(
2190
+ workspace: scratch,
2191
+ session: scratch,
2192
+ );
2193
+ await reinstalled.session(scratch).refreshCredentials(
2194
+ authToken: lane.authToken,
2195
+ authorSecret: lane.authorSecret,
2196
+ );
2433
2197
  for (final ws in targets) {
2434
- await reinstalled.connect(
2435
- cloud: lane.cloud,
2198
+ final interaction = await reinstalled.bindSession(
2436
2199
  workspace: ws,
2437
2200
  session: ws,
2438
- clientId: '${lane.clientId}-reinstall-$ws',
2439
- authToken: lane.authToken,
2440
- authorSecret: lane.authorSecret,
2201
+ credentialSourceSession: scratch,
2202
+ );
2203
+ reinstalled.nomosInternalRegisterCredentialProvider(
2204
+ interaction: interaction,
2205
+ cloud: lane.cloud,
2206
+ signIn: ({deviceChallenge}) async => lane.authToken,
2441
2207
  );
2442
2208
  }
2443
- bridge = reinstalled.session(home);
2209
+ reception = reinstalled.session(home);
2444
2210
  _sessionNames
2445
2211
  ..clear()
2446
2212
  ..addAll(targets);
2447
- for (final ws in targets) {
2448
- await reinstalled.session(ws).autoSync(fallbackMs: 60000);
2449
- }
2450
2213
  }
2451
2214
 
2452
- /// Deterministic, idempotent, total-ordered teardown: close the bridge and all live watches, stop the runner
2215
+ /// Deterministic, idempotent, total-ordered teardown: close the reception and all live watches, stop the runner
2453
2216
  /// (SIGTERM → 5s deadline → SIGKILL, via [NodeRunnerTransport.dispose]), then delete the temp dir. Safe to
2454
2217
  /// call twice; safe to call after a failed setup. Nothing left running, nothing left on disk.
2455
2218
  /// Disposal must ALWAYS complete (never leave a hung teardown holding the test runner): the transport
@@ -2464,21 +2227,21 @@ class NomosTestHarness {
2464
2227
  '[nomos-test-harness] phase dispose (≤${(_phaseTimeout ?? _kOtherDeadline).inSeconds}s) …',
2465
2228
  );
2466
2229
  try {
2467
- // The bridge owns the transport's one message subscription and every watch controller. Closing the
2230
+ // The reception owns the transport's one message subscription and every watch controller. Closing the
2468
2231
  // transport first makes its incoming stream wait on consumers that can no longer receive their
2469
2232
  // unwatch/done messages—a deterministic teardown deadlock in reactive Flutter tests.
2470
2233
  await _plane.dispose().timeout(
2471
2234
  _phaseTimeout ?? _kOtherDeadline,
2472
2235
  onTimeout: () => throw HarnessPhaseTimeout(
2473
- phase: 'dispose: close bridge watches',
2236
+ phase: 'dispose: close reception watches',
2474
2237
  workspace: workspace,
2475
2238
  elapsed: _phaseTimeout ?? _kOtherDeadline,
2476
- awaiting: 'NomosBridge.dispose',
2239
+ awaiting: 'DartReception.dispose',
2477
2240
  ),
2478
2241
  );
2479
2242
  } catch (e) {
2480
2243
  _diag(
2481
- '[nomos-test-harness] bridge dispose error (continuing to runner cleanup): $e',
2244
+ '[nomos-test-harness] reception dispose error (continuing to runner cleanup): $e',
2482
2245
  );
2483
2246
  }
2484
2247
  try {
@@ -2513,79 +2276,46 @@ class NomosTestHarness {
2513
2276
  }
2514
2277
  }
2515
2278
 
2516
- // ── offline install (a short-lived node child; produces the restore snapshot) ──
2517
-
2518
- static Future<Map<String, Object?>> _runInstall({
2519
- required String node,
2520
- required bool useInRepoScript,
2521
- required String inRepoScript,
2522
- required String githolonCmd,
2523
- required List<String> lawUsdzPaths,
2524
- required String workspace,
2525
- required String snapshot,
2526
- String? runtimeDir,
2527
- void Function(String line)? onDiagnostic,
2528
- }) async {
2529
- // Both lanes take the SAME flags (--law/--workspace/--out/--runtime) and print the SAME one-line JSON
2530
- // contract; only the leading command differs (node <script> vs `<githolon...> harness-install`).
2531
- final common = <String>[
2532
- '--law',
2533
- lawUsdzPaths.join(','),
2534
- '--workspace',
2535
- workspace,
2536
- '--out',
2537
- snapshot,
2538
- if (runtimeDir != null) ...['--runtime', runtimeDir],
2539
- ];
2540
- final String executable;
2541
- final List<String> args;
2542
- if (useInRepoScript) {
2543
- executable = node;
2544
- args = [inRepoScript, ...common];
2545
- } else {
2546
- // `githolonCmd` is a command line (e.g. `npx githolon` or `node /abs/cli.mjs`) — split into tokens.
2547
- final tokens = githolonCmd
2548
- .trim()
2549
- .split(RegExp(r'\s+'))
2550
- .where((t) => t.isNotEmpty)
2551
- .toList();
2552
- if (tokens.isEmpty) {
2553
- throw NomosTestHarnessException(
2554
- 'no install helper resolved — pass installHelper: (an offline-install.ts path, or a githolon command)',
2279
+ static Future<String> _resolveTestEntry(String node, String from) async {
2280
+ String? packageConfigPath = Platform.packageConfig;
2281
+ if (packageConfigPath == null) {
2282
+ var directory = Directory(from).absolute;
2283
+ while (true) {
2284
+ final candidate = File(
2285
+ '${directory.path}${Platform.pathSeparator}.dart_tool${Platform.pathSeparator}package_config.json',
2555
2286
  );
2556
- }
2557
- executable = tokens.first;
2558
- args = [...tokens.skip(1), 'harness-install', ...common];
2559
- }
2560
- final tag = useInRepoScript ? 'offline-install' : 'harness-install';
2561
- final r = await Process.run(executable, args);
2562
- final err = (r.stderr as String).trim();
2563
- if (err.isNotEmpty) {
2564
- for (final line in err.split('\n')) {
2565
- (onDiagnostic ?? stderr.writeln)('[$tag] $line');
2287
+ if (candidate.existsSync()) {
2288
+ packageConfigPath = candidate.uri.toString();
2289
+ break;
2290
+ }
2291
+ final parent = directory.parent;
2292
+ if (parent.path == directory.path) break;
2293
+ directory = parent;
2566
2294
  }
2567
2295
  }
2568
- if (r.exitCode != 0) {
2569
- throw NomosTestHarnessException(
2570
- 'offline law install for "$workspace" failed (exit ${r.exitCode}): ${err.isEmpty ? "(no stderr)" : err}',
2296
+ if (packageConfigPath != null) {
2297
+ final packages = await loadPackageConfigUri(
2298
+ Uri.base.resolve(packageConfigPath),
2571
2299
  );
2572
- }
2573
- // The helper prints one JSON line on success; verify it before spawning the runner.
2574
- final out = (r.stdout as String).trim();
2575
- final last = out.isEmpty ? '' : out.split('\n').last;
2576
- Map<String, Object?> decoded = const {};
2577
- try {
2578
- final d = jsonDecode(last);
2579
- if (d is Map) decoded = d.cast<String, Object?>();
2580
- } catch (_) {}
2581
- if (decoded['ok'] != true || !File(snapshot).existsSync()) {
2582
- throw NomosTestHarnessException(
2583
- 'offline law install produced no snapshot (stdout: ${out.isEmpty ? "(empty)" : out})',
2300
+ final bundled = packages.resolve(
2301
+ Uri.parse('package:nomos_client/src/runner/node-entry.mjs'),
2584
2302
  );
2303
+ if (bundled != null && bundled.scheme == 'file') {
2304
+ final path = bundled.toFilePath();
2305
+ if (File(path).existsSync()) return path;
2306
+ }
2585
2307
  }
2586
- // { ok, snapshot, workspace, lawHashes, frameworkHash } — the caller keeps lawHashes/frameworkHash so a
2587
- // `.births()` directive can be driven (its genesis installs the framework + law by hash).
2588
- return decoded;
2308
+ final result = await Process.run(
2309
+ node,
2310
+ ['-p', "require.resolve('@githolon/runtime/runner/node-entry.mjs')"],
2311
+ workingDirectory: from,
2312
+ );
2313
+ final resolved = result.stdout.toString().trim();
2314
+ if (result.exitCode == 0 && resolved.isNotEmpty) return resolved;
2315
+ throw NomosTestHarnessException(
2316
+ 'the Nomos release contains no bundled node peer. '
2317
+ 'Build @githolon/runtime, reinstall nomos_client, or pass nodeEntry only while developing Nomos.',
2318
+ );
2589
2319
  }
2590
2320
 
2591
2321
  static Future<String> _resolveEntry(String node, String from) async {
@@ -2619,10 +2349,10 @@ class NomosTestHarness {
2619
2349
  }
2620
2350
  // PREFER the BUILT .mjs (the published-consumer entry): node refuses type-stripping under node_modules
2621
2351
  // (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING — co2 DEFECT P3-002), so the raw .ts entry only boots
2622
- // in-repo. The .mjs ships from @githolon/client ≥0.97.2; the .ts fallback keeps in-repo dev working.
2352
+ // in-repo. The .mjs ships from @githolon/runtime ≥0.97.2; the .ts fallback keeps in-repo dev working.
2623
2353
  for (final entry in const [
2624
- '@githolon/client/runner/node-entry.mjs',
2625
- '@githolon/client/runner/node-entry.ts',
2354
+ '@githolon/runtime/runner/node-entry.mjs',
2355
+ '@githolon/runtime/runner/node-entry.ts',
2626
2356
  ]) {
2627
2357
  final r = await Process.run(
2628
2358
  node,
@@ -2636,82 +2366,8 @@ class NomosTestHarness {
2636
2366
  }
2637
2367
  throw NomosTestHarnessException(
2638
2368
  'the Nomos release contains no bundled node runner and '
2639
- '@githolon/client/runner/node-entry.{mjs,ts} cannot be resolved from $from. '
2369
+ '@githolon/runtime/runner/node-entry.{mjs,ts} cannot be resolved from $from. '
2640
2370
  'Reinstall nomos_client, or pass nodeEntry: only when developing Nomos itself.',
2641
2371
  );
2642
2372
  }
2643
2373
  }
2644
-
2645
- /// A typed, self-describing snapshot of ONE workspace's local holon (co2 #5). [snapshotB64] is the exact
2646
- /// base64 `connect(restoreFrom:)` consumes — the whole nomos.git incl. un-synced writes + the DLQ — plus the
2647
- /// metadata that makes a staged snapshot mechanically migratable: the [workspace], its ledger [head] at
2648
- /// capture, its installed [lawHashes], and [capturedAt] (UTC).
2649
- class WorkspaceSnapshot {
2650
- final String workspace;
2651
- final String snapshotB64;
2652
- final String head;
2653
- final List<String> lawHashes;
2654
- final DateTime capturedAt;
2655
-
2656
- const WorkspaceSnapshot({
2657
- required this.workspace,
2658
- required this.snapshotB64,
2659
- required this.head,
2660
- required this.lawHashes,
2661
- required this.capturedAt,
2662
- });
2663
-
2664
- /// JSON round-trip so a staged snapshot persists to disk verbatim (the bytes + provenance).
2665
- Map<String, Object?> toJson() => {
2666
- 'workspace': workspace,
2667
- 'snapshotB64': snapshotB64,
2668
- 'head': head,
2669
- 'lawHashes': lawHashes,
2670
- 'capturedAt': capturedAt.toUtc().toIso8601String(),
2671
- };
2672
-
2673
- factory WorkspaceSnapshot.fromJson(Map<String, Object?> j) =>
2674
- WorkspaceSnapshot(
2675
- workspace: j['workspace'] as String,
2676
- snapshotB64: j['snapshotB64'] as String,
2677
- head: (j['head'] ?? '') as String,
2678
- lawHashes:
2679
- ((j['lawHashes'] as List?) ?? const []).map((e) => '$e').toList(),
2680
- capturedAt: DateTime.parse(
2681
- (j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String())
2682
- as String,
2683
- ),
2684
- );
2685
-
2686
- @override
2687
- String toString() =>
2688
- 'WorkspaceSnapshot($workspace @ ${head.isEmpty ? "∅" : head.substring(0, head.length.clamp(0, 10))}, '
2689
- '${lawHashes.length} law(s), ${capturedAt.toIso8601String()})';
2690
- }
2691
-
2692
- /// A typed snapshot of a WHOLE plane — every connected workspace keyed by name (co2 #5). Restore it with
2693
- /// [NomosTestHarness.restorePlane] to migrate a staged multi-workspace AppSim scene wholesale.
2694
- class PlaneSnapshot {
2695
- final Map<String, WorkspaceSnapshot> workspaces;
2696
- final DateTime capturedAt;
2697
-
2698
- const PlaneSnapshot({required this.workspaces, required this.capturedAt});
2699
-
2700
- Map<String, Object?> toJson() => {
2701
- 'workspaces': workspaces.map((k, v) => MapEntry(k, v.toJson())),
2702
- 'capturedAt': capturedAt.toUtc().toIso8601String(),
2703
- };
2704
-
2705
- factory PlaneSnapshot.fromJson(Map<String, Object?> j) => PlaneSnapshot(
2706
- workspaces: ((j['workspaces'] as Map?) ?? const {}).map(
2707
- (k, v) => MapEntry(
2708
- '$k',
2709
- WorkspaceSnapshot.fromJson((v as Map).cast<String, Object?>()),
2710
- ),
2711
- ),
2712
- capturedAt: DateTime.parse(
2713
- (j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String())
2714
- as String,
2715
- ),
2716
- );
2717
- }