@githolon/dsl 0.108.6 → 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 +2083 -1248
  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
@@ -0,0 +1,4280 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
4
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
5
+
6
+ /// DART RECEPTION — the language-native application boundary. A Flutter app
7
+ /// talks to the one Peer Office implementation over one typed protocol. Which
8
+ /// Platform Shell carries that protocol is deliberately invisible here.
9
+ ///
10
+ /// A platform round-trip is asynchronous. Reception owns language-native
11
+ /// reactive streams and disposable presentation projections. The typed
12
+ /// [PeerOfficeConnection] is injected by the Platform Shell, so no platform
13
+ /// embedding can grow its own workspace lifecycle here.
14
+ @internal
15
+ library;
16
+
17
+ import 'package:meta/meta.dart' show internal;
18
+
19
+ import 'dart:async';
20
+ import 'dart:convert';
21
+ import 'dart:developer' as developer;
22
+ import 'dart:typed_data';
23
+
24
+ import 'package:fixnum/fixnum.dart' as fixnum;
25
+ import 'package:nomos_peer_host/nomos_peer_host.dart' show NomosSignIn;
26
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as nomos_protocol;
27
+ import 'package:nomos_types/src/proto/google/protobuf/any.pb.dart'
28
+ as google_protobuf;
29
+ import 'package:nomos_types/nomos_types.dart'
30
+ show
31
+ nomosErrorFromInfo,
32
+ NomosErrorInfo,
33
+ ParentSignerEnrollmentMissing,
34
+ NomosRef,
35
+ VersionedNomosRef,
36
+ ReferenceVersionStatus,
37
+ ReferenceVersionCurrent,
38
+ ReferenceVersionOutdated,
39
+ ReferenceVersionUnavailable,
40
+ NomosCustodyEvent,
41
+ NomosLocalWorkRejectedEvent;
42
+ import 'package:nomos_types/src/nomos_ref.dart' show NmsInternalNomosRefs;
43
+ import 'package:nomos_types/src/versioned_nomos_ref.dart'
44
+ show NmsInternalVersionedNomosRefs;
45
+ import 'package:nomos_types/src/foreign_ref.dart'
46
+ show NomosForeignReader, NomosForeignWorkspaceReader;
47
+
48
+ import '../generated_call.dart'
49
+ show
50
+ NmsGeneratedCall,
51
+ NmsGeneratedQuery,
52
+ NmsGeneratedResult,
53
+ NmsInternalGeneratedResults;
54
+ import '../application_model_adoption.dart'
55
+ show
56
+ NmsApprovedModelLawPackageSource,
57
+ NomosApprovedModel,
58
+ NomosModelAdoption,
59
+ NomosModelAdoptionOutcome,
60
+ NomosModelAdoptionPhase,
61
+ NomosModelAdoptionProgress;
62
+ import '../application_framework_calls.dart'
63
+ show OwnedCustody, OwnedDeviceRequest;
64
+ import '../kernel_primitives.dart' show NmsKernelPrimitives;
65
+ import '../observed_session_directory.dart' show NmsObservedSessionDirectory;
66
+ import '../replica_readiness.dart' show nmsUntilReplicaReady;
67
+ import '../operation_receipts.dart'
68
+ show NmsOperationReceiptProjection, NomosOperationReceipt;
69
+ import 'event_desk.dart' show ReceptionEventDesk;
70
+ import 'extremum_read.dart' as extrema;
71
+ import 'sync_diagnostics.dart';
72
+ import 'local_copies.dart' show NomosPreservedLocalCopy, ReceptionLocalCopies;
73
+ import 'progress_desk.dart'
74
+ show
75
+ KernelProgress,
76
+ KernelProgressOperation,
77
+ NomosSpanEvent,
78
+ NomosSpanPhase;
79
+ import '../signing_identity_kernel.dart' show NmsSigningIdentityKernel;
80
+ import '../sync_status_reducer.dart';
81
+ import '../sync_status_projection.dart'
82
+ show SyncPhase, SyncStatus, TransferProgress, WorkspaceFollowStatus;
83
+ import '../typed_watch_registry.dart' show NmsTypedWatchHandle;
84
+
85
+ export '../sync_status_projection.dart'
86
+ show
87
+ SyncPhase,
88
+ SyncStatus,
89
+ TransferProgress,
90
+ WorkspaceFollowStatus,
91
+ WorkspaceFollowPhase;
92
+ export '../operation_receipts.dart'
93
+ show NomosOperationPhase, NomosOperationReceipt, NomosOperationStatus;
94
+ export 'progress_desk.dart'
95
+ show
96
+ KernelLawHydrationStep,
97
+ KernelProgress,
98
+ KernelProgressOperation,
99
+ KernelProgressPhase,
100
+ KernelProgressUnit,
101
+ NomosSpanEvent,
102
+ NomosSpanPhase;
103
+ import '../runtime_v2_gateway.dart' show NmsRuntimeV2Gateway;
104
+ import '../runtime_v2_transport.dart'
105
+ show NomosPlatformCredentialProviders, NomosKernelCallTransport;
106
+ import 'peer_office_connection.dart' show PeerOfficeConnection;
107
+
108
+ export '../runtime_v2_transport.dart' show NomosSessionCredentials;
109
+
110
+ // Watches are level-triggered by local offer acknowledgements and by remote
111
+ // custody convergence. This is only a lost-doorbell safety poll; a short idle
112
+ // interval repeatedly transferred whole unchanged projections across FFI.
113
+ const int _defaultWatchSafetyPollMs = 60000;
114
+
115
+ /// Language-native view of the resident Peer Runtime. It binds cheap
116
+ /// interaction handles and transports typed application operations. Placement,
117
+ /// Git/SQLite state, Replica Follow, credentials and recovery remain owned by
118
+ /// the peer behind this boundary.
119
+
120
+ /// One stable bounded page from an authenticated declared-query index. [cursor] is opaque framework
121
+ /// state: pass it back unchanged to request the next page; [done] means this page exhausted the query.
122
+ final class NomosPage<T> {
123
+ const NomosPage({
124
+ required this.rows,
125
+ required this.cursor,
126
+ required this.done,
127
+ });
128
+
129
+ final List<T> rows;
130
+ final String? cursor;
131
+ final bool done;
132
+ }
133
+
134
+ /// Typed authority evidence carried by an offer that plans exactly one child birth. The protobuf
135
+ /// declaration is the one source of truth for Rust, TypeScript and Dart; USDA is its durable state
136
+ /// projection, not another transport type. The legacy reception envelope carries only these protobuf bytes.
137
+ typedef NomosBirthCertificate = nomos_protocol.BirthCertificate;
138
+
139
+ /// Typed compare-and-merge witness carried by an offer. The protobuf schema is
140
+ /// the only transport shape; generated conflict helpers never rebuild it as a
141
+ /// string-keyed map.
142
+ typedef NomosConflictSnapshot = nomos_protocol.ConflictSnapshot;
143
+
144
+ /// Typed result of one child-workspace birth. The kernel protobuf is the
145
+ /// transport authority; hosts do not reinterpret birth state through maps.
146
+ typedef NomosBirthOutcome = nomos_protocol.BirthOutcome;
147
+
148
+ typedef NomosBirthOutcomeStatus = nomos_protocol.BirthOutcomeStatus;
149
+
150
+ /// THE EXPECTED RUNNER-BUNDLE VERSION FLOOR — the @githolon/runtime release this nomos_client version
151
+ /// ships alongside. The bundle stamps its own version into the ready frame; anything OLDER (or a
152
+ /// pre-stamp bundle, which reports nothing) is a stale baked asset and fails loudly at boot.
153
+ /// MUST move with the paired @githolon/runtime release — a release-hygiene test pairs it with the
154
+ /// runner stamp (the pubspec-pins pattern).
155
+ const String kExpectedRunnerBundleVersion = '0.100.6';
156
+
157
+ /// A STALE BAKED RUNNER BUNDLE: the app's built assets carry an older nomos-runner.js than this
158
+ /// nomos_client release ships — Flutter's build cache kept it across a pub upgrade. Thrown from
159
+ /// [DartReception.ready] at boot (loud, immediate, named) instead of letting one-version-old runtime
160
+ /// behavior masquerade as framework bugs.
161
+ class StaleRunnerBundle implements Exception {
162
+ StaleRunnerBundle({required this.found, required this.expected});
163
+
164
+ /// The bundle's self-reported version (null = a pre-stamp bundle, definitionally stale).
165
+ final String? found;
166
+
167
+ /// The minimum version this nomos_client release expects ([kExpectedRunnerBundleVersion]).
168
+ final String expected;
169
+
170
+ @override
171
+ String toString() =>
172
+ 'StaleRunnerBundle: the baked nomos-runner.js is ${found ?? "un-stamped (pre-0.77.1)"} but this '
173
+ 'nomos_client expects >= $expected. Flutter kept an old bundle across the upgrade — run '
174
+ '`flutter clean` and rebuild (verify with shasum of flutter_assets/.../nomos-runner.js), or pass '
175
+ 'DartReception(..., allowStaleRunnerBundle: true) for a deliberate mixed-version setup.';
176
+ }
177
+
178
+ /// One opaque point in this workspace's verified history. Applications use it as the handle passed to a
179
+ /// generated client's `past.at(moment)`; [id] is framework identity, not a business field and need never be
180
+ /// parsed. The remaining fields make a timeline useful without exposing commit or event-sourcing ceremony.
181
+ class NomosMoment {
182
+ final String id;
183
+ final DateTime? occurredAt;
184
+ final String? performedBy;
185
+ final String? domain;
186
+ final String? action;
187
+
188
+ const NomosMoment({
189
+ required this.id,
190
+ this.occurredAt,
191
+ this.performedBy,
192
+ this.domain,
193
+ this.action,
194
+ });
195
+ }
196
+
197
+ /// One law-minted aggregate from a `.creates(...)` offer: the full typed [id] (`<domain>.<Type>_<uuidv7>`)
198
+ /// and its [type] (`<domain>.<Type>`).
199
+ class NomosCreated {
200
+ final String type;
201
+ final String id;
202
+
203
+ /// The workspace the offer ran on (NomosRef). Additive — defaults to '' on older runners that
204
+ /// don't stamp it; then [ref] is workspace-blank (still a valid locator on the local plane).
205
+ final String workspace;
206
+ const NomosCreated({
207
+ required this.type,
208
+ required this.id,
209
+ this.workspace = '',
210
+ });
211
+
212
+ /// The canonical [NomosRef] view — the `{workspace,type,id}` triple, followable/serializable
213
+ /// (`nomos://<workspace>/<type>/<id>`). One reference, everywhere.
214
+ NomosRef<Object?> get ref => NmsInternalNomosRefs.create<Object?>(
215
+ workspace: workspace,
216
+ type: type,
217
+ id: id,
218
+ );
219
+ @override
220
+ String toString() => id;
221
+ }
222
+
223
+ /// A bounding box for spatial reads — the R*Tree probe's extent. Spatial is a GENERIC 2D index,
224
+ /// so the CANONICAL axes are axis-neutral (`[minX, minY] .. [maxX, maxY]`); the axes' meaning
225
+ /// (earth coordinates, floor-plan mm, anything) is the tenant's business. The original geographic
226
+ /// `minLng`/`minLat`/`maxLng`/`maxLat` named params remain accepted forever (era rule) — pass either
227
+ /// source-level shape. The exact Runtime v2 protobuf boundary carries only the canonical axes.
228
+ class NomosBbox {
229
+ final double minX;
230
+ final double minY;
231
+ final double maxX;
232
+ final double maxY;
233
+
234
+ /// Construct from either the canonical generic axes (`minX`/`minY`/`maxX`/`maxY`) OR the
235
+ /// geographic aliases (`minLng`/`minLat`/`maxLng`/`maxLat`). Provide exactly one shape per axis.
236
+ NomosBbox({
237
+ double? minX,
238
+ double? minY,
239
+ double? maxX,
240
+ double? maxY,
241
+ double? minLng,
242
+ double? minLat,
243
+ double? maxLng,
244
+ double? maxLat,
245
+ }) : minX = (minX ?? minLng)!,
246
+ minY = (minY ?? minLat)!,
247
+ maxX = (maxX ?? maxLng)!,
248
+ maxY = (maxY ?? maxLat)!;
249
+
250
+ /// Deprecated geographic accessors — spatial axes are generic. Prefer [minX]/[minY]/[maxX]/[maxY].
251
+ @Deprecated(
252
+ 'Spatial axes are generic; use minX/minY. Kept for source compat.',
253
+ )
254
+ double get minLng => minX;
255
+ @Deprecated(
256
+ 'Spatial axes are generic; use minX/minY. Kept for source compat.',
257
+ )
258
+ double get minLat => minY;
259
+ @Deprecated(
260
+ 'Spatial axes are generic; use maxX/maxY. Kept for source compat.',
261
+ )
262
+ double get maxLng => maxX;
263
+ @Deprecated(
264
+ 'Spatial axes are generic; use maxX/maxY. Kept for source compat.',
265
+ )
266
+ double get maxLat => maxY;
267
+ }
268
+
269
+ // ── E2E SEALING (the sealing client) — the GENERIC key-custody surface. Generated domain clients bake the
270
+ // per-domain map + seal/read helpers, but these types are domain-agnostic, so they live here (in the
271
+ // nomos_client library) exactly like [MissingOfferAuthority] — one definition, re-exported per client. ──
272
+
273
+ /// A held E2E scope key: the base64 symmetric key + the epoch it belongs to (forward-only rotation).
274
+ class NomosScopeKey {
275
+ final String scope;
276
+ final int epoch;
277
+ final String key;
278
+
279
+ /// Framework-private signing-key subject used to pin `.owned()` custody to one approved device.
280
+ final String? ownedKeySubject;
281
+
282
+ /// Framework-private encrypted handoff, present only while seating the first device.
283
+ final OwnedCustody? ownedCustody;
284
+
285
+ /// Framework-private deterministic child address, present only for an unborn owned business aggregate.
286
+ final String? workspace;
287
+ const NomosScopeKey({
288
+ required this.scope,
289
+ required this.epoch,
290
+ required this.key,
291
+ this.ownedKeySubject,
292
+ this.ownedCustody,
293
+ this.workspace,
294
+ });
295
+ }
296
+
297
+ /// The entire app-facing multi-device surface for owned business state. The
298
+ /// request is one compiler-generated framework value; transport encoders may
299
+ /// package it as USDA for QR/share without turning it into an opaque string.
300
+ class NomosDevices {
301
+ final NomosSession session;
302
+ final String domain;
303
+ final String domainHash;
304
+ final String? actor;
305
+ final String scope;
306
+ final Future<NomosOfferOutcome> Function(
307
+ NomosOwnedDeviceApprovalPreparation preparation,
308
+ ) approvePrepared;
309
+
310
+ const NomosDevices({
311
+ required this.session,
312
+ required this.domain,
313
+ required this.domainHash,
314
+ required this.actor,
315
+ required this.scope,
316
+ required this.approvePrepared,
317
+ });
318
+
319
+ Future<OwnedDeviceRequest> request() =>
320
+ session.nomosOwnedDeviceRequest(scope);
321
+
322
+ Future<NomosOfferOutcome> approve(OwnedDeviceRequest request) async {
323
+ final principal = actor;
324
+ if (principal == null || principal.trim().isEmpty) {
325
+ throw StateError('devices.approve() needs a signed-in Nomos session');
326
+ }
327
+ final payload = await session.nomosPrepareOwnedDeviceApproval(
328
+ request,
329
+ scope,
330
+ );
331
+ return approvePrepared(payload);
332
+ }
333
+ }
334
+
335
+ /// The one business-level recovery policy surface. The handoff protocol stays inside Nomos.
336
+ class NomosRecovery {
337
+ final NomosSession session;
338
+ final String domain;
339
+ final String domainHash;
340
+ final String scope;
341
+
342
+ const NomosRecovery({
343
+ required this.session,
344
+ required this.domain,
345
+ required this.domainHash,
346
+ required this.scope,
347
+ });
348
+
349
+ /// Opt into all-device-loss recovery with [person]. This is intentionally one-way for a workspace:
350
+ /// a principal who has legitimately recovered a key cannot be made to forget it later.
351
+ Future<nomos_protocol.OwnedIdentityProtection> protectWith(String person) {
352
+ if (person.trim().isEmpty) {
353
+ throw ArgumentError(
354
+ 'recovery.protectWith() needs the person who will recover you',
355
+ );
356
+ }
357
+ return session.nomosProtectOwnedIdentity(
358
+ scope: scope,
359
+ domain: domain,
360
+ domainHash: domainHash,
361
+ recoveryPrincipal: person,
362
+ );
363
+ }
364
+ }
365
+
366
+ /// The app-supplied KEY CUSTODY surface (E2E sealing). Supply one to a generated `<Domain>Client` and its
367
+ /// encrypted directive fields SEAL automatically before dispatch + encrypted read fields DECRYPT
368
+ /// automatically. [WrappedKeyStore] is the default over the on-chain WrappedKeys; an app may implement this
369
+ /// to source keys from secure storage (the key custody policy stays the app's; the protocol plumbing does not).
370
+ abstract class NomosKeyStore {
371
+ /// The CURRENT (highest-epoch) scope key, for SEALING a write. null ⇒ EncryptionKeyUnavailable.
372
+ Future<NomosScopeKey?> scopeKey(String scope);
373
+
374
+ /// EVERY held key for these scopes (all epochs), injected on reads so old ciphertext also decrypts.
375
+ Future<List<NomosScopeKey>> readKeys(List<String> scopes);
376
+ }
377
+
378
+ /// Thrown when a write must seal an encrypted field but the KeyStore holds no key for its scope — the write
379
+ /// NEVER falls through to a plaintext dispatch (the sid-envelope gate would refuse it anyway; we pre-fire).
380
+ class EncryptionKeyUnavailable implements Exception {
381
+ final String scope;
382
+ final String field;
383
+ String get code => 'nomos.crypto.key_unavailable';
384
+ String get recommendedAction => 'obtainScopeKey';
385
+ EncryptionKeyUnavailable(this.scope, this.field);
386
+ @override
387
+ String toString() =>
388
+ 'EncryptionKeyUnavailable: cannot seal "$field": no scope key held for "$scope" (obtain the cap + wrap the scope epoch first)';
389
+ }
390
+
391
+ // ── E2E OWNER CEREMONY (the sealing client, slice 2). Domain-
392
+ // agnostic, so it lives here (re-exported per client) exactly like the KeyStore. It composes the framework
393
+ // key-epoch fan-out (`advanceKeyEpoch`, dispatched by its REAL id) with the domain's own grant/revoke
394
+ // directive, resolving the roster + each member's wrap-target pubkey FROM THE CHAIN — so the operator-blind
395
+ // handshake (grant → advance epoch → wrap to every member) is ONE call. Mirror of the TS `_ownerEncryption`. ──
396
+
397
+ /// Thrown when a `.births()` offer ADMITTED on the parent (a head, a 200) but a child workspace did NOT come
398
+ /// up — its `birthOutcomes` entry carried a `status` other than "born" (e.g. `"failed"`/`"refused"` with code
399
+ /// `BirthLawPackageUnavailable`, when the child's law package is not resolvable from the parent's custody).
400
+ /// Without this, the app would open the child and poll `head:null` until timeout with no reason. Callers catch
401
+ /// it to see the child name(s), the kernel [code], and the [reason]. [head] is the parent write that landed.
402
+ class BirthIncomplete implements Exception {
403
+ /// The failed typed kernel outcomes, status != [NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN].
404
+ final List<NomosBirthOutcome> failedBirths;
405
+
406
+ /// The parent write's local head — the parent offer itself sealed; only the child did not come up.
407
+ final String head;
408
+
409
+ BirthIncomplete(this.failedBirths, {this.head = ''});
410
+
411
+ /// The first failed child's kernel code (e.g. `BirthLawPackageUnavailable`), or `BirthFailed`.
412
+ String get code =>
413
+ failedBirths.isNotEmpty && failedBirths.first.code.isNotEmpty
414
+ ? failedBirths.first.code
415
+ : 'BirthFailed';
416
+
417
+ /// The child workspace name(s) that did not come up.
418
+ List<String> get workspaces =>
419
+ failedBirths.map((o) => o.workspace).where((s) => s.isNotEmpty).toList();
420
+
421
+ @override
422
+ String toString() {
423
+ final list = failedBirths
424
+ .map(
425
+ (o) =>
426
+ '${o.workspace.isEmpty ? '?' : o.workspace} [${o.status.name}${o.code.isNotEmpty ? ': ${o.code}' : ''}]'
427
+ '${o.reason.isNotEmpty ? ' — ${o.reason}' : ''}',
428
+ )
429
+ .join('; ');
430
+ final lawUnavailable = failedBirths.any(
431
+ (o) => o.code == 'BirthLawPackageUnavailable',
432
+ );
433
+ return 'BirthIncomplete: ${failedBirths.length} child workspace(s) did NOT come up — $list. '
434
+ 'The parent offer admitted, but the child was not fully born, so opening it would poll head:null '
435
+ 'until timeout. ${lawUnavailable ? "The child's law package is not resolvable from the parent's "
436
+ "custody — deploy it to the parent (POST /v2/workspaces/<parent>/domains) BEFORE birthing." : "See the outcome's reason/code."}';
437
+ }
438
+ }
439
+
440
+ nomos_protocol.NomosRef _nomosRefToProtocol<TAggregate>(
441
+ NomosRef<TAggregate> reference,
442
+ ) =>
443
+ nomos_protocol.NomosRef(
444
+ workspace: NmsInternalNomosRefs.workspace(reference),
445
+ aggregateType: NmsInternalNomosRefs.type(reference),
446
+ aggregateId: NmsInternalNomosRefs.id(reference),
447
+ );
448
+
449
+ /// One exact key wrap prepared by the framework-owned custody machinery.
450
+ /// Generated clients lower this value directly to canonical USDA; it is never a
451
+ /// tenant-authored dictionary.
452
+ class NomosOwnedWrapPreparation {
453
+ final String recipientPrincipal;
454
+ final String recipientPubKey;
455
+ final String hpkeEpk;
456
+ final String ct;
457
+
458
+ const NomosOwnedWrapPreparation({
459
+ required this.recipientPrincipal,
460
+ required this.recipientPubKey,
461
+ required this.hpkeEpk,
462
+ required this.ct,
463
+ });
464
+ }
465
+
466
+ /// A wrap which explicitly identifies the historic epoch it opens.
467
+ class NomosHistoricOwnedWrapPreparation extends NomosOwnedWrapPreparation {
468
+ final int epoch;
469
+
470
+ const NomosHistoricOwnedWrapPreparation({
471
+ required super.recipientPrincipal,
472
+ required super.recipientPubKey,
473
+ required super.hpkeEpk,
474
+ required super.ct,
475
+ required this.epoch,
476
+ });
477
+ }
478
+
479
+ /// Exhaustive result of the framework-owned record preparation operation.
480
+ /// Each subtype corresponds to one law directive and therefore one generated
481
+ /// USDA encoder.
482
+ sealed class NomosOwnedRecordPreparation {
483
+ final String ownedKeySubject;
484
+ final OwnedCustody? ownedCustody;
485
+
486
+ const NomosOwnedRecordPreparation({
487
+ required this.ownedKeySubject,
488
+ this.ownedCustody,
489
+ });
490
+ }
491
+
492
+ class NomosOwnedSharePreparation extends NomosOwnedRecordPreparation {
493
+ final String recordId;
494
+ final String peer;
495
+ final int epoch;
496
+ final String sharedAt;
497
+ final List<NomosHistoricOwnedWrapPreparation> wraps;
498
+
499
+ const NomosOwnedSharePreparation({
500
+ required super.ownedKeySubject,
501
+ super.ownedCustody,
502
+ required this.recordId,
503
+ required this.peer,
504
+ required this.epoch,
505
+ required this.sharedAt,
506
+ required this.wraps,
507
+ });
508
+ }
509
+
510
+ class NomosOwnedUnsharePreparation extends NomosOwnedRecordPreparation {
511
+ final String recordId;
512
+ final String peer;
513
+ final int epoch;
514
+ final String changedAt;
515
+ final List<NomosOwnedWrapPreparation> wraps;
516
+
517
+ const NomosOwnedUnsharePreparation({
518
+ required super.ownedKeySubject,
519
+ super.ownedCustody,
520
+ required this.recordId,
521
+ required this.peer,
522
+ required this.epoch,
523
+ required this.changedAt,
524
+ required this.wraps,
525
+ });
526
+ }
527
+
528
+ class NomosOwnedTransferPreparation extends NomosOwnedRecordPreparation {
529
+ final String recordId;
530
+ final String newOwner;
531
+ final List<String> previousReaders;
532
+ final int epoch;
533
+ final String changedAt;
534
+ final List<NomosHistoricOwnedWrapPreparation> historicWraps;
535
+
536
+ const NomosOwnedTransferPreparation({
537
+ required super.ownedKeySubject,
538
+ super.ownedCustody,
539
+ required this.recordId,
540
+ required this.newOwner,
541
+ required this.previousReaders,
542
+ required this.epoch,
543
+ required this.changedAt,
544
+ required this.historicWraps,
545
+ });
546
+ }
547
+
548
+ /// Exact framework preparation for the atomic device-approval directive.
549
+ class NomosOwnedDeviceApprovalPreparation {
550
+ final String newKeySubject;
551
+ final OwnedCustody ownedCustody;
552
+ final String verifiedVia;
553
+ final int validFromEpoch;
554
+ final String approvedAt;
555
+ final String ownedKeySubject;
556
+
557
+ const NomosOwnedDeviceApprovalPreparation({
558
+ required this.newKeySubject,
559
+ required this.ownedCustody,
560
+ required this.verifiedVia,
561
+ required this.validFromEpoch,
562
+ required this.approvedAt,
563
+ required this.ownedKeySubject,
564
+ });
565
+ }
566
+
567
+ /// The result of [DartReception.offerCreates]: the new local [head], every aggregate the plan minted, and —
568
+ /// for a `.births()` directive — the child workspace(s) the offer brought into being on this local plane.
569
+ /// The proof material a generated offer presents to the kernel.
570
+ ///
571
+ /// Generated law chooses this; application callers do not. [sessionCredentials] preserves the
572
+ /// ordinary session posture. [enrolledDevice] deliberately withholds any provider token so a
573
+ /// stale provider epoch cannot prevent an already enrolled device from authoring a recovery offer.
574
+ enum NomosOfferAuthenticationProof { sessionCredentials, enrolledDevice }
575
+
576
+ /// The kernel's exact diagnosis of the provider credential held by this session.
577
+ ///
578
+ /// This deliberately does not expose protobuf names to generated application
579
+ /// clients. It is a typed framework fact used to decide whether a parent-law
580
+ /// epoch recovery is applicable; callers never inspect an error string.
581
+ enum NomosProviderTokenVerification {
582
+ unspecified,
583
+ valid,
584
+ noMatchingKey,
585
+ expired,
586
+ notYetValid,
587
+ issuerMismatch,
588
+ audienceMismatch,
589
+ badSignature,
590
+ malformed,
591
+ unsupportedAlgorithm,
592
+ badKey,
593
+ missingSubject,
594
+ noVerifier,
595
+ missingToken,
596
+ }
597
+
598
+ /// Current kernel-authenticated facts needed before retrying an ordinary offer.
599
+ final class NomosAuthenticationPosture {
600
+ const NomosAuthenticationPosture({
601
+ required this.providerToken,
602
+ required this.enrolledDevice,
603
+ required this.resolvedPrincipal,
604
+ this.birthEvidence =
605
+ nomos_protocol.BirthEvidenceStatus.BIRTH_EVIDENCE_STATUS_UNSPECIFIED,
606
+ this.birthEvidenceDetail = '',
607
+ });
608
+
609
+ final NomosProviderTokenVerification providerToken;
610
+ final bool enrolledDevice;
611
+ final String resolvedPrincipal;
612
+ final nomos_protocol.BirthEvidenceStatus birthEvidence;
613
+ final String birthEvidenceDetail;
614
+
615
+ bool get needsBirthDelegation =>
616
+ birthEvidence ==
617
+ nomos_protocol
618
+ .BirthEvidenceStatus.BIRTH_EVIDENCE_STATUS_DELEGATION_REQUIRED;
619
+
620
+ bool isReadyToBirthFor(String expectedPrincipal) =>
621
+ isReadyFor(expectedPrincipal) &&
622
+ (birthEvidence ==
623
+ nomos_protocol.BirthEvidenceStatus.BIRTH_EVIDENCE_STATUS_READY ||
624
+ birthEvidence ==
625
+ nomos_protocol
626
+ .BirthEvidenceStatus.BIRTH_EVIDENCE_STATUS_NOT_REQUIRED);
627
+
628
+ bool isReadyFor(String expectedPrincipal) =>
629
+ providerToken == NomosProviderTokenVerification.valid &&
630
+ enrolledDevice &&
631
+ expectedPrincipal.isNotEmpty &&
632
+ resolvedPrincipal == expectedPrincipal;
633
+ }
634
+
635
+ /// A typed kernel-call refusal. Generated clients may inspect [code] only to
636
+ /// choose another typed kernel query; prose remains diagnostic, never control
637
+ /// flow.
638
+ final class NomosKernelCallFailure implements Exception {
639
+ const NomosKernelCallFailure({required this.code, required this.message});
640
+
641
+ final String code;
642
+ final String message;
643
+
644
+ @override
645
+ String toString() => 'NomosKernelCallFailure($code: $message)';
646
+ }
647
+
648
+ class NomosOfferOutcome {
649
+ final String head;
650
+ final String intentId;
651
+ final List<NomosCreated> created;
652
+
653
+ /// Every business aggregate touched by the sealed intent, including creates,
654
+ /// mutations, deterministic ensures and archives.
655
+ final List<NomosCreated> affected;
656
+
657
+ /// The workspace the offer ran on (NomosRef). Additive; '' on older runners. Each [created] ref already
658
+ /// carries this, so `outcome.created.first.ref` is the full followable `{workspace,type,id}` locator.
659
+ final String workspace;
660
+
661
+ /// The born child workspace name(s), when the offer ran a `.births()`
662
+ /// directive (else empty). `bindWorkspace(name)` returns a stable sibling
663
+ /// interaction handle; the peer routes each later operation from placement.
664
+ final List<String> born;
665
+
666
+ /// Typed per-child outcomes reported by the kernel, when present.
667
+ final List<NomosBirthOutcome> birthOutcomes;
668
+
669
+ final NomosOperationReceipt? _receipt;
670
+
671
+ /// Awaitable remote-custody witness for this exact write.
672
+ NomosOperationReceipt get receipt =>
673
+ _receipt ??
674
+ (throw StateError(
675
+ 'This legacy offer outcome has no operation receipt; regenerate the application client.',
676
+ ));
677
+
678
+ const NomosOfferOutcome({
679
+ required this.head,
680
+ this.intentId = '',
681
+ required this.created,
682
+ this.affected = const [],
683
+ this.workspace = '',
684
+ this.born = const [],
685
+ this.birthOutcomes = const [],
686
+ NomosOperationReceipt? receipt,
687
+ }) : _receipt = receipt;
688
+
689
+ /// TRUE when the offer COMMITTED — it advanced the ledger to a new head (a write landed and/or a child
690
+ /// was born). A REFUSED offer never yields an outcome (it throws / surfaces its typed law error), so a
691
+ /// present outcome with a head is a success. Sugar so app code writes `if (outcome.ok)` instead of
692
+ /// hand-checking `head`/`created`/`born` (the co2 friction: "there's no `.ok`").
693
+ bool get ok => head.isNotEmpty;
694
+
695
+ /// The born child workspace(s) as followable [NomosRef]s — hand one to [DartReception.follow]
696
+ /// (`final estate = await engine.follow(outcome.bornRefs.single);`) so app code never carries a
697
+ /// loose workspace string. The ref's type is the reserved `nomos.Workspace` locator tag.
698
+ List<NomosRef<Object?>> get bornRefs => born
699
+ .map(
700
+ (w) => NmsInternalNomosRefs.create<Object?>(
701
+ workspace: w,
702
+ type: 'nomos.Workspace',
703
+ id: w,
704
+ ),
705
+ )
706
+ .toList();
707
+ }
708
+
709
+ /// A generated protobuf offer response that did not admit the proposed
710
+ /// change. Keeping the response typed preserves every exhaustive kernel
711
+ /// outcome without translating it through an exception-shaped map.
712
+ final class NomosTypedOfferFailure implements Exception {
713
+ const NomosTypedOfferFailure(this.response);
714
+
715
+ final nomos_protocol.OfferResponse response;
716
+
717
+ @override
718
+ String toString() {
719
+ final detail = switch (response.whichOutcome()) {
720
+ nomos_protocol.OfferResponse_Outcome.refused =>
721
+ '${response.refused.refusal.code.name}: ${response.refused.refusal.message}',
722
+ nomos_protocol.OfferResponse_Outcome.deadLettered =>
723
+ response.deadLettered.reason,
724
+ nomos_protocol.OfferResponse_Outcome.quarantined =>
725
+ response.quarantined.reason,
726
+ nomos_protocol.OfferResponse_Outcome.blockedOnDependency =>
727
+ response.blockedOnDependency.detail,
728
+ final outcome => outcome.name,
729
+ };
730
+ return 'NomosTypedOfferFailure($detail)';
731
+ }
732
+ }
733
+
734
+ /// One compiler-declared OSGi Contract Namespace requirement.
735
+ ///
736
+ /// Generated clients carry this named value to the local kernel. The kernel
737
+ /// describes its own capabilities through [nomos_protocol.OsgiResourceResponse];
738
+ /// no host-fetched JSON or generic attribute map participates in the bind.
739
+ final class NmsKernelContractRequirement {
740
+ const NmsKernelContractRequirement({
741
+ required this.namespace,
742
+ required this.name,
743
+ required this.version,
744
+ });
745
+
746
+ final String namespace;
747
+ final String name;
748
+ final String version;
749
+ }
750
+
751
+ final class NomosKernelContractMismatch implements Exception {
752
+ const NomosKernelContractMismatch(this.missing, {this.lawHash});
753
+
754
+ final List<NmsKernelContractRequirement> missing;
755
+ final String? lawHash;
756
+
757
+ @override
758
+ String toString() {
759
+ final contracts = missing
760
+ .map((requirement) => '${requirement.name}=${requirement.version}')
761
+ .join(', ');
762
+ final normalizedLaw =
763
+ lawHash?.replaceFirst(RegExp(r'^(sha256:|policy:)'), '');
764
+ final law = normalizedLaw == null || normalizedLaw.isEmpty
765
+ ? ''
766
+ : ' required by law ${normalizedLaw.substring(0, normalizedLaw.length.clamp(0, 8))}';
767
+ return 'NomosKernelContractMismatch($contracts$law)';
768
+ }
769
+ }
770
+
771
+ /// Exact read-only answer to an authoring preflight. It is runtime posture,
772
+ /// not an authorization grant: the target kernel still judges the real offer.
773
+ final class NmsAuthoringPreflight {
774
+ const NmsAuthoringPreflight({
775
+ required this.admissible,
776
+ required this.workspace,
777
+ required this.directive,
778
+ required this.cause,
779
+ required this.causes,
780
+ this.actor,
781
+ this.keyHash,
782
+ this.domain,
783
+ this.relation,
784
+ this.object,
785
+ this.detail,
786
+ });
787
+
788
+ final bool admissible;
789
+ final String workspace;
790
+ final String directive;
791
+ final nomos_protocol.AuthoringPreflightCause cause;
792
+ final List<nomos_protocol.AuthoringPreflightCause> causes;
793
+ final String? actor;
794
+ final String? keyHash;
795
+ final String? domain;
796
+ final String? relation;
797
+ final String? object;
798
+ final String? detail;
799
+ }
800
+
801
+ /// Ephemeral correlation counters owned by one Reception plane and shared by
802
+ /// every workspace view over that plane. They are presentation/protocol state,
803
+ /// never custody or kernel truth.
804
+ final class _ReceptionSequences {
805
+ int request = 0;
806
+ int localSpan = 0;
807
+ }
808
+
809
+ final class _CompatibilityCacheEntry {
810
+ _CompatibilityCacheEntry(this.matrix, this.answer);
811
+
812
+ final Uint8List matrix;
813
+ final Future<nomos_protocol.InterfaceCompatibilityVerdict> answer;
814
+ }
815
+
816
+ bool _sameBytes(Uint8List left, Uint8List right) {
817
+ if (identical(left, right)) return true;
818
+ if (left.length != right.length) return false;
819
+ for (var index = 0; index < left.length; index += 1) {
820
+ if (left[index] != right[index]) return false;
821
+ }
822
+ return true;
823
+ }
824
+
825
+ /// Dart's language-native Reception. It presents typed sessions, values and
826
+ /// streams over one [PeerOfficeConnection] supplied by the Platform Shell.
827
+ /// A tenant-registered repair for `nomos.authz.signer_enrollment_missing`:
828
+ /// given the typed refusal from a child workspace, author the parent's nominal
829
+ /// child-authentication repair and resolve true once the child seats this
830
+ /// device's key (the reception then retries the refused offer once).
831
+ typedef NomosChildAuthenticationRepair = Future<bool> Function(
832
+ ParentSignerEnrollmentMissing refusal,
833
+ );
834
+
835
+ /// Proof that the Peer Office has bound one interaction and named its exact
836
+ /// credential capability. Only [DartReception.bindSession] can create this.
837
+ ///
838
+ /// Platform shells must carry this value to credential registration, making
839
+ /// "register before bind" unrepresentable at the Dart API boundary.
840
+ final class NomosBoundInteraction {
841
+ const NomosBoundInteraction._({
842
+ required this.session,
843
+ required this.workspace,
844
+ required this.credentialCapability,
845
+ });
846
+
847
+ final String session;
848
+ final String workspace;
849
+ final String credentialCapability;
850
+ }
851
+
852
+ class DartReception {
853
+ final PeerOfficeConnection _transport;
854
+ final ReceptionEventDesk _events;
855
+ // A disposable client projection of Runtime v2's live sessions. It provides
856
+ // stable Dart handles but is never custody or lifecycle authority.
857
+ final NmsObservedSessionDirectory<NomosSession> _sessions;
858
+ // Ephemeral application handles over stable receipt identities emitted by
859
+ // the kernel/runtime. No admission truth is manufactured in Dart.
860
+ final NmsOperationReceiptProjection _receipts;
861
+ final _ReceptionSequences _sequences;
862
+ final NmsRuntimeV2Gateway _runtimeV2;
863
+ final Map<String, List<_CompatibilityCacheEntry>> _compatibilityBySession =
864
+ <String, List<_CompatibilityCacheEntry>>{};
865
+ StreamSubscription<nomos_protocol.RuntimeEvent>? _runtimeEventSub;
866
+
867
+ DartReception(
868
+ PeerOfficeConnection transport, {
869
+ this.allowStaleRunnerBundle = false,
870
+ }) : _transport = transport,
871
+ _events = ReceptionEventDesk(),
872
+ _sessions = NmsObservedSessionDirectory<NomosSession>(),
873
+ _receipts = NmsOperationReceiptProjection(),
874
+ _sequences = _ReceptionSequences(),
875
+ _runtimeV2 = NmsRuntimeV2Gateway(transport) {
876
+ _runtimeEventSub = _transport.runtimeEvents.listen(
877
+ _onRuntimeEvent,
878
+ onError: _onTransportError,
879
+ );
880
+ }
881
+
882
+ void _onRuntimeEvent(nomos_protocol.RuntimeEvent event) {
883
+ if (event.hasReady()) {
884
+ final stale = _staleBundleError(event.ready.runtimeVersion);
885
+ if (stale != null && !allowStaleRunnerBundle) {
886
+ _events.failReady(stale);
887
+ } else {
888
+ _events.markReady();
889
+ }
890
+ return;
891
+ }
892
+ if (event.hasWatchChanged()) {
893
+ _eventPlane._events.typedWatches.deliver(event.watchChanged);
894
+ return;
895
+ }
896
+ if (event.hasModelAdoptionProgress()) {
897
+ // The office brings a workspace onto the application's approved law and
898
+ // reports each phase; this projects the observation onto the workspace's
899
+ // sync status and the adoption stream. No lifecycle step is chosen here.
900
+ final progress = NomosModelAdoptionProgress.fromRuntime(
901
+ event.modelAdoptionProgress,
902
+ );
903
+ _eventPlane._events.publishModelAdoption(progress);
904
+ _eventPlane._invalidateCompatibility(
905
+ event.hasSession() ? event.session : null,
906
+ );
907
+ _eventPlane._updateScopedStatus(
908
+ event.hasSession() ? event.session : null,
909
+ (current) => current.copyWith(modelAdoption: progress),
910
+ );
911
+ return;
912
+ }
913
+ if (event.hasSyncCompleted()) {
914
+ final completed = event.syncCompleted;
915
+ final session = event.hasSession() ? event.session : null;
916
+ _eventPlane._events.publishSync(event);
917
+ _eventPlane._invalidateCompatibility(session);
918
+ if (session != null) {
919
+ if (!completed.hasFailure() && completed.hasResult()) {
920
+ _eventPlane._advanceOperationReceipts(session, completed.result);
921
+ }
922
+ }
923
+ final observedAt = DateTime.now();
924
+ _eventPlane._updateScopedStatus(
925
+ session,
926
+ (current) => reduceSyncCompletion(current, completed, observedAt),
927
+ );
928
+ return;
929
+ }
930
+ if (event.hasLawChanged()) {
931
+ _eventPlane._invalidateCompatibility(
932
+ event.hasSession() ? event.session : null,
933
+ );
934
+ return;
935
+ }
936
+ if (event.hasLinkChanged()) {
937
+ _eventPlane._events.publishSync(event);
938
+ final session = event.hasSession() ? event.session : null;
939
+ if (event.linkChanged.status ==
940
+ nomos_protocol.LinkStatus.LINK_STATUS_CLOSED) {
941
+ final closedAt = DateTime.now();
942
+ _eventPlane._updateScopedStatus(
943
+ session,
944
+ // A settled, live workspace was known current until this moment.
945
+ (current) => current.copyWith(
946
+ phase: SyncPhase.offline,
947
+ live: false,
948
+ lastSyncedAt: current.live && current.phase == SyncPhase.idle
949
+ ? closedAt
950
+ : null,
951
+ ),
952
+ );
953
+ } else if (event.linkChanged.status ==
954
+ nomos_protocol.LinkStatus.LINK_STATUS_OPEN) {
955
+ _eventPlane._updateScopedStatus(
956
+ session,
957
+ (current) => current.copyWith(
958
+ live: true,
959
+ phase:
960
+ current.phase == SyncPhase.offline ? SyncPhase.checking : null,
961
+ ),
962
+ );
963
+ }
964
+ return;
965
+ }
966
+ if (event.hasFollowsSummary()) {
967
+ final summary = event.followsSummary;
968
+ _eventPlane._events.syncStatuses.updateFollows(
969
+ summary.followed,
970
+ summary.settled,
971
+ live: summary.live,
972
+ observedAt: summary.hasObservedAtUnixMillis()
973
+ ? DateTime.fromMillisecondsSinceEpoch(
974
+ summary.observedAtUnixMillis.toInt())
975
+ : null,
976
+ statuses: [
977
+ for (final follow in summary.workspaces)
978
+ WorkspaceFollowStatus.fromRuntime(follow),
979
+ ],
980
+ );
981
+ return;
982
+ }
983
+ if (event.hasCustodyChanged()) {
984
+ _eventPlane._events.publishCustody(
985
+ NomosCustodyEvent.fromRuntime(
986
+ event.custodyChanged,
987
+ session: event.hasSession() ? event.session : null,
988
+ ),
989
+ );
990
+ return;
991
+ }
992
+ if (event.hasForeignPlacementIgnored()) {
993
+ final ignored = event.foreignPlacementIgnored;
994
+ developer.log(
995
+ 'ignored a ${ignored.source} placement at ${ignored.origin}'
996
+ '${ignored.workspace.isEmpty ? '' : ' for ${ignored.workspace}'}: '
997
+ 'not one of this host\'s configured providers',
998
+ name: 'nomos.origin-fence',
999
+ );
1000
+ _eventPlane._events.publishForeignPlacementIgnored(ignored);
1001
+ return;
1002
+ }
1003
+ if (event.hasLocalCopyPreserved()) {
1004
+ _eventPlane._events.publishLocalCopyPreserved(
1005
+ NomosPreservedLocalCopy.fromRuntime(event.localCopyPreserved),
1006
+ );
1007
+ return;
1008
+ }
1009
+ if (event.hasLocalWorkRejected()) {
1010
+ _eventPlane._events.publishLocalWorkRejected(
1011
+ NomosLocalWorkRejectedEvent.fromRuntime(
1012
+ event.localWorkRejected,
1013
+ session: event.hasSession() ? event.session : null,
1014
+ ),
1015
+ );
1016
+ return;
1017
+ }
1018
+ if (event.hasTransferProgress()) {
1019
+ final progress = TransferProgress.fromRuntime(event.transferProgress);
1020
+ _eventPlane._events.progress.observeTransfer(progress);
1021
+ _eventPlane._updateScopedStatus(
1022
+ event.hasSession() ? event.session : null,
1023
+ (current) => reduceTransferProgress(current, progress),
1024
+ );
1025
+ return;
1026
+ }
1027
+ if (event.hasMaintenanceProgress()) {
1028
+ _eventPlane._events.observeMaintenance(event.maintenanceProgress);
1029
+ return;
1030
+ }
1031
+ if (event.hasReceivingGroupConvergence()) {
1032
+ // The receiving-group convergence tail (posture → recovery → authentication →
1033
+ // verification), reported by the office to every session bound to the group.
1034
+ _eventPlane._events
1035
+ .publishReceivingGroupConvergence(event.receivingGroupConvergence);
1036
+ return;
1037
+ }
1038
+ if (event.hasKernelProgress()) {
1039
+ final progress = KernelProgress.fromRuntime(event.kernelProgress);
1040
+ _eventPlane._events.progress.observeKernel(progress);
1041
+ final session = event.hasSession()
1042
+ ? event.session
1043
+ : _eventPlane._sessions.boundCount == 1
1044
+ ? _eventPlane._sessions.soleBoundSession
1045
+ : null;
1046
+ if (progress.operation == KernelProgressOperation.restoreMain &&
1047
+ session != null &&
1048
+ _eventPlane._sessions.isBound(session)) {
1049
+ _eventPlane._events.observeRestore(
1050
+ _eventPlane._sessions.workspaceFor(session),
1051
+ progress,
1052
+ );
1053
+ }
1054
+ return;
1055
+ }
1056
+ if (event.hasSpan()) {
1057
+ final message = NomosSpanEvent.fromRuntime(
1058
+ event.span,
1059
+ session: event.hasSession() ? event.session : null,
1060
+ );
1061
+ _eventPlane._events.progress.observeSpan(message);
1062
+ }
1063
+ }
1064
+
1065
+ void _onTransportError(Object error, StackTrace stackTrace) {
1066
+ _events.failReady(error, stackTrace);
1067
+ }
1068
+
1069
+ void _invalidateCompatibility(String? session) {
1070
+ final cache = _eventPlane._compatibilityBySession;
1071
+ if (session == null || session.isEmpty) {
1072
+ cache.clear();
1073
+ } else {
1074
+ cache.remove(session);
1075
+ }
1076
+ }
1077
+
1078
+ /// Escape hatch for DELIBERATE mixed-version setups (framework dev, A/B probes): accept a runner
1079
+ /// bundle older than [kExpectedRunnerBundleVersion] instead of failing [ready].
1080
+ final bool allowStaleRunnerBundle;
1081
+
1082
+ /// null = acceptable; otherwise the typed boot error. "unbundled-dev" (the in-repo source lane)
1083
+ /// always passes; a missing stamp means a pre-stamp (definitionally stale) bundle.
1084
+ StaleRunnerBundle? _staleBundleError(Object? bundleVersion) {
1085
+ final v = bundleVersion is String ? bundleVersion : null;
1086
+ if (v == 'unbundled-dev') return null;
1087
+ if (v != null && _semverAtLeast(v, kExpectedRunnerBundleVersion))
1088
+ return null;
1089
+ return StaleRunnerBundle(found: v, expected: kExpectedRunnerBundleVersion);
1090
+ }
1091
+
1092
+ static bool _semverAtLeast(String v, String min) {
1093
+ List<int> parse(String s) =>
1094
+ s.split('-').first.split('.').map((p) => int.tryParse(p) ?? 0).toList();
1095
+ final a = parse(v), b = parse(min);
1096
+ for (var i = 0; i < 3; i++) {
1097
+ final x = i < a.length ? a[i] : 0, y = i < b.length ? b[i] : 0;
1098
+ if (x != y) return x > y;
1099
+ }
1100
+ return true;
1101
+ }
1102
+
1103
+ /// A SESSION VIEW's super-constructor: shares the parent's [_transport] but does NOT subscribe (the parent
1104
+ /// owns the one message loop + all correlation). Only [NomosSession] uses it.
1105
+ /// CHILD AUTHENTICATION REPAIR — the tenant-registered lane behind
1106
+ /// `nomos.authz.signer_enrollment_missing`. A child workspace judges a device
1107
+ /// key it has never seen (a key enrolled into the parent AFTER the child was
1108
+ /// born, or a child born on another device). The parent's law already owns the
1109
+ /// nominal repair (`repairsChildAuthenticationFor`: an enrolled-device intent
1110
+ /// that emits one signer effect into the child). The framework cannot name
1111
+ /// that tenant intent, so the application registers it here once; the
1112
+ /// reception then invokes it on the refusal, single-flight per workspace,
1113
+ /// and retries the refused offer exactly once when the repair reports that
1114
+ /// the child now seats this key. Absent a registration the refusal surfaces
1115
+ /// untouched, as before.
1116
+ NomosChildAuthenticationRepair? _childAuthenticationRepair;
1117
+ final Map<String, Future<bool>> _childAuthenticationRepairs = {};
1118
+
1119
+ /// Register (or clear with null) the child-authentication repair for every
1120
+ /// session on this plane. Views delegate to the root, so registering on any
1121
+ /// session covers all workspaces.
1122
+ void adoptChildAuthenticationRepair(NomosChildAuthenticationRepair? repair) {
1123
+ _eventPlane._childAuthenticationRepair = repair;
1124
+ }
1125
+
1126
+ Future<bool> _repairChildAuthentication(
1127
+ String workspace,
1128
+ ParentSignerEnrollmentMissing refusal,
1129
+ ) {
1130
+ final plane = _eventPlane;
1131
+ final repair = plane._childAuthenticationRepair;
1132
+ if (repair == null) return Future<bool>.value(false);
1133
+ return plane._childAuthenticationRepairs.putIfAbsent(workspace, () {
1134
+ // ignore: avoid_print
1135
+ print('[nomos-offer] repairing child authentication for $workspace');
1136
+ return repair(refusal).then((repaired) {
1137
+ // ignore: avoid_print
1138
+ print('[nomos-offer] child authentication repair for $workspace: '
1139
+ '${repaired ? 'seated' : 'not seated'}');
1140
+ return repaired;
1141
+ }, onError: (Object error, StackTrace stackTrace) {
1142
+ // ignore: avoid_print
1143
+ print('[nomos-offer] child authentication repair for $workspace '
1144
+ 'failed: $error');
1145
+ return false;
1146
+ }).whenComplete(() {
1147
+ // Block body on purpose: `remove` returns the stored future and a
1148
+ // returned future would make whenComplete wait on itself.
1149
+ plane._childAuthenticationRepairs.remove(workspace);
1150
+ });
1151
+ });
1152
+ }
1153
+
1154
+ DartReception._view(DartReception parent)
1155
+ : _transport = parent._transport,
1156
+ _events = parent._events,
1157
+ _sessions = parent._sessions,
1158
+ _receipts = parent._receipts,
1159
+ _sequences = parent._sequences,
1160
+ _runtimeV2 = parent._runtimeV2,
1161
+ allowStaleRunnerBundle = false;
1162
+
1163
+ /// THE EVENT PLANE — the ONE reception whose mutable dispatch registries every event/push resolves against.
1164
+ ///
1165
+ /// WHY THIS EXISTS (the closed bug class): a [NomosSession] extends this class but shares its [parent]'s
1166
+ /// transport + message loop — [_onMessage] only ever runs on the PARENT, so events resolve against the
1167
+ /// PARENT's controller maps ([_watches], [_compatWatches], the progress desk,
1168
+ /// [_sessions], …). Any Stream-returning method / stateful getter that touched `this`'s copies on a
1169
+ /// session registered on a DEAD registry and never emitted (real shipped instances: `watchPendingConflicts`
1170
+ /// pre-0.48.1, and the five hand-written session watch overrides this seam retired).
1171
+ ///
1172
+ /// THE RULE (tripwire-enforced by `test/event_plane_seam_test.dart`): EVERY member that reads or writes
1173
+ /// event-dispatch / plane state MUST do so via `_eventPlane.<field>` — never a bare field access. Requests
1174
+ /// still go through `this._call` (a session's override stamps `args.session`), so a session's watch
1175
+ /// registers its controller on the PARENT's registry while the wire op still carries the SESSION's
1176
+ /// workspace. Chases to the root, so nested delegation stays correct.
1177
+ DartReception get _eventPlane => this;
1178
+
1179
+ /// The session name this view is pinned to (null on the plane itself). Lets base implementations be
1180
+ /// session-aware (per-workspace compat caching / law-change streams) with no per-method overrides.
1181
+ String? get _sessionName => null;
1182
+
1183
+ /// Completes once the runner has posted `{ event: "ready" }` (the wasm + client are booted).
1184
+ Future<void> get ready => _events.ready;
1185
+
1186
+ /// Invoke one generated application-runtime command. Request correlation and
1187
+ /// every outcome discriminator stay in generated protobuf types; callers
1188
+ /// must exhaustively select the response arm they requested.
1189
+ Future<nomos_protocol.RuntimeResponse> _invokeRuntimeV2(
1190
+ nomos_protocol.RuntimeRequest request,
1191
+ ) async {
1192
+ final plane = _eventPlane;
1193
+ final id = 'reception-${plane._sequences.localSpan++}';
1194
+ final operation = request.whichCommand().name;
1195
+ final started = DateTime.now().millisecondsSinceEpoch;
1196
+ void observe(NomosSpanPhase phase) => plane._events.progress.observeSpan(
1197
+ NomosSpanEvent(
1198
+ phase: phase,
1199
+ id: id,
1200
+ name: operation,
1201
+ track: 'reception',
1202
+ session: _sessionName,
1203
+ startedAtMillis: started,
1204
+ endedAtMillis: phase == NomosSpanPhase.end
1205
+ ? DateTime.now().millisecondsSinceEpoch
1206
+ : null,
1207
+ ),
1208
+ );
1209
+ observe(NomosSpanPhase.begin);
1210
+ try {
1211
+ return await plane._runtimeV2.invoke(request);
1212
+ } finally {
1213
+ observe(NomosSpanPhase.end);
1214
+ }
1215
+ }
1216
+
1217
+ Future<nomos_protocol.FrameworkResponse> _invokeFramework(
1218
+ nomos_protocol.FrameworkRequest request,
1219
+ ) async {
1220
+ final response = await _invokeRuntimeV2(
1221
+ nomos_protocol.RuntimeRequest(framework: request),
1222
+ );
1223
+ if (!response.hasFramework()) {
1224
+ throw StateError(
1225
+ 'Runtime v2 returned ${response.whichOutcome().name} for a framework operation',
1226
+ );
1227
+ }
1228
+ return response.framework;
1229
+ }
1230
+
1231
+ String _runtimeSession([String? explicit]) {
1232
+ if (explicit != null && explicit.isNotEmpty) return explicit;
1233
+ final scoped = _sessionName;
1234
+ if (scoped != null && scoped.isNotEmpty) return scoped;
1235
+ final sessions = _eventPlane._sessions;
1236
+ if (sessions.boundCount == 1) return sessions.soleBoundSession;
1237
+ if (!sessions.hasBoundSessions) {
1238
+ throw StateError('No Nomos workspace is connected');
1239
+ }
1240
+ throw StateError(
1241
+ 'Multiple Nomos workspaces are connected; select a session');
1242
+ }
1243
+
1244
+ String _workspaceForSession(String session) =>
1245
+ _eventPlane._sessions.workspaceFor(session);
1246
+
1247
+ NomosPlatformCredentialProviders? get _platformCredentialProviders {
1248
+ final Object transport = _eventPlane._transport;
1249
+ return transport is NomosPlatformCredentialProviders ? transport : null;
1250
+ }
1251
+
1252
+ /// The law model this application release approves, retained on the plane
1253
+ /// for every workspace it opens. Not a callback and not lifecycle state: the
1254
+ /// office decides whether a workspace needs anything.
1255
+ NomosApprovedModel? _approvedApplicationModel;
1256
+
1257
+ // Transport single-flight only: repeated acquisitions must not submit the
1258
+ // same background paper while its first invocation is still running. The
1259
+ // office still owns lifecycle, authority, outcomes and bulk-work limits.
1260
+ // Key by session AND immutable approved-model input, never workspace alone
1261
+ // across planes/identities. Terminal outcomes are not cached.
1262
+ late final Map<(String, NomosApprovedModel), Future<void>>
1263
+ _backgroundModelAdoptions = {};
1264
+
1265
+ NomosApprovedModel? get nomosInternalApprovedApplicationModel =>
1266
+ _eventPlane._approvedApplicationModel;
1267
+
1268
+ /// Retain the application's approved law model and register its package
1269
+ /// with the platform transport, so the runtime can stage the exact package
1270
+ /// on a workspace that still serves the previous law. Every workspace this
1271
+ /// plane opens afterwards (home, estate, catalogue) is offered to the
1272
+ /// model-adoption desk once it is ready.
1273
+ void retainApprovedApplicationModel(NomosApprovedModel model) {
1274
+ final providers = _platformCredentialProviders;
1275
+ if (providers == null) {
1276
+ throw UnsupportedError(
1277
+ 'This Nomos platform transport cannot supply the application law package',
1278
+ );
1279
+ }
1280
+ _eventPlane._approvedApplicationModel = model;
1281
+ providers.registerLawPackageSource(
1282
+ NmsApprovedModelLawPackageSource(model),
1283
+ );
1284
+ }
1285
+
1286
+ Future<void> _unwatch(String subscriptionId) async {
1287
+ final response = await _invokeRuntimeV2(
1288
+ nomos_protocol.RuntimeRequest(
1289
+ unwatch: nomos_protocol.UnwatchRequest(
1290
+ subscriptionId: subscriptionId,
1291
+ ),
1292
+ ),
1293
+ );
1294
+ if (!response.hasWatchStopped()) {
1295
+ throw StateError(
1296
+ 'Runtime v2 returned ${response.whichOutcome().name} for unwatch',
1297
+ );
1298
+ }
1299
+ }
1300
+
1301
+ NomosOperationReceipt _registerOperationReceipt(
1302
+ String workspace,
1303
+ String intentId,
1304
+ ) {
1305
+ final plane = _eventPlane;
1306
+ // A locally accepted write is not current until custody convergence
1307
+ // advances its receipt. Mark its workspace immediately so a same-tick
1308
+ // `whenCurrent()` cannot return using the previous metrics snapshot.
1309
+ final current = plane._currentSessionStatus(workspace);
1310
+ plane._pushSessionStatus(
1311
+ workspace,
1312
+ locallyAdmitted(current),
1313
+ );
1314
+ plane._foldApplicationStatusFromSessions();
1315
+ return plane._receipts.register(workspace, intentId);
1316
+ }
1317
+
1318
+ void _advanceOperationReceipts(
1319
+ String workspace,
1320
+ nomos_protocol.SyncWorkspaceResponse result,
1321
+ ) {
1322
+ _eventPlane._receipts.advance(workspace, result);
1323
+ }
1324
+
1325
+ /// The parent-write receipt which brought this child workspace into being.
1326
+ /// Ordinary bindings of pre-existing workspaces have no creation receipt.
1327
+ NomosOperationReceipt? nomosInternalWorkspaceCreationReceipt() {
1328
+ final workspace = _sessionName;
1329
+ return workspace == null ? null : _eventPlane._receipts.birthFor(workspace);
1330
+ }
1331
+
1332
+ // ── the runtime-aware surface (the boundary the generated Dart client wraps) ──
1333
+
1334
+ /// Return a stable, cheap [NomosSession] handle pinned to [workspace]. A
1335
+ /// handle may be created before it is bound; [bindSession] attaches only the
1336
+ /// application identity. The first read or write is routed independently by
1337
+ /// the resident peer.
1338
+ NomosSession session(String workspace) => _eventPlane._sessions.handle(
1339
+ workspace,
1340
+ () => NomosSession(_eventPlane, workspace),
1341
+ );
1342
+
1343
+ /// A [NomosForeignReader] backed by this reception's multi-workspace plane. Generated application methods
1344
+ /// use it privately to follow a declared cross-workspace business reference.
1345
+ /// `foreignWorkspace(ws).queryById(id)` binds a cheap interaction handle on
1346
+ /// demand and reads it. Placement and remote routing
1347
+ /// remain decisions of the peer runtime; the application supplies no cloud
1348
+ /// locator or custody policy. This is a locator resolver, not a foreign key.
1349
+ NomosForeignReader foreignReader() => _ReceptionForeignReader(this);
1350
+
1351
+ /// The workspace session names currently bound to this language view.
1352
+ List<String> get boundSessions => _eventPlane._sessions.boundSnapshot();
1353
+
1354
+ ReceptionSyncDiagnostics get syncDiagnostics => ReceptionSyncDiagnostics(
1355
+ read: () => List.unmodifiable([
1356
+ for (final session in _eventPlane._sessions.boundNames)
1357
+ WorkspaceSyncObservation(
1358
+ session: session,
1359
+ workspace: _eventPlane._sessions.workspaceFor(session),
1360
+ status: _eventPlane._events.syncStatuses.statusFor(session),
1361
+ ),
1362
+ ]),
1363
+ changes: _eventPlane._events.syncStatuses.changes,
1364
+ );
1365
+
1366
+ /// This device's local workspace copies: their reborn state, and a preview
1367
+ /// and drop of one copy so it is refetched from its current provider.
1368
+ ReceptionLocalCopies get localCopies => ReceptionLocalCopies(
1369
+ invoke: _invokeRuntimeV2,
1370
+ changes: _eventPlane._events.copyChanges,
1371
+ preparationFor: _eventPlane._events.preparationFor,
1372
+ statusesFor: (sessions) => List.unmodifiable([
1373
+ for (final session in sessions)
1374
+ _eventPlane._events.syncStatuses.statusFor(session),
1375
+ ]),
1376
+ preservations: _eventPlane._events.localCopiesPreserved,
1377
+ recentPreservations: () =>
1378
+ _eventPlane._events.recentLocalCopiesPreserved,
1379
+ );
1380
+
1381
+ /// Observe the peer's local physical workspace inventory. This is not an
1382
+ /// interaction-session list and performs no open, acquisition or sync work.
1383
+ Future<List<nomos_protocol.WorkspaceSummary>> listWorkspaces() async {
1384
+ final response = await _invokeRuntimeV2(
1385
+ nomos_protocol.RuntimeRequest(
1386
+ listWorkspaces: nomos_protocol.ListWorkspacesRequest(),
1387
+ ),
1388
+ );
1389
+ if (!response.hasWorkspaceList()) {
1390
+ throw StateError(
1391
+ 'Runtime v2 returned ${response.whichOutcome().name} for listWorkspaces',
1392
+ );
1393
+ }
1394
+ return response.workspaceList.workspaces;
1395
+ }
1396
+
1397
+ /// THE OFFICE ANSWERS A BIRTH'S WORKSPACE NAME; the client never re-derives it.
1398
+ ///
1399
+ /// A keyed birth's workspace name is a pure function of a key the caller already
1400
+ /// owns (`birth.keyed({ namespace, key })`), so a born workspace is addressable
1401
+ /// with zero persistence. That function is LAW (`dsl/src/births.ts`), and a second
1402
+ /// copy of it here would be a second implementation of a law fact: change either
1403
+ /// side and the two silently address different workspaces. The paper binds no
1404
+ /// session and mounts nothing, so it can be asked before the first workspace is
1405
+ /// open.
1406
+ Future<String> nmsKeyedBirthWorkspace({
1407
+ required String namespace,
1408
+ required String key,
1409
+ }) =>
1410
+ _resolveWorkspaceName(
1411
+ nomos_protocol.ResolveWorkspaceNameRequest(
1412
+ keyed: nomos_protocol.KeyedBirthName(namespace: namespace, key: key),
1413
+ ),
1414
+ );
1415
+
1416
+ /// Fetch [workspace] onto this device now, reading nothing (the explicit form of
1417
+ /// what a read of an unheld workspace reaches): placement discovery, then the
1418
+ /// Replica Follow that placement retains, which mounts the provider's head alone.
1419
+ /// Completes once the follow exists; the fetch itself runs in the background.
1420
+ Future<void> nmsFetchWorkspace(String workspace) async {
1421
+ final response = await _invokeRuntimeV2(
1422
+ nomos_protocol.RuntimeRequest(
1423
+ fetchWorkspace:
1424
+ nomos_protocol.FetchWorkspaceRequest(workspace: workspace),
1425
+ ),
1426
+ );
1427
+ if (!response.hasWorkspaceFetch()) {
1428
+ throw StateError(
1429
+ 'Runtime v2 returned ${response.whichOutcome().name} for a workspace fetch',
1430
+ );
1431
+ }
1432
+ }
1433
+
1434
+ /// The framework's identity anchor for [principal], as the office names it.
1435
+ Future<String> nmsReceivingGroupWorkspace(String principal) =>
1436
+ _resolveWorkspaceName(
1437
+ nomos_protocol.ResolveWorkspaceNameRequest(
1438
+ receivingGroup: nomos_protocol.ReceivingGroupName(
1439
+ principal: principal,
1440
+ ),
1441
+ ),
1442
+ );
1443
+
1444
+ Future<String> _resolveWorkspaceName(
1445
+ nomos_protocol.ResolveWorkspaceNameRequest request,
1446
+ ) async {
1447
+ final response = await _invokeRuntimeV2(
1448
+ nomos_protocol.RuntimeRequest(resolveWorkspaceName: request),
1449
+ );
1450
+ if (!response.hasResolvedWorkspaceName()) {
1451
+ throw StateError(
1452
+ 'Runtime v2 returned ${response.whichOutcome().name} for a workspace name',
1453
+ );
1454
+ }
1455
+ return response.resolvedWorkspaceName.workspace;
1456
+ }
1457
+
1458
+ /// Bind one ephemeral application interaction to a workspace identity.
1459
+ ///
1460
+ /// This does not inspect placement, mount Git, attach SQLite, authenticate a
1461
+ /// remote or start Replica Follow work. Each operation is routed by the Peer
1462
+ /// Runtime when it is executed.
1463
+ Future<NomosBoundInteraction> bindSession({
1464
+ required String workspace,
1465
+ String? credentialSourceSession,
1466
+ String? principal,
1467
+ String? session,
1468
+ }) async {
1469
+ final ws = session ?? workspace;
1470
+ final response = await _invokeRuntimeV2(
1471
+ nomos_protocol.RuntimeRequest(
1472
+ bindSession: nomos_protocol.BindSessionRequest(
1473
+ workspace: workspace,
1474
+ session: ws,
1475
+ credentialSourceSession: credentialSourceSession,
1476
+ principal: principal,
1477
+ ),
1478
+ ),
1479
+ );
1480
+ if (!response.hasSessionBound()) {
1481
+ throw StateError(
1482
+ 'Runtime v2 returned ${response.whichOutcome().name} for session binding',
1483
+ );
1484
+ }
1485
+ final bound = response.sessionBound;
1486
+ _eventPlane._sessions.observeBound(
1487
+ session: bound.session,
1488
+ workspace: bound.workspace,
1489
+ credentialCapability: bound.credentialCapability,
1490
+ );
1491
+ _eventPlane._events.syncStatuses.ensureSession(
1492
+ ws,
1493
+ const SyncStatus(),
1494
+ );
1495
+ _eventPlane._foldApplicationStatusFromSessions();
1496
+ // Binding a workspace is where its ONE device-signed model transition is offered
1497
+ // (nomos2 architecture/old_law_child_bootstrap.md). This is deliberately the lowest
1498
+ // binding primitive and not `bindWorkspace`: a child reached through a foreign read
1499
+ // or a credential-less follow binds HERE and never through that door, so a trigger
1500
+ // one layer up silently skips exactly the workspaces the lane exists for.
1501
+ // The office refuses cheaply and locally — an unseated session never reaches a kernel
1502
+ // read — so this never gates the caller on the outcome.
1503
+ this
1504
+ .session(bound.session)
1505
+ .nomosInternalAdoptApplicationModelInBackground();
1506
+ return NomosBoundInteraction._(
1507
+ session: bound.session,
1508
+ workspace: bound.workspace,
1509
+ credentialCapability: bound.credentialCapability,
1510
+ );
1511
+ }
1512
+
1513
+ /// Register a renewable credential provider for one already-bound
1514
+ /// interaction capability.
1515
+ ///
1516
+ /// This is a Platform Shell capability, not placement or Replica Follow
1517
+ /// state. The peer remains the sole owner of deciding when the capability is
1518
+ /// used by background work.
1519
+ void nomosInternalRegisterCredentialProvider({
1520
+ required NomosBoundInteraction interaction,
1521
+ required String cloud,
1522
+ required NomosSignIn signIn,
1523
+ }) {
1524
+ if (cloud.trim().isEmpty) {
1525
+ throw ArgumentError.value(
1526
+ cloud,
1527
+ 'cloud',
1528
+ 'Nomos credential provider URL cannot be empty',
1529
+ );
1530
+ }
1531
+ final providers = _platformCredentialProviders;
1532
+ if (providers == null) {
1533
+ throw UnsupportedError(
1534
+ 'This Nomos platform transport cannot provide renewable credentials',
1535
+ );
1536
+ }
1537
+ final sessions = _eventPlane._sessions;
1538
+ if (!sessions.isBound(interaction.session) ||
1539
+ sessions.workspaceFor(interaction.session) != interaction.workspace ||
1540
+ sessions.credentialCapabilityFor(interaction.session) !=
1541
+ interaction.credentialCapability) {
1542
+ throw StateError(
1543
+ "Cannot register credentials for stale interaction "
1544
+ "'${interaction.session}'",
1545
+ );
1546
+ }
1547
+ providers.registerCredentialProvider(
1548
+ capability: interaction.credentialCapability,
1549
+ cloud: cloud,
1550
+ signIn: signIn,
1551
+ );
1552
+ }
1553
+
1554
+ /// Custody discontinuities surface here as typed observations. They do not
1555
+ /// create a second interactive controller lifecycle in the peer runtime.
1556
+ Stream<NomosCustodyEvent> get custodyEvents =>
1557
+ _eventPlane._events.custodyEvents;
1558
+
1559
+ /// THE LOCAL-WORK-REJECTED STREAM (seated_barrier_and_seal_obligation.md Part 4): the cloud's gate
1560
+ /// refused work this device's own kernel had accepted. Refused intents are already durably parked
1561
+ /// (client DLQ + the workspace's refused/* refs) and surviving local work already replayed when an
1562
+ /// event arrives — bind honest UX to it: `retryable` ⇒ "your changes are queued and will retry";
1563
+ /// otherwise ⇒ "your changes were preserved for review — contact support". Never render the raw
1564
+ /// refusal text as the headline.
1565
+ Stream<NomosLocalWorkRejectedEvent> get localWorkRejectedEvents =>
1566
+ _eventPlane._events.localWorkRejectedEvents;
1567
+
1568
+ // ── THE DEAD-LETTER QUEUE (refused-but-legitimate work is NEVER lost) — the Dart mirror of the
1569
+ // JS holon's DLQ surface (`deadLetters()` / `retryDeadLetter()` / `discardDeadLetter()`). A
1570
+ // DOMAIN rejection (the law couldn't merge it — edge admission or a local replay refusal)
1571
+ // parks the FULL intent durably in the workspace tree (it rides export()/restore); the app
1572
+ // inspects, the domain dev ships a law fix, retry unjams. ──
1573
+
1574
+ /// The parked dead letters; intent bytes stay in durable custody.
1575
+ Future<List<nomos_protocol.DeadLetterEntry>> deadLetters(
1576
+ {String? session}) async {
1577
+ final response = await _invokeRuntimeV2(
1578
+ nomos_protocol.RuntimeRequest(
1579
+ deadLetter: nomos_protocol.DeadLetterRequest(
1580
+ session: _runtimeSession(session),
1581
+ list: nomos_protocol.ListDeadLettersRequest(),
1582
+ ),
1583
+ ),
1584
+ );
1585
+ if (!response.hasDeadLetter() || !response.deadLetter.hasList()) {
1586
+ throw StateError(
1587
+ 'Runtime v2 returned ${response.whichOutcome().name} for deadLetters',
1588
+ );
1589
+ }
1590
+ return List.unmodifiable(response.deadLetter.list.entries);
1591
+ }
1592
+
1593
+ /// Retry one dead letter (after the law fix landed): pulls canon first (it may already be
1594
+ /// admitted), else re-offers the parked bytes locally. `{ok, head?|alreadyOnMain?|error?}` —
1595
+ /// a refusal leaves the entry parked (with the fresh error), never lost.
1596
+ Future<nomos_protocol.DeadLetterRetry> retryDeadLetter(
1597
+ String id, {
1598
+ String? session,
1599
+ }) async {
1600
+ final response = await _invokeRuntimeV2(
1601
+ nomos_protocol.RuntimeRequest(
1602
+ deadLetter: nomos_protocol.DeadLetterRequest(
1603
+ session: _runtimeSession(session),
1604
+ retry: nomos_protocol.RetryDeadLetterRequest(intentId: id),
1605
+ ),
1606
+ ),
1607
+ );
1608
+ if (!response.hasDeadLetter() || !response.deadLetter.hasRetry()) {
1609
+ throw StateError(
1610
+ 'Runtime v2 returned ${response.whichOutcome().name} for retryDeadLetter',
1611
+ );
1612
+ }
1613
+ return response.deadLetter.retry;
1614
+ }
1615
+
1616
+ /// Discard one dead letter — the app's EXPLICIT choice to drop the work. `{ok}`.
1617
+ Future<nomos_protocol.DeadLetterDiscard> discardDeadLetter(
1618
+ String id, {
1619
+ String? session,
1620
+ }) async {
1621
+ final response = await _invokeRuntimeV2(
1622
+ nomos_protocol.RuntimeRequest(
1623
+ deadLetter: nomos_protocol.DeadLetterRequest(
1624
+ session: _runtimeSession(session),
1625
+ discard: nomos_protocol.DiscardDeadLetterRequest(intentId: id),
1626
+ ),
1627
+ ),
1628
+ );
1629
+ if (!response.hasDeadLetter() || !response.deadLetter.hasDiscard()) {
1630
+ throw StateError(
1631
+ 'Runtime v2 returned ${response.whichOutcome().name} for discardDeadLetter',
1632
+ );
1633
+ }
1634
+ return response.deadLetter.discard;
1635
+ }
1636
+
1637
+ /// The existing offer operation carrying one exact compiler-generated
1638
+ /// Protobuf message. This is not a second write endpoint: generated domain
1639
+ /// methods use it to reach the same kernel [OfferRequest] without a generic
1640
+ /// map or JavaScript serializer. The kernel alone authors canonical USDA.
1641
+ NomosErrorInfo _offerFailureInfo(
1642
+ nomos_protocol.CallResponse response,
1643
+ String workspace,
1644
+ NmsGeneratedCall call,
1645
+ ) =>
1646
+ NomosErrorInfo(
1647
+ code: response.failure.code,
1648
+ message: response.failure.message,
1649
+ component: 'nomos.kernel',
1650
+ workspace: workspace,
1651
+ domain: call.domain,
1652
+ directiveId: call.directive,
1653
+ operation: call.directive,
1654
+ );
1655
+
1656
+ /// The kernel's author gate reports an unenrolled child signer as an offer
1657
+ /// refusal. Only that exact verified-key verdict may enter the parent's
1658
+ /// existing repair lane; other forged-principal refusals remain terminal.
1659
+ ParentSignerEnrollmentMissing? _missingChildSignerFromOffer(
1660
+ nomos_protocol.CallResponse response,
1661
+ String workspace,
1662
+ NmsGeneratedCall call,
1663
+ ) {
1664
+ if (!response.hasOffer() || !response.offer.hasRefused()) return null;
1665
+ final refusal = response.offer.refused.refusal;
1666
+ if (refusal.code !=
1667
+ nomos_protocol
1668
+ .KernelRefusalCode.KERNEL_REFUSAL_CODE_FORGED_PRINCIPAL ||
1669
+ !refusal.hasDetails() ||
1670
+ refusal.details.fields['actor']?.text != 'unenrolled-signer-key') {
1671
+ return null;
1672
+ }
1673
+ return ParentSignerEnrollmentMissing(
1674
+ message: refusal.message,
1675
+ workspace: workspace,
1676
+ actor: refusal.details.fields['claimed']?.text,
1677
+ directiveId: call.directive,
1678
+ );
1679
+ }
1680
+
1681
+ @internal
1682
+ Future<NomosOfferOutcome> offerGeneratedCallOutcome({
1683
+ required NmsGeneratedCall call,
1684
+ String? actor,
1685
+ String? domainHash,
1686
+ String? session,
1687
+ String? authorSecret,
1688
+ NomosBirthCertificate? birthCertificate,
1689
+ Iterable<NomosConflictSnapshot> conflictSnapshots = const [],
1690
+ NomosOfferAuthenticationProof authenticationProof =
1691
+ NomosOfferAuthenticationProof.sessionCredentials,
1692
+ }) async {
1693
+ final runtimeSession = session ?? _sessionName;
1694
+ if (runtimeSession == null || runtimeSession.isEmpty) {
1695
+ throw StateError(
1696
+ 'A typed offer must be bound to a NomosSession',
1697
+ );
1698
+ }
1699
+ final transport = _transport;
1700
+ if (transport is! NomosKernelCallTransport) {
1701
+ throw UnsupportedError(
1702
+ 'This host does not implement the generated protobuf kernel lane',
1703
+ );
1704
+ }
1705
+ final kernelTransport = transport as NomosKernelCallTransport;
1706
+ final plane = _eventPlane;
1707
+ final workspace = _workspaceForSession(runtimeSession);
1708
+ final explicitSecret = authorSecret;
1709
+ final credentialUse = explicitSecret != null
1710
+ ? nomos_protocol.CredentialUse.CREDENTIAL_USE_CALL_PROVIDED
1711
+ : switch (authenticationProof) {
1712
+ NomosOfferAuthenticationProof.sessionCredentials =>
1713
+ nomos_protocol.CredentialUse.CREDENTIAL_USE_SESSION,
1714
+ NomosOfferAuthenticationProof.enrolledDevice =>
1715
+ nomos_protocol.CredentialUse.CREDENTIAL_USE_ENROLLED_DEVICE,
1716
+ };
1717
+ // Intentionally visible in device logs: this is the OBD port for a failed
1718
+ // lawful dispatch. It names no token, device secret or tenant payload.
1719
+ // ignore: avoid_print
1720
+ print('[nomos-offer] dispatch ${call.domain}/${call.directive} in '
1721
+ '$workspace using ${authenticationProof.name}');
1722
+ Future<nomos_protocol.CallResponse> attempt() {
1723
+ final requestNumber = ++plane._sequences.request;
1724
+ return kernelTransport.callKernel(
1725
+ session: runtimeSession,
1726
+ request: nomos_protocol.CallRequest(
1727
+ protocolVersion: 1,
1728
+ requestId: '$runtimeSession:offer:$requestNumber',
1729
+ offer: nomos_protocol.OfferRequest(
1730
+ call: nomos_protocol.DirectiveCall(
1731
+ domain: call.domain,
1732
+ directive: call.directive,
1733
+ operation: google_protobuf.Any(
1734
+ typeUrl: call.protobufType,
1735
+ value: call.protobufPayload,
1736
+ ),
1737
+ ),
1738
+ authoring: nomos_protocol.AuthoringOptions(
1739
+ actor: actor ?? '',
1740
+ // Ordinary generated calls select a peer-runtime credential
1741
+ // capability below. An explicit secret is a temporary operator
1742
+ // compatibility lane and remains client-supplied until removed.
1743
+ authorSecret: explicitSecret == null
1744
+ ? const <int>[]
1745
+ : base64Decode(explicitSecret),
1746
+ deferProjection: true,
1747
+ spans: true,
1748
+ ),
1749
+ controllerLawHash: domainHash,
1750
+ conflictSnapshots: conflictSnapshots,
1751
+ birthCertificate: birthCertificate,
1752
+ ),
1753
+ ),
1754
+ credentialUse: credentialUse,
1755
+ );
1756
+ }
1757
+
1758
+ var response = await attempt();
1759
+ if (response.hasFailure() &&
1760
+ response.failure.code == 'nomos.authz.signer_enrollment_missing') {
1761
+ final refusal = nomosErrorFromInfo(
1762
+ _offerFailureInfo(response, workspace, call),
1763
+ );
1764
+ if (refusal is ParentSignerEnrollmentMissing &&
1765
+ await _repairChildAuthentication(workspace, refusal)) {
1766
+ response = await attempt();
1767
+ }
1768
+ }
1769
+ final childSignerRefusal =
1770
+ _missingChildSignerFromOffer(response, workspace, call);
1771
+ if (childSignerRefusal != null &&
1772
+ await _repairChildAuthentication(workspace, childSignerRefusal)) {
1773
+ response = await attempt();
1774
+ }
1775
+ if (response.hasFailure()) {
1776
+ // ignore: avoid_print
1777
+ print('[nomos-offer] refused ${call.domain}/${call.directive} in '
1778
+ '$workspace: ${response.failure.code}: ${response.failure.message}');
1779
+ if (response.failure.code.startsWith('nomos.auth.') ||
1780
+ response.failure.code.startsWith('nomos.authz.')) {
1781
+ throw nomosErrorFromInfo(_offerFailureInfo(response, workspace, call));
1782
+ }
1783
+ throw NomosKernelCallFailure(
1784
+ code: response.failure.code,
1785
+ message: response.failure.message,
1786
+ );
1787
+ }
1788
+ if (!response.hasOffer()) {
1789
+ throw StateError(
1790
+ 'Kernel returned ${response.whichOutcome().name} for an offer request',
1791
+ );
1792
+ }
1793
+ final offer = response.offer;
1794
+ if (!offer.hasAdmitted()) throw NomosTypedOfferFailure(offer);
1795
+ final admitted = offer.admitted;
1796
+ if (admitted.hasKernelSpans()) {
1797
+ for (final span in admitted.kernelSpans.operations) {
1798
+ final phases = span.phases
1799
+ .map((phase) => '${phase.name}=${phase.value}us')
1800
+ .join(',');
1801
+ final notes =
1802
+ span.notes.map((note) => '${note.name}=${note.value}').join(',');
1803
+ // Deliberately emitted as diagnostics rather than exposed as application
1804
+ // state. This is the native OBD port for finding work that escaped the
1805
+ // local-first foreground budget without inventing another wire format.
1806
+ // ignore: avoid_print
1807
+ print('[nomos-kernel-perf] ${span.label} total=${span.totalUs}us '
1808
+ 'phases={$phases} notes={$notes}');
1809
+ }
1810
+ }
1811
+ // ignore: avoid_print
1812
+ print('[nomos-offer] admitted ${call.domain}/${call.directive} in '
1813
+ '$workspace as ${admitted.intentId} at ${admitted.head}');
1814
+ final birthOutcomes = List<NomosBirthOutcome>.unmodifiable(
1815
+ admitted.birthOutcomes,
1816
+ );
1817
+ final failedBirths = birthOutcomes
1818
+ .where(
1819
+ (birth) =>
1820
+ birth.status != NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN,
1821
+ )
1822
+ .toList(growable: false);
1823
+ if (failedBirths.isNotEmpty) {
1824
+ throw BirthIncomplete(failedBirths, head: admitted.head);
1825
+ }
1826
+ final receipt =
1827
+ _registerOperationReceipt(runtimeSession, admitted.intentId);
1828
+ final born = admitted.born
1829
+ .map((child) => child.workspace)
1830
+ .where((child) => child.isNotEmpty)
1831
+ .toList(growable: false);
1832
+ for (final child in born) {
1833
+ plane._receipts.rememberBirth(child, receipt);
1834
+ }
1835
+ return NomosOfferOutcome(
1836
+ head: admitted.head,
1837
+ intentId: admitted.intentId,
1838
+ receipt: receipt,
1839
+ workspace: workspace,
1840
+ born: born,
1841
+ birthOutcomes: birthOutcomes,
1842
+ created: admitted.created
1843
+ .map(
1844
+ (created) => NomosCreated(
1845
+ type: created.type,
1846
+ id: created.id,
1847
+ workspace: workspace,
1848
+ ),
1849
+ )
1850
+ .toList(growable: false),
1851
+ affected: admitted.affected
1852
+ .map(
1853
+ (affected) => NomosCreated(
1854
+ type: affected.type,
1855
+ id: affected.id,
1856
+ workspace: workspace,
1857
+ ),
1858
+ )
1859
+ .toList(growable: false),
1860
+ );
1861
+ }
1862
+
1863
+ /// Prepare the exact authenticated execution slice a generated client is about to use. This is
1864
+ /// a readiness hint only: compilation, composition, cache custody and all trust decisions remain
1865
+ /// inside the kernel. Generated `bind` calls it so first-use cost is paid before the client becomes
1866
+ /// interactive, rather than on the person's first button press.
1867
+ Future<void> prepareDomain({
1868
+ required String domain,
1869
+ String? directiveId,
1870
+ required String packageHash,
1871
+ }) async {
1872
+ final response = await _invokeRuntimeV2(
1873
+ nomos_protocol.RuntimeRequest(
1874
+ prepareDomain: nomos_protocol.PrepareDomainRequest(
1875
+ session: _runtimeSession(),
1876
+ domain: domain,
1877
+ packageSha256: packageHash,
1878
+ directive: directiveId,
1879
+ ),
1880
+ ),
1881
+ );
1882
+ if (!response.hasDomainPrepared()) {
1883
+ throw StateError(
1884
+ 'Runtime v2 returned ${response.whichOutcome().name} for prepareDomain',
1885
+ );
1886
+ }
1887
+ }
1888
+
1889
+ /// FOLLOW A [NomosRef] — bind the referenced workspace to an interaction
1890
+ /// session and hand back its stable language-native handle. No custody or
1891
+ /// replica lifecycle is started by this method. Typed client construction is:
1892
+ /// `final estate = await engine.follow(outcome.bornRefs.single); final app = await EstateClient.bind(estate);`
1893
+ /// No loose `{workspaceId, aggregateId}` string pairs in app code.
1894
+ Future<NomosSession> follow<TAggregate>(
1895
+ NomosRef<TAggregate> ref, {
1896
+ String? credentialSourceSession,
1897
+ }) async {
1898
+ final ws = NmsInternalNomosRefs.workspace(ref);
1899
+ if (ws.isEmpty) {
1900
+ throw ArgumentError.value(
1901
+ ref,
1902
+ 'ref',
1903
+ 'follow: the NomosRef carries no workspace — only a workspace-complete ref is followable',
1904
+ );
1905
+ }
1906
+ final source = credentialSourceSession ?? _sessionName;
1907
+ if (source != null && source.isNotEmpty) {
1908
+ return bindWorkspace(
1909
+ ws,
1910
+ credentialSourceSession: source,
1911
+ );
1912
+ }
1913
+ if (!_eventPlane._sessions.isBound(ws)) {
1914
+ await bindSession(
1915
+ workspace: ws,
1916
+ session: ws,
1917
+ );
1918
+ }
1919
+ return session(ws);
1920
+ }
1921
+
1922
+ /// Bind another workspace interaction. Placement and Replica Follow remain
1923
+ /// entirely peer-owned and are evaluated only when an operation is executed.
1924
+ ///
1925
+ Future<NomosSession> bindWorkspace(
1926
+ String workspace, {
1927
+ String? credentialSourceSession,
1928
+ }) async {
1929
+ if (!_eventPlane._sessions.isBound(workspace)) {
1930
+ await bindSession(
1931
+ workspace: workspace,
1932
+ session: workspace,
1933
+ credentialSourceSession: credentialSourceSession ?? _sessionName,
1934
+ );
1935
+ }
1936
+ return session(workspace);
1937
+ }
1938
+
1939
+ /// Reserve a KERNEL-MINTED typed id (`<TypeTag>_<uuidv7>`) via the holon's front-door mint — for app-side
1940
+ /// references (routing keys, optimistic UI). `.creates` payloads still carry NO id: the LAW mints those
1941
+ /// (see [offerCreates], which returns them).
1942
+ Future<String> mintId(String typeTag) async =>
1943
+ (await _kernelPrimitives().mintTypedId(
1944
+ typeTag: typeTag,
1945
+ capturedUnixMillis: DateTime.now().millisecondsSinceEpoch,
1946
+ ))
1947
+ .id;
1948
+
1949
+ /// Refresh client-held credentials on this already-open workspace without
1950
+ /// replacing its resident holon or refolding custody. This grants no
1951
+ /// authority: every offer is still judged by the kernel's author gate.
1952
+ Future<void> refreshCredentials({
1953
+ String? authToken,
1954
+ String? authorSecret,
1955
+ }) async {
1956
+ if (authToken == null && authorSecret == null) return;
1957
+ final response = await _invokeRuntimeV2(
1958
+ nomos_protocol.RuntimeRequest(
1959
+ refreshCredentials: nomos_protocol.RefreshCredentialsRequest(
1960
+ session: _runtimeSession(),
1961
+ authToken: authToken == null ? null : utf8.encode(authToken),
1962
+ authorSecret:
1963
+ authorSecret == null ? null : base64Decode(authorSecret),
1964
+ ),
1965
+ ),
1966
+ );
1967
+ if (!response.hasCredentialsRefreshed()) {
1968
+ throw StateError(
1969
+ 'Runtime v2 returned ${response.whichOutcome().name} for credential refresh',
1970
+ );
1971
+ }
1972
+ }
1973
+
1974
+ /// @internal The immutable genesis of a sibling custody chain. This is observational metadata used to
1975
+ /// compose an ordinary source-enrolment offer; the kernel still reopens and proves every foreign read.
1976
+ Future<String> nomosWorkspaceGenesis(
1977
+ String workspace, {
1978
+ String? session,
1979
+ }) async {
1980
+ final response = await _invokeRuntimeV2(
1981
+ nomos_protocol.RuntimeRequest(
1982
+ readWorkspaceGenesis: nomos_protocol.ReadWorkspaceGenesisRequest(
1983
+ session: _runtimeSession(session),
1984
+ workspace: workspace,
1985
+ ),
1986
+ ),
1987
+ );
1988
+ if (!response.hasWorkspaceGenesis()) {
1989
+ throw StateError(
1990
+ 'Runtime v2 returned ${response.whichOutcome().name} for workspace genesis',
1991
+ );
1992
+ }
1993
+ return response.workspaceGenesis.genesis;
1994
+ }
1995
+
1996
+ /// Carry the generated law's OSGi Contract Namespace requirements to THE DESK
1997
+ /// THAT OWNS COMPATIBILITY, and raise its verdict.
1998
+ ///
1999
+ /// Whether this kernel provides a required contract is a compatibility
2000
+ /// judgement, and compatibility has one owner (`CompatibilityDesk`). Comparing
2001
+ /// the kernel's `OsgiResourceResponse` attribute by attribute here made this
2002
+ /// client a second compatibility engine — and the TypeScript client's
2003
+ /// equivalent was an empty method, so the two disagreed about the same law.
2004
+ /// Reception now transports the requirements and renders the answer.
2005
+ Future<void> assertKernelCoherence(
2006
+ List<NmsKernelContractRequirement> kernelContracts, {
2007
+ String? lawHash,
2008
+ String? session,
2009
+ }) async {
2010
+ if (kernelContracts.isEmpty) return;
2011
+ final runtimeSession = _runtimeSession(session);
2012
+ final response = await _invokeRuntimeV2(
2013
+ nomos_protocol.RuntimeRequest(
2014
+ framework: nomos_protocol.FrameworkRequest(
2015
+ session: runtimeSession,
2016
+ checkKernelContracts: nomos_protocol.CheckKernelContractsRequest(
2017
+ session: runtimeSession,
2018
+ requirements: kernelContracts.map(
2019
+ (requirement) => nomos_protocol.KernelContractRequirement(
2020
+ namespace: requirement.namespace,
2021
+ name: requirement.name,
2022
+ version: requirement.version,
2023
+ ),
2024
+ ),
2025
+ lawHash: lawHash,
2026
+ ),
2027
+ ),
2028
+ ),
2029
+ );
2030
+ if (!response.hasFramework() || !response.framework.hasKernelContracts()) {
2031
+ throw StateError(
2032
+ 'Runtime v2 returned ${response.whichOutcome().name} for the kernel contract verdict',
2033
+ );
2034
+ }
2035
+ final verdict = response.framework.kernelContracts;
2036
+ if (verdict.coherent) return;
2037
+ throw NomosKernelContractMismatch(
2038
+ verdict.missing
2039
+ .map((requirement) => NmsKernelContractRequirement(
2040
+ namespace: requirement.namespace,
2041
+ name: requirement.name,
2042
+ version: requirement.version,
2043
+ ))
2044
+ .toList(growable: false),
2045
+ lawHash: verdict.hasLawHash() ? verdict.lawHash : lawHash,
2046
+ );
2047
+ }
2048
+
2049
+ /// Framework-private organisational read used by generated `describe()`. The runner joins the parent
2050
+ /// ledger's verified birth witnesses and lifecycle facts; tenant code only receives safe child links.
2051
+ Future<List<nomos_protocol.ChildWorkspace>> nomosFrameworkChildWorkspaces(
2052
+ {String? namespace}) async {
2053
+ final response = await _invokeRuntimeV2(
2054
+ nomos_protocol.RuntimeRequest(
2055
+ listChildWorkspaces: nomos_protocol.ListChildWorkspacesRequest(
2056
+ session: _runtimeSession(),
2057
+ namespace: namespace,
2058
+ ),
2059
+ ),
2060
+ );
2061
+ if (!response.hasChildWorkspaces()) {
2062
+ throw StateError(
2063
+ 'Runtime v2 returned ${response.whichOutcome().name} for child workspaces',
2064
+ );
2065
+ }
2066
+ return List.unmodifiable(response.childWorkspaces.children);
2067
+ }
2068
+
2069
+ /// Mint an ed25519 DEVICE KEY in-kernel → `{secret, public}` (base64). The SECRET stays on this device
2070
+ /// (persist it in secure storage); pass it to [bindSession] as `authorSecret` (or per-offer `signWith`) so the
2071
+ /// holon SIGNS each intent. The PUBLIC half is what gets enrolled so the gate re-verifies the signature on
2072
+ /// replay — the durable, cryptographic answer to "who authored this?" (the warrant). Mint once per device.
2073
+ Future<nomos_protocol.SigningKeyPair> authorKeygen() async =>
2074
+ _signingIdentityKernel().generate(
2075
+ requestId:
2076
+ '${_runtimeSession()}:signing-key:${++_eventPlane._sequences.request}',
2077
+ );
2078
+
2079
+ /// VA ROOT-OF-TRUST: sign a BirthCert through the kernel `cert_sign` op (the SINGLE source of the cert
2080
+ /// byte-layout — no Dart copy). [spec] is the generated certificate message with an empty signature;
2081
+ /// `deviceSecret` is the secret half, handed to the LOCAL kernel only (it never reaches a remote peer).
2082
+ /// Returns the same generated type with its signature populated, suitable for an offer.
2083
+ Future<NomosBirthCertificate> signBirthCert(
2084
+ NomosBirthCertificate spec,
2085
+ List<int> deviceSecret,
2086
+ ) async {
2087
+ final runtimeSession = _runtimeSession();
2088
+ final transport = _eventPlane._transport;
2089
+ if (transport is! NomosKernelCallTransport) {
2090
+ throw UnsupportedError(
2091
+ 'This peer does not expose generated kernel calls',
2092
+ );
2093
+ }
2094
+ final requestId =
2095
+ '$runtimeSession:sign-birth:${++_eventPlane._sequences.request}';
2096
+ final response = await (transport as NomosKernelCallTransport).callKernel(
2097
+ session: runtimeSession,
2098
+ credentialUse: nomos_protocol.CredentialUse.CREDENTIAL_USE_NONE,
2099
+ request: nomos_protocol.CallRequest(
2100
+ protocolVersion: 1,
2101
+ requestId: requestId,
2102
+ signBirthCertificate: nomos_protocol.SignBirthCertificateRequest(
2103
+ certificate: spec,
2104
+ signerSecret: deviceSecret,
2105
+ ),
2106
+ ),
2107
+ );
2108
+ if (response.hasFailure()) {
2109
+ throw StateError('${response.failure.code}: ${response.failure.message}');
2110
+ }
2111
+ if (!response.hasSignBirthCertificate()) {
2112
+ throw StateError(
2113
+ 'Kernel returned ${response.whichOutcome().name} for birth-certificate signing',
2114
+ );
2115
+ }
2116
+ return response.signBirthCertificate.certificate;
2117
+ }
2118
+
2119
+ /// Compose and sign the enroll-once delegation using exact generated
2120
+ /// fields. This is ordinary certificate construction followed by the
2121
+ /// Runtime v2 signing operation; there is no separate command protocol.
2122
+ Future<NomosBirthCertificate> mintKeylessDelegation({
2123
+ required String user,
2124
+ required List<int> devicePublic,
2125
+ required String childName,
2126
+ required String parentId,
2127
+ required String verifiedVia,
2128
+ required String idpToken,
2129
+ required int capturedClock,
2130
+ int epoch = 0,
2131
+ required List<int> deviceSecret,
2132
+ }) =>
2133
+ signBirthCert(
2134
+ nomos_protocol.BirthCertificate(
2135
+ child: childName.isEmpty ? user : childName,
2136
+ founder: user,
2137
+ verified: true,
2138
+ parentKey: devicePublic,
2139
+ parentId: parentId,
2140
+ epoch: fixnum.Int64(epoch),
2141
+ verifiedVia: verifiedVia,
2142
+ idpToken: idpToken,
2143
+ capturedClock: fixnum.Int64(capturedClock),
2144
+ ),
2145
+ deviceSecret,
2146
+ );
2147
+
2148
+ /// Mint the child's OWN birth certificate for a Home birth: a plain device-signed cert
2149
+ /// `{child, founder, verified, parentKey: P_U, parentId: platform, epoch}` with NO keyless fields.
2150
+ /// The keyless delegation ([mintKeylessDelegation]) rides the chain as its own link above this one;
2151
+ /// passing the delegation itself as the birth certificate stores the chain as
2152
+ /// `[delegation, delegation, platform]`, and every grandchild the Home births then seats a keyless
2153
+ /// link under a keyless link — a shape the kernel's verify walk refuses.
2154
+ Future<NomosBirthCertificate> mintHomeBirthCertificate({
2155
+ required String user,
2156
+ required List<int> devicePublic,
2157
+ required String childName,
2158
+ required String parentId,
2159
+ int epoch = 0,
2160
+ required List<int> deviceSecret,
2161
+ }) =>
2162
+ signBirthCert(
2163
+ nomos_protocol.BirthCertificate(
2164
+ child: childName,
2165
+ founder: user,
2166
+ verified: true,
2167
+ parentKey: devicePublic,
2168
+ parentId: parentId,
2169
+ epoch: fixnum.Int64(epoch),
2170
+ ),
2171
+ deviceSecret,
2172
+ );
2173
+
2174
+ /// Derive the device key's base64 PUBLIC half from its base64 SECRET seed, inside the LOCAL kernel
2175
+ /// (the secret never leaves the device). The recovery lanes use this when only the secret was
2176
+ /// persisted (the device-key store keeps just the seed), including low-level device-bound enrollment.
2177
+ Future<nomos_protocol.SigningPublicKey> devicePublicOf(
2178
+ Uint8List secret,
2179
+ ) async =>
2180
+ _signingIdentityKernel().derivePublic(
2181
+ requestId:
2182
+ '${_runtimeSession()}:signing-public:${++_eventPlane._sequences.request}',
2183
+ secret: secret,
2184
+ );
2185
+
2186
+ NmsSigningIdentityKernel _signingIdentityKernel() {
2187
+ final transport = _eventPlane._transport;
2188
+ if (transport is! NomosKernelCallTransport) {
2189
+ throw UnsupportedError(
2190
+ 'This peer does not expose generated kernel calls',
2191
+ );
2192
+ }
2193
+ return NmsSigningIdentityKernel(
2194
+ transport: transport as NomosKernelCallTransport,
2195
+ session: _runtimeSession(),
2196
+ );
2197
+ }
2198
+
2199
+ /// PARENT-AUTHORITY POSTURE (read-only diagnostic): is this device key a LIVE `signer` for
2200
+ /// `principal` on `parent`'s chain (`identity:<principal>#signer@key:<sha256(publicKey)>`)? One O(1)
2201
+ /// relation read over the open gateway; nothing is authored. Returns
2202
+ /// `{parent, principal, keyHash, object, relation, enrolled, revoked, checkedAt}`.
2203
+ /// Pass `remote: true` to force the CLOUD-gateway read even when this holon IS the parent — the
2204
+ /// seated barrier's proof-by-read: `enrolled: true` then means the shared chain durably holds this
2205
+ /// device's signer tuple, not merely that the local fold ran.
2206
+ Future<nomos_protocol.ParentSignerEnrollment> parentSignerEnrollment({
2207
+ required String parent,
2208
+ required String principal,
2209
+ String? keyHash,
2210
+ String? publicKey,
2211
+ String? authToken,
2212
+ bool remote = false,
2213
+ }) async {
2214
+ final response = await _invokeFramework(
2215
+ nomos_protocol.FrameworkRequest(
2216
+ session: _runtimeSession(),
2217
+ parentSignerEnrollment: nomos_protocol.ParentSignerEnrollmentRequest(
2218
+ parent: parent,
2219
+ principal: principal,
2220
+ keySha256: keyHash,
2221
+ publicKey: publicKey == null ? null : base64Decode(publicKey),
2222
+ authToken: authToken == null ? null : utf8.encode(authToken),
2223
+ remote: remote,
2224
+ ),
2225
+ ),
2226
+ );
2227
+ if (!response.hasParentSignerEnrollment()) {
2228
+ throw StateError(
2229
+ 'Runtime v2 returned ${response.whichResult().name} for parent signer posture',
2230
+ );
2231
+ }
2232
+ return response.parentSignerEnrollment;
2233
+ }
2234
+
2235
+ /// IDEMPOTENT PARENT-SIGNER RECOVERY — ensure this device key is a live `signer` on `parent`,
2236
+ /// lawfully restoring the tuple a parent custody REBIRTH lost (the device key + the born home stay
2237
+ /// valid; no home rebirth, no hand-posted secrets). Already enrolled ⇒ pure no-op (`repaired:false`).
2238
+ /// Absent ⇒ re-authors the enrollment via the parent's OWN installed signing directive, DISCOVERED
2239
+ /// from its law (never a hardcoded framework domain — a tenant-composed `co2_platform` works);
2240
+ /// `domain`/`directiveId` override the discovery. A REVOKED key is never silently re-enrolled.
2241
+ /// TWO LAWFUL LANES, kernel-decided: a WARRANTED parent (the reborn-platform case) admits a fresh
2242
+ /// device-signer only via the keyless `enrollFirstDevice` — pass the device-bound [idToken]
2243
+ /// (+ [verifiedVia], the parent's AuthProvider issuer); a PRE-WARRANT parent admits the founding
2244
+ /// `enrollSigner` (no token needed).
2245
+ /// Returns `{parent, principal, keyHash, enrolled, repaired, domain?, directiveId?, checkedAt}`.
2246
+ Future<nomos_protocol.EnsureParentSignerEnrollmentResult>
2247
+ ensureParentSignerEnrollment({
2248
+ required String parent,
2249
+ required String principal,
2250
+ String? publicKey,
2251
+ String? authorSecret,
2252
+ String? domain,
2253
+ String? directiveId,
2254
+ String? authToken,
2255
+ String? idToken,
2256
+ String? verifiedVia,
2257
+ }) async {
2258
+ final response = await _invokeFramework(
2259
+ nomos_protocol.FrameworkRequest(
2260
+ session: _runtimeSession(),
2261
+ repairParentSignerEnrollment:
2262
+ nomos_protocol.EnsureParentSignerEnrollmentRequest(
2263
+ parent: parent,
2264
+ principal: principal,
2265
+ publicKey: publicKey == null ? null : base64Decode(publicKey),
2266
+ authorSecret:
2267
+ authorSecret == null ? null : base64Decode(authorSecret),
2268
+ domain: domain,
2269
+ directive: directiveId,
2270
+ authToken: authToken == null ? null : utf8.encode(authToken),
2271
+ idToken: idToken == null ? null : utf8.encode(idToken),
2272
+ verifiedVia: verifiedVia,
2273
+ ),
2274
+ ),
2275
+ );
2276
+ if (!response.hasRepairedParentSignerEnrollment()) {
2277
+ throw StateError(
2278
+ 'Runtime v2 returned ${response.whichResult().name} for parent signer repair',
2279
+ );
2280
+ }
2281
+ return response.repairedParentSignerEnrollment;
2282
+ }
2283
+
2284
+ /// Read-only, exact Runtime v2 authoring preflight. [ws] may select a
2285
+ /// colocated or remote target; live credentials stay in the peer session.
2286
+ Future<NmsAuthoringPreflight> canAuthor(
2287
+ String directiveId, {
2288
+ String? ws,
2289
+ String? principal,
2290
+ String? publicKey,
2291
+ String? keyHash,
2292
+ }) async {
2293
+ final response = await _invokeRuntimeV2(
2294
+ nomos_protocol.RuntimeRequest(
2295
+ authoringPreflight: nomos_protocol.AuthoringPreflightRequest(
2296
+ session: _runtimeSession(),
2297
+ directive: directiveId,
2298
+ targetWorkspace: ws,
2299
+ principal: principal,
2300
+ publicKey: publicKey == null ? null : base64Decode(publicKey),
2301
+ keyHash: keyHash,
2302
+ ),
2303
+ ),
2304
+ );
2305
+ if (!response.hasAuthoringPreflight()) {
2306
+ throw StateError(
2307
+ 'Runtime v2 returned ${response.whichOutcome().name} for authoring preflight',
2308
+ );
2309
+ }
2310
+ final result = response.authoringPreflight;
2311
+ return NmsAuthoringPreflight(
2312
+ admissible: result.admissible,
2313
+ workspace: result.workspace,
2314
+ directive: result.directive,
2315
+ cause: result.cause,
2316
+ causes: List.unmodifiable(result.causes),
2317
+ actor: result.hasActor() ? result.actor : null,
2318
+ keyHash: result.hasKeyHash() ? result.keyHash : null,
2319
+ domain: result.hasDomain() ? result.domain : null,
2320
+ relation: result.hasRelation() ? result.relation : null,
2321
+ object: result.hasObject() ? result.object : null,
2322
+ detail: result.hasDetail() ? result.detail : null,
2323
+ );
2324
+ }
2325
+
2326
+ /// VERIFY_CHAIN — the kernel's from-genesis self-verification of THIS holon's folded ledger (read-only:
2327
+ /// contiguity → every intent re-admitted through the ONE gate against the state folded from the prefix). For
2328
+ /// a WARRANTED home this RE-FIRES the parent-attested-birth cert door on the cold-replay lane — so a
2329
+ /// freshly-connected peer that CLONES a home and runs this IS the offline peer-verify (replay IS validation;
2330
+ /// no new op). Returns the kernel verdict `{valid, error?, plansRerun?}`.
2331
+ Future<nomos_protocol.VerifyWorkspaceChainResult> verifyChain() async {
2332
+ final response = await _invokeFramework(
2333
+ nomos_protocol.FrameworkRequest(
2334
+ session: _runtimeSession(),
2335
+ verifyWorkspaceChain: nomos_protocol.VerifyWorkspaceChainRequest(),
2336
+ ),
2337
+ );
2338
+ if (!response.hasVerifyWorkspaceChain()) {
2339
+ throw StateError(
2340
+ 'Runtime v2 returned ${response.whichResult().name} for chain verification',
2341
+ );
2342
+ }
2343
+ return response.verifyWorkspaceChain;
2344
+ }
2345
+
2346
+ /// Offer one compiler-generated call to a remote workspace without cloning
2347
+ /// that workspace onto this device. The resident source session signs the
2348
+ /// target-kernel-prepared USDA; Runtime v2 carries the nominal call and the
2349
+ /// admitted target head without a command string or tenant map.
2350
+ Future<nomos_protocol.RemoteGeneratedOfferAccepted>
2351
+ nomosInternalOfferRemoteGeneratedCall({
2352
+ required String targetWorkspace,
2353
+ required String actor,
2354
+ required String authorSecret,
2355
+ String? authToken,
2356
+ required NmsGeneratedCall call,
2357
+ NomosBirthCertificate? birthCertificate,
2358
+ String? requiredTargetHead,
2359
+ }) async {
2360
+ final response = await _invokeRuntimeV2(
2361
+ nomos_protocol.RuntimeRequest(
2362
+ remoteGeneratedOffer: nomos_protocol.RemoteGeneratedOfferRequest(
2363
+ sourceSession: _runtimeSession(),
2364
+ targetWorkspace: targetWorkspace,
2365
+ actor: actor,
2366
+ authorSecret: base64Decode(authorSecret),
2367
+ authToken: utf8.encode(authToken ?? ''),
2368
+ call: nomos_protocol.DirectiveCall(
2369
+ domain: call.domain,
2370
+ directive: call.directive,
2371
+ operation: google_protobuf.Any(
2372
+ typeUrl: call.protobufType,
2373
+ value: call.protobufPayload,
2374
+ ),
2375
+ ),
2376
+ birthCertificate: birthCertificate,
2377
+ requiredTargetHead: requiredTargetHead,
2378
+ ),
2379
+ ),
2380
+ );
2381
+ if (!response.hasRemoteGeneratedOffer()) {
2382
+ throw StateError(
2383
+ 'Runtime v2 returned ${response.whichOutcome().name} for a remote generated offer',
2384
+ );
2385
+ }
2386
+ return response.remoteGeneratedOffer;
2387
+ }
2388
+
2389
+ nomos_protocol.QueryAuthority _nmsQueryAuthority(
2390
+ List<NomosScopeKey>? keys,
2391
+ String? principal,
2392
+ String? session, {
2393
+ String? authorSecret,
2394
+ }) {
2395
+ final runtimeSession = session ?? _sessionName;
2396
+ if (runtimeSession == null || runtimeSession.isEmpty) {
2397
+ throw StateError('A nominal query must be bound to a NomosSession');
2398
+ }
2399
+ return nomos_protocol.QueryAuthority(
2400
+ principalClaim: principal ?? '',
2401
+ // The runtime supplies the live session token, device capability and
2402
+ // observation time immediately before dispatch. Only explicit operator
2403
+ // overrides remain embedded during the compatibility cutover.
2404
+ authorSecret:
2405
+ authorSecret == null ? const <int>[] : base64Decode(authorSecret),
2406
+ keys: (keys ?? const <NomosScopeKey>[]).map(
2407
+ (key) => nomos_protocol.InjectedKey(
2408
+ scope: key.scope,
2409
+ epoch: fixnum.Int64(key.epoch),
2410
+ key: base64Decode(key.key),
2411
+ ),
2412
+ ),
2413
+ );
2414
+ }
2415
+
2416
+ Future<Uint8List> _nmsKernelQueryUsda({
2417
+ required String? session,
2418
+ required nomos_protocol.QueryAuthority authority,
2419
+ required nomos_protocol.QueryRequest query,
2420
+ }) async {
2421
+ final elapsed = Stopwatch()..start();
2422
+ // ignore: avoid_print
2423
+ print(
2424
+ '[nomos-local-read] begin session=${session ?? _sessionName ?? ''} '
2425
+ 'query=${query.hasRows() ? query.rows.id : query.whichQuery().name}',
2426
+ );
2427
+ late final nomos_protocol.QueryResponse response;
2428
+ try {
2429
+ response = await _nmsKernelQuery(
2430
+ session: session,
2431
+ authority: authority,
2432
+ query: query,
2433
+ );
2434
+ } catch (error, stackTrace) {
2435
+ elapsed.stop();
2436
+ // Keep local-read refusals in ordinary device logs. Generated streams
2437
+ // may deliver errors without a visible widget, so developer.log alone
2438
+ // is not an adequate operator diagnostic on iOS.
2439
+ // ignore: avoid_print
2440
+ print(
2441
+ '[nomos-local-read] failed '
2442
+ 'session=${session ?? _sessionName ?? ''} '
2443
+ 'query=${query.hasRows() ? query.rows.id : query.whichQuery().name} '
2444
+ 'elapsedUs=${elapsed.elapsedMicroseconds} error=$error',
2445
+ );
2446
+ developer.log(
2447
+ 'Local nominal query failed',
2448
+ name: 'nomos.local_read',
2449
+ error: error,
2450
+ stackTrace: stackTrace,
2451
+ time: DateTime.now(),
2452
+ sequenceNumber: elapsed.elapsedMicroseconds,
2453
+ );
2454
+ rethrow;
2455
+ }
2456
+ elapsed.stop();
2457
+ if (!response.hasResultUsda()) {
2458
+ throw StateError(
2459
+ 'Kernel returned ${response.whichResult().name} without a nominal USDA query result',
2460
+ );
2461
+ }
2462
+ final result = Uint8List.fromList(response.resultUsda);
2463
+ // This is the local read-plane equivalent of an OBD port: keep the
2464
+ // workspace/query binding and result size visible in ordinary host logs.
2465
+ // ignore: avoid_print
2466
+ print(
2467
+ '[nomos-local-read] session=${session ?? _sessionName ?? ''} '
2468
+ 'query=${query.hasRows() ? query.rows.id : query.whichQuery().name} '
2469
+ 'resultBytes=${result.length} elapsedUs=${elapsed.elapsedMicroseconds}',
2470
+ );
2471
+ developer.log(
2472
+ 'Local nominal query completed '
2473
+ 'session=${session ?? _sessionName ?? ''} '
2474
+ 'query=${query.hasRows() ? query.rows.id : query.whichQuery().name} '
2475
+ 'resultBytes=${result.length} elapsedUs=${elapsed.elapsedMicroseconds}',
2476
+ name: 'nomos.local_read',
2477
+ time: DateTime.now(),
2478
+ sequenceNumber: elapsed.elapsedMicroseconds,
2479
+ );
2480
+ return result;
2481
+ }
2482
+
2483
+ Future<NmsGeneratedResult> _nmsKernelApplicationQuery({
2484
+ required String? session,
2485
+ required nomos_protocol.QueryAuthority authority,
2486
+ required nomos_protocol.QueryRequest query,
2487
+ }) async {
2488
+ final response = await _nmsKernelQuery(
2489
+ session: session,
2490
+ authority: authority,
2491
+ query: query,
2492
+ resultFormat: nomos_protocol
2493
+ .QueryResultFormat.QUERY_RESULT_FORMAT_APPLICATION_PROTOBUF,
2494
+ );
2495
+ if (!response.hasApplicationResult()) {
2496
+ throw StateError(
2497
+ 'Kernel returned ${response.whichResult().name} without the generated application result',
2498
+ );
2499
+ }
2500
+ final result = response.applicationResult;
2501
+ return NmsInternalGeneratedResults.create(
2502
+ protobufType: result.typeUrl,
2503
+ protobufPayload: Uint8List.fromList(result.value),
2504
+ );
2505
+ }
2506
+
2507
+ Future<nomos_protocol.QueryResponse> _nmsKernelQuery({
2508
+ required String? session,
2509
+ required nomos_protocol.QueryAuthority authority,
2510
+ required nomos_protocol.QueryRequest query,
2511
+ nomos_protocol.QueryResultFormat resultFormat =
2512
+ nomos_protocol.QueryResultFormat.QUERY_RESULT_FORMAT_NOMINAL_USDA,
2513
+ }) async {
2514
+ final runtimeSession = session ?? _sessionName;
2515
+ if (runtimeSession == null || runtimeSession.isEmpty) {
2516
+ throw StateError('A nominal query must be bound to a NomosSession');
2517
+ }
2518
+ final plane = _eventPlane;
2519
+ final transport = plane._transport;
2520
+ if (transport is! NomosKernelCallTransport) {
2521
+ throw UnsupportedError(
2522
+ 'This host does not implement the generated Runtime v2 protobuf lane',
2523
+ );
2524
+ }
2525
+ query
2526
+ ..authority = authority
2527
+ ..spans = true
2528
+ ..resultFormat = resultFormat;
2529
+ // NomosSession is a view over one local plane, not an independent runtime.
2530
+ // Correlation therefore belongs to the plane just like watches and legacy
2531
+ // reception calls. Keeping this counter on a child view lets two views mint
2532
+ // overlapping logical request identities despite sharing one executor.
2533
+ // A copy still being fetched or opened answers "not ready yet": ask again
2534
+ // rather than hand every caller a state that passes by itself.
2535
+ final response = await nmsUntilReplicaReady(
2536
+ () {
2537
+ final requestNumber = ++plane._sequences.request;
2538
+ return (transport as NomosKernelCallTransport).callKernel(
2539
+ session: runtimeSession,
2540
+ // An explicit device key is an additional proof, not a replacement for
2541
+ // the session's renewable provider credential. The peer office owns
2542
+ // the live credential capability and must still attach it before the
2543
+ // kernel judges the query.
2544
+ credentialUse: nomos_protocol.CredentialUse.CREDENTIAL_USE_SESSION,
2545
+ request: nomos_protocol.CallRequest(
2546
+ protocolVersion: 1,
2547
+ requestId: '$runtimeSession:query:$requestNumber',
2548
+ query: query,
2549
+ ),
2550
+ );
2551
+ },
2552
+ synced: _eventPlane._events.workspacesSynced,
2553
+ );
2554
+ if (response.hasFailure()) {
2555
+ throw StateError('${response.failure.code}: ${response.failure.message}');
2556
+ }
2557
+ if (!response.hasQuery()) {
2558
+ throw StateError(
2559
+ 'Kernel returned ${response.whichOutcome().name} without a query result',
2560
+ );
2561
+ }
2562
+ return response.query;
2563
+ }
2564
+
2565
+ NomosProviderTokenVerification _providerTokenVerification(
2566
+ nomos_protocol.ProviderTokenVerificationStatus status,
2567
+ ) =>
2568
+ switch (status) {
2569
+ nomos_protocol.ProviderTokenVerificationStatus
2570
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_VALID =>
2571
+ NomosProviderTokenVerification.valid,
2572
+ nomos_protocol.ProviderTokenVerificationStatus
2573
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_NO_MATCHING_KEY =>
2574
+ NomosProviderTokenVerification.noMatchingKey,
2575
+ nomos_protocol.ProviderTokenVerificationStatus
2576
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_EXPIRED =>
2577
+ NomosProviderTokenVerification.expired,
2578
+ nomos_protocol.ProviderTokenVerificationStatus
2579
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_NOT_YET_VALID =>
2580
+ NomosProviderTokenVerification.notYetValid,
2581
+ nomos_protocol.ProviderTokenVerificationStatus
2582
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_ISSUER_MISMATCH =>
2583
+ NomosProviderTokenVerification.issuerMismatch,
2584
+ nomos_protocol.ProviderTokenVerificationStatus
2585
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_AUDIENCE_MISMATCH =>
2586
+ NomosProviderTokenVerification.audienceMismatch,
2587
+ nomos_protocol.ProviderTokenVerificationStatus
2588
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_BAD_SIGNATURE =>
2589
+ NomosProviderTokenVerification.badSignature,
2590
+ nomos_protocol.ProviderTokenVerificationStatus
2591
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_MALFORMED =>
2592
+ NomosProviderTokenVerification.malformed,
2593
+ nomos_protocol.ProviderTokenVerificationStatus
2594
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_UNSUPPORTED_ALGORITHM =>
2595
+ NomosProviderTokenVerification.unsupportedAlgorithm,
2596
+ nomos_protocol.ProviderTokenVerificationStatus
2597
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_BAD_KEY =>
2598
+ NomosProviderTokenVerification.badKey,
2599
+ nomos_protocol.ProviderTokenVerificationStatus
2600
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_MISSING_SUBJECT =>
2601
+ NomosProviderTokenVerification.missingSubject,
2602
+ nomos_protocol.ProviderTokenVerificationStatus
2603
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_NO_VERIFIER =>
2604
+ NomosProviderTokenVerification.noVerifier,
2605
+ nomos_protocol.ProviderTokenVerificationStatus
2606
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_MISSING_TOKEN =>
2607
+ NomosProviderTokenVerification.missingToken,
2608
+ nomos_protocol.ProviderTokenVerificationStatus
2609
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_UNSPECIFIED =>
2610
+ NomosProviderTokenVerification.unspecified,
2611
+ _ => NomosProviderTokenVerification.unspecified,
2612
+ };
2613
+
2614
+ /// Ask the kernel—not the host—to verify the session's current provider
2615
+ /// token and enrolled-device relation against the workspace's current law.
2616
+ @internal
2617
+ Future<NomosAuthenticationPosture> nmsAuthenticationPosture({
2618
+ String? session,
2619
+ String? principal,
2620
+ String? authorSecret,
2621
+ bool includeBirthEvidence = false,
2622
+ }) async {
2623
+ final response = await _nmsKernelQuery(
2624
+ session: session,
2625
+ authority: _nmsQueryAuthority(
2626
+ null,
2627
+ principal,
2628
+ session,
2629
+ authorSecret: authorSecret,
2630
+ ),
2631
+ query: nomos_protocol.QueryRequest(
2632
+ authenticationPosture: nomos_protocol.AuthenticationPostureQuery(
2633
+ includeBirthEvidence: includeBirthEvidence),
2634
+ ),
2635
+ );
2636
+ if (!response.hasAuthenticationPosture()) {
2637
+ throw StateError(
2638
+ 'Kernel returned ${response.whichResult().name}, expected authenticationPosture',
2639
+ );
2640
+ }
2641
+ final posture = response.authenticationPosture;
2642
+ final result = NomosAuthenticationPosture(
2643
+ providerToken: _providerTokenVerification(posture.providerToken),
2644
+ enrolledDevice: posture.enrolledDevice,
2645
+ resolvedPrincipal: posture.resolvedPrincipal,
2646
+ birthEvidence: posture.birthEvidence.status,
2647
+ birthEvidenceDetail: posture.birthEvidence.detail,
2648
+ );
2649
+ print(
2650
+ '[nomos-auth-posture] workspace=${_eventPlane._sessions.workspaceFor(session ?? _sessionName ?? '')} '
2651
+ 'token=${result.providerToken.name} enrolled=${result.enrolledDevice} '
2652
+ 'principal=${result.resolvedPrincipal.isEmpty ? 'unresolved' : 'resolved'}');
2653
+ return result;
2654
+ }
2655
+
2656
+ /// THE RECEIVING-GROUP GATE — one Runtime v2 paper (`OpenReceivingGroupRequest`,
2657
+ /// `architecture/receiving_group_lifecycle.md` cut 3). The session named here
2658
+ /// must already be bound to the principal's receiving-group workspace. The
2659
+ /// shared desk owns whatever placement, identity and birth work the operation
2660
+ /// requires. Its convergence tail runs as background correspondence and reports on
2661
+ /// [receivingGroupConvergence]. [contract] is the application's law identity
2662
+ /// from the compiled descriptor; tenant seed steps are law, never carried
2663
+ /// here. A typed refusal surfaces as a [NomosRuntimeError] carrying the
2664
+ /// desk's own code (`nomos.runtime.receiving_group_*`).
2665
+ @internal
2666
+ Future<nomos_protocol.ReceivingGroupOpened> nmsOpenReceivingGroup({
2667
+ required String application,
2668
+ required String principal,
2669
+ required String authToken,
2670
+ required nomos_protocol.ApplicationBirthContract contract,
2671
+ bool verifyHistory = false,
2672
+ String? session,
2673
+ nomos_protocol.ConsoleEnrolmentRoute? enrolment,
2674
+ }) async {
2675
+ final response = await _invokeRuntimeV2(
2676
+ nomos_protocol.RuntimeRequest(
2677
+ openReceivingGroup: nomos_protocol.OpenReceivingGroupRequest(
2678
+ session: _runtimeSession(session),
2679
+ application: application,
2680
+ principal: principal,
2681
+ authToken: utf8.encode(authToken),
2682
+ contract: contract,
2683
+ verifyHistory: verifyHistory,
2684
+ enrolment: enrolment,
2685
+ ),
2686
+ ),
2687
+ );
2688
+ if (!response.hasReceivingGroupOpened()) {
2689
+ throw StateError(
2690
+ 'Runtime v2 returned ${response.whichOutcome().name} for openReceivingGroup',
2691
+ );
2692
+ }
2693
+ return response.receivingGroupOpened;
2694
+ }
2695
+
2696
+ /// Wait until the kernel proves that the provider token verifies, the device
2697
+ /// is enrolled, and the resolved principal is the session principal — or
2698
+ /// until [patience] runs out, in which case the LAST OBSERVED posture is
2699
+ /// returned and the caller judges it. No timer or receipt is the business
2700
+ /// postcondition; only the kernel's own reading of custody is.
2701
+ ///
2702
+ /// Peer Office owns repeated local observations, backoff and the deadline.
2703
+ /// Its retained-follow lifecycle independently owns convergence; Reception
2704
+ /// submits one desired postcondition and maps its terminal result.
2705
+ @internal
2706
+ Future<NomosAuthenticationPosture> nmsAwaitAuthenticationReady({
2707
+ String? session,
2708
+ String? principal,
2709
+ String? authorSecret,
2710
+ bool requireBirthEvidence = false,
2711
+ Duration patience = const Duration(seconds: 60),
2712
+ Duration initialBackoff = const Duration(milliseconds: 500),
2713
+ Duration maximumBackoff = const Duration(seconds: 8),
2714
+ }) async {
2715
+ final result =
2716
+ await _awaitAuthentication(nomos_protocol.AwaitAuthenticationRequest(
2717
+ session: _runtimeSession(session),
2718
+ principal: principal ?? '',
2719
+ criterion: requireBirthEvidence
2720
+ ? nomos_protocol.AuthenticationReadinessCriterion
2721
+ .AUTHENTICATION_READINESS_CRITERION_BIRTH_EVIDENCE
2722
+ : nomos_protocol.AuthenticationReadinessCriterion
2723
+ .AUTHENTICATION_READINESS_CRITERION_ENROLLED_DEVICE,
2724
+ patienceMillis: patience.inMilliseconds,
2725
+ initialBackoffMillis: initialBackoff.inMilliseconds,
2726
+ maximumBackoffMillis: maximumBackoff.inMilliseconds,
2727
+ authorSecret: authorSecret == null ? null : base64Decode(authorSecret),
2728
+ ));
2729
+ return _authenticationPostureView(result.posture);
2730
+ }
2731
+
2732
+ Future<nomos_protocol.AuthenticationReadiness> _awaitAuthentication(
2733
+ nomos_protocol.AwaitAuthenticationRequest command,
2734
+ ) async {
2735
+ final response = await _invokeRuntimeV2(
2736
+ nomos_protocol.RuntimeRequest(awaitAuthentication: command));
2737
+ if (!response.hasAuthenticationReadiness() ||
2738
+ !response.authenticationReadiness.hasPosture()) {
2739
+ throw StateError(
2740
+ 'Runtime v2 returned ${response.whichOutcome().name} without an authentication posture');
2741
+ }
2742
+ return response.authenticationReadiness;
2743
+ }
2744
+
2745
+ NomosAuthenticationPosture _authenticationPostureView(
2746
+ nomos_protocol.AuthenticationPostureResponse posture,
2747
+ ) =>
2748
+ NomosAuthenticationPosture(
2749
+ providerToken: _providerTokenVerification(posture.providerToken),
2750
+ enrolledDevice: posture.enrolledDevice,
2751
+ resolvedPrincipal: posture.resolvedPrincipal,
2752
+ birthEvidence: posture.birthEvidence.status,
2753
+ birthEvidenceDetail: posture.birthEvidence.detail,
2754
+ );
2755
+
2756
+ Future<nomos_protocol.QueryResponse> _nmsKernelHistoricQuery({
2757
+ required NomosMoment moment,
2758
+ required String? session,
2759
+ required nomos_protocol.QueryAuthority authority,
2760
+ required nomos_protocol.QueryRequest query,
2761
+ nomos_protocol.QueryResultFormat resultFormat =
2762
+ nomos_protocol.QueryResultFormat.QUERY_RESULT_FORMAT_NOMINAL_USDA,
2763
+ }) async {
2764
+ final runtimeSession = session ?? _sessionName;
2765
+ if (runtimeSession == null || runtimeSession.isEmpty) {
2766
+ throw StateError(
2767
+ 'A nominal historic query must be bound to a NomosSession');
2768
+ }
2769
+ query
2770
+ ..authority = authority
2771
+ ..spans = true
2772
+ ..resultFormat = resultFormat;
2773
+ final response = await nmsUntilReplicaReady(
2774
+ () => _invokeRuntimeV2(
2775
+ nomos_protocol.RuntimeRequest(
2776
+ historicQuery: nomos_protocol.HistoricQueryRequest(
2777
+ session: runtimeSession,
2778
+ moment: moment.id,
2779
+ query: query,
2780
+ ),
2781
+ ),
2782
+ ),
2783
+ synced: _eventPlane._events.workspacesSynced,
2784
+ );
2785
+ if (response.hasFailure()) {
2786
+ throw StateError('${response.failure.code}: ${response.failure.message}');
2787
+ }
2788
+ if (!response.hasKernelCall()) {
2789
+ throw StateError(
2790
+ 'Runtime v2 returned ${response.whichOutcome().name} for a historic query',
2791
+ );
2792
+ }
2793
+ final kernel = response.kernelCall;
2794
+ if (kernel.hasFailure()) {
2795
+ throw StateError('${kernel.failure.code}: ${kernel.failure.message}');
2796
+ }
2797
+ if (!kernel.hasQuery()) {
2798
+ throw StateError(
2799
+ 'Kernel returned ${kernel.whichOutcome().name} without a historic query result',
2800
+ );
2801
+ }
2802
+ return kernel.query;
2803
+ }
2804
+
2805
+ Future<Uint8List> _nmsKernelHistoricQueryUsda({
2806
+ required NomosMoment moment,
2807
+ required String? session,
2808
+ required nomos_protocol.QueryAuthority authority,
2809
+ required nomos_protocol.QueryRequest query,
2810
+ }) async {
2811
+ final response = await _nmsKernelHistoricQuery(
2812
+ moment: moment,
2813
+ session: session,
2814
+ authority: authority,
2815
+ query: query,
2816
+ );
2817
+ if (!response.hasResultUsda()) {
2818
+ throw StateError(
2819
+ 'Kernel returned ${response.whichResult().name} without a nominal historic USDA result',
2820
+ );
2821
+ }
2822
+ return Uint8List.fromList(response.resultUsda);
2823
+ }
2824
+
2825
+ Future<NmsGeneratedResult> _nmsKernelHistoricApplicationQuery({
2826
+ required NomosMoment moment,
2827
+ required String? session,
2828
+ required nomos_protocol.QueryAuthority authority,
2829
+ required nomos_protocol.QueryRequest query,
2830
+ }) async {
2831
+ final response = await _nmsKernelHistoricQuery(
2832
+ moment: moment,
2833
+ session: session,
2834
+ authority: authority,
2835
+ query: query,
2836
+ resultFormat: nomos_protocol
2837
+ .QueryResultFormat.QUERY_RESULT_FORMAT_APPLICATION_PROTOBUF,
2838
+ );
2839
+ if (!response.hasApplicationResult()) {
2840
+ throw StateError(
2841
+ 'Kernel returned ${response.whichResult().name} without the generated historic application result',
2842
+ );
2843
+ }
2844
+ final result = response.applicationResult;
2845
+ return NmsInternalGeneratedResults.create(
2846
+ protobufType: result.typeUrl,
2847
+ protobufPayload: Uint8List.fromList(result.value),
2848
+ );
2849
+ }
2850
+
2851
+ /// Declared application query over the shared typed peer-office boundary.
2852
+ ///
2853
+ /// The peer office transports an operation-specific Protobuf result. Only
2854
+ /// the generated application client owns the corresponding decoder.
2855
+ Future<NmsGeneratedResult> nmsQuery(
2856
+ NmsGeneratedQuery query, [
2857
+ List<NomosScopeKey>? keys,
2858
+ String? principal,
2859
+ String? session,
2860
+ ]) =>
2861
+ _nmsKernelApplicationQuery(
2862
+ session: session,
2863
+ authority: _nmsQueryAuthority(keys, principal, session),
2864
+ query: nomos_protocol.QueryRequest(
2865
+ rows: nomos_protocol.RowsQuery(
2866
+ id: query.query,
2867
+ principal: principal ?? '',
2868
+ applicationArguments: google_protobuf.Any(
2869
+ typeUrl: query.protobufType,
2870
+ value: query.protobufPayload,
2871
+ ),
2872
+ ),
2873
+ ),
2874
+ );
2875
+
2876
+ /// The same declared application query at one authenticated historic moment.
2877
+ Future<NmsGeneratedResult> nmsQueryAt(
2878
+ NomosMoment moment,
2879
+ NmsGeneratedQuery query, [
2880
+ List<NomosScopeKey>? keys,
2881
+ String? principal,
2882
+ String? session,
2883
+ ]) =>
2884
+ _nmsKernelHistoricApplicationQuery(
2885
+ moment: moment,
2886
+ session: session,
2887
+ authority: _nmsQueryAuthority(keys, principal, session),
2888
+ query: nomos_protocol.QueryRequest(
2889
+ rows: nomos_protocol.RowsQuery(
2890
+ id: query.query,
2891
+ principal: principal ?? '',
2892
+ applicationArguments: google_protobuf.Any(
2893
+ typeUrl: query.protobufType,
2894
+ value: query.protobufPayload,
2895
+ ),
2896
+ ),
2897
+ ),
2898
+ );
2899
+
2900
+ /// Reactive form of [nmsQuery]. Decode the exact result delivered by the
2901
+ /// kernel watch; the notification is data, not a request to query it again.
2902
+ Stream<NmsGeneratedResult> nmsWatch(
2903
+ NmsGeneratedQuery query, {
2904
+ List<NomosScopeKey>? keys,
2905
+ String? principal,
2906
+ String? session,
2907
+ int intervalMs = _defaultWatchSafetyPollMs,
2908
+ }) =>
2909
+ _nmsWatchQuery(
2910
+ nomos_protocol.QueryRequest(
2911
+ rows: nomos_protocol.RowsQuery(
2912
+ id: query.query,
2913
+ principal: principal ?? '',
2914
+ applicationArguments: google_protobuf.Any(
2915
+ typeUrl: query.protobufType,
2916
+ value: query.protobufPayload,
2917
+ ),
2918
+ ),
2919
+ ),
2920
+ resultFormat: nomos_protocol
2921
+ .QueryResultFormat.QUERY_RESULT_FORMAT_APPLICATION_PROTOBUF,
2922
+ decode: (changed) {
2923
+ if (!changed.hasApplicationResult()) {
2924
+ throw StateError(
2925
+ 'Generated watch returned no typed application result');
2926
+ }
2927
+ return NmsInternalGeneratedResults.create(
2928
+ protobufType: changed.applicationResult.typeUrl,
2929
+ protobufPayload:
2930
+ Uint8List.fromList(changed.applicationResult.value),
2931
+ );
2932
+ },
2933
+ keys: keys,
2934
+ principal: principal,
2935
+ session: session,
2936
+ intervalMs: intervalMs,
2937
+ );
2938
+
2939
+ /// Reactive form of [nmsQueryUsda]. Subscription mechanics are Runtime v2
2940
+ /// protobuf; each emission is the kernel's nominal USDA result and is decoded
2941
+ /// only by the generated domain client which owns [query].
2942
+ Stream<Uint8List> nmsWatchUsda(
2943
+ NmsGeneratedQuery query, {
2944
+ List<NomosScopeKey>? keys,
2945
+ String? principal,
2946
+ String? session,
2947
+ int intervalMs = _defaultWatchSafetyPollMs,
2948
+ }) =>
2949
+ _nmsWatchQueryUsda(
2950
+ nomos_protocol.QueryRequest(
2951
+ rows: nomos_protocol.RowsQuery(
2952
+ id: query.query,
2953
+ principal: principal ?? '',
2954
+ applicationArguments: google_protobuf.Any(
2955
+ typeUrl: query.protobufType,
2956
+ value: query.protobufPayload,
2957
+ ),
2958
+ ),
2959
+ ),
2960
+ keys: keys,
2961
+ principal: principal,
2962
+ session: session,
2963
+ intervalMs: intervalMs,
2964
+ );
2965
+
2966
+ /// Reactive by-id read. The generated domain decoder supplies the nominal
2967
+ /// aggregate type; this framework method owns only the protobuf mechanics.
2968
+ Stream<Uint8List> nmsWatchByIdUsda(
2969
+ String aggregateType,
2970
+ String id, {
2971
+ List<NomosScopeKey>? keys,
2972
+ String? principal,
2973
+ String? session,
2974
+ int intervalMs = _defaultWatchSafetyPollMs,
2975
+ }) =>
2976
+ _nmsWatchQueryUsda(
2977
+ nomos_protocol.QueryRequest(
2978
+ byId: nomos_protocol.ByIdQuery(
2979
+ aggregateType: aggregateType,
2980
+ aggregateId: id,
2981
+ principal: principal ?? '',
2982
+ ),
2983
+ ),
2984
+ keys: keys,
2985
+ principal: principal,
2986
+ session: session,
2987
+ intervalMs: intervalMs,
2988
+ );
2989
+
2990
+ /// Reactive type-index read. The aggregate type is a dedicated protobuf
2991
+ /// field, never a magic application query name or argument bag.
2992
+ Stream<Uint8List> nmsWatchByTypeUsda(
2993
+ String aggregateType, {
2994
+ List<NomosScopeKey>? keys,
2995
+ String? principal,
2996
+ String? session,
2997
+ int intervalMs = _defaultWatchSafetyPollMs,
2998
+ }) =>
2999
+ _nmsWatchQueryUsda(
3000
+ nomos_protocol.QueryRequest(
3001
+ byType: nomos_protocol.ByTypeQuery(
3002
+ aggregateType: aggregateType,
3003
+ principal: principal ?? '',
3004
+ ),
3005
+ ),
3006
+ keys: keys,
3007
+ principal: principal,
3008
+ session: session,
3009
+ intervalMs: intervalMs,
3010
+ );
3011
+
3012
+ Stream<Uint8List> _nmsWatchQueryUsda(
3013
+ nomos_protocol.QueryRequest request, {
3014
+ List<NomosScopeKey>? keys,
3015
+ String? principal,
3016
+ String? session,
3017
+ int intervalMs = _defaultWatchSafetyPollMs,
3018
+ }) =>
3019
+ _nmsWatchQuery(
3020
+ request,
3021
+ resultFormat:
3022
+ nomos_protocol.QueryResultFormat.QUERY_RESULT_FORMAT_NOMINAL_USDA,
3023
+ decode: (changed) {
3024
+ if (changed.hasApplicationResult()) {
3025
+ throw StateError('Nominal watch returned an application result');
3026
+ }
3027
+ return Uint8List.fromList(changed.resultUsda);
3028
+ },
3029
+ keys: keys,
3030
+ principal: principal,
3031
+ session: session,
3032
+ intervalMs: intervalMs,
3033
+ );
3034
+
3035
+ Stream<T> _nmsWatchQuery<T>(
3036
+ nomos_protocol.QueryRequest request, {
3037
+ required nomos_protocol.QueryResultFormat resultFormat,
3038
+ required T Function(nomos_protocol.WatchChanged) decode,
3039
+ List<NomosScopeKey>? keys,
3040
+ String? principal,
3041
+ String? session,
3042
+ int intervalMs = _defaultWatchSafetyPollMs,
3043
+ }) {
3044
+ final runtimeSession = session ?? _sessionName;
3045
+ if (runtimeSession == null || runtimeSession.isEmpty) {
3046
+ throw StateError('A nominal watch must be bound to a NomosSession');
3047
+ }
3048
+ final plane = _eventPlane;
3049
+ request
3050
+ ..authority = _nmsQueryAuthority(keys, principal, runtimeSession)
3051
+ ..spans = true
3052
+ ..resultFormat = resultFormat;
3053
+ final NmsTypedWatchHandle<T> watch = plane._events.typedWatches.open<T>(
3054
+ session: runtimeSession,
3055
+ decode: decode,
3056
+ stop: _unwatch,
3057
+ start: (subscriptionId) async {
3058
+ final response = await _invokeRuntimeV2(
3059
+ nomos_protocol.RuntimeRequest(
3060
+ watch: nomos_protocol.WatchRequest(
3061
+ session: runtimeSession,
3062
+ subscriptionId: subscriptionId,
3063
+ kind: nomos_protocol.WatchKind.WATCH_KIND_QUERY,
3064
+ query: request,
3065
+ intervalMillis: intervalMs,
3066
+ ),
3067
+ ),
3068
+ );
3069
+ if (!response.hasWatchStarted() ||
3070
+ response.watchStarted.subscriptionId != subscriptionId) {
3071
+ throw StateError(
3072
+ 'Runtime v2 returned ${response.whichOutcome().name} for nominal watch',
3073
+ );
3074
+ }
3075
+ },
3076
+ );
3077
+ return watch.stream;
3078
+ }
3079
+
3080
+ /// Bounded declared application query over the shared typed peer-office
3081
+ /// boundary. Cursor and completion are fields of the generated result type.
3082
+ Future<NmsGeneratedResult> nmsQueryPage(
3083
+ NmsGeneratedQuery query, {
3084
+ int pageSize = 100,
3085
+ String? cursor,
3086
+ List<NomosScopeKey>? keys,
3087
+ String? principal,
3088
+ String? session,
3089
+ }) {
3090
+ if (pageSize < 1 || pageSize > 1000) {
3091
+ throw RangeError.range(pageSize, 1, 1000, 'pageSize');
3092
+ }
3093
+ return _nmsKernelApplicationQuery(
3094
+ session: session,
3095
+ authority: _nmsQueryAuthority(keys, principal, session),
3096
+ query: nomos_protocol.QueryRequest(
3097
+ page: nomos_protocol.PageQuery(
3098
+ id: query.query,
3099
+ limit: pageSize,
3100
+ after: cursor ?? '',
3101
+ principal: principal ?? '',
3102
+ applicationArguments: google_protobuf.Any(
3103
+ typeUrl: query.protobufType,
3104
+ value: query.protobufPayload,
3105
+ ),
3106
+ ),
3107
+ ),
3108
+ );
3109
+ }
3110
+
3111
+ /// Framework-private by-id form of [nmsQueryUsda].
3112
+ Future<Uint8List> nmsQueryByIdUsda(
3113
+ String aggregateType,
3114
+ String id, [
3115
+ List<NomosScopeKey>? keys,
3116
+ String? principal,
3117
+ String? session,
3118
+ ]) =>
3119
+ _nmsKernelQueryUsda(
3120
+ session: session,
3121
+ authority: _nmsQueryAuthority(keys, principal, session),
3122
+ query: nomos_protocol.QueryRequest(
3123
+ byId: nomos_protocol.ByIdQuery(
3124
+ aggregateType: aggregateType,
3125
+ aggregateId: id,
3126
+ principal: principal ?? '',
3127
+ ),
3128
+ ),
3129
+ );
3130
+
3131
+ Future<Uint8List> nmsQueryByIdAtUsda(
3132
+ NomosMoment moment,
3133
+ String aggregateType,
3134
+ String id, [
3135
+ List<NomosScopeKey>? keys,
3136
+ String? principal,
3137
+ String? session,
3138
+ ]) =>
3139
+ _nmsKernelHistoricQueryUsda(
3140
+ moment: moment,
3141
+ session: session,
3142
+ authority: _nmsQueryAuthority(keys, principal, session),
3143
+ query: nomos_protocol.QueryRequest(
3144
+ byId: nomos_protocol.ByIdQuery(
3145
+ aggregateType: aggregateType,
3146
+ aggregateId: id,
3147
+ principal: principal ?? '',
3148
+ ),
3149
+ ),
3150
+ );
3151
+
3152
+ /// Framework-private type-index read. Aggregate type is a fixed protobuf field, not a fake
3153
+ /// application query argument.
3154
+ Future<Uint8List> nmsQueryByTypeUsda(
3155
+ String aggregateType, [
3156
+ List<NomosScopeKey>? keys,
3157
+ String? principal,
3158
+ String? session,
3159
+ ]) =>
3160
+ _nmsKernelQueryUsda(
3161
+ session: session,
3162
+ authority: _nmsQueryAuthority(keys, principal, session),
3163
+ query: nomos_protocol.QueryRequest(
3164
+ byType: nomos_protocol.ByTypeQuery(
3165
+ aggregateType: aggregateType,
3166
+ principal: principal ?? '',
3167
+ ),
3168
+ ),
3169
+ );
3170
+
3171
+ Future<Uint8List> nmsQueryByTypeAtUsda(
3172
+ NomosMoment moment,
3173
+ String aggregateType, [
3174
+ List<NomosScopeKey>? keys,
3175
+ String? principal,
3176
+ String? session,
3177
+ ]) =>
3178
+ _nmsKernelHistoricQueryUsda(
3179
+ moment: moment,
3180
+ session: session,
3181
+ authority: _nmsQueryAuthority(keys, principal, session),
3182
+ query: nomos_protocol.QueryRequest(
3183
+ byType: nomos_protocol.ByTypeQuery(
3184
+ aggregateType: aggregateType,
3185
+ principal: principal ?? '',
3186
+ ),
3187
+ ),
3188
+ );
3189
+
3190
+ /// Exact spatial-index read for generated domain clients. [spatialId] is
3191
+ /// generated from the installed DSL graph; the application supplies only
3192
+ /// the typed [bbox]. Rows remain canonical USDA until its generated decoder.
3193
+ Future<Uint8List> nmsSpatialWithinUsda(
3194
+ String spatialId,
3195
+ NomosBbox bbox, [
3196
+ String? session,
3197
+ ]) =>
3198
+ _nmsKernelQueryUsda(
3199
+ session: session,
3200
+ authority: _nmsQueryAuthority(null, null, session),
3201
+ query: nomos_protocol.QueryRequest(
3202
+ spatial: nomos_protocol.SpatialQuery(
3203
+ id: spatialId,
3204
+ minX: bbox.minX,
3205
+ minY: bbox.minY,
3206
+ maxX: bbox.maxX,
3207
+ maxY: bbox.maxY,
3208
+ ),
3209
+ ),
3210
+ );
3211
+
3212
+ /// Reactive counterpart of [nmsSpatialWithinUsda]. Runtime v2 owns the
3213
+ /// subscription; every value is still the kernel's canonical USDA image.
3214
+ Stream<Uint8List> nmsWatchSpatialUsda(
3215
+ String spatialId,
3216
+ NomosBbox bbox, {
3217
+ String? session,
3218
+ int intervalMs = _defaultWatchSafetyPollMs,
3219
+ }) =>
3220
+ _nmsWatchQueryUsda(
3221
+ nomos_protocol.QueryRequest(
3222
+ spatial: nomos_protocol.SpatialQuery(
3223
+ id: spatialId,
3224
+ minX: bbox.minX,
3225
+ minY: bbox.minY,
3226
+ maxX: bbox.maxX,
3227
+ maxY: bbox.maxY,
3228
+ ),
3229
+ ),
3230
+ session: session,
3231
+ intervalMs: intervalMs,
3232
+ );
3233
+
3234
+ Future<int> _nmsTally(
3235
+ String id,
3236
+ String? groupKey, {
3237
+ required bool sum,
3238
+ String? session,
3239
+ NomosMoment? moment,
3240
+ }) async {
3241
+ final tally = nomos_protocol.TallyQuery(
3242
+ id: id,
3243
+ groupKey: groupKey,
3244
+ );
3245
+ final query = nomos_protocol.QueryRequest();
3246
+ if (sum) {
3247
+ query.sum = tally;
3248
+ } else {
3249
+ query.count = tally;
3250
+ }
3251
+ final authority = _nmsQueryAuthority(null, null, session);
3252
+ final result = moment == null
3253
+ ? await _nmsKernelQuery(
3254
+ session: session,
3255
+ authority: authority,
3256
+ query: query,
3257
+ )
3258
+ : await _nmsKernelHistoricQuery(
3259
+ moment: moment,
3260
+ session: session,
3261
+ authority: authority,
3262
+ query: query,
3263
+ );
3264
+ if (!result.hasIntegerValue()) {
3265
+ throw StateError(
3266
+ 'Kernel returned ${result.whichResult().name} without an exact integer tally',
3267
+ );
3268
+ }
3269
+ return result.integerValue.toInt();
3270
+ }
3271
+
3272
+ Future<int> nmsCount(String countId, [String? groupKey, String? session]) =>
3273
+ _nmsTally(countId, groupKey, sum: false, session: session);
3274
+
3275
+ /// Observe the kernel's maintained tally; no row transfer or client recount.
3276
+ Stream<int> nmsWatchCount(String countId, [String? groupKey]) =>
3277
+ _nmsWatchQuery(
3278
+ nomos_protocol.QueryRequest(
3279
+ count: nomos_protocol.TallyQuery(id: countId, groupKey: groupKey),
3280
+ ),
3281
+ resultFormat:
3282
+ nomos_protocol.QueryResultFormat.QUERY_RESULT_FORMAT_LEGACY_VALUE,
3283
+ decode: (changed) {
3284
+ if (!changed.hasIntegerValue()) {
3285
+ throw StateError('Count watch returned no integer tally');
3286
+ }
3287
+ return changed.integerValue.toInt();
3288
+ },
3289
+ );
3290
+
3291
+ Future<int> nmsCountAt(
3292
+ NomosMoment moment,
3293
+ String countId, [
3294
+ String? groupKey,
3295
+ String? session,
3296
+ ]) =>
3297
+ _nmsTally(
3298
+ countId,
3299
+ groupKey,
3300
+ sum: false,
3301
+ session: session,
3302
+ moment: moment,
3303
+ );
3304
+
3305
+ Future<int> nmsSum(String sumId, [String? groupKey, String? session]) =>
3306
+ _nmsTally(sumId, groupKey, sum: true, session: session);
3307
+
3308
+ /// Read an installed min/max declaration through the ordinary authenticated pipe.
3309
+ Future<int?> nmsExtremum(String id, String kind, [String? groupKey]) =>
3310
+ _nmsExtremum(id, kind, groupKey);
3311
+
3312
+ Future<int?> nmsExtremumAt(NomosMoment moment, String id, String kind,
3313
+ [String? groupKey]) =>
3314
+ _nmsExtremum(id, kind, groupKey, moment: moment);
3315
+
3316
+ Future<int?> _nmsExtremum(String id, String kind, String? groupKey,
3317
+ {NomosMoment? moment}) async {
3318
+ final query = extrema.extremumQuery(id, kind, groupKey);
3319
+ final authority = _nmsQueryAuthority(null, null, null);
3320
+ const format =
3321
+ nomos_protocol.QueryResultFormat.QUERY_RESULT_FORMAT_LEGACY_VALUE;
3322
+ final result = moment == null
3323
+ ? await _nmsKernelQuery(
3324
+ session: null,
3325
+ authority: authority,
3326
+ query: query,
3327
+ resultFormat: format)
3328
+ : await _nmsKernelHistoricQuery(
3329
+ moment: moment,
3330
+ session: null,
3331
+ authority: authority,
3332
+ query: query,
3333
+ resultFormat: format);
3334
+ return extrema.extremumValue(result);
3335
+ }
3336
+
3337
+ Future<int> nmsSumAt(
3338
+ NomosMoment moment,
3339
+ String sumId, [
3340
+ String? groupKey,
3341
+ String? session,
3342
+ ]) =>
3343
+ _nmsTally(
3344
+ sumId,
3345
+ groupKey,
3346
+ sum: true,
3347
+ session: session,
3348
+ moment: moment,
3349
+ );
3350
+
3351
+ /// Compare an immutable publication pin with the stable series' one compiled
3352
+ /// current pointer. This is an O(current-state) check: it neither resolves the
3353
+ /// pinned publication nor reconstructs aggregate history.
3354
+ ///
3355
+ /// THE OFFICE STATES THE STATUS. This used to walk the kernel's transitional
3356
+ /// `LEGACY_VALUE` object — `unique[0].outcome.status`, then `latest`'s string
3357
+ /// fields — which made the client responsible for a shape the kernel had not
3358
+ /// yet given a generated response. The desk decodes it once and answers a typed
3359
+ /// report; Reception only renders the arm it names.
3360
+ Future<ReferenceVersionStatus<TVersion>>
3361
+ checkReferenceVersion<TVersion, TSeries>(
3362
+ VersionedNomosRef<TVersion, TSeries> reference) async {
3363
+ final response = await _invokeRuntimeV2(
3364
+ nomos_protocol.RuntimeRequest(
3365
+ checkReferenceVersions: nomos_protocol.CheckReferenceVersionsRequest(
3366
+ session: _runtimeSession(),
3367
+ refs: [
3368
+ nomos_protocol.ReferenceVersionCheck(
3369
+ pinned: _nomosRefToProtocol(reference.pinned),
3370
+ series: _nomosRefToProtocol(reference.series),
3371
+ currentField: NmsInternalVersionedNomosRefs.currentField(
3372
+ reference,
3373
+ ),
3374
+ ),
3375
+ ],
3376
+ ),
3377
+ ),
3378
+ );
3379
+ if (!response.hasReferenceVersions() ||
3380
+ response.referenceVersions.outcomes.isEmpty) {
3381
+ return ReferenceVersionUnavailable<TVersion>(
3382
+ 'Runtime v2 returned ${response.whichOutcome().name} for reference versions',
3383
+ );
3384
+ }
3385
+ final outcome = response.referenceVersions.outcomes.first;
3386
+ switch (outcome.status) {
3387
+ case nomos_protocol
3388
+ .ReferenceVersionStatus.REFERENCE_VERSION_STATUS_CURRENT:
3389
+ return ReferenceVersionCurrent<TVersion>(reference.pinned);
3390
+ case nomos_protocol
3391
+ .ReferenceVersionStatus.REFERENCE_VERSION_STATUS_OUTDATED:
3392
+ final latest = outcome.latest;
3393
+ return ReferenceVersionOutdated<TVersion>(
3394
+ NmsInternalNomosRefs.create<TVersion>(
3395
+ workspace: latest.workspace,
3396
+ type: latest.aggregateType,
3397
+ id: latest.aggregateId,
3398
+ ),
3399
+ );
3400
+ default:
3401
+ return ReferenceVersionUnavailable<TVersion>(
3402
+ outcome.hasDetail() ? outcome.detail : 'unknown',
3403
+ );
3404
+ }
3405
+ }
3406
+
3407
+ /// Verified points in this workspace's history, oldest to newest. The generated client presents these
3408
+ /// as `client.past.moments()`; callers pass the opaque [NomosMoment] back to `past.at(moment)` and never
3409
+ /// need to know that the framework uses commit identities underneath.
3410
+ Future<List<NomosMoment>> moments({int limit = 100, String? session}) async {
3411
+ final response = await _invokeRuntimeV2(
3412
+ nomos_protocol.RuntimeRequest(
3413
+ listWorkspaceMoments: nomos_protocol.ListWorkspaceMomentsRequest(
3414
+ session: _runtimeSession(session),
3415
+ limit: limit,
3416
+ ),
3417
+ ),
3418
+ );
3419
+ if (!response.hasWorkspaceMoments()) {
3420
+ throw StateError(
3421
+ 'Runtime v2 returned ${response.whichOutcome().name} for workspace moments',
3422
+ );
3423
+ }
3424
+ return response.workspaceMoments.moments
3425
+ .map(
3426
+ (moment) => NomosMoment(
3427
+ id: moment.id,
3428
+ occurredAt: moment.hasOccurredAtUnixMillis()
3429
+ ? DateTime.fromMillisecondsSinceEpoch(
3430
+ moment.occurredAtUnixMillis.toInt(),
3431
+ isUtc: true,
3432
+ )
3433
+ : null,
3434
+ performedBy: moment.hasPerformedBy() ? moment.performedBy : null,
3435
+ domain: moment.hasDomain() ? moment.domain : null,
3436
+ action: moment.hasAction() ? moment.action : null,
3437
+ ),
3438
+ )
3439
+ .toList(growable: false);
3440
+ }
3441
+
3442
+ NomosScopeKey _scopeKeyFromFramework(
3443
+ nomos_protocol.OwnedScopeKey key, {
3444
+ required String fallbackScope,
3445
+ }) =>
3446
+ NomosScopeKey(
3447
+ scope: key.scope.isEmpty ? fallbackScope : key.scope,
3448
+ epoch: key.epoch.toInt(),
3449
+ key: base64Encode(key.key),
3450
+ ownedKeySubject: key.hasOwnedKeySubject() ? key.ownedKeySubject : null,
3451
+ ownedCustody: key.hasOwnedCustodyUsda()
3452
+ ? OwnedCustody.decodeUsda(
3453
+ Uint8List.fromList(key.ownedCustodyUsda),
3454
+ )
3455
+ : null,
3456
+ workspace: key.hasWorkspace() ? key.workspace : null,
3457
+ );
3458
+
3459
+ /// Mint an X25519 device keypair through the exact generated kernel frame.
3460
+ Future<nomos_protocol.EncryptionIdentityResponse> cryptoKeygen() =>
3461
+ _kernelPrimitives().generateEncryptionIdentity();
3462
+
3463
+ /// AUTHOR-SIDE SEAL: encrypt `plaintext` into its on-chain ciphertext envelope, bound to the scope + #58
3464
+ /// stable sids (rename-safe). Returns the envelope string the encrypted field value is written as.
3465
+ Future<String> cryptoSealField({
3466
+ required String scopeKey,
3467
+ required String scope,
3468
+ required int epoch,
3469
+ required String aggSid,
3470
+ required String fieldSid,
3471
+ required String plaintext,
3472
+ }) async {
3473
+ final response = await _invokeFramework(
3474
+ nomos_protocol.FrameworkRequest(
3475
+ session: _runtimeSession(),
3476
+ sealField: nomos_protocol.SealFieldRequest(
3477
+ scopeKey: base64Decode(scopeKey),
3478
+ scope: scope,
3479
+ epoch: fixnum.Int64(epoch),
3480
+ aggregateSid: aggSid,
3481
+ fieldSid: fieldSid,
3482
+ plaintext: plaintext,
3483
+ ),
3484
+ ),
3485
+ );
3486
+ if (!response.hasSealField()) {
3487
+ throw StateError(
3488
+ 'Runtime v2 returned ${response.whichResult().name} for field sealing',
3489
+ );
3490
+ }
3491
+ return response.sealField.envelope;
3492
+ }
3493
+
3494
+ /// FRAMEWORK-INTERNAL `.owned()` key lane. The connected JS holon derives epoch 0 from its
3495
+ /// already-persisted device credential; generated clients call this without exposing key custody to apps.
3496
+ Future<NomosScopeKey> nomosOwnedScopeKey(
3497
+ String scope, {
3498
+ String? recordType,
3499
+ String? recordId,
3500
+ }) async {
3501
+ final response = await _invokeFramework(
3502
+ nomos_protocol.FrameworkRequest(
3503
+ session: _runtimeSession(),
3504
+ ownedScopeKey: nomos_protocol.OwnedScopeKeyRequest(
3505
+ scope: scope,
3506
+ record: recordType != null && recordId != null
3507
+ ? nomos_protocol.OwnedRecordRef(
3508
+ type: recordType,
3509
+ id: recordId,
3510
+ )
3511
+ : null,
3512
+ ),
3513
+ ),
3514
+ );
3515
+ if (!response.hasOwnedScopeKey()) {
3516
+ throw StateError(
3517
+ 'Runtime v2 returned ${response.whichResult().name} for owned scope key',
3518
+ );
3519
+ }
3520
+ return _scopeKeyFromFramework(response.ownedScopeKey, fallbackScope: scope);
3521
+ }
3522
+
3523
+ /// Every key epoch this principal currently holds for one owned record. Generated reads use the
3524
+ /// complete set so history remains readable after rotations, shares and transfers.
3525
+ Future<List<NomosScopeKey>> nomosOwnedScopeKeys(
3526
+ String scope, {
3527
+ required String recordType,
3528
+ required String recordId,
3529
+ }) async {
3530
+ final response = await _invokeFramework(
3531
+ nomos_protocol.FrameworkRequest(
3532
+ session: _runtimeSession(),
3533
+ ownedScopeKeys: nomos_protocol.OwnedScopeKeysRequest(
3534
+ scope: scope,
3535
+ record: nomos_protocol.OwnedRecordRef(
3536
+ type: recordType,
3537
+ id: recordId,
3538
+ ),
3539
+ ),
3540
+ ),
3541
+ );
3542
+ if (!response.hasOwnedScopeKeys()) {
3543
+ throw StateError(
3544
+ 'Runtime v2 returned ${response.whichResult().name} for owned scope keys',
3545
+ );
3546
+ }
3547
+ return response.ownedScopeKeys.keys
3548
+ .map((key) => _scopeKeyFromFramework(key, fallbackScope: scope))
3549
+ .toList(growable: false);
3550
+ }
3551
+
3552
+ /// Make this signed-in principal ready to receive record shares. Generated clients call this while
3553
+ /// binding; tenant code never handles a key, certificate or identity tuple.
3554
+ Future<void> nomosEnsureOwnedIdentity({
3555
+ required String scope,
3556
+ required String domain,
3557
+ String? domainHash,
3558
+ }) async {
3559
+ final response = await _invokeFramework(
3560
+ nomos_protocol.FrameworkRequest(
3561
+ session: _runtimeSession(),
3562
+ ensureOwnedIdentity: nomos_protocol.EnsureOwnedIdentityRequest(
3563
+ scope: scope,
3564
+ domain: domain,
3565
+ domainSha256: domainHash,
3566
+ ),
3567
+ ),
3568
+ );
3569
+ if (!response.hasOwnedIdentityEnsured()) {
3570
+ throw StateError(
3571
+ 'Runtime v2 returned ${response.whichResult().name} while ensuring owned identity',
3572
+ );
3573
+ }
3574
+ }
3575
+
3576
+ /// Opt into designated-peer all-device-loss recovery. All protocol fields remain in the JS/kernel client.
3577
+ Future<nomos_protocol.OwnedIdentityProtection> nomosProtectOwnedIdentity({
3578
+ required String scope,
3579
+ required String domain,
3580
+ required String domainHash,
3581
+ required String recoveryPrincipal,
3582
+ }) async {
3583
+ final response = await _invokeFramework(
3584
+ nomos_protocol.FrameworkRequest(
3585
+ session: _runtimeSession(),
3586
+ protectOwnedIdentity: nomos_protocol.ProtectOwnedIdentityRequest(
3587
+ scope: scope,
3588
+ domain: domain,
3589
+ domainSha256: domainHash,
3590
+ recoveryPrincipal: recoveryPrincipal,
3591
+ ),
3592
+ ),
3593
+ );
3594
+ if (!response.hasProtectOwnedIdentity()) {
3595
+ throw StateError(
3596
+ 'Runtime v2 returned ${response.whichResult().name} while protecting owned identity',
3597
+ );
3598
+ }
3599
+ return response.protectOwnedIdentity;
3600
+ }
3601
+
3602
+ /// Prepare the private framework payload behind generated share/unshare/transfer methods.
3603
+ Future<NomosOwnedRecordPreparation>
3604
+ nomosPrepareOwnedRecordAction<TAggregate>({
3605
+ required String action,
3606
+ required String scope,
3607
+ required NomosRef<TAggregate> record,
3608
+ required String peer,
3609
+ }) async {
3610
+ final protocolAction = switch (action) {
3611
+ 'share' => nomos_protocol.OwnedRecordAction.OWNED_RECORD_ACTION_SHARE,
3612
+ 'unshare' => nomos_protocol.OwnedRecordAction.OWNED_RECORD_ACTION_UNSHARE,
3613
+ 'transfer' =>
3614
+ nomos_protocol.OwnedRecordAction.OWNED_RECORD_ACTION_TRANSFER,
3615
+ _ => throw ArgumentError.value(action, 'action', 'unknown owned action'),
3616
+ };
3617
+ final response = await _invokeFramework(
3618
+ nomos_protocol.FrameworkRequest(
3619
+ session: _runtimeSession(),
3620
+ prepareOwnedRecordAction:
3621
+ nomos_protocol.PrepareOwnedRecordActionRequest(
3622
+ action: protocolAction,
3623
+ scope: scope,
3624
+ record: nomos_protocol.OwnedRecordRef(
3625
+ type: NmsInternalNomosRefs.type(record),
3626
+ id: NmsInternalNomosRefs.id(record),
3627
+ ),
3628
+ peer: peer,
3629
+ ),
3630
+ ),
3631
+ );
3632
+ if (!response.hasPrepareOwnedRecordAction()) {
3633
+ throw StateError(
3634
+ 'Runtime v2 returned ${response.whichResult().name} for owned record preparation',
3635
+ );
3636
+ }
3637
+ final value = response.prepareOwnedRecordAction;
3638
+ final ownedCustody = value.hasOwnedCustodyUsda()
3639
+ ? OwnedCustody.decodeUsda(
3640
+ Uint8List.fromList(value.ownedCustodyUsda),
3641
+ )
3642
+ : null;
3643
+ NomosOwnedWrapPreparation wrap(nomos_protocol.OwnedKeyWrap value) =>
3644
+ NomosOwnedWrapPreparation(
3645
+ recipientPrincipal: value.recipientPrincipal,
3646
+ recipientPubKey: value.recipientPublicKey,
3647
+ hpkeEpk: base64Encode(value.ephemeralPublicKey),
3648
+ ct: base64Encode(value.ciphertext),
3649
+ );
3650
+ NomosHistoricOwnedWrapPreparation historicWrap(
3651
+ nomos_protocol.OwnedKeyWrap value,
3652
+ ) =>
3653
+ NomosHistoricOwnedWrapPreparation(
3654
+ recipientPrincipal: value.recipientPrincipal,
3655
+ recipientPubKey: value.recipientPublicKey,
3656
+ hpkeEpk: base64Encode(value.ephemeralPublicKey),
3657
+ ct: base64Encode(value.ciphertext),
3658
+ epoch: value.hasEpoch() ? value.epoch.toInt() : 0,
3659
+ );
3660
+ switch (value.whichAction()) {
3661
+ case nomos_protocol.OwnedRecordPreparation_Action.share:
3662
+ final prepared = value.share;
3663
+ return NomosOwnedSharePreparation(
3664
+ ownedKeySubject: value.ownedKeySubject,
3665
+ ownedCustody: ownedCustody,
3666
+ recordId: prepared.recordId,
3667
+ peer: prepared.peer,
3668
+ epoch: prepared.epoch.toInt(),
3669
+ sharedAt: prepared.sharedAt,
3670
+ wraps: prepared.wraps.map(historicWrap).toList(growable: false),
3671
+ );
3672
+ case nomos_protocol.OwnedRecordPreparation_Action.unshare:
3673
+ final prepared = value.unshare;
3674
+ return NomosOwnedUnsharePreparation(
3675
+ ownedKeySubject: value.ownedKeySubject,
3676
+ ownedCustody: ownedCustody,
3677
+ recordId: prepared.recordId,
3678
+ peer: prepared.peer,
3679
+ epoch: prepared.epoch.toInt(),
3680
+ changedAt: prepared.changedAt,
3681
+ wraps: prepared.wraps.map(wrap).toList(growable: false),
3682
+ );
3683
+ case nomos_protocol.OwnedRecordPreparation_Action.transfer:
3684
+ final prepared = value.transfer;
3685
+ return NomosOwnedTransferPreparation(
3686
+ ownedKeySubject: value.ownedKeySubject,
3687
+ ownedCustody: ownedCustody,
3688
+ recordId: prepared.recordId,
3689
+ newOwner: prepared.newOwner,
3690
+ previousReaders: List.unmodifiable(prepared.previousReaders),
3691
+ epoch: prepared.epoch.toInt(),
3692
+ changedAt: prepared.changedAt,
3693
+ historicWraps:
3694
+ prepared.historicWraps.map(historicWrap).toList(growable: false),
3695
+ );
3696
+ case nomos_protocol.OwnedRecordPreparation_Action.notSet:
3697
+ throw const FormatException(
3698
+ 'Nomos owned record preparation has no result arm',
3699
+ );
3700
+ }
3701
+ }
3702
+
3703
+ /// FRAMEWORK-INTERNAL business birth: derive the unborn child's owned key from its deterministic address.
3704
+ Future<NomosScopeKey> nomosOwnedBirthScopeKey(
3705
+ String scope,
3706
+ String namespace,
3707
+ Object? key,
3708
+ String recordType,
3709
+ String recordId,
3710
+ ) async {
3711
+ final response = await _invokeFramework(
3712
+ nomos_protocol.FrameworkRequest(
3713
+ session: _runtimeSession(),
3714
+ ownedBirthScopeKey: nomos_protocol.OwnedBirthScopeKeyRequest(
3715
+ scope: scope,
3716
+ namespace: namespace,
3717
+ key: key.toString(),
3718
+ record: nomos_protocol.OwnedRecordRef(
3719
+ type: recordType,
3720
+ id: recordId,
3721
+ ),
3722
+ ),
3723
+ ),
3724
+ );
3725
+ if (!response.hasOwnedBirthScopeKey()) {
3726
+ throw StateError(
3727
+ 'Runtime v2 returned ${response.whichResult().name} for owned birth scope key',
3728
+ );
3729
+ }
3730
+ return _scopeKeyFromFramework(response.ownedBirthScopeKey,
3731
+ fallbackScope: scope);
3732
+ }
3733
+
3734
+ /// FRAMEWORK-INTERNAL: issue an opaque request for this session's current device.
3735
+ Future<OwnedDeviceRequest> nomosOwnedDeviceRequest(String scope) async {
3736
+ final response = await _nmsKernelQuery(
3737
+ session: _sessionName,
3738
+ authority: _nmsQueryAuthority(null, null, _sessionName),
3739
+ query: nomos_protocol.QueryRequest(
3740
+ ownedDeviceRequest:
3741
+ nomos_protocol.OwnedDeviceRequestQuery(scope: scope),
3742
+ ),
3743
+ );
3744
+ if (!response.hasOwnedDeviceRequest()) {
3745
+ throw StateError(
3746
+ 'Kernel returned ${response.whichResult().name}, expected ownedDeviceRequest',
3747
+ );
3748
+ }
3749
+ return OwnedDeviceRequest.decodeUsda(
3750
+ Uint8List.fromList(response.ownedDeviceRequest.requestUsda),
3751
+ );
3752
+ }
3753
+
3754
+ /// FRAMEWORK-INTERNAL: validate an opaque request and prepare the atomic signer + custody offer.
3755
+ Future<NomosOwnedDeviceApprovalPreparation> nomosPrepareOwnedDeviceApproval(
3756
+ OwnedDeviceRequest request,
3757
+ String scope,
3758
+ ) async {
3759
+ final response = await _nmsKernelQuery(
3760
+ session: _sessionName,
3761
+ authority: _nmsQueryAuthority(null, null, _sessionName),
3762
+ query: nomos_protocol.QueryRequest(
3763
+ ownedPrepareApproval: nomos_protocol.OwnedPrepareApprovalQuery(
3764
+ scope: scope,
3765
+ requestUsda: request.encodeUsda(),
3766
+ ),
3767
+ ),
3768
+ );
3769
+ if (!response.hasOwnedPrepareApproval()) {
3770
+ throw StateError(
3771
+ 'Kernel returned ${response.whichResult().name}, expected ownedPrepareApproval',
3772
+ );
3773
+ }
3774
+ final prepared = response.ownedPrepareApproval;
3775
+ return NomosOwnedDeviceApprovalPreparation(
3776
+ newKeySubject: prepared.newKeySubject,
3777
+ ownedCustody: OwnedCustody.decodeUsda(
3778
+ Uint8List.fromList(prepared.ownedCustodyUsda),
3779
+ ),
3780
+ verifiedVia: prepared.verifiedVia,
3781
+ validFromEpoch: prepared.validFromEpoch.toInt(),
3782
+ approvedAt: DateTime.fromMillisecondsSinceEpoch(
3783
+ prepared.approvedAtUnixMillis.toInt(),
3784
+ isUtc: true,
3785
+ ).toIso8601String(),
3786
+ ownedKeySubject: prepared.ownedKeySubject,
3787
+ );
3788
+ }
3789
+
3790
+ /// Wrap a scope key to a recipient using the exact generated kernel frame.
3791
+ Future<nomos_protocol.WrapScopeKeyResponse> cryptoWrapKey({
3792
+ required String recipientPub,
3793
+ required String scopeKey,
3794
+ }) =>
3795
+ _kernelPrimitives().wrapScopeKey(
3796
+ recipientPublicKey: base64Decode(recipientPub),
3797
+ scopeKey: base64Decode(scopeKey),
3798
+ );
3799
+
3800
+ /// Unwrap an on-chain key using the exact generated kernel frame.
3801
+ Future<Uint8List> cryptoUnwrapKey({
3802
+ required String secret,
3803
+ required String hpkeEpk,
3804
+ required String ct,
3805
+ }) async =>
3806
+ Uint8List.fromList(
3807
+ (await _kernelPrimitives().unwrapScopeKey(
3808
+ recipientSecretKey: base64Decode(secret),
3809
+ ephemeralPublicKey: base64Decode(hpkeEpk),
3810
+ ciphertext: base64Decode(ct),
3811
+ ))
3812
+ .scopeKey,
3813
+ );
3814
+
3815
+ NmsKernelPrimitives _kernelPrimitives() {
3816
+ final transport = _eventPlane._transport;
3817
+ if (transport is! NomosKernelCallTransport) {
3818
+ throw UnsupportedError(
3819
+ 'This peer does not expose generated kernel calls',
3820
+ );
3821
+ }
3822
+ return NmsKernelPrimitives(
3823
+ transport: transport as NomosKernelCallTransport,
3824
+ session: _runtimeSession(),
3825
+ );
3826
+ }
3827
+
3828
+ /// Framework-filtered maintained conflicts. The kernel returns authenticated
3829
+ /// USDA; only the generated client knows the composed application field types.
3830
+ Future<Uint8List> nmsPendingConflictsUsda(
3831
+ String? aggregateType, [
3832
+ String? session,
3833
+ ]) =>
3834
+ _nmsKernelQueryUsda(
3835
+ session: session,
3836
+ authority: _nmsQueryAuthority(null, null, session),
3837
+ query: nomos_protocol.QueryRequest(
3838
+ pendingConflicts: nomos_protocol.PendingConflictsQuery(
3839
+ aggregateType: aggregateType,
3840
+ ),
3841
+ ),
3842
+ );
3843
+
3844
+ /// Counts-only workspace summary over the maintained conflict index.
3845
+ Future<Uint8List> nmsPendingConflictSummaryUsda([
3846
+ String? session,
3847
+ ]) =>
3848
+ _nmsKernelQueryUsda(
3849
+ session: session,
3850
+ authority: _nmsQueryAuthority(null, null, session),
3851
+ query: nomos_protocol.QueryRequest(
3852
+ pendingConflictSummary: nomos_protocol.PendingConflictSummaryQuery(),
3853
+ ),
3854
+ );
3855
+
3856
+ /// The local ledger tip (an O(1) ref read). Cheap change signal — persistence skips re-exporting an
3857
+ /// unchanged snapshot when [head] matches the last saved tip. Runtime v2 carries this as an optional
3858
+ /// protobuf field so an unborn workspace is distinct from a malformed reception result.
3859
+ Future<String> head() async {
3860
+ final response = await _invokeRuntimeV2(
3861
+ nomos_protocol.RuntimeRequest(
3862
+ workspaceStatus: nomos_protocol.WorkspaceStatusRequest(
3863
+ session: _runtimeSession(),
3864
+ ),
3865
+ ),
3866
+ );
3867
+ if (!response.hasWorkspaceStatus()) {
3868
+ throw StateError(
3869
+ 'Runtime v2 returned ${response.whichOutcome().name}, expected workspaceStatus',
3870
+ );
3871
+ }
3872
+ return response.workspaceStatus.hasHead()
3873
+ ? response.workspaceStatus.head
3874
+ : '';
3875
+ }
3876
+
3877
+ /// Generated background-sync telemetry. Each item is a Runtime v2 event
3878
+ /// whose exact arm is either [SyncCompleted] or [LinkChanged].
3879
+ Stream<nomos_protocol.RuntimeEvent> get syncEvents =>
3880
+ _eventPlane._events.syncEvents;
3881
+
3882
+ /// LIVE pack byte-progress — the real loading bar. Each event carries `received`/`total` (the host's
3883
+ /// actual `Content-Length`, known before the body) + `kind` ('delta'|'full'). Emits only while a `/pack`
3884
+ /// transfer is moving (small syncs ride the inline offer-result piggyback — instant, no bar).
3885
+ Stream<TransferProgress> get transferProgress =>
3886
+ _eventPlane._events.progress.transfers;
3887
+
3888
+ /// Live typed progress from the canonical kernel while a long synchronous
3889
+ /// operation is in flight (currently cold custody verification/replay).
3890
+ Stream<KernelProgress> get kernelProgress =>
3891
+ _eventPlane._events.progress.kernel;
3892
+
3893
+ /// THE APP-READY SYNC STATE — a truthful aggregation of sync rounds, the doorbell, transfer progress, and a
3894
+ /// typed sync/link/transfer events. Map straight to a sync-state page. Reading
3895
+ /// this stream has no kernel or network side effect. [currentStatus] is the
3896
+ /// latest snapshot for a non-stream read.
3897
+ Stream<SyncStatus> get syncStatus {
3898
+ final session = _sessionName;
3899
+ return session == null
3900
+ ? _eventPlane._events.syncStatuses.applicationStream
3901
+ : _eventPlane._events.syncStatuses.sessionStream(session);
3902
+ }
3903
+
3904
+ SyncStatus get currentStatus {
3905
+ final session = _sessionName;
3906
+ return session == null
3907
+ ? _eventPlane._events.syncStatuses.applicationStatus
3908
+ : _eventPlane._events.syncStatuses.statusFor(session);
3909
+ }
3910
+
3911
+ SyncStatus _currentSessionStatus(String session) =>
3912
+ _eventPlane._events.syncStatuses.statusFor(session);
3913
+
3914
+ void _pushSessionStatus(String session, SyncStatus status) {
3915
+ _eventPlane._events.syncStatuses.pushSession(session, status);
3916
+ }
3917
+
3918
+ void _updateScopedStatus(
3919
+ String? session,
3920
+ SyncStatus Function(SyncStatus current) update,
3921
+ ) {
3922
+ _eventPlane._events.syncStatuses.update(
3923
+ session: session,
3924
+ connectedSessions: _eventPlane._sessions.boundNames,
3925
+ change: update,
3926
+ );
3927
+ }
3928
+
3929
+ void _foldApplicationStatusFromSessions() {
3930
+ _eventPlane._events.syncStatuses.foldApplication(
3931
+ _eventPlane._sessions.boundNames,
3932
+ );
3933
+ }
3934
+
3935
+ /// THE RECEIVING-GROUP CONVERGENCE TAIL as the office reports it (`ReceivingGroupConvergence`
3936
+ /// runtime events): the phase in progress, then one terminal CONVERGED or DEFERRED. A workspace
3937
+ /// session sees only its own group's events; the root reception sees the whole plane. Presentation
3938
+ /// only — the tail never fails the gate and the office retries it on the next open.
3939
+ Stream<nomos_protocol.ReceivingGroupConvergence>
3940
+ get receivingGroupConvergence {
3941
+ final session = _sessionName;
3942
+ final all = _eventPlane._events.receivingGroupConvergence;
3943
+ if (session == null) return all;
3944
+ return all.where((event) =>
3945
+ event.workspace ==
3946
+ (_eventPlane._sessions.isBound(session)
3947
+ ? _eventPlane._sessions.workspaceFor(session)
3948
+ : session));
3949
+ }
3950
+
3951
+ /// LIVE per-layer spans of the local pipeline (begin/end as they happen): mutex wait, githolon author,
3952
+ /// push, converge … The host renders in-progress bars so a hang is visible WHILE it blocks, not after.
3953
+ Stream<NomosSpanEvent> get spanEvents => _eventPlane._events.progress.spans;
3954
+
3955
+ /// The buffered recent span events — replay these on first build to recover the BOOT flamepath
3956
+ /// (fetch wasm / open local custody / instantiate) that streamed before the UI subscribed.
3957
+ List<NomosSpanEvent> get recentSpans =>
3958
+ _eventPlane._events.progress.recentSpans;
3959
+
3960
+ /// TIER-2 SYNC SIZE — how many bytes this workspace would transfer to catch up to main, WITHOUT downloading
3961
+ /// them (a HEAD on the serving pack; the host answers Content-Length only, cached ~15s). Sum across your open
3962
+ /// workspaces for an aggregate "X MB to sync" total BEFORE downloading. `{ bytes, kind:'delta'|'full'|
3963
+ /// 'up-to-date' }`; `bytes` is null if the host omitted the length.
3964
+ Future<nomos_protocol.WorkspaceTransferEstimate> syncSize() async {
3965
+ final response = await _invokeRuntimeV2(
3966
+ nomos_protocol.RuntimeRequest(
3967
+ estimateWorkspaceTransfer:
3968
+ nomos_protocol.EstimateWorkspaceTransferRequest(
3969
+ session: _runtimeSession(),
3970
+ ),
3971
+ ),
3972
+ );
3973
+ if (!response.hasWorkspaceTransferEstimate()) {
3974
+ throw StateError(
3975
+ 'Runtime v2 returned ${response.whichOutcome().name} for transfer estimate',
3976
+ );
3977
+ }
3978
+ return response.workspaceTransferEstimate;
3979
+ }
3980
+
3981
+ /// Carry one compiler-authored USDA matrix to the kernel and receive the
3982
+ /// desk's complete verdict: the kernel's per-member examination, plus the one
3983
+ /// correspondence fact — whether missing law might still arrive — that a client
3984
+ /// would otherwise be tempted to infer from a commit count.
3985
+ Future<nomos_protocol.InterfaceCompatibilityVerdict> checkCompatibility(
3986
+ List<int> applicationMatrixUsda,
3987
+ ) {
3988
+ final session = _runtimeSession();
3989
+ final matrix = Uint8List.fromList(applicationMatrixUsda);
3990
+ final entries = _eventPlane._compatibilityBySession.putIfAbsent(
3991
+ session,
3992
+ () => <_CompatibilityCacheEntry>[],
3993
+ );
3994
+ for (final entry in entries) {
3995
+ if (_sameBytes(entry.matrix, matrix)) return entry.answer;
3996
+ }
3997
+ late final _CompatibilityCacheEntry entry;
3998
+ final answer = _checkCompatibility(session, matrix).catchError(
3999
+ (Object error, StackTrace stackTrace) {
4000
+ entries.remove(entry);
4001
+ Error.throwWithStackTrace(error, stackTrace);
4002
+ },
4003
+ );
4004
+ entry = _CompatibilityCacheEntry(matrix, answer);
4005
+ entries.add(entry);
4006
+ return answer;
4007
+ }
4008
+
4009
+ Future<nomos_protocol.InterfaceCompatibilityVerdict> _checkCompatibility(
4010
+ String session,
4011
+ Uint8List applicationMatrixUsda,
4012
+ ) async {
4013
+ final response = await _invokeFramework(
4014
+ nomos_protocol.FrameworkRequest(
4015
+ session: session,
4016
+ checkInterfaceCompatibility:
4017
+ nomos_protocol.CheckInterfaceCompatibilityRequest(
4018
+ applicationMatrixUsda: applicationMatrixUsda,
4019
+ ),
4020
+ ),
4021
+ );
4022
+ if (!response.hasInterfaceCompatibilityVerdict()) {
4023
+ throw StateError(
4024
+ 'Runtime v2 returned ${response.whichResult().name} for interface compatibility',
4025
+ );
4026
+ }
4027
+ return response.interfaceCompatibilityVerdict;
4028
+ }
4029
+
4030
+ /// Bind and return one stable language-native session handle.
4031
+ Future<NomosSession> open({
4032
+ required String workspace,
4033
+ String? credentialSourceSession,
4034
+ String? principal,
4035
+ String? session,
4036
+ }) async {
4037
+ await bindSession(
4038
+ workspace: workspace,
4039
+ credentialSourceSession: credentialSourceSession,
4040
+ principal: principal,
4041
+ session: session,
4042
+ );
4043
+ return this.session(session ?? workspace);
4044
+ }
4045
+
4046
+ /// Tear down the application realm, then stop the transport subscription and
4047
+ /// fail any remaining in-flight calls. The native kernel plane may remain
4048
+ /// resident for custody/cache reuse, but its application-owned sessions,
4049
+ /// watches and continuous-sync loops must not survive this client.
4050
+ Future<void> dispose() async {
4051
+ try {
4052
+ // Stop every typed watch first, while the realm and transport can still
4053
+ // answer the unwatch; closing the realm underneath live watches leaves
4054
+ // their cancellations racing the transport teardown.
4055
+ await _events.typedWatches.close().timeout(const Duration(seconds: 10));
4056
+ } catch (_) {
4057
+ // A dead peer cannot acknowledge unwatch; the realm close below (or the
4058
+ // host's transport disposal) releases the subscriptions regardless.
4059
+ }
4060
+ try {
4061
+ final response = await _invokeRuntimeV2(
4062
+ nomos_protocol.RuntimeRequest(
4063
+ closeRealm: nomos_protocol.CloseRealmRequest(),
4064
+ ),
4065
+ ).timeout(const Duration(seconds: 10));
4066
+ if (!response.hasRealmClosed()) {
4067
+ throw StateError(
4068
+ 'Runtime v2 returned ${response.whichOutcome().name} for closeRealm',
4069
+ );
4070
+ }
4071
+ } catch (_) {
4072
+ // A dead/unstarted embedding has no reachable runner to release. The
4073
+ // enclosing engine host is still disposed below by NomosScope; never let
4074
+ // teardown strand the Flutter lifecycle on an unavailable transport.
4075
+ }
4076
+ await _runtimeEventSub?.cancel();
4077
+ _runtimeEventSub = null;
4078
+ await _events.close();
4079
+ _receipts.close();
4080
+ }
4081
+ }
4082
+
4083
+ /// A LIVE WORKSPACE on a device's local plane — the FIRST-CLASS multi-workspace object. Everything about
4084
+ /// Nomos is multi-workspace: a running app holds ONE [DartReception] (this device's local holarchy plane) and
4085
+ /// MANY [NomosSession]s on it — the user's home, each estate, each catalogue, any locally-born child — a
4086
+ /// dozen or hundreds, interacting offline. Get one with `reception.session('estate')` and hand it to a generated
4087
+ /// typed client: `EstateClient(reception.session('estate'))`.
4088
+ ///
4089
+ /// A session is an ephemeral interaction view pinned to one workspace. It is
4090
+ /// not custody, placement, a Replica Follow or a cloud connection. The peer
4091
+ /// owns those durable lifecycles independently; this view only addresses
4092
+ /// application operations and observations.
4093
+ ///
4094
+ /// Implementation: it EXTENDS [DartReception] but holds no state of its own: it shares the [parent]
4095
+ /// plane's ONE transport, correlation, and watch streams, and overrides the sole low-level [_call] to stamp
4096
+ /// `args.session` and route through the parent — so request/reply, watches, and telemetry all resolve on the
4097
+ /// one message loop with no aliasing. The single-workspace [DartReception] methods are documented sugar over an
4098
+ /// inferred default session; a multi-workspace app works through session handles.
4099
+ class NomosSession extends DartReception {
4100
+ /// The device's local plane every op routes through (the shared [DartReception]).
4101
+ final DartReception parent;
4102
+
4103
+ /// The workspace name this session is pinned to.
4104
+ final String workspace;
4105
+
4106
+ NomosSession(this.parent, this.workspace) : super._view(parent);
4107
+
4108
+ /// THE SEAM (see [DartReception._eventPlane]): all event dispatch + plane state resolves on the PARENT's
4109
+ /// registries (chased to the root, so nested delegation stays correct). This ONE override is why the base
4110
+ /// class's Stream-returning methods work unmodified on a session — no per-method overrides.
4111
+ @override
4112
+ DartReception get _eventPlane => parent._eventPlane;
4113
+
4114
+ /// Base implementations scope law-state (compat / onLawChanged) to THIS workspace via this.
4115
+ @override
4116
+ String? get _sessionName => workspace;
4117
+
4118
+ /// Whether this interaction handle has been bound to the resident peer.
4119
+ bool get isBound => parent.boundSessions.contains(workspace);
4120
+
4121
+ /// Sibling workspaces resolve on the shared plane — delegate to the parent's registry (never nest a
4122
+ /// session inside a session).
4123
+ @override
4124
+ NomosSession session(String workspace) => parent.session(workspace);
4125
+
4126
+ /// Offer this workspace to the Peer Office model-adoption desk: observe its
4127
+ /// current law for the application's domain keys, and when it still serves
4128
+ /// the previous law, stage the approved package, prove the transition and
4129
+ /// promote it through the ordinary signed lifecycle offers. The office runs
4130
+ /// the whole procedure and answers `current`, `adopted`, a typed `deferred`
4131
+ /// or — with [dryRun] — `previewed`; this only transports the paper.
4132
+ Future<NomosModelAdoption> adoptApplicationModel(
4133
+ {bool dryRun = false}) async {
4134
+ final model = nomosInternalApprovedApplicationModel;
4135
+ if (model == null) {
4136
+ throw StateError(
4137
+ 'No approved application model is retained on this plane; supply one at the application Reception.',
4138
+ );
4139
+ }
4140
+ final response = await _invokeRuntimeV2(
4141
+ nomos_protocol.RuntimeRequest(
4142
+ adoptApplicationModel: nomos_protocol.AdoptApplicationModelRequest(
4143
+ session: workspace,
4144
+ lawSha256: model.lawSha256,
4145
+ domainKeys: model.domainKeys,
4146
+ acceptanceReason: model.reason,
4147
+ dryRun: dryRun,
4148
+ ),
4149
+ ),
4150
+ );
4151
+ if (response.hasFailure()) {
4152
+ throw StateError(
4153
+ '${response.failure.code}: ${response.failure.message}',
4154
+ );
4155
+ }
4156
+ if (!response.hasApplicationModelAdopted()) {
4157
+ throw StateError(
4158
+ 'Runtime v2 returned ${response.whichOutcome().name} for model adoption',
4159
+ );
4160
+ }
4161
+ return NomosModelAdoption.fromRuntime(response.applicationModelAdopted);
4162
+ }
4163
+
4164
+ /// The model-adoption desk's observations for this workspace.
4165
+ Stream<NomosModelAdoptionProgress> get modelAdoptionProgress =>
4166
+ _eventPlane._events.modelAdoption
4167
+ .where((progress) => progress.workspace == workspace);
4168
+
4169
+ /// Framework seam: offer this workspace to the model-adoption desk without
4170
+ /// gating the caller on it. A transport failure is projected as a deferred
4171
+ /// observation so the sync-status surface shows it; nothing is retried.
4172
+ void nomosInternalAdoptApplicationModelInBackground() {
4173
+ final plane = _eventPlane;
4174
+ final model = nomosInternalApprovedApplicationModel;
4175
+ if (model == null) return;
4176
+ final key = (workspace, model);
4177
+ unawaited(plane._backgroundModelAdoptions.putIfAbsent(
4178
+ key,
4179
+ () =>
4180
+ adoptApplicationModel().then<void>((_) {}, onError: (Object error) {
4181
+ final progress = NomosModelAdoptionProgress(
4182
+ workspace: workspace,
4183
+ phase: NomosModelAdoptionPhase.observe,
4184
+ outcome: NomosModelAdoptionOutcome.deferred,
4185
+ detail: 'the adoption paper could not be transported: $error',
4186
+ );
4187
+ plane._events.publishModelAdoption(progress);
4188
+ plane._updateScopedStatus(
4189
+ workspace,
4190
+ (current) => current.copyWith(modelAdoption: progress),
4191
+ );
4192
+ }).whenComplete(() {
4193
+ plane._backgroundModelAdoptions.remove(key);
4194
+ })));
4195
+ }
4196
+
4197
+ /// Bind a sibling interaction on the shared language view. The sibling may
4198
+ /// share this session's credential capability; it inherits no placement,
4199
+ /// custody or synchronization lifecycle.
4200
+ @override
4201
+ Future<NomosSession> bindWorkspace(
4202
+ String workspace, {
4203
+ String? credentialSourceSession,
4204
+ }) =>
4205
+ parent.bindWorkspace(
4206
+ workspace,
4207
+ credentialSourceSession: credentialSourceSession ?? this.workspace,
4208
+ );
4209
+
4210
+ /// A followed ref binds on the shared language view; the peer routes its operations.
4211
+ @override
4212
+ Future<NomosSession> follow<TAggregate>(
4213
+ NomosRef<TAggregate> ref, {
4214
+ String? credentialSourceSession,
4215
+ }) =>
4216
+ parent.follow(
4217
+ ref,
4218
+ credentialSourceSession: credentialSourceSession ?? workspace,
4219
+ );
4220
+
4221
+ // The parent owns the ONE message loop + lifecycle; a session never subscribes, never signals ready of its
4222
+ // own, and never tears the plane down (the harness owns `parent.dispose()`).
4223
+ @override
4224
+ Future<void> get ready => parent.ready;
4225
+ @override
4226
+ Future<void> dispose() async {}
4227
+
4228
+ // NO per-method Stream/watch/snapshot overrides live here anymore — the base class routes ALL event
4229
+ // dispatch + plane state through [_eventPlane] (and law-scopes compat/onLawChanged via [_sessionName]),
4230
+ // and the watch ops still carry this session's workspace because their registration goes through the
4231
+ // overridden [_call]. Adding a `Stream`-returning override below is almost certainly re-opening the
4232
+ // dead-registry bug class — see `test/event_plane_seam_test.dart`.
4233
+ }
4234
+
4235
+ /// A Reception-backed foreign reader over a peer's multi-workspace [DartReception]
4236
+ /// plane. Constructed via `reception.foreignReader()`; a generated `ForeignRef`
4237
+ /// resolves against it. The peer runtime decides how the target is served.
4238
+ class _ReceptionForeignReader implements NomosForeignReader {
4239
+ _ReceptionForeignReader(this._reception);
4240
+ final DartReception _reception;
4241
+
4242
+ @override
4243
+ NomosForeignWorkspaceReader foreignWorkspace(String workspace) =>
4244
+ _ReceptionForeignWorkspaceReader(_reception, workspace);
4245
+ }
4246
+
4247
+ /// A per-workspace read handle which binds [_workspace] lazily on first use.
4248
+ class _ReceptionForeignWorkspaceReader implements NomosForeignWorkspaceReader {
4249
+ _ReceptionForeignWorkspaceReader(this._reception, this._workspace);
4250
+ final DartReception _reception;
4251
+ final String _workspace;
4252
+ Future<void>? _binding;
4253
+
4254
+ // Bind once. The first read is then routed by the runtime according to the
4255
+ // target's placement; typed unavailability distinguishes an absent local
4256
+ // replica from a remote peer that cannot currently be reached.
4257
+ Future<void> _ensureBound() => _binding ??= _reception.bindSession(
4258
+ workspace: _workspace,
4259
+ session: _workspace,
4260
+ );
4261
+
4262
+ @override
4263
+ Future<Uint8List> queryById(String aggregateType, String id) async {
4264
+ await _ensureBound();
4265
+ return _reception.session(_workspace).nmsQueryByIdUsda(aggregateType, id);
4266
+ }
4267
+
4268
+ // A FOREIGN WATCH IS AN ORDINARY WATCH. It used to re-read the record on every
4269
+ // plane sync tick — a polling policy chosen in one client, so a foreign ref
4270
+ // behaved differently here than anywhere else and than it does in TypeScript.
4271
+ // Once bound, the foreign workspace is a session like any other, so the watch
4272
+ // goes down the office's own lane: the subscription, its interval, its
4273
+ // invalidation and its failure handling all belong to the desks that own them.
4274
+ @override
4275
+ Stream<Uint8List>? watchById(String aggregateType, String id) =>
4276
+ Stream<void>.fromFuture(_ensureBound()).asyncExpand(
4277
+ (_) =>
4278
+ _reception.session(_workspace).nmsWatchByIdUsda(aggregateType, id),
4279
+ );
4280
+ }