@githolon/dsl 0.108.0 → 0.108.2

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 (95) hide show
  1. package/dart/CHANGELOG.md +5 -0
  2. package/dart/lib/src/application_framework_calls.dart +299 -0
  3. package/dart/lib/src/generated_call.dart +96 -0
  4. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  5. package/dart/lib/src/node_runner_transport.dart +77 -1
  6. package/dart/lib/src/result_usda.dart +487 -0
  7. package/dart/lib/src/runner/node-entry.mjs +6401 -2102
  8. package/dart/lib/src/runner/offline-install.mjs +1538 -163
  9. package/dart/lib/src/runtime_v2_gateway.dart +54 -0
  10. package/dart/lib/src/runtime_v2_transport.dart +117 -0
  11. package/dart/lib/src/session_bound.dart +7 -2
  12. package/dart/lib/src/test_harness.dart +193 -112
  13. package/dart/lib/src/webview_bridge.dart +2120 -569
  14. package/dart/pubspec.yaml +2 -2
  15. package/dart/test/acquire_session_credentials_test.dart +1 -1
  16. package/dart/test/acquire_session_test.dart +68 -14
  17. package/dart/test/adv_realm_interference_test.dart +6 -4
  18. package/dart/test/cluster_births_test.dart +11 -8
  19. package/dart/test/cluster_local_autosync_test.dart +1 -1
  20. package/dart/test/cluster_scale_test.dart +3 -2
  21. package/dart/test/dlq_session_surface_test.dart +52 -35
  22. package/dart/test/first_app_gauntlet_test.dart +1 -1
  23. package/dart/test/local_sync_inert_test.dart +23 -26
  24. package/dart/test/offer_authentication_proof_test.dart +193 -0
  25. package/dart/test/result_usda_test.dart +49 -0
  26. package/dart/test/session_bound_test.dart +8 -11
  27. package/dart/test/stale_bundle_test.dart +18 -3
  28. package/dart/test/test_harness_test.dart +14 -27
  29. package/dart/test/webview_bridge_test.dart +504 -83
  30. package/dart_types/CHANGELOG.md +7 -0
  31. package/dart_types/lib/nomos_kernel_protocol.dart +3 -2
  32. package/dart_types/lib/src/custody.dart +64 -19
  33. package/dart_types/lib/src/local_work_rejected.dart +30 -45
  34. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pb.dart +169 -0
  35. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pbenum.dart +11 -0
  36. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +2021 -234
  37. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +175 -8
  38. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +8664 -0
  39. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +369 -0
  40. package/dart_types/lib/src/surfaced.dart +30 -25
  41. package/dart_types/pubspec.yaml +1 -1
  42. package/dart_types/test/surfaced_test.dart +2 -1
  43. package/package.json +1 -1
  44. package/runtime/generated-application-host.js +5736 -2047
  45. package/src/authoring.ts +3 -2
  46. package/src/births.ts +21 -0
  47. package/src/build_package.ts +30 -0
  48. package/src/business_birth.ts +39 -35
  49. package/src/codegen_call_usda.ts +1111 -0
  50. package/src/codegen_dart.ts +207 -282
  51. package/src/codegen_dart_application.ts +195 -252
  52. package/src/codegen_dart_client.ts +1978 -593
  53. package/src/codegen_dart_framework_calls.ts +291 -0
  54. package/src/codegen_dart_scenario.ts +7 -0
  55. package/src/codegen_proof.ts +3 -4
  56. package/src/codegen_provider_dart.ts +21 -7
  57. package/src/codegen_query_usda.ts +214 -0
  58. package/src/codegen_result_usda.ts +685 -0
  59. package/src/codegen_ts.ts +3945 -1111
  60. package/src/compile_package_main.ts +360 -32
  61. package/src/directive.ts +19 -4
  62. package/src/engine_entry.ts +45 -4
  63. package/src/framework/application_home.ts +2 -0
  64. package/src/framework/auth_provider_law.ts +262 -20
  65. package/src/framework/home.ts +32 -28
  66. package/src/framework/owned_custody.ts +19 -11
  67. package/src/framework/owned_custody_value.ts +51 -0
  68. package/src/framework/relations.ts +57 -21
  69. package/src/framework/signing_identity.ts +34 -0
  70. package/src/framework/workspaces.ts +31 -9
  71. package/src/framework_call_definitions.ts +53 -0
  72. package/src/framework_read_definitions.ts +45 -0
  73. package/src/identity_frontier.ts +58 -0
  74. package/src/index.ts +20 -1
  75. package/src/intent.ts +216 -16
  76. package/src/law_check.ts +19 -1
  77. package/src/law_graph.ts +57 -8
  78. package/src/law_graph_usda.ts +73 -5
  79. package/src/law_value_type.ts +106 -19
  80. package/src/read.ts +47 -0
  81. package/src/read_projection_namespace.ts +7 -0
  82. package/src/shape_compat.ts +38 -39
  83. package/src/stable_ids.ts +37 -2
  84. package/src/stable_ids_for_module.ts +64 -4
  85. package/src/test_call_usda.ts +83 -0
  86. package/src/wire.ts +36 -2
  87. package/src/wire_encode.ts +45 -1
  88. package/src/workspace_sharing.ts +27 -10
  89. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbjson.dart +0 -6096
  90. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbserver.dart +0 -50
  91. package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pbjson.dart +0 -178
  92. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pb.dart +0 -828
  93. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbenum.dart +0 -465
  94. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbjson.dart +0 -573
  95. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbserver.dart +0 -50
@@ -20,7 +20,9 @@ import 'package:meta/meta.dart' show internal;
20
20
  import 'dart:async';
21
21
  import 'dart:convert';
22
22
  import 'dart:math';
23
+ import 'dart:typed_data';
23
24
 
25
+ import 'package:fixnum/fixnum.dart' as fixnum;
24
26
  import 'package:nomos_types/nomos_kernel_protocol.dart' as nomos_protocol;
25
27
  import 'package:nomos_types/nomos_types.dart'
26
28
  show
@@ -45,6 +47,14 @@ import 'package:nomos_types/src/foreign_ref.dart'
45
47
 
46
48
  import 'session_bound.dart'
47
49
  show DomainClientModelAdoptionRequired, DomainNotInstalledForSession;
50
+ import 'generated_call.dart' show NmsGeneratedCall, NmsGeneratedQuery;
51
+ import 'application_framework_calls.dart' show OwnedCustody, OwnedDeviceRequest;
52
+ import 'runtime_v2_gateway.dart' show NmsRuntimeV2Gateway;
53
+ import 'runtime_v2_transport.dart'
54
+ show
55
+ NomosKernelCallTransport,
56
+ NomosRuntimeV2EventTransport,
57
+ NomosRuntimeV2Transport;
48
58
 
49
59
  /// THE ENGINE (architecture/session_bound_clients.md): the multi-workspace RUNTIME — mount/connect,
50
60
  /// the session registry, sync, custody + recovery, the home ceremony, and [NomosBridge.follow]
@@ -76,6 +86,48 @@ typedef NomosBirthCertificate = nomos_protocol.BirthCertificate;
76
86
  /// with its signature populated; the signing secret is a separate argument and never enters the value.
77
87
  typedef NomosBirthCertificateSpec = nomos_protocol.BirthCertificate;
78
88
 
89
+ /// Typed compare-and-merge witness carried by an offer. The protobuf schema is
90
+ /// the only transport shape; generated conflict helpers never rebuild it as a
91
+ /// string-keyed map.
92
+ typedef NomosConflictSnapshot = nomos_protocol.ConflictSnapshot;
93
+
94
+ /// Typed result of one child-workspace birth. The kernel protobuf is the
95
+ /// transport authority; hosts do not reinterpret birth state through maps.
96
+ typedef NomosBirthOutcome = nomos_protocol.BirthOutcome;
97
+
98
+ typedef NomosBirthOutcomeStatus = nomos_protocol.BirthOutcomeStatus;
99
+
100
+ NomosBirthOutcomeStatus _birthOutcomeStatusFromLegacyWire(Object? value) {
101
+ switch (value?.toString().toLowerCase()) {
102
+ case 'born':
103
+ return NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN;
104
+ case 'deferred':
105
+ return NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_DEFERRED;
106
+ case 'failed':
107
+ return NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_FAILED;
108
+ case 'refused':
109
+ return NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_REFUSED;
110
+ case 'unwarranted_cert':
111
+ case 'unwarrantedcert':
112
+ return NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_UNWARRANTED_CERT;
113
+ default:
114
+ return NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_UNSPECIFIED;
115
+ }
116
+ }
117
+
118
+ NomosBirthOutcome _birthOutcomeFromLegacyWire(Map<dynamic, dynamic> value) =>
119
+ NomosBirthOutcome(
120
+ workspace: value['workspace']?.toString(),
121
+ status: _birthOutcomeStatusFromLegacyWire(value['status']),
122
+ reason: value['reason']?.toString(),
123
+ code: value['code']?.toString(),
124
+ replayed: value['replayed'] == true,
125
+ lawHash: value['lawHash']?.toString(),
126
+ parentWorkspace: value['parentWorkspace']?.toString(),
127
+ childWorkspace: value['childWorkspace']?.toString(),
128
+ certificateRefusalReason: value['certificateRefusalReason']?.toString(),
129
+ );
130
+
79
131
  /// The protocol version the runner and the bridge agree on (`webview-runner.mjs` `WEBVIEW_BRIDGE_VERSION`).
80
132
  const int kWebviewBridgeVersion = 1;
81
133
 
@@ -107,10 +159,10 @@ class StaleRunnerBundle implements Exception {
107
159
  'NomosBridge(..., allowStaleRunnerBundle: true) for a deliberate mixed-version setup.';
108
160
  }
109
161
 
110
- /// The platform channel to the WebView runner: send a JSON-shaped message; receive the runner's messages.
111
- /// Web impl jsifies/dartifies over `dart:js_util`; mobile impl JSON-encodes over a WebView JS-channel. The
112
- /// bridge treats messages as plain `Map<String, Object?>` — the transport owns the channel serialization.
113
- abstract interface class BridgeTransport {
162
+ /// The application transport. Every host must provide the generated Runtime
163
+ /// v2 frame lane; [send] and [messages] remain only for operations and pushed
164
+ /// events that have not completed that migration yet.
165
+ abstract interface class BridgeTransport implements NomosRuntimeV2Transport {
114
166
  void send(Map<String, Object?> message);
115
167
  Stream<Map<String, Object?>> get messages;
116
168
  }
@@ -132,11 +184,17 @@ class TransferProgress {
132
184
  ? null
133
185
  : (received / total!).clamp(0.0, 1.0);
134
186
  bool get done => total != null && received >= total!;
135
- factory TransferProgress.fromMessage(Map<String, Object?> m) =>
187
+ factory TransferProgress.fromRuntime(
188
+ nomos_protocol.TransferProgress progress,
189
+ ) =>
136
190
  TransferProgress(
137
- received: _wireInteger(m['received']) ?? 0,
138
- total: _wireInteger(m['total']),
139
- kind: m['kind']?.toString() ?? 'delta',
191
+ received: progress.receivedBytes.toInt(),
192
+ total: progress.hasTotalBytes() ? progress.totalBytes.toInt() : null,
193
+ kind: switch (progress.kind) {
194
+ nomos_protocol.TransferKind.TRANSFER_KIND_FULL => 'full',
195
+ nomos_protocol.TransferKind.TRANSFER_KIND_PUSH => 'push',
196
+ _ => 'delta',
197
+ },
140
198
  );
141
199
  }
142
200
 
@@ -215,46 +273,56 @@ final class KernelProgress {
215
273
  double? get fraction =>
216
274
  (total <= 0) ? null : (completed / total).clamp(0.0, 1.0);
217
275
 
218
- factory KernelProgress.fromMessage(Map<String, Object?> message) =>
219
- KernelProgress(
220
- sequence: _wireInteger(message['sequence']) ?? 0,
221
- operation: KernelProgressOperation.values.firstWhere(
222
- (value) => value.name == message['operation'],
223
- orElse: () => KernelProgressOperation.unspecified,
224
- ),
225
- phase: KernelProgressPhase.values.firstWhere(
226
- (value) => value.name == message['phase'],
227
- orElse: () => KernelProgressPhase.unspecified,
228
- ),
229
- completed: _wireInteger(message['completed']) ?? 0,
230
- total: _wireInteger(message['total']) ?? 0,
231
- unit: KernelProgressUnit.values.firstWhere(
232
- (value) => value.name == message['unit'],
233
- orElse: () => KernelProgressUnit.unspecified,
234
- ),
235
- terminal: message['terminal'] == true,
236
- lawHydrationStep: KernelLawHydrationStep.values.firstWhere(
237
- (value) => value.name == message['lawHydrationStep'],
238
- orElse: () => KernelLawHydrationStep.unspecified,
239
- ),
240
- lawPackageSha256: message['lawPackageSha256']?.toString() ?? '',
241
- hostObservedElapsedMicros:
242
- _wireInteger(message['hostObservedElapsedMicros']) ?? 0,
243
- hostObservedPhaseElapsedMicros:
244
- _wireInteger(message['hostObservedPhaseElapsedMicros']) ?? 0,
245
- kernelAllocatorLiveBytes:
246
- _wireInteger(message['kernelAllocatorLiveBytes']) ?? 0,
247
- kernelAllocatorPeakBytes:
248
- _wireInteger(message['kernelAllocatorPeakBytes']) ?? 0,
249
- processResidentBytes:
250
- _wireInteger(message['processResidentBytes']) ?? 0,
251
- processPeakResidentBytes:
252
- _wireInteger(message['processPeakResidentBytes']) ?? 0,
253
- );
276
+ factory KernelProgress.fromRuntime(
277
+ nomos_protocol.KernelProgressObserved observed,
278
+ ) {
279
+ final snapshot = observed.snapshot;
280
+ return KernelProgress(
281
+ sequence: snapshot.sequence.toInt(),
282
+ operation: snapshot.operation.value == 1
283
+ ? KernelProgressOperation.restoreMain
284
+ : KernelProgressOperation.unspecified,
285
+ phase: switch (snapshot.phase.value) {
286
+ 1 => KernelProgressPhase.checkCustody,
287
+ 2 => KernelProgressPhase.readChain,
288
+ 3 => KernelProgressPhase.initializeEngine,
289
+ 4 => KernelProgressPhase.readTimeline,
290
+ 5 => KernelProgressPhase.hydrateLaw,
291
+ 6 => KernelProgressPhase.recoveryPrescan,
292
+ 7 => KernelProgressPhase.verifyIntents,
293
+ 8 => KernelProgressPhase.promoteCustody,
294
+ 9 => KernelProgressPhase.rebuildFrontier,
295
+ 10 => KernelProgressPhase.complete,
296
+ _ => KernelProgressPhase.unspecified,
297
+ },
298
+ completed: snapshot.completedUnits.toInt(),
299
+ total: snapshot.totalUnits.toInt(),
300
+ unit: switch (snapshot.unit.value) {
301
+ 1 => KernelProgressUnit.intent,
302
+ 2 => KernelProgressUnit.lawPackage,
303
+ _ => KernelProgressUnit.unspecified,
304
+ },
305
+ terminal: snapshot.terminal,
306
+ lawHydrationStep: switch (snapshot.lawHydrationStep.value) {
307
+ 1 => KernelLawHydrationStep.readClosure,
308
+ 2 => KernelLawHydrationStep.composeStage,
309
+ 3 => KernelLawHydrationStep.deriveSchemas,
310
+ 4 => KernelLawHydrationStep.loadPolicyShell,
311
+ 5 => KernelLawHydrationStep.packageComplete,
312
+ _ => KernelLawHydrationStep.unspecified,
313
+ },
314
+ lawPackageSha256: snapshot.lawPackageSha256,
315
+ hostObservedElapsedMicros: observed.hostObservedElapsedMicros.toInt(),
316
+ hostObservedPhaseElapsedMicros:
317
+ observed.hostObservedPhaseElapsedMicros.toInt(),
318
+ kernelAllocatorLiveBytes: snapshot.allocatorLiveBytes.toInt(),
319
+ kernelAllocatorPeakBytes: snapshot.allocatorPeakBytes.toInt(),
320
+ processResidentBytes: observed.processResidentBytes.toInt(),
321
+ processPeakResidentBytes: observed.processPeakResidentBytes.toInt(),
322
+ );
323
+ }
254
324
  }
255
325
 
256
- int? _wireInteger(Object? value) => value is num ? value.toInt() : null;
257
-
258
326
  /// One opaque point in this workspace's verified history. Applications use it as the handle passed to a
259
327
  /// generated client's `past.at(moment)`; [id] is framework identity, not a business field and need never be
260
328
  /// parsed. The remaining fields make a timeline useful without exposing commit or event-sourcing ceremony.
@@ -387,7 +455,7 @@ class NomosCreated {
387
455
  /// so the CANONICAL axes are axis-neutral (`[minX, minY] .. [maxX, maxY]`); the axes' meaning
388
456
  /// (earth coordinates, floor-plan mm, anything) is the tenant's business. The original geographic
389
457
  /// `minLng`/`minLat`/`maxLng`/`maxLat` named params remain accepted forever (era rule) — pass either
390
- /// shape. On the wire `toJson()` emits BOTH key sets, so any holon runtime (old or new) reads it.
458
+ /// source-level shape. The exact Runtime v2 protobuf boundary carries only the canonical axes.
391
459
  class NomosBbox {
392
460
  final double minX;
393
461
  final double minY;
@@ -427,18 +495,6 @@ class NomosBbox {
427
495
  'Spatial axes are generic; use maxX/maxY. Kept for source compat.',
428
496
  )
429
497
  double get maxLat => maxY;
430
-
431
- Map<String, Object?> toJson() => {
432
- 'minX': minX,
433
- 'minY': minY,
434
- 'maxX': maxX,
435
- 'maxY': maxY,
436
- // Geographic aliases so a pre-genericisation runner (which reads minLng/…) still works.
437
- 'minLng': minX,
438
- 'minLat': minY,
439
- 'maxLng': maxX,
440
- 'maxLat': maxY,
441
- };
442
498
  }
443
499
 
444
500
  /// One member's structural compat verdict — `ok`, plus the exact break [reason] when not.
@@ -526,7 +582,7 @@ class NomosScopeKey {
526
582
  final String? ownedKeySubject;
527
583
 
528
584
  /// Framework-private encrypted handoff, present only while seating the first device.
529
- final String? ownedCustody;
585
+ final OwnedCustody? ownedCustody;
530
586
 
531
587
  /// Framework-private deterministic child address, present only for an unborn owned business aggregate.
532
588
  final String? workspace;
@@ -548,14 +604,18 @@ class NomosScopeKey {
548
604
  };
549
605
  }
550
606
 
551
- /// The entire app-facing multi-device surface for owned business state. The request is deliberately an
552
- /// opaque string: apps may copy, share or QR it, but never parse certificates, key material or signer law.
607
+ /// The entire app-facing multi-device surface for owned business state. The
608
+ /// request is one compiler-generated framework value; transport encoders may
609
+ /// package it as USDA for QR/share without turning it into an opaque string.
553
610
  class NomosDevices {
554
611
  final NomosBridge bridge;
555
612
  final String domain;
556
613
  final String domainHash;
557
614
  final String? actor;
558
615
  final String scope;
616
+ final Future<NomosOfferOutcome> Function(
617
+ NomosOwnedDeviceApprovalPreparation preparation,
618
+ ) approvePrepared;
559
619
 
560
620
  const NomosDevices({
561
621
  required this.bridge,
@@ -563,11 +623,12 @@ class NomosDevices {
563
623
  required this.domainHash,
564
624
  required this.actor,
565
625
  required this.scope,
626
+ required this.approvePrepared,
566
627
  });
567
628
 
568
- Future<String> request() => bridge.nomosOwnedDeviceRequest(scope);
629
+ Future<OwnedDeviceRequest> request() => bridge.nomosOwnedDeviceRequest(scope);
569
630
 
570
- Future<NomosOfferOutcome> approve(String request) async {
631
+ Future<NomosOfferOutcome> approve(OwnedDeviceRequest request) async {
571
632
  final principal = actor ?? bridge.principal;
572
633
  if (principal == null || principal.trim().isEmpty) {
573
634
  throw StateError('devices.approve() needs a signed-in Nomos session');
@@ -576,13 +637,7 @@ class NomosDevices {
576
637
  request,
577
638
  scope,
578
639
  );
579
- return bridge.offerOutcome(
580
- domain: domain,
581
- directiveId: 'nomos_internal_approve_owned_device_7c1f5e',
582
- payload: payload,
583
- actor: principal,
584
- domainHash: domainHash,
585
- );
640
+ return approvePrepared(payload);
586
641
  }
587
642
  }
588
643
 
@@ -739,8 +794,8 @@ class EncryptionRosterMismatch implements Exception {
739
794
  /// Without this, the app would open the child and poll `head:null` until timeout with no reason. Callers catch
740
795
  /// it to see the child name(s), the kernel [code], and the [reason]. [head] is the parent write that landed.
741
796
  class BirthIncomplete implements Exception {
742
- /// The raw failed birth outcomes (`{ workspace, status, reason, code }`), status != "born".
743
- final List<Map<String, Object?>> failedBirths;
797
+ /// The failed typed kernel outcomes, status != [NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN].
798
+ final List<NomosBirthOutcome> failedBirths;
744
799
 
745
800
  /// The parent write's local head — the parent offer itself sealed; only the child did not come up.
746
801
  final String head;
@@ -749,28 +804,25 @@ class BirthIncomplete implements Exception {
749
804
 
750
805
  /// The first failed child's kernel code (e.g. `BirthLawPackageUnavailable`), or `BirthFailed`.
751
806
  String get code =>
752
- (failedBirths.isNotEmpty
753
- ? failedBirths.first['code']?.toString()
754
- : null) ??
755
- 'BirthFailed';
807
+ failedBirths.isNotEmpty && failedBirths.first.code.isNotEmpty
808
+ ? failedBirths.first.code
809
+ : 'BirthFailed';
756
810
 
757
811
  /// The child workspace name(s) that did not come up.
758
- List<String> get workspaces => failedBirths
759
- .map((o) => (o['workspace'] ?? '').toString())
760
- .where((s) => s.isNotEmpty)
761
- .toList();
812
+ List<String> get workspaces =>
813
+ failedBirths.map((o) => o.workspace).where((s) => s.isNotEmpty).toList();
762
814
 
763
815
  @override
764
816
  String toString() {
765
817
  final list = failedBirths
766
818
  .map(
767
819
  (o) =>
768
- '${o['workspace'] ?? '?'} [${o['status']}${o['code'] != null ? ': ${o['code']}' : ''}]'
769
- '${o['reason'] != null ? ' — ${o['reason']}' : ''}',
820
+ '${o.workspace.isEmpty ? '?' : o.workspace} [${o.status.name}${o.code.isNotEmpty ? ': ${o.code}' : ''}]'
821
+ '${o.reason.isNotEmpty ? ' — ${o.reason}' : ''}',
770
822
  )
771
823
  .join('; ');
772
824
  final lawUnavailable = failedBirths.any(
773
- (o) => o['code'] == 'BirthLawPackageUnavailable',
825
+ (o) => o.code == 'BirthLawPackageUnavailable',
774
826
  );
775
827
  return 'BirthIncomplete: ${failedBirths.length} child workspace(s) did NOT come up — $list. '
776
828
  'The parent offer admitted, but the child was not fully born, so opening it would poll head:null '
@@ -792,6 +844,254 @@ class NomosEpochResult {
792
844
  });
793
845
  }
794
846
 
847
+ String _nomosOwnedRequiredString(
848
+ Map<String, Object?> source,
849
+ String field,
850
+ ) {
851
+ final value = source[field];
852
+ if (value is! String || value.isEmpty) {
853
+ throw FormatException('Nomos owned preparation is missing String $field');
854
+ }
855
+ return value;
856
+ }
857
+
858
+ int _nomosOwnedRequiredInt(Map<String, Object?> source, String field) {
859
+ final value = source[field];
860
+ if (value is! num || value.toInt() != value) {
861
+ throw FormatException('Nomos owned preparation is missing int $field');
862
+ }
863
+ return value.toInt();
864
+ }
865
+
866
+ List<String> _nomosOwnedRequiredStrings(
867
+ Map<String, Object?> source,
868
+ String field,
869
+ ) {
870
+ final value = source[field];
871
+ if (value is! List || value.any((item) => item is! String)) {
872
+ throw FormatException(
873
+ 'Nomos owned preparation is missing List<String> $field');
874
+ }
875
+ return value.cast<String>().toList(growable: false);
876
+ }
877
+
878
+ OwnedDeviceRequest _nomosOwnedDeviceRequestFromTransport(Object? raw) {
879
+ if (raw is! Map) {
880
+ throw const FormatException('Nomos owned device request is not an object');
881
+ }
882
+ final value = Map<String, Object?>.from(raw);
883
+ const fields = <String>{
884
+ 'v',
885
+ 'workspace',
886
+ 'principal',
887
+ 'scope',
888
+ 'signingPublic',
889
+ 'wrappingPublic',
890
+ 'signature',
891
+ };
892
+ if (value.keys.any((key) => !fields.contains(key)) ||
893
+ fields.any((key) => !value.containsKey(key))) {
894
+ throw const FormatException(
895
+ 'Nomos owned device request has an invalid field set');
896
+ }
897
+ return OwnedDeviceRequest(
898
+ v: _nomosOwnedRequiredInt(value, 'v'),
899
+ workspace: _nomosOwnedRequiredString(value, 'workspace'),
900
+ principal: _nomosOwnedRequiredString(value, 'principal'),
901
+ scope: _nomosOwnedRequiredString(value, 'scope'),
902
+ signingPublic: _nomosOwnedRequiredString(value, 'signingPublic'),
903
+ wrappingPublic: _nomosOwnedRequiredString(value, 'wrappingPublic'),
904
+ signature: _nomosOwnedRequiredString(value, 'signature'),
905
+ );
906
+ }
907
+
908
+ OwnedCustody _nomosOwnedCustodyFromTransport(Object? raw) {
909
+ if (raw is! Map) {
910
+ throw const FormatException('Nomos owned custody is not an object');
911
+ }
912
+ final value = Map<String, Object?>.from(raw);
913
+ const fields = <String>{
914
+ 'v',
915
+ 'request',
916
+ 'hpkeEpk',
917
+ 'ct',
918
+ 'keyCommitment',
919
+ 'principalWrappingPublic',
920
+ };
921
+ if (value.keys.any((key) => !fields.contains(key)) ||
922
+ fields.any((key) => !value.containsKey(key))) {
923
+ throw const FormatException('Nomos owned custody has an invalid field set');
924
+ }
925
+ return OwnedCustody(
926
+ v: _nomosOwnedRequiredInt(value, 'v'),
927
+ request: _nomosOwnedDeviceRequestFromTransport(value['request']),
928
+ hpkeEpk: _nomosOwnedRequiredString(value, 'hpkeEpk'),
929
+ ct: _nomosOwnedRequiredString(value, 'ct'),
930
+ keyCommitment: _nomosOwnedRequiredString(value, 'keyCommitment'),
931
+ principalWrappingPublic:
932
+ _nomosOwnedRequiredString(value, 'principalWrappingPublic'),
933
+ );
934
+ }
935
+
936
+ /// One exact key wrap prepared by the framework-owned custody machinery.
937
+ /// Generated clients lower this value directly to canonical USDA; it is never a
938
+ /// tenant-authored dictionary.
939
+ class NomosOwnedWrapPreparation {
940
+ final String recipientPrincipal;
941
+ final String recipientPubKey;
942
+ final String hpkeEpk;
943
+ final String ct;
944
+
945
+ const NomosOwnedWrapPreparation({
946
+ required this.recipientPrincipal,
947
+ required this.recipientPubKey,
948
+ required this.hpkeEpk,
949
+ required this.ct,
950
+ });
951
+
952
+ factory NomosOwnedWrapPreparation.fromTransport(
953
+ Map<String, Object?> value,
954
+ ) {
955
+ return NomosOwnedWrapPreparation(
956
+ recipientPrincipal:
957
+ _nomosOwnedRequiredString(value, 'recipientPrincipal'),
958
+ recipientPubKey: _nomosOwnedRequiredString(value, 'recipientPubKey'),
959
+ hpkeEpk: _nomosOwnedRequiredString(value, 'hpkeEpk'),
960
+ ct: _nomosOwnedRequiredString(value, 'ct'),
961
+ );
962
+ }
963
+ }
964
+
965
+ /// A wrap which explicitly identifies the historic epoch it opens.
966
+ class NomosHistoricOwnedWrapPreparation extends NomosOwnedWrapPreparation {
967
+ final int epoch;
968
+
969
+ const NomosHistoricOwnedWrapPreparation({
970
+ required super.recipientPrincipal,
971
+ required super.recipientPubKey,
972
+ required super.hpkeEpk,
973
+ required super.ct,
974
+ required this.epoch,
975
+ });
976
+
977
+ factory NomosHistoricOwnedWrapPreparation.fromTransport(
978
+ Map<String, Object?> value,
979
+ ) {
980
+ return NomosHistoricOwnedWrapPreparation(
981
+ recipientPrincipal:
982
+ _nomosOwnedRequiredString(value, 'recipientPrincipal'),
983
+ recipientPubKey: _nomosOwnedRequiredString(value, 'recipientPubKey'),
984
+ hpkeEpk: _nomosOwnedRequiredString(value, 'hpkeEpk'),
985
+ ct: _nomosOwnedRequiredString(value, 'ct'),
986
+ epoch: _nomosOwnedRequiredInt(value, 'epoch'),
987
+ );
988
+ }
989
+ }
990
+
991
+ /// Exhaustive result of the framework-owned record preparation operation.
992
+ /// Each subtype corresponds to one law directive and therefore one generated
993
+ /// USDA encoder.
994
+ sealed class NomosOwnedRecordPreparation {
995
+ final String ownedKeySubject;
996
+ final OwnedCustody? ownedCustody;
997
+
998
+ const NomosOwnedRecordPreparation({
999
+ required this.ownedKeySubject,
1000
+ this.ownedCustody,
1001
+ });
1002
+ }
1003
+
1004
+ class NomosOwnedSharePreparation extends NomosOwnedRecordPreparation {
1005
+ final String recordId;
1006
+ final String peer;
1007
+ final int epoch;
1008
+ final String sharedAt;
1009
+ final List<NomosHistoricOwnedWrapPreparation> wraps;
1010
+
1011
+ const NomosOwnedSharePreparation({
1012
+ required super.ownedKeySubject,
1013
+ super.ownedCustody,
1014
+ required this.recordId,
1015
+ required this.peer,
1016
+ required this.epoch,
1017
+ required this.sharedAt,
1018
+ required this.wraps,
1019
+ });
1020
+ }
1021
+
1022
+ class NomosOwnedUnsharePreparation extends NomosOwnedRecordPreparation {
1023
+ final String recordId;
1024
+ final String peer;
1025
+ final int epoch;
1026
+ final String changedAt;
1027
+ final List<NomosOwnedWrapPreparation> wraps;
1028
+
1029
+ const NomosOwnedUnsharePreparation({
1030
+ required super.ownedKeySubject,
1031
+ super.ownedCustody,
1032
+ required this.recordId,
1033
+ required this.peer,
1034
+ required this.epoch,
1035
+ required this.changedAt,
1036
+ required this.wraps,
1037
+ });
1038
+ }
1039
+
1040
+ class NomosOwnedTransferPreparation extends NomosOwnedRecordPreparation {
1041
+ final String recordId;
1042
+ final String newOwner;
1043
+ final List<String> previousReaders;
1044
+ final int epoch;
1045
+ final String changedAt;
1046
+ final List<NomosHistoricOwnedWrapPreparation> historicWraps;
1047
+
1048
+ const NomosOwnedTransferPreparation({
1049
+ required super.ownedKeySubject,
1050
+ super.ownedCustody,
1051
+ required this.recordId,
1052
+ required this.newOwner,
1053
+ required this.previousReaders,
1054
+ required this.epoch,
1055
+ required this.changedAt,
1056
+ required this.historicWraps,
1057
+ });
1058
+ }
1059
+
1060
+ /// Exact framework preparation for the atomic device-approval directive.
1061
+ class NomosOwnedDeviceApprovalPreparation {
1062
+ final String newKeySubject;
1063
+ final OwnedCustody ownedCustody;
1064
+ final String verifiedVia;
1065
+ final int validFromEpoch;
1066
+ final String approvedAt;
1067
+ final String ownedKeySubject;
1068
+
1069
+ const NomosOwnedDeviceApprovalPreparation({
1070
+ required this.newKeySubject,
1071
+ required this.ownedCustody,
1072
+ required this.verifiedVia,
1073
+ required this.validFromEpoch,
1074
+ required this.approvedAt,
1075
+ required this.ownedKeySubject,
1076
+ });
1077
+
1078
+ factory NomosOwnedDeviceApprovalPreparation.fromTransport(
1079
+ Map<String, Object?> value,
1080
+ ) {
1081
+ return NomosOwnedDeviceApprovalPreparation(
1082
+ newKeySubject: _nomosOwnedRequiredString(value, 'newKeySubject'),
1083
+ ownedCustody: _nomosOwnedCustodyFromTransport(value['ownedCustody']),
1084
+ verifiedVia: _nomosOwnedRequiredString(value, 'verifiedVia'),
1085
+ validFromEpoch: _nomosOwnedRequiredInt(value, 'validFromEpoch'),
1086
+ approvedAt: _nomosOwnedRequiredString(value, 'approvedAt'),
1087
+ ownedKeySubject: _nomosOwnedRequiredString(
1088
+ value,
1089
+ '__nomos_owned_device_key_subject_7c1f5e',
1090
+ ),
1091
+ );
1092
+ }
1093
+ }
1094
+
795
1095
  /// THE OWNER CEREMONY surface — `<Domain>Client(bridge, actor: owner).encryption`. All three verbs open a
796
1096
  /// FRESH epoch (a fresh 32-byte secret) and fan the wraps to the resolved roster; the membership change (if
797
1097
  /// any) is the domain grant/revoke dispatched first, in the same call.
@@ -1059,6 +1359,67 @@ class NomosEncryption {
1059
1359
  /// for a `.births()` directive — the child workspace(s) the offer brought into being on this local plane.
1060
1360
  enum NomosOperationPhase { locallyAccepted, admittedToRemoteCustody, converged }
1061
1361
 
1362
+ /// The proof material a generated offer presents to the kernel.
1363
+ ///
1364
+ /// Generated law chooses this; application callers do not. [sessionCredentials] preserves the
1365
+ /// ordinary session posture. [enrolledDevice] deliberately withholds any provider token so a
1366
+ /// stale provider epoch cannot prevent an already enrolled device from authoring a recovery offer.
1367
+ enum NomosOfferAuthenticationProof { sessionCredentials, enrolledDevice }
1368
+
1369
+ /// The kernel's exact diagnosis of the provider credential held by this session.
1370
+ ///
1371
+ /// This deliberately does not expose protobuf names to generated application
1372
+ /// clients. It is a typed framework fact used to decide whether a parent-law
1373
+ /// epoch recovery is applicable; callers never inspect an error string.
1374
+ enum NomosProviderTokenVerification {
1375
+ unspecified,
1376
+ valid,
1377
+ noMatchingKey,
1378
+ expired,
1379
+ notYetValid,
1380
+ issuerMismatch,
1381
+ audienceMismatch,
1382
+ badSignature,
1383
+ malformed,
1384
+ unsupportedAlgorithm,
1385
+ badKey,
1386
+ missingSubject,
1387
+ noVerifier,
1388
+ missingToken,
1389
+ }
1390
+
1391
+ /// Current kernel-authenticated facts needed before retrying an ordinary offer.
1392
+ final class NomosAuthenticationPosture {
1393
+ const NomosAuthenticationPosture({
1394
+ required this.providerToken,
1395
+ required this.enrolledDevice,
1396
+ required this.resolvedPrincipal,
1397
+ });
1398
+
1399
+ final NomosProviderTokenVerification providerToken;
1400
+ final bool enrolledDevice;
1401
+ final String resolvedPrincipal;
1402
+
1403
+ bool isReadyFor(String expectedPrincipal) =>
1404
+ providerToken == NomosProviderTokenVerification.valid &&
1405
+ enrolledDevice &&
1406
+ expectedPrincipal.isNotEmpty &&
1407
+ resolvedPrincipal == expectedPrincipal;
1408
+ }
1409
+
1410
+ /// A typed kernel-call refusal. Generated clients may inspect [code] only to
1411
+ /// choose another typed kernel query; prose remains diagnostic, never control
1412
+ /// flow.
1413
+ final class NomosKernelCallFailure implements Exception {
1414
+ const NomosKernelCallFailure({required this.code, required this.message});
1415
+
1416
+ final String code;
1417
+ final String message;
1418
+
1419
+ @override
1420
+ String toString() => 'NomosKernelCallFailure($code: $message)';
1421
+ }
1422
+
1062
1423
  /// Product-facing progress for one accepted write. Transport topology and custody identifiers remain private.
1063
1424
  final class NomosOperationStatus {
1064
1425
  const NomosOperationStatus(this.phase, {this.updatedAt});
@@ -1126,8 +1487,8 @@ class NomosOfferOutcome {
1126
1487
  /// it IN-PLACE and opens it as a live sibling session — reads/writes flow into the child at once (#42).
1127
1488
  final List<String> born;
1128
1489
 
1129
- /// The raw per-child birth outcomes the holon reported (`{ workspace, genesisHead, … }`), when present.
1130
- final List<Map<String, Object?>> birthOutcomes;
1490
+ /// Typed per-child outcomes reported by the kernel, when present.
1491
+ final List<NomosBirthOutcome> birthOutcomes;
1131
1492
 
1132
1493
  /// LEGACY GATEWAY LANE ONLY: the parent gateway's verdict, when this outcome wraps a deprecated
1133
1494
  /// cross-workspace `createWorkspace` relay (the old generated-birth shape). Null on the one
@@ -1175,6 +1536,31 @@ class NomosOfferOutcome {
1175
1536
  .toList();
1176
1537
  }
1177
1538
 
1539
+ /// A generated protobuf offer response that did not admit the proposed
1540
+ /// change. Keeping the response typed preserves every exhaustive kernel
1541
+ /// outcome without translating it through an exception-shaped map.
1542
+ final class NomosTypedOfferFailure implements Exception {
1543
+ const NomosTypedOfferFailure(this.response);
1544
+
1545
+ final nomos_protocol.OfferResponse response;
1546
+
1547
+ @override
1548
+ String toString() {
1549
+ final detail = switch (response.whichOutcome()) {
1550
+ nomos_protocol.OfferResponse_Outcome.refused =>
1551
+ '${response.refused.refusal.code.name}: ${response.refused.refusal.message}',
1552
+ nomos_protocol.OfferResponse_Outcome.deadLettered =>
1553
+ response.deadLettered.reason,
1554
+ nomos_protocol.OfferResponse_Outcome.quarantined =>
1555
+ response.quarantined.reason,
1556
+ nomos_protocol.OfferResponse_Outcome.blockedOnDependency =>
1557
+ response.blockedOnDependency.detail,
1558
+ final outcome => outcome.name,
1559
+ };
1560
+ return 'NomosTypedOfferFailure($detail)';
1561
+ }
1562
+ }
1563
+
1178
1564
  /// OPEN A REF'S WORKSPACE DIRECTLY (co2 DX ask) — `await outcome.bornRefs.single.acquire(bridge)`. Sugar over
1179
1565
  /// [NomosBridge.acquireSession] keyed by the ref's `workspace`: an already-open session returns as-is; a
1180
1566
  /// device-resident/parked/just-born workspace opens LOCAL-ONLY in place (no cloud, no credentials); only a
@@ -1261,6 +1647,7 @@ class NomosBridge {
1261
1647
  final Map<int, Completer<Object?>> _pending = {};
1262
1648
  final Map<int, String> _pendingOps = {};
1263
1649
  final Map<int, NomosTraceContext> _pendingTraces = {};
1650
+ final Map<int, StackTrace> _pendingCallStacks = {};
1264
1651
  final Map<String, StreamController<List<dynamic>>> _watches = {};
1265
1652
  // SHAPE COMPATIBILITY (#72): compat bindings emit a verdict MAP, not a row list.
1266
1653
  final Map<String, StreamController<Map<String, Object?>>> _compatWatches = {};
@@ -1272,8 +1659,8 @@ class NomosBridge {
1272
1659
  final Map<String, StreamController<NomosCompat>> _lawChangedBySession = {};
1273
1660
  final StreamController<NomosCompat> _lawChanged =
1274
1661
  StreamController<NomosCompat>.broadcast();
1275
- final StreamController<Map<String, Object?>> _syncEvents =
1276
- StreamController<Map<String, Object?>>.broadcast();
1662
+ final StreamController<nomos_protocol.RuntimeEvent> _syncEvents =
1663
+ StreamController<nomos_protocol.RuntimeEvent>.broadcast();
1277
1664
  final StreamController<Map<String, Object?>> _signalEvents =
1278
1665
  StreamController<Map<String, Object?>>.broadcast();
1279
1666
  // CUSTODY LIFECYCLE (rebirth_and_custody_lifecycle.md): each `custody` event carries the raw map (session +
@@ -1321,13 +1708,20 @@ class NomosBridge {
1321
1708
  // workspace is a live session on it. [_sessionNames] tracks what [connect] has opened; [_sessionViews]
1322
1709
  // caches the per-workspace [NomosSession] handle so `bridge.session(ws)` is stable + cheap.
1323
1710
  final Set<String> _sessionNames = {};
1711
+ final Map<String, String> _workspaceBySession = {};
1324
1712
  final Map<String, NomosSession> _sessionViews = {};
1325
1713
  // The gate-bound identity retained from each session's auth token. Generated clients consume this so
1326
1714
  // application code never has to thread an actor into ordinary business methods.
1327
1715
  final Map<String, String> _sessionPrincipals = {};
1716
+ // Runtime-v2 offers need the exact captured credentials, not a reconstructed
1717
+ // JSON envelope. They remain process-local and are passed only as protobuf
1718
+ // bytes to the resident kernel.
1719
+ final Map<String, String> _sessionAuthTokens = {};
1328
1720
  // Device signing custody follows the application session internally. A generated application opening a
1329
1721
  // locally-born child inherits its current session's device key; tenant code never handles authorSecret.
1330
1722
  final Map<String, String> _sessionAuthorSecrets = {};
1723
+ final Map<String, int> _sessionOfferSequences = {};
1724
+ final Map<String, int> _sessionReplicas = {};
1331
1725
  // Stateful generated applications retain the refreshable cloud access that opened their primary
1332
1726
  // session. A generated read following a NomosRef can therefore mount a genuinely-absent workspace
1333
1727
  // without asking tenant code for cloud URLs, tokens, sessions or signing keys. Kept per source
@@ -1352,12 +1746,165 @@ class NomosBridge {
1352
1746
  // methods expose only the typed receipt and its product-language lifecycle.
1353
1747
  final Map<String, Map<String, NomosOperationReceipt>> _operationReceipts = {};
1354
1748
  final Map<String, NomosOperationReceipt> _birthReceipts = {};
1749
+ final Map<String, StreamController<Uint8List>> _nominalWatchControllers = {};
1355
1750
  int _seq = 0;
1751
+ late final NmsRuntimeV2Gateway _runtimeV2 = NmsRuntimeV2Gateway(_transport);
1356
1752
  final String _traceId = _newTraceId();
1357
1753
  late final StreamSubscription<Map<String, Object?>> _sub;
1754
+ StreamSubscription<nomos_protocol.RuntimeEvent>? _runtimeEventSub;
1358
1755
 
1359
1756
  NomosBridge(this._transport, {this.allowStaleRunnerBundle = false}) {
1360
1757
  _sub = _transport.messages.listen(_onMessage, onError: _onTransportError);
1758
+ final transport = _transport;
1759
+ if (transport is NomosRuntimeV2EventTransport) {
1760
+ final eventTransport = transport as NomosRuntimeV2EventTransport;
1761
+ _runtimeEventSub = eventTransport.runtimeEvents.listen(
1762
+ _onRuntimeEvent,
1763
+ onError: _onTransportError,
1764
+ );
1765
+ }
1766
+ }
1767
+
1768
+ void _onRuntimeEvent(nomos_protocol.RuntimeEvent event) {
1769
+ if (event.hasReady()) {
1770
+ final stale = _staleBundleError(event.ready.runtimeVersion);
1771
+ if (stale != null && !allowStaleRunnerBundle) {
1772
+ if (!_ready.isCompleted) _ready.completeError(stale);
1773
+ } else if (!_ready.isCompleted) {
1774
+ _ready.complete();
1775
+ }
1776
+ return;
1777
+ }
1778
+ if (event.hasWatchChanged()) {
1779
+ final changed = event.watchChanged;
1780
+ final controller =
1781
+ _eventPlane._nominalWatchControllers[changed.subscriptionId];
1782
+ if (controller == null || controller.isClosed) return;
1783
+ if (changed.hasFailure()) {
1784
+ controller.addError(
1785
+ StateError('${changed.failure.code}: ${changed.failure.message}'),
1786
+ );
1787
+ return;
1788
+ }
1789
+ controller.add(Uint8List.fromList(changed.resultUsda));
1790
+ return;
1791
+ }
1792
+ if (event.hasSyncCompleted()) {
1793
+ final completed = event.syncCompleted;
1794
+ final session = event.hasSession() ? event.session : null;
1795
+ if (!_eventPlane._syncEvents.isClosed) {
1796
+ _eventPlane._syncEvents.add(event);
1797
+ }
1798
+ if (completed.hasFailure()) {
1799
+ if (_eventPlane._syncNeedsCredentialRefresh(completed) &&
1800
+ session != null) {
1801
+ unawaited(_eventPlane._refreshSessionCredential(session));
1802
+ }
1803
+ _eventPlane._updateScopedStatus(
1804
+ session,
1805
+ (current) => current.copyWith(
1806
+ phase: SyncPhase.error,
1807
+ clearTransfer: true,
1808
+ lastError: completed.failure.message,
1809
+ ),
1810
+ );
1811
+ return;
1812
+ }
1813
+ final result = completed.result;
1814
+ if (session != null) {
1815
+ _eventPlane._advanceOperationReceipts(session, result);
1816
+ if (_eventPlane._syncNeedsCredentialRefresh(completed)) {
1817
+ unawaited(_eventPlane._refreshSessionCredential(session));
1818
+ }
1819
+ }
1820
+ if (result.offline) {
1821
+ _eventPlane._updateScopedStatus(
1822
+ session,
1823
+ (current) => current.copyWith(
1824
+ phase: SyncPhase.offline,
1825
+ clearTransfer: true,
1826
+ lastError: result.hasPushError() ? result.pushError.message : null,
1827
+ ),
1828
+ );
1829
+ } else if (result.pushFailed) {
1830
+ _eventPlane._updateScopedStatus(
1831
+ session,
1832
+ (current) => current.copyWith(
1833
+ phase: SyncPhase.error,
1834
+ clearTransfer: true,
1835
+ lastError: _eventPlane._syncPushFailureDetail(result),
1836
+ ),
1837
+ );
1838
+ } else {
1839
+ _eventPlane._updateScopedStatus(
1840
+ session,
1841
+ (current) => current.copyWith(
1842
+ phase: SyncPhase.idle,
1843
+ clearTransfer: true,
1844
+ clearError: true,
1845
+ lastSyncedAt: DateTime.now(),
1846
+ ),
1847
+ );
1848
+ }
1849
+ return;
1850
+ }
1851
+ if (event.hasLinkChanged()) {
1852
+ if (!_eventPlane._syncEvents.isClosed) {
1853
+ _eventPlane._syncEvents.add(event);
1854
+ }
1855
+ if (event.linkChanged.status ==
1856
+ nomos_protocol.LinkStatus.LINK_STATUS_CLOSED) {
1857
+ _eventPlane._updateScopedStatus(
1858
+ event.hasSession() ? event.session : null,
1859
+ (current) => current.copyWith(phase: SyncPhase.offline),
1860
+ );
1861
+ }
1862
+ return;
1863
+ }
1864
+ if (event.hasCustodyChanged()) {
1865
+ if (!_eventPlane._custodyEvents.isClosed) {
1866
+ _eventPlane._custodyEvents.add(
1867
+ NomosCustodyEvent.fromRuntime(
1868
+ event.custodyChanged,
1869
+ session: event.hasSession() ? event.session : null,
1870
+ ),
1871
+ );
1872
+ }
1873
+ return;
1874
+ }
1875
+ if (event.hasLocalWorkRejected()) {
1876
+ if (!_eventPlane._localWorkRejectedEvents.isClosed) {
1877
+ _eventPlane._localWorkRejectedEvents.add(
1878
+ NomosLocalWorkRejectedEvent.fromRuntime(
1879
+ event.localWorkRejected,
1880
+ session: event.hasSession() ? event.session : null,
1881
+ ),
1882
+ );
1883
+ }
1884
+ return;
1885
+ }
1886
+ if (event.hasTransferProgress()) {
1887
+ final progress = TransferProgress.fromRuntime(event.transferProgress);
1888
+ if (!_eventPlane._transferEvents.isClosed) {
1889
+ _eventPlane._transferEvents.add(progress);
1890
+ }
1891
+ _eventPlane._updateScopedStatus(
1892
+ event.hasSession() ? event.session : null,
1893
+ (current) => current.copyWith(
1894
+ phase: progress.done ? SyncPhase.syncing : SyncPhase.transferring,
1895
+ transfer: progress,
1896
+ clearTransfer: progress.done,
1897
+ ),
1898
+ );
1899
+ return;
1900
+ }
1901
+ if (event.hasKernelProgress()) {
1902
+ if (!_eventPlane._kernelProgressEvents.isClosed) {
1903
+ _eventPlane._kernelProgressEvents.add(
1904
+ KernelProgress.fromRuntime(event.kernelProgress),
1905
+ );
1906
+ }
1907
+ }
1361
1908
  }
1362
1909
 
1363
1910
  void _onTransportError(Object error, StackTrace stackTrace) {
@@ -1366,6 +1913,7 @@ class NomosBridge {
1366
1913
  _pending.clear();
1367
1914
  _pendingOps.clear();
1368
1915
  _pendingTraces.clear();
1916
+ _pendingCallStacks.clear();
1369
1917
  for (final completer in pending) {
1370
1918
  if (!completer.isCompleted) completer.completeError(error, stackTrace);
1371
1919
  }
@@ -1508,114 +2056,6 @@ class NomosBridge {
1508
2056
  }
1509
2057
  return;
1510
2058
  }
1511
- if (event == 'sync' || event == 'link') {
1512
- // autoSync telemetry: each round { event:'sync', reason, result } and socket state { event:'link', status }.
1513
- if (!_syncEvents.isClosed) _syncEvents.add(m);
1514
- if (event == 'sync') {
1515
- final result = (m['result'] as Map?)?.cast<String, Object?>();
1516
- final session = m['session']?.toString();
1517
- if (session != null && result != null) {
1518
- _advanceOperationReceipts(session, result);
1519
- if (_syncNeedsCredentialRefresh(m, result)) {
1520
- unawaited(_refreshSessionCredential(session));
1521
- }
1522
- }
1523
- if (m['ok'] == false) {
1524
- // A rejected/failed round is not a successful sync timestamp. Keep the stable typed envelope on
1525
- // telemetry; the app-ready status needs only one legible error and an honest error phase.
1526
- final detail = m['error']?.toString() ?? 'automatic sync failed';
1527
- _updateScopedStatus(
1528
- session,
1529
- (current) => current.copyWith(
1530
- phase: SyncPhase.error,
1531
- clearTransfer: true,
1532
- lastError: detail,
1533
- ),
1534
- );
1535
- } else if (result?['offline'] == true) {
1536
- // Offline is expected transport debt. The durable local branch remains ahead and autoSync owns the
1537
- // retry; do not claim a completed sync or overwrite the previous successful timestamp.
1538
- _updateScopedStatus(
1539
- session,
1540
- (current) => current.copyWith(
1541
- phase: SyncPhase.offline,
1542
- clearTransfer: true,
1543
- lastError: (result?['pushError'] as Map?)?['error']?.toString(),
1544
- ),
1545
- );
1546
- } else if (result?['pushFailed'] == true) {
1547
- // The sync round itself completed, but shared custody refused the
1548
- // push. `stillAhead` is durable debt, not success: surface the
1549
- // cloud's typed refusal on the owning workspace so generated
1550
- // `whenCurrent()` calls fail with the real cause instead of timing
1551
- // out behind a generic saving state.
1552
- final detail = _syncPushFailureDetail(result!);
1553
- _updateScopedStatus(
1554
- session,
1555
- (current) => current.copyWith(
1556
- phase: SyncPhase.error,
1557
- clearTransfer: true,
1558
- lastError: detail,
1559
- ),
1560
- );
1561
- } else {
1562
- // A completed round → idle (the next metrics poll folds in residual ahead/behind), stamp the time.
1563
- _updateScopedStatus(
1564
- session,
1565
- (current) => current.copyWith(
1566
- phase: SyncPhase.idle,
1567
- clearTransfer: true,
1568
- clearError: true,
1569
- lastSyncedAt: DateTime.now(),
1570
- ),
1571
- );
1572
- }
1573
- } else if (m['status'] == 'closed' || m['status'] == 'offline') {
1574
- _updateScopedStatus(
1575
- m['session']?.toString(),
1576
- (current) => current.copyWith(phase: SyncPhase.offline),
1577
- );
1578
- }
1579
- return;
1580
- }
1581
- if (event == 'custody') {
1582
- // Custody lifecycle: { event:'custody', session, custody:{ kind, break, … } }. Decode + fan out so the
1583
- // app can react (kind "ask" → show a dialog then call resolveCustodyBreak; others are informational).
1584
- final ev = NomosCustodyEvent.fromMessage(m);
1585
- if (ev != null && !_custodyEvents.isClosed) _custodyEvents.add(ev);
1586
- return;
1587
- }
1588
- if (event == 'localWorkRejected') {
1589
- // LOCAL-WORK-REJECTED: { event:'localWorkRejected', session, localWorkRejected:{ workspace,
1590
- // refused, parked, resetTo, retryable, source } }. The framework already parked the refused
1591
- // intents + replayed survivors; the app's only job is the honest message (retryable ⇒ "queued,
1592
- // will retry"; terminal ⇒ "preserved for review — contact support").
1593
- final ev = NomosLocalWorkRejectedEvent.fromMessage(m);
1594
- if (ev != null && !_localWorkRejectedEvents.isClosed) {
1595
- _localWorkRejectedEvents.add(ev);
1596
- }
1597
- return;
1598
- }
1599
- if (event == 'transfer') {
1600
- // LIVE pack byte-progress { received, total, kind } — the real loading bar (host Content-Length).
1601
- final tp = TransferProgress.fromMessage(m);
1602
- if (!_transferEvents.isClosed) _transferEvents.add(tp);
1603
- _updateScopedStatus(
1604
- m['session']?.toString(),
1605
- (current) => current.copyWith(
1606
- phase: tp.done ? SyncPhase.syncing : SyncPhase.transferring,
1607
- transfer: tp,
1608
- clearTransfer: tp.done,
1609
- ),
1610
- );
1611
- return;
1612
- }
1613
- if (event == 'kernelProgress') {
1614
- if (!_kernelProgressEvents.isClosed) {
1615
- _kernelProgressEvents.add(KernelProgress.fromMessage(m));
1616
- }
1617
- return;
1618
- }
1619
2059
  if (event == 'trace') {
1620
2060
  // the realtime cloud flamepath: { event:'trace', trace:{at, oid, edge, clients} } — one per admission.
1621
2061
  if (!_flameEvents.isClosed) _flameEvents.add(m);
@@ -1647,6 +2087,7 @@ class NomosBridge {
1647
2087
  final completer = _pending.remove(id);
1648
2088
  final op = _pendingOps.remove(id);
1649
2089
  final trace = _pendingTraces.remove(id) ?? const NomosTraceContext();
2090
+ final callStack = _pendingCallStacks.remove(id) ?? StackTrace.current;
1650
2091
  if (completer == null) return;
1651
2092
  if (m['ok'] == true) {
1652
2093
  completer.complete(m['result']);
@@ -1656,14 +2097,15 @@ class NomosBridge {
1656
2097
  // [BirthIncomplete] here (apps switch on the type + entries, never scrape the prose).
1657
2098
  final failedBirths = ((m['errorInfo'] as Map?)?['failedBirths'] as List?)
1658
2099
  ?.whereType<Map>()
1659
- .map((e) => e.cast<String, Object?>())
1660
- .toList();
2100
+ .map(_birthOutcomeFromLegacyWire)
2101
+ .toList(growable: false);
1661
2102
  if (failedBirths != null && failedBirths.isNotEmpty) {
1662
2103
  completer.completeError(
1663
2104
  BirthIncomplete(
1664
2105
  failedBirths,
1665
2106
  head: ((m['errorInfo'] as Map?)?['head'] ?? '').toString(),
1666
2107
  ),
2108
+ callStack,
1667
2109
  );
1668
2110
  return;
1669
2111
  }
@@ -1673,7 +2115,7 @@ class NomosBridge {
1673
2115
  fallbackTrace: trace,
1674
2116
  fallbackMessage: 'the githolon refused the request',
1675
2117
  );
1676
- completer.completeError(nomosErrorFromInfo(info));
2118
+ completer.completeError(nomosErrorFromInfo(info), callStack);
1677
2119
  }
1678
2120
  }
1679
2121
 
@@ -1691,6 +2133,7 @@ class NomosBridge {
1691
2133
  sampled: true,
1692
2134
  );
1693
2135
  _pendingTraces[id] = trace;
2136
+ _pendingCallStacks[id] = StackTrace.current;
1694
2137
  try {
1695
2138
  _transport.send({
1696
2139
  'v': kWebviewBridgeVersion,
@@ -1710,11 +2153,49 @@ class NomosBridge {
1710
2153
  _pending.remove(id);
1711
2154
  _pendingOps.remove(id);
1712
2155
  _pendingTraces.remove(id);
2156
+ _pendingCallStacks.remove(id);
1713
2157
  completer.completeError(error, stackTrace);
1714
2158
  }
1715
2159
  return completer.future;
1716
2160
  }
1717
2161
 
2162
+ /// Invoke one generated application-runtime command. Request correlation and
2163
+ /// every outcome discriminator stay in generated protobuf types; callers
2164
+ /// must exhaustively select the response arm they requested.
2165
+ Future<nomos_protocol.RuntimeResponse> _invokeRuntimeV2(
2166
+ nomos_protocol.RuntimeRequest request,
2167
+ ) async {
2168
+ return _eventPlane._runtimeV2.invoke(request);
2169
+ }
2170
+
2171
+ String _runtimeSession([String? explicit]) {
2172
+ if (explicit != null && explicit.isNotEmpty) return explicit;
2173
+ final scoped = _sessionName;
2174
+ if (scoped != null && scoped.isNotEmpty) return scoped;
2175
+ final sessions = _eventPlane._sessionNames;
2176
+ if (sessions.length == 1) return sessions.single;
2177
+ if (sessions.isEmpty) {
2178
+ throw StateError('No Nomos workspace is connected');
2179
+ }
2180
+ throw StateError(
2181
+ 'Multiple Nomos workspaces are connected; select a session');
2182
+ }
2183
+
2184
+ Future<void> _unwatch(String subscriptionId) async {
2185
+ final response = await _invokeRuntimeV2(
2186
+ nomos_protocol.RuntimeRequest(
2187
+ unwatch: nomos_protocol.UnwatchRequest(
2188
+ subscriptionId: subscriptionId,
2189
+ ),
2190
+ ),
2191
+ );
2192
+ if (!response.hasWatchStopped()) {
2193
+ throw StateError(
2194
+ 'Runtime v2 returned ${response.whichOutcome().name} for unwatch',
2195
+ );
2196
+ }
2197
+ }
2198
+
1718
2199
  Map<String, Object?> _asMap(Object? v) =>
1719
2200
  (v as Map?)?.cast<String, Object?>() ?? const {};
1720
2201
  List<Map<String, dynamic>> _asRows(Object? v) =>
@@ -1722,23 +2203,6 @@ class NomosBridge {
1722
2203
  .map((e) => (e as Map).cast<String, dynamic>())
1723
2204
  .toList();
1724
2205
 
1725
- String? _principalFromToken(String? token) {
1726
- if (token == null || token.isEmpty) return null;
1727
- try {
1728
- final parts = token.split('.');
1729
- if (parts.length < 2) return null;
1730
- final payload = jsonDecode(
1731
- utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))),
1732
- );
1733
- final sub = payload is Map ? payload['sub'] : null;
1734
- if (sub is! String || sub.trim().isEmpty) return null;
1735
- final value = sub.trim();
1736
- return 'user:$value';
1737
- } catch (_) {
1738
- return null;
1739
- }
1740
- }
1741
-
1742
2206
  NomosOperationReceipt _registerOperationReceipt(
1743
2207
  String workspace,
1744
2208
  String intentId,
@@ -1770,19 +2234,19 @@ class NomosBridge {
1770
2234
 
1771
2235
  void _advanceOperationReceipts(
1772
2236
  String workspace,
1773
- Map<String, Object?> result,
2237
+ nomos_protocol.SyncWorkspaceResponse result,
1774
2238
  ) {
1775
2239
  final pending = _eventPlane._operationReceipts[workspace];
1776
2240
  if (pending == null || pending.isEmpty) return;
1777
2241
 
1778
- Iterable<String> ids(String key) => ((result[key] as List?) ?? const [])
1779
- .whereType<String>()
1780
- .where((value) => value.isNotEmpty);
1781
-
1782
- for (final intentId in ids('admittedIntentIds')) {
2242
+ for (final intentId in result.admittedIntentIds.where(
2243
+ (value) => value.isNotEmpty,
2244
+ )) {
1783
2245
  pending[intentId]?._advance(NomosOperationPhase.admittedToRemoteCustody);
1784
2246
  }
1785
- for (final intentId in ids('convergedIntentIds')) {
2247
+ for (final intentId in result.convergedIntentIds.where(
2248
+ (value) => value.isNotEmpty,
2249
+ )) {
1786
2250
  final receipt = pending.remove(intentId);
1787
2251
  receipt?._advance(NomosOperationPhase.converged);
1788
2252
  }
@@ -1790,24 +2254,24 @@ class NomosBridge {
1790
2254
  }
1791
2255
 
1792
2256
  bool _syncNeedsCredentialRefresh(
1793
- Map<String, Object?> message,
1794
- Map<String, Object?> result,
2257
+ nomos_protocol.SyncCompleted event,
1795
2258
  ) {
1796
- final push = (result['pushError'] as Map?)?.cast<String, Object?>();
1797
- final status = push?['status'];
2259
+ final status =
2260
+ event.result.hasPushError() ? event.result.pushError.status : 0;
1798
2261
  if (status == 401 || status == 403) return true;
1799
- final text =
1800
- '${message['error'] ?? ''} ${push?['error'] ?? ''}'.toLowerCase();
2262
+ final text = '${event.hasFailure() ? event.failure.message : ''} '
2263
+ '${event.result.hasPushError() ? event.result.pushError.message : ''}'
2264
+ .toLowerCase();
1801
2265
  return text.contains('unauthenticated') ||
1802
2266
  text.contains('token expired') ||
1803
2267
  text.contains('invalid token');
1804
2268
  }
1805
2269
 
1806
- String _syncPushFailureDetail(Map<String, Object?> result) {
1807
- final push = (result['pushError'] as Map?)?.cast<String, Object?>();
1808
- final status = push?['status'];
1809
- final reason = push?['error'] ?? push?['body'];
1810
- final prefix = status == null
2270
+ String _syncPushFailureDetail(nomos_protocol.SyncWorkspaceResponse result) {
2271
+ final push = result.hasPushError() ? result.pushError : null;
2272
+ final status = push?.status;
2273
+ final reason = push?.message;
2274
+ final prefix = status == null || status == 0
1811
2275
  ? 'Shared custody refused this workspace change'
1812
2276
  : 'Shared custody refused this workspace change (HTTP $status)';
1813
2277
  return reason == null || reason.toString().trim().isEmpty
@@ -1888,19 +2352,18 @@ class NomosBridge {
1888
2352
  /// call `bridge.acquireSession(ws)` / `ref.acquire(bridge)` before using it;
1889
2353
  /// • `"parked"` — custody resident but the holon is LRU-disposed; it re-mounts on demand (acquire).
1890
2354
  /// `syncPosture` is `"local-only"` (no cloud counterpart) or `"cloud-bound"`.
1891
- Future<List<Map<String, Object?>>> listWorkspaces() async {
1892
- final connected = _eventPlane._sessionNames.toSet();
1893
- return (((_asMap(await _call('listWorkspaces'))['workspaces']) as List?) ??
1894
- const [])
1895
- .map((e) => (e as Map).cast<String, Object?>())
1896
- .map((w) {
1897
- final ws = w['workspace']?.toString() ?? '';
1898
- // `connected` is the app's own connect() truth; `parked`/resident come from the runner realm.
1899
- w['state'] = (w['parked'] == true)
1900
- ? 'parked'
1901
- : (connected.contains(ws) ? 'connected' : 'resident');
1902
- return w;
1903
- }).toList();
2355
+ Future<List<nomos_protocol.WorkspaceSummary>> listWorkspaces() async {
2356
+ final response = await _invokeRuntimeV2(
2357
+ nomos_protocol.RuntimeRequest(
2358
+ listWorkspaces: nomos_protocol.ListWorkspacesRequest(),
2359
+ ),
2360
+ );
2361
+ if (!response.hasWorkspaceList()) {
2362
+ throw StateError(
2363
+ 'Runtime v2 returned ${response.whichOutcome().name} for listWorkspaces',
2364
+ );
2365
+ }
2366
+ return response.workspaceList.workspaces;
1904
2367
  }
1905
2368
 
1906
2369
  /// Framework-private plane persistence primitive. It snapshots exact local custody even when [workspace]
@@ -1974,6 +2437,7 @@ class NomosBridge {
1974
2437
  _eventPlane._sessionNames.add(
1975
2438
  ws,
1976
2439
  ); // the open succeeded — the session is real now
2440
+ _eventPlane._workspaceBySession[ws] = workspace;
1977
2441
  // Opening succeeded, but scoped metrics have not yet proved that this
1978
2442
  // workspace is current. Unknown readiness must fail closed: otherwise a
1979
2443
  // generated `whenCurrent()` can observe the default idle value and return
@@ -1983,9 +2447,13 @@ class NomosBridge {
1983
2447
  () => const SyncStatus(phase: SyncPhase.syncing),
1984
2448
  );
1985
2449
  _eventPlane._foldApplicationStatusFromSessions();
1986
- final boundPrincipal = _principalFromToken(authToken);
1987
- if (boundPrincipal != null)
1988
- _eventPlane._sessionPrincipals[ws] = boundPrincipal;
2450
+ // An opaque bearer token is proof material for the kernel, not a JSON
2451
+ // identity document for the host. The session acquires a principal claim
2452
+ // only from an explicit generated application context; decoding `sub`
2453
+ // here would falsely turn an unverified host observation into authority.
2454
+ if (authToken != null && authToken.isNotEmpty) {
2455
+ _eventPlane._sessionAuthTokens[ws] = authToken;
2456
+ }
1989
2457
  if (authorSecret != null && authorSecret.isNotEmpty) {
1990
2458
  _eventPlane._sessionAuthorSecrets[ws] = authorSecret;
1991
2459
  }
@@ -2023,19 +2491,37 @@ class NomosBridge {
2023
2491
  );
2024
2492
 
2025
2493
  /// Continue an `ask`-policy custody break with the user's decision (adopt|reject) — the async UX lane.
2026
- Future<Map<String, Object?>> resolveCustodyBreak(
2027
- String decision, {
2494
+ Future<nomos_protocol.CustodyBreakResolved> resolveCustodyBreak(
2495
+ nomos_protocol.CustodyBreakDecision decision, {
2028
2496
  String? session,
2029
- }) async =>
2030
- _asMap(
2031
- await _call('resolveCustodyBreak', {
2032
- 'decision': decision,
2033
- if (session != null) 'session': session,
2034
- }),
2497
+ }) async {
2498
+ if (decision ==
2499
+ nomos_protocol
2500
+ .CustodyBreakDecision.CUSTODY_BREAK_DECISION_UNSPECIFIED) {
2501
+ throw ArgumentError.value(
2502
+ decision,
2503
+ 'decision',
2504
+ 'must be ADOPT or REJECT',
2035
2505
  );
2036
-
2037
- /// The parked salvaged intents (re-offerable local work an `adopt` rescued) — metadata only.
2038
- Future<List<Map<String, Object?>>> salvagedIntents({String? session}) async {
2506
+ }
2507
+ final response = await _invokeRuntimeV2(
2508
+ nomos_protocol.RuntimeRequest(
2509
+ resolveCustodyBreak: nomos_protocol.ResolveCustodyBreakRequest(
2510
+ session: _runtimeSession(session),
2511
+ decision: decision,
2512
+ ),
2513
+ ),
2514
+ );
2515
+ if (!response.hasCustodyBreakResolved()) {
2516
+ throw StateError(
2517
+ 'Runtime v2 returned ${response.whichOutcome().name} for resolveCustodyBreak',
2518
+ );
2519
+ }
2520
+ return response.custodyBreakResolved;
2521
+ }
2522
+
2523
+ /// The parked salvaged intents (re-offerable local work an `adopt` rescued) — metadata only.
2524
+ Future<List<Map<String, Object?>>> salvagedIntents({String? session}) async {
2039
2525
  final r = _asMap(
2040
2526
  await _call('salvagedIntents', {if (session != null) 'session': session}),
2041
2527
  );
@@ -2075,42 +2561,68 @@ class NomosBridge {
2075
2561
  // parks the FULL intent durably in the workspace tree (it rides export()/restore); the app
2076
2562
  // inspects, the domain dev ships a law fix, retry unjams. ──
2077
2563
 
2078
- /// The parked dead letters — metadata only (`{id, oid, source, error, domain, directiveId, at}`);
2079
- /// the intent bytes stay in the durable queue.
2080
- Future<List<Map<String, Object?>>> deadLetters({String? session}) async {
2081
- final r = _asMap(
2082
- await _call('deadLetters', {if (session != null) 'session': session}),
2564
+ /// The parked dead letters; intent bytes stay in durable custody.
2565
+ Future<List<nomos_protocol.DeadLetterEntry>> deadLetters(
2566
+ {String? session}) async {
2567
+ final response = await _invokeRuntimeV2(
2568
+ nomos_protocol.RuntimeRequest(
2569
+ deadLetter: nomos_protocol.DeadLetterRequest(
2570
+ session: _runtimeSession(session),
2571
+ list: nomos_protocol.ListDeadLettersRequest(),
2572
+ ),
2573
+ ),
2083
2574
  );
2084
- return ((r['dead'] as List?) ?? const [])
2085
- .map((e) => (e as Map).cast<String, Object?>())
2086
- .toList();
2575
+ if (!response.hasDeadLetter() || !response.deadLetter.hasList()) {
2576
+ throw StateError(
2577
+ 'Runtime v2 returned ${response.whichOutcome().name} for deadLetters',
2578
+ );
2579
+ }
2580
+ return List.unmodifiable(response.deadLetter.list.entries);
2087
2581
  }
2088
2582
 
2089
2583
  /// Retry one dead letter (after the law fix landed): pulls canon first (it may already be
2090
2584
  /// admitted), else re-offers the parked bytes locally. `{ok, head?|alreadyOnMain?|error?}` —
2091
2585
  /// a refusal leaves the entry parked (with the fresh error), never lost.
2092
- Future<Map<String, Object?>> retryDeadLetter(
2586
+ Future<nomos_protocol.DeadLetterRetry> retryDeadLetter(
2093
2587
  String id, {
2094
2588
  String? session,
2095
- }) async =>
2096
- _asMap(
2097
- await _call('retryDeadLetter', {
2098
- 'id': id,
2099
- if (session != null) 'session': session,
2100
- }),
2589
+ }) async {
2590
+ final response = await _invokeRuntimeV2(
2591
+ nomos_protocol.RuntimeRequest(
2592
+ deadLetter: nomos_protocol.DeadLetterRequest(
2593
+ session: _runtimeSession(session),
2594
+ retry: nomos_protocol.RetryDeadLetterRequest(intentId: id),
2595
+ ),
2596
+ ),
2597
+ );
2598
+ if (!response.hasDeadLetter() || !response.deadLetter.hasRetry()) {
2599
+ throw StateError(
2600
+ 'Runtime v2 returned ${response.whichOutcome().name} for retryDeadLetter',
2101
2601
  );
2602
+ }
2603
+ return response.deadLetter.retry;
2604
+ }
2102
2605
 
2103
2606
  /// Discard one dead letter — the app's EXPLICIT choice to drop the work. `{ok}`.
2104
- Future<Map<String, Object?>> discardDeadLetter(
2607
+ Future<nomos_protocol.DeadLetterDiscard> discardDeadLetter(
2105
2608
  String id, {
2106
2609
  String? session,
2107
- }) async =>
2108
- _asMap(
2109
- await _call('discardDeadLetter', {
2110
- 'id': id,
2111
- if (session != null) 'session': session,
2112
- }),
2610
+ }) async {
2611
+ final response = await _invokeRuntimeV2(
2612
+ nomos_protocol.RuntimeRequest(
2613
+ deadLetter: nomos_protocol.DeadLetterRequest(
2614
+ session: _runtimeSession(session),
2615
+ discard: nomos_protocol.DiscardDeadLetterRequest(intentId: id),
2616
+ ),
2617
+ ),
2618
+ );
2619
+ if (!response.hasDeadLetter() || !response.deadLetter.hasDiscard()) {
2620
+ throw StateError(
2621
+ 'Runtime v2 returned ${response.whichOutcome().name} for discardDeadLetter',
2113
2622
  );
2623
+ }
2624
+ return response.deadLetter.discard;
2625
+ }
2114
2626
 
2115
2627
  /// SALVAGE — best-effort extraction of this holon's local-only authored work (un-synced pending
2116
2628
  /// writes + the DLQ), each with its full `changeB64` bytes: `{pendingWrites, deadLetters}`. The
@@ -2233,6 +2745,164 @@ class NomosBridge {
2233
2745
  birthCertificate: birthCertificate,
2234
2746
  );
2235
2747
 
2748
+ /// The existing offer operation with its tenant intent authored as canonical
2749
+ /// USDA and carried by the generated protobuf ABI. This is not a second write
2750
+ /// endpoint: generated domain methods use it to reach the same kernel
2751
+ /// [OfferRequest] without a generic map or JavaScript serializer.
2752
+ @internal
2753
+ Future<NomosOfferOutcome> offerCallUsdaOutcome({
2754
+ required NmsGeneratedCall call,
2755
+ String? actor,
2756
+ String? domainHash,
2757
+ String? session,
2758
+ String? authorSecret,
2759
+ NomosBirthCertificate? birthCertificate,
2760
+ Iterable<NomosConflictSnapshot> conflictSnapshots = const [],
2761
+ NomosOfferAuthenticationProof authenticationProof =
2762
+ NomosOfferAuthenticationProof.sessionCredentials,
2763
+ }) async {
2764
+ final runtimeSession = session ?? _sessionName;
2765
+ if (runtimeSession == null || runtimeSession.isEmpty) {
2766
+ throw StateError(
2767
+ 'A typed offer must be bound to a connected NomosSession',
2768
+ );
2769
+ }
2770
+ final transport = _transport;
2771
+ if (transport is! NomosKernelCallTransport) {
2772
+ throw UnsupportedError(
2773
+ 'This host does not implement the generated protobuf kernel lane',
2774
+ );
2775
+ }
2776
+ final kernelTransport = transport as NomosKernelCallTransport;
2777
+ final plane = _eventPlane;
2778
+ final workspace =
2779
+ plane._workspaceBySession[runtimeSession] ?? runtimeSession;
2780
+ final sequence = (plane._sessionOfferSequences[runtimeSession] ?? 0) + 1;
2781
+ plane._sessionOfferSequences[runtimeSession] = sequence;
2782
+ final random = Random.secure();
2783
+ final replica = plane._sessionReplicas.putIfAbsent(
2784
+ workspace,
2785
+ () => random.nextInt(0x7fffffff) + 1,
2786
+ );
2787
+ final now = DateTime.now().millisecondsSinceEpoch;
2788
+ final sessionToken = plane._sessionAuthTokens[runtimeSession];
2789
+ final secret = authorSecret ?? plane._sessionAuthorSecrets[runtimeSession];
2790
+ if (authenticationProof == NomosOfferAuthenticationProof.enrolledDevice &&
2791
+ (secret == null || secret.isEmpty)) {
2792
+ throw StateError(
2793
+ 'This law requires proof from an enrolled device, but the session has no device key',
2794
+ );
2795
+ }
2796
+ final token = switch (authenticationProof) {
2797
+ NomosOfferAuthenticationProof.sessionCredentials => sessionToken,
2798
+ NomosOfferAuthenticationProof.enrolledDevice => null,
2799
+ };
2800
+ final response = await kernelTransport.callKernel(
2801
+ session: runtimeSession,
2802
+ request: nomos_protocol.CallRequest(
2803
+ protocolVersion: 1,
2804
+ requestId: '$runtimeSession:offer:$sequence',
2805
+ offer: nomos_protocol.OfferRequest(
2806
+ target: nomos_protocol.WorkspaceTarget(
2807
+ repo: '/work/ws/$runtimeSession',
2808
+ workspace: workspace,
2809
+ branch: 'main',
2810
+ ),
2811
+ call: nomos_protocol.DirectiveCall(
2812
+ domain: call.domain,
2813
+ directive: call.directive,
2814
+ callUsda: call.canonicalUsda,
2815
+ ),
2816
+ authoring: nomos_protocol.AuthoringOptions(
2817
+ actor: actor ?? principal ?? '',
2818
+ authToken: token == null ? const <int>[] : utf8.encode(token),
2819
+ authorSecret: secret == null ? const <int>[] : base64Decode(secret),
2820
+ rng: List<double>.generate(64, (_) => random.nextDouble()),
2821
+ deferProjection: true,
2822
+ spans: true,
2823
+ sequence: fixnum.Int64(sequence),
2824
+ clock: nomos_protocol.CapturedClock(
2825
+ physicalMillis: fixnum.Int64(now),
2826
+ logical: 0,
2827
+ replica: fixnum.Int64(replica),
2828
+ ),
2829
+ ),
2830
+ controllerLawHash: domainHash,
2831
+ conflictSnapshots: conflictSnapshots,
2832
+ birthCertificate: birthCertificate,
2833
+ ),
2834
+ ),
2835
+ );
2836
+ if (response.hasFailure()) {
2837
+ throw NomosKernelCallFailure(
2838
+ code: response.failure.code,
2839
+ message: response.failure.message,
2840
+ );
2841
+ }
2842
+ if (!response.hasOffer()) {
2843
+ throw StateError(
2844
+ 'Kernel returned ${response.whichOutcome().name} for an offer request',
2845
+ );
2846
+ }
2847
+ final offer = response.offer;
2848
+ if (!offer.hasAdmitted()) throw NomosTypedOfferFailure(offer);
2849
+ final admitted = offer.admitted;
2850
+ final birthOutcomes = List<NomosBirthOutcome>.unmodifiable(
2851
+ admitted.birthOutcomes,
2852
+ );
2853
+ final failedBirths = birthOutcomes
2854
+ .where(
2855
+ (birth) =>
2856
+ birth.status != NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN,
2857
+ )
2858
+ .toList(growable: false);
2859
+ if (failedBirths.isNotEmpty) {
2860
+ throw BirthIncomplete(failedBirths, head: admitted.head);
2861
+ }
2862
+ final receipt =
2863
+ _registerOperationReceipt(runtimeSession, admitted.intentId);
2864
+ final born = admitted.born
2865
+ .map((child) => child.workspace)
2866
+ .where((child) => child.isNotEmpty)
2867
+ .toList(growable: false);
2868
+ for (final child in born) {
2869
+ plane._birthReceipts[child] = receipt;
2870
+ final offerPrincipal = actor ?? principal;
2871
+ if (offerPrincipal != null && offerPrincipal.isNotEmpty) {
2872
+ plane._sessionPrincipals[child] = offerPrincipal;
2873
+ }
2874
+ if (secret != null && secret.isNotEmpty) {
2875
+ plane._sessionAuthorSecrets[child] = secret;
2876
+ }
2877
+ }
2878
+ return NomosOfferOutcome(
2879
+ head: admitted.head,
2880
+ intentId: admitted.intentId,
2881
+ receipt: receipt,
2882
+ workspace: workspace,
2883
+ born: born,
2884
+ birthOutcomes: birthOutcomes,
2885
+ created: admitted.created
2886
+ .map(
2887
+ (created) => NomosCreated(
2888
+ type: created.type,
2889
+ id: created.id,
2890
+ workspace: workspace,
2891
+ ),
2892
+ )
2893
+ .toList(growable: false),
2894
+ affected: admitted.affected
2895
+ .map(
2896
+ (affected) => NomosCreated(
2897
+ type: affected.type,
2898
+ id: affected.id,
2899
+ workspace: workspace,
2900
+ ),
2901
+ )
2902
+ .toList(growable: false),
2903
+ );
2904
+ }
2905
+
2236
2906
  /// THE ONE OUTCOME-RETURNING OFFER LANE — [offerDirective] answering the full typed [NomosOfferOutcome]
2237
2907
  /// (head + law-minted `created` NomosRefs + births), for EVERY generated method (create or mutate). A pure
2238
2908
  /// mutation returns `created: []`; a `.creates(...)` plan returns the minted aggregate id(s), each already
@@ -2278,12 +2948,15 @@ class NomosBridge {
2278
2948
  base64Encode(birthCertificate.writeToBuffer()),
2279
2949
  }),
2280
2950
  );
2281
- } catch (e) {
2282
- throw await _wrapDomainNotInstalled(
2283
- e,
2284
- domain: domain,
2285
- domainHash: domainHash,
2286
- requestedWorkspace: session,
2951
+ } catch (e, stack) {
2952
+ Error.throwWithStackTrace(
2953
+ await _wrapDomainNotInstalled(
2954
+ e,
2955
+ domain: domain,
2956
+ domainHash: domainHash,
2957
+ requestedWorkspace: session,
2958
+ ),
2959
+ stack,
2287
2960
  );
2288
2961
  }
2289
2962
  final t1 = DateTime.now().millisecondsSinceEpoch;
@@ -2314,15 +2987,18 @@ class NomosBridge {
2314
2987
  }
2315
2988
  final births = ((r['birthOutcomes'] as List?) ?? const [])
2316
2989
  .whereType<Map>()
2317
- .map((m) => m.cast<String, Object?>())
2318
- .toList();
2990
+ .map(_birthOutcomeFromLegacyWire)
2991
+ .toList(growable: false);
2319
2992
  // FAIL FAST on a birth that did NOT come up. The JS holon (index.mjs) already throws on this, so we
2320
2993
  // normally never see it here — but a runner/bundle skew (an older runner that passes birthOutcomes
2321
2994
  // through without throwing) must NOT let a silent partial birth reach the app to strand on head:null.
2322
2995
  // A `status` other than "born" is a failure; throw the typed [BirthIncomplete].
2323
2996
  final failedBirths = births
2324
- .where((m) => m['status'] != null && m['status'] != 'born')
2325
- .toList();
2997
+ .where(
2998
+ (birth) =>
2999
+ birth.status != NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN,
3000
+ )
3001
+ .toList(growable: false);
2326
3002
  if (failedBirths.isNotEmpty) {
2327
3003
  throw BirthIncomplete(failedBirths, head: (r['head'] ?? '').toString());
2328
3004
  }
@@ -2335,9 +3011,7 @@ class NomosBridge {
2335
3011
  (e) => (e is Map ? (e['workspace'] ?? '') : e).toString(),
2336
3012
  )
2337
3013
  else
2338
- ...births
2339
- .map((m) => (m['workspace'] ?? '').toString())
2340
- .where((s) => s.isNotEmpty),
3014
+ ...births.map((birth) => birth.workspace).where((s) => s.isNotEmpty),
2341
3015
  ]..removeWhere((s) => s.isEmpty);
2342
3016
  final birthPrincipal = actor ?? principal;
2343
3017
  if (birthPrincipal != null && birthPrincipal.isNotEmpty) {
@@ -2446,11 +3120,21 @@ class NomosBridge {
2446
3120
  String? directiveId,
2447
3121
  required String packageHash,
2448
3122
  }) async {
2449
- await _call('prepareDomain', {
2450
- 'domain': domain,
2451
- if (directiveId != null) 'directiveId': directiveId,
2452
- 'packageHash': packageHash,
2453
- });
3123
+ final response = await _invokeRuntimeV2(
3124
+ nomos_protocol.RuntimeRequest(
3125
+ prepareDomain: nomos_protocol.PrepareDomainRequest(
3126
+ session: _runtimeSession(),
3127
+ domain: domain,
3128
+ packageSha256: packageHash,
3129
+ directive: directiveId,
3130
+ ),
3131
+ ),
3132
+ );
3133
+ if (!response.hasDomainPrepared()) {
3134
+ throw StateError(
3135
+ 'Runtime v2 returned ${response.whichOutcome().name} for prepareDomain',
3136
+ );
3137
+ }
2454
3138
  }
2455
3139
 
2456
3140
  /// FOLLOW A [NomosRef] — THE cross-workspace primitive (session_bound_clients.md ask 8): mount
@@ -2615,7 +3299,7 @@ class NomosBridge {
2615
3299
  var resident = false;
2616
3300
  try {
2617
3301
  resident = (await listWorkspaces()).any(
2618
- (w) => w['workspace']?.toString() == workspace,
3302
+ (candidate) => candidate.workspace == workspace,
2619
3303
  );
2620
3304
  } catch (_) {
2621
3305
  // a pre-connect plane has no registry yet — fall through to the credentials decision below.
@@ -2718,6 +3402,13 @@ class NomosBridge {
2718
3402
  // together. Otherwise home signs correctly after first-device enrolment
2719
3403
  // while a later catalogue/estate opens with the old empty default key.
2720
3404
  final session = _sessionName;
3405
+ if (session != null && authToken != null) {
3406
+ if (authToken.isEmpty) {
3407
+ _eventPlane._sessionAuthTokens.remove(session);
3408
+ } else {
3409
+ _eventPlane._sessionAuthTokens[session] = authToken;
3410
+ }
3411
+ }
2721
3412
  if (session != null && authorSecret != null) {
2722
3413
  if (authorSecret.isEmpty) {
2723
3414
  _eventPlane._sessionAuthorSecrets.remove(session);
@@ -2836,12 +3527,15 @@ class NomosBridge {
2836
3527
  if (authToken != null) 'authToken': authToken,
2837
3528
  }),
2838
3529
  );
2839
- } catch (e) {
2840
- throw await _wrapDomainNotInstalled(
2841
- e,
2842
- domain: domain,
2843
- domainHash: domainHash,
2844
- requestedWorkspace: parent,
3530
+ } catch (e, stack) {
3531
+ Error.throwWithStackTrace(
3532
+ await _wrapDomainNotInstalled(
3533
+ e,
3534
+ domain: domain,
3535
+ domainHash: domainHash,
3536
+ requestedWorkspace: parent,
3537
+ ),
3538
+ stack,
2845
3539
  );
2846
3540
  }
2847
3541
  final verdict = (r['verdict'] as Map?)?.cast<String, Object?>() ?? r;
@@ -2949,20 +3643,30 @@ class NomosBridge {
2949
3643
  /// Framework-private exact-history adoption seam. Generated clients expose this as
2950
3644
  /// `model.adopt()` and, only when prior business outcomes differ, a chained
2951
3645
  /// `decision.accept(reason)`. Candidate identities, heads and receipts stay here.
2952
- Future<Map<String, Object?>> nomosFrameworkAdoptGeneratedModel({
3646
+ Future<nomos_protocol.ModelLifecycleResponse>
3647
+ nomosFrameworkAdoptGeneratedModel({
2953
3648
  required String domain,
2954
3649
  required String candidateHash,
2955
3650
  String? because,
2956
3651
  }) async {
2957
- final r = _asMap(
2958
- await _call('adoptGeneratedModel', {
2959
- 'domain': domain,
2960
- 'candidateHash': candidateHash,
2961
- if (because != null) 'because': because,
2962
- }),
3652
+ final response = await _invokeRuntimeV2(
3653
+ nomos_protocol.RuntimeRequest(
3654
+ modelLifecycle: nomos_protocol.ModelLifecycleRequest(
3655
+ session: _runtimeSession(),
3656
+ adopt: nomos_protocol.AdoptGeneratedModelRequest(
3657
+ domain: domain,
3658
+ candidateHash: candidateHash,
3659
+ acceptanceReason: because,
3660
+ ),
3661
+ ),
3662
+ ),
2963
3663
  );
2964
- return ((r['verdict'] as Map?) ?? const <String, Object?>{})
2965
- .cast<String, Object?>();
3664
+ if (!response.hasModelLifecycle()) {
3665
+ throw StateError(
3666
+ 'Runtime v2 returned ${response.whichOutcome().name} for model adoption',
3667
+ );
3668
+ }
3669
+ return response.modelLifecycle;
2966
3670
  }
2967
3671
 
2968
3672
  /// Framework-private generated-model staging seam. The generated client owns
@@ -2973,30 +3677,54 @@ class NomosBridge {
2973
3677
  required String candidateHash,
2974
3678
  required List<int> lawUsdz,
2975
3679
  }) async {
2976
- await _call('stageGeneratedModel', {
2977
- 'domain': domain,
2978
- 'candidateHash': candidateHash,
2979
- 'lawUsdz': base64Encode(lawUsdz),
2980
- });
3680
+ final response = await _invokeRuntimeV2(
3681
+ nomos_protocol.RuntimeRequest(
3682
+ modelLifecycle: nomos_protocol.ModelLifecycleRequest(
3683
+ session: _runtimeSession(),
3684
+ stage: nomos_protocol.StageGeneratedModelRequest(
3685
+ domain: domain,
3686
+ candidateHash: candidateHash,
3687
+ lawUsdz: lawUsdz,
3688
+ ),
3689
+ ),
3690
+ ),
3691
+ );
3692
+ if (!response.hasModelLifecycle() || !response.modelLifecycle.accepted) {
3693
+ throw StateError(
3694
+ response.hasModelLifecycle() && response.modelLifecycle.hasMessage()
3695
+ ? response.modelLifecycle.message
3696
+ : 'Runtime v2 refused generated model staging',
3697
+ );
3698
+ }
2981
3699
  }
2982
3700
 
2983
3701
  /// Framework-private exact-history restoration seam. Generated clients expose
2984
3702
  /// `model.restorePrevious()` and a chained `decision.accept(reason)`; predecessor
2985
3703
  /// identity, derived dispositions, heads and receipts stay below this bridge.
2986
- Future<Map<String, Object?>> nomosFrameworkRestorePreviousGeneratedModel({
3704
+ Future<nomos_protocol.ModelLifecycleResponse>
3705
+ nomosFrameworkRestorePreviousGeneratedModel({
2987
3706
  required String domain,
2988
3707
  required String currentHash,
2989
3708
  String? because,
2990
3709
  }) async {
2991
- final r = _asMap(
2992
- await _call('restorePreviousGeneratedModel', {
2993
- 'domain': domain,
2994
- 'currentHash': currentHash,
2995
- if (because != null) 'because': because,
2996
- }),
3710
+ final response = await _invokeRuntimeV2(
3711
+ nomos_protocol.RuntimeRequest(
3712
+ modelLifecycle: nomos_protocol.ModelLifecycleRequest(
3713
+ session: _runtimeSession(),
3714
+ restore: nomos_protocol.RestoreGeneratedModelRequest(
3715
+ domain: domain,
3716
+ currentHash: currentHash,
3717
+ acceptanceReason: because,
3718
+ ),
3719
+ ),
3720
+ ),
2997
3721
  );
2998
- return ((r['verdict'] as Map?) ?? const <String, Object?>{})
2999
- .cast<String, Object?>();
3722
+ if (!response.hasModelLifecycle()) {
3723
+ throw StateError(
3724
+ 'Runtime v2 returned ${response.whichOutcome().name} for model restoration',
3725
+ );
3726
+ }
3727
+ return response.modelLifecycle;
3000
3728
  }
3001
3729
 
3002
3730
  /// HISTORY of one aggregate (a todo) — the full action timeline from the ledger: every intent that touched
@@ -3231,10 +3959,62 @@ class NomosBridge {
3231
3959
 
3232
3960
  /// Framework-private receiving-group setup. Generated application hosts own this ceremony; tenant code
3233
3961
  /// receives only the resulting business application.
3234
- Future<Map<String, Object?>> nomosInternalBirthReceivingGroup(
3235
- Map<String, Object?> spec,
3236
- ) async =>
3237
- _asMap(await _call('nomosInternalBirthReceivingGroup', spec));
3962
+ Future<nomos_protocol.ApplicationCustodyEstablished>
3963
+ nomosInternalBirthReceivingGroup({
3964
+ required String parent,
3965
+ required String principal,
3966
+ required String subject,
3967
+ required Map<String, Object?> device,
3968
+ required String authToken,
3969
+ required String idToken,
3970
+ required String verifiedVia,
3971
+ required String bornAt,
3972
+ required NmsGeneratedCall enrollFirstDeviceCall,
3973
+ required NmsGeneratedCall birthReceivingGroupCall,
3974
+ bool confirm = true,
3975
+ }) async {
3976
+ final secret = device['secret'];
3977
+ final public = device['public'];
3978
+ if (secret is! String || public is! String) {
3979
+ throw ArgumentError(
3980
+ 'application custody requires a generated device keypair',
3981
+ );
3982
+ }
3983
+ final response = await _invokeRuntimeV2(
3984
+ nomos_protocol.RuntimeRequest(
3985
+ establishApplicationCustody:
3986
+ nomos_protocol.EstablishApplicationCustodyRequest(
3987
+ session: _runtimeSession(),
3988
+ parent: parent,
3989
+ principal: principal,
3990
+ subject: subject,
3991
+ deviceSecret: base64Decode(secret),
3992
+ devicePublic: base64Decode(public),
3993
+ authToken: authToken,
3994
+ idToken: idToken,
3995
+ verifiedVia: verifiedVia,
3996
+ bornAt: bornAt,
3997
+ enrollFirstDevice: nomos_protocol.DirectiveCall(
3998
+ domain: enrollFirstDeviceCall.domain,
3999
+ directive: enrollFirstDeviceCall.directive,
4000
+ callUsda: enrollFirstDeviceCall.canonicalUsda,
4001
+ ),
4002
+ birthReceivingGroup: nomos_protocol.DirectiveCall(
4003
+ domain: birthReceivingGroupCall.domain,
4004
+ directive: birthReceivingGroupCall.directive,
4005
+ callUsda: birthReceivingGroupCall.canonicalUsda,
4006
+ ),
4007
+ confirm: confirm,
4008
+ ),
4009
+ ),
4010
+ );
4011
+ if (!response.hasApplicationCustodyEstablished()) {
4012
+ throw StateError(
4013
+ 'Runtime v2 returned ${response.whichOutcome().name} for application custody',
4014
+ );
4015
+ }
4016
+ return response.applicationCustodyEstablished;
4017
+ }
3238
4018
 
3239
4019
  /// Sign a cross-custody SHARE attestation `{secret, asserterWorkspace, object, relation, subject,
3240
4020
  /// attestedAt}` → `{publicKey, signature}`. This is what drives `recordShare` into an invitee's home
@@ -3274,75 +4054,772 @@ class NomosBridge {
3274
4054
  }),
3275
4055
  );
3276
4056
 
3277
- /// A declared query's rows from the local projection. `keys` (E2E sealing) injects the reader's held
3278
- /// scope keys so encrypted fields AEAD-open in-wasm; absent ⇒ they stay opaque envelopes (today's
3279
- /// behavior). `principal` stamps the private-read gate (defaults to the connection's bound principal).
3280
- /// Both are OMITTED from the wire when absent, so a public un-keyed read is byte-identical to before.
3281
- Future<List<Map<String, dynamic>>> query(
3282
- String queryId, [
3283
- Map<String, Object?> params = const {},
4057
+ nomos_protocol.QueryAuthority _nmsQueryAuthority(
3284
4058
  List<NomosScopeKey>? keys,
3285
4059
  String? principal,
3286
4060
  String? session,
3287
- ]) async {
3288
- return _asRows(
3289
- await _call('query', {
3290
- 'queryId': queryId,
3291
- 'params': params,
3292
- ...?_readAuthArgs(keys, principal),
3293
- if (session != null) 'session': session,
3294
- }),
4061
+ ) {
4062
+ final runtimeSession = session ?? _sessionName;
4063
+ if (runtimeSession == null || runtimeSession.isEmpty) {
4064
+ throw StateError(
4065
+ 'A nominal query must be bound to a connected NomosSession');
4066
+ }
4067
+ final plane = _eventPlane;
4068
+ final token = plane._sessionAuthTokens[runtimeSession];
4069
+ final secret = plane._sessionAuthorSecrets[runtimeSession];
4070
+ return nomos_protocol.QueryAuthority(
4071
+ principalClaim:
4072
+ principal ?? plane._sessionPrincipals[runtimeSession] ?? '',
4073
+ authToken: token == null ? const <int>[] : utf8.encode(token),
4074
+ nowUnix: fixnum.Int64(
4075
+ DateTime.now().millisecondsSinceEpoch ~/ 1000,
4076
+ ),
4077
+ authorSecret: secret == null ? const <int>[] : base64Decode(secret),
4078
+ keys: (keys ?? const <NomosScopeKey>[]).map(
4079
+ (key) => nomos_protocol.InjectedKey(
4080
+ scope: key.scope,
4081
+ epoch: fixnum.Int64(key.epoch),
4082
+ key: base64Decode(key.key),
4083
+ ),
4084
+ ),
3295
4085
  );
3296
4086
  }
3297
4087
 
3298
- /// One bounded query page. Unlike [query], this never requires the kernel to materialize or serialize
3299
- /// the complete result set merely to show the first screen.
3300
- Future<NomosPage<Map<String, dynamic>>> queryPage(
3301
- String queryId, {
3302
- Map<String, Object?> params = const {},
3303
- int pageSize = 100,
3304
- String? cursor,
3305
- List<NomosScopeKey>? keys,
3306
- String? principal,
3307
- String? session,
4088
+ Future<Uint8List> _nmsKernelQueryUsda({
4089
+ required String? session,
4090
+ required nomos_protocol.QueryAuthority authority,
4091
+ required nomos_protocol.QueryRequest query,
3308
4092
  }) async {
3309
- if (pageSize < 1 || pageSize > 1000) {
3310
- throw RangeError.range(pageSize, 1, 1000, 'pageSize');
3311
- }
3312
- final result = _asMap(
3313
- await _call('queryPage', {
3314
- 'queryId': queryId,
3315
- 'params': params,
3316
- 'pageSize': pageSize,
3317
- if (cursor != null) 'cursor': cursor,
3318
- ...?_readAuthArgs(keys, principal),
3319
- if (session != null) 'session': session,
3320
- }),
4093
+ final response = await _nmsKernelQuery(
4094
+ session: session,
4095
+ authority: authority,
4096
+ query: query,
3321
4097
  );
3322
- return NomosPage<Map<String, dynamic>>(
3323
- rows: _asRows(result),
3324
- cursor: result['cursor'] as String?,
3325
- done: result['done'] == true,
4098
+ if (!response.hasResultUsda()) {
4099
+ throw StateError(
4100
+ 'Kernel returned ${response.whichResult().name} without a nominal USDA query result',
4101
+ );
4102
+ }
4103
+ return Uint8List.fromList(response.resultUsda);
4104
+ }
4105
+
4106
+ Future<nomos_protocol.QueryResponse> _nmsKernelQuery({
4107
+ required String? session,
4108
+ required nomos_protocol.QueryAuthority authority,
4109
+ required nomos_protocol.QueryRequest query,
4110
+ }) async {
4111
+ final runtimeSession = session ?? _sessionName;
4112
+ if (runtimeSession == null || runtimeSession.isEmpty) {
4113
+ throw StateError(
4114
+ 'A nominal query must be bound to a connected NomosSession');
4115
+ }
4116
+ final workspace =
4117
+ _eventPlane._workspaceBySession[runtimeSession] ?? runtimeSession;
4118
+ final transport = _transport;
4119
+ if (transport is! NomosKernelCallTransport) {
4120
+ throw UnsupportedError(
4121
+ 'This host does not implement the generated Runtime v2 protobuf lane',
4122
+ );
4123
+ }
4124
+ query
4125
+ ..target = nomos_protocol.WorkspaceTarget(
4126
+ repo: '/work/ws/$runtimeSession',
4127
+ workspace: workspace,
4128
+ branch: 'main',
4129
+ )
4130
+ ..authority = authority
4131
+ ..spans = true
4132
+ ..resultFormat =
4133
+ nomos_protocol.QueryResultFormat.QUERY_RESULT_FORMAT_NOMINAL_USDA;
4134
+ final requestNumber = ++_seq;
4135
+ final response = await (transport as NomosKernelCallTransport).callKernel(
4136
+ session: runtimeSession,
4137
+ request: nomos_protocol.CallRequest(
4138
+ protocolVersion: 1,
4139
+ requestId: '$runtimeSession:query:$requestNumber',
4140
+ query: query,
4141
+ ),
3326
4142
  );
4143
+ if (response.hasFailure()) {
4144
+ throw StateError('${response.failure.code}: ${response.failure.message}');
4145
+ }
4146
+ if (!response.hasQuery()) {
4147
+ throw StateError(
4148
+ 'Kernel returned ${response.whichOutcome().name} without a query result',
4149
+ );
4150
+ }
4151
+ return response.query;
3327
4152
  }
3328
4153
 
3329
- /// A single aggregate by id (the by-id read). `keys`/`principal`: see [query].
3330
- Future<List<Map<String, dynamic>>> queryById(
3331
- String id, [
3332
- List<NomosScopeKey>? keys,
3333
- String? principal,
4154
+ NomosProviderTokenVerification _providerTokenVerification(
4155
+ nomos_protocol.ProviderTokenVerificationStatus status,
4156
+ ) =>
4157
+ switch (status) {
4158
+ nomos_protocol.ProviderTokenVerificationStatus
4159
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_VALID =>
4160
+ NomosProviderTokenVerification.valid,
4161
+ nomos_protocol.ProviderTokenVerificationStatus
4162
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_NO_MATCHING_KEY =>
4163
+ NomosProviderTokenVerification.noMatchingKey,
4164
+ nomos_protocol.ProviderTokenVerificationStatus
4165
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_EXPIRED =>
4166
+ NomosProviderTokenVerification.expired,
4167
+ nomos_protocol.ProviderTokenVerificationStatus
4168
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_NOT_YET_VALID =>
4169
+ NomosProviderTokenVerification.notYetValid,
4170
+ nomos_protocol.ProviderTokenVerificationStatus
4171
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_ISSUER_MISMATCH =>
4172
+ NomosProviderTokenVerification.issuerMismatch,
4173
+ nomos_protocol.ProviderTokenVerificationStatus
4174
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_AUDIENCE_MISMATCH =>
4175
+ NomosProviderTokenVerification.audienceMismatch,
4176
+ nomos_protocol.ProviderTokenVerificationStatus
4177
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_BAD_SIGNATURE =>
4178
+ NomosProviderTokenVerification.badSignature,
4179
+ nomos_protocol.ProviderTokenVerificationStatus
4180
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_MALFORMED =>
4181
+ NomosProviderTokenVerification.malformed,
4182
+ nomos_protocol.ProviderTokenVerificationStatus
4183
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_UNSUPPORTED_ALGORITHM =>
4184
+ NomosProviderTokenVerification.unsupportedAlgorithm,
4185
+ nomos_protocol.ProviderTokenVerificationStatus
4186
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_BAD_KEY =>
4187
+ NomosProviderTokenVerification.badKey,
4188
+ nomos_protocol.ProviderTokenVerificationStatus
4189
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_MISSING_SUBJECT =>
4190
+ NomosProviderTokenVerification.missingSubject,
4191
+ nomos_protocol.ProviderTokenVerificationStatus
4192
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_NO_VERIFIER =>
4193
+ NomosProviderTokenVerification.noVerifier,
4194
+ nomos_protocol.ProviderTokenVerificationStatus
4195
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_MISSING_TOKEN =>
4196
+ NomosProviderTokenVerification.missingToken,
4197
+ nomos_protocol.ProviderTokenVerificationStatus
4198
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_UNSPECIFIED =>
4199
+ NomosProviderTokenVerification.unspecified,
4200
+ _ => NomosProviderTokenVerification.unspecified,
4201
+ };
4202
+
4203
+ /// Ask the kernel—not the host—to verify the session's current provider
4204
+ /// token and enrolled-device relation against the workspace's current law.
4205
+ @internal
4206
+ Future<NomosAuthenticationPosture> nmsAuthenticationPosture({
3334
4207
  String? session,
3335
- ]) async =>
3336
- _asRows(
3337
- await _call('queryById', {
3338
- 'id': id,
3339
- ...?_readAuthArgs(keys, principal),
3340
- if (session != null) 'session': session,
3341
- }),
4208
+ String? principal,
4209
+ }) async {
4210
+ final response = await _nmsKernelQuery(
4211
+ session: session,
4212
+ authority: _nmsQueryAuthority(null, principal, session),
4213
+ query: nomos_protocol.QueryRequest(
4214
+ authenticationPosture: nomos_protocol.AuthenticationPostureQuery(),
4215
+ ),
4216
+ );
4217
+ if (!response.hasAuthenticationPosture()) {
4218
+ throw StateError(
4219
+ 'Kernel returned ${response.whichResult().name}, expected authenticationPosture',
3342
4220
  );
4221
+ }
4222
+ final posture = response.authenticationPosture;
4223
+ return NomosAuthenticationPosture(
4224
+ providerToken: _providerTokenVerification(posture.providerToken),
4225
+ enrolledDevice: posture.enrolledDevice,
4226
+ resolvedPrincipal: posture.resolvedPrincipal,
4227
+ );
4228
+ }
3343
4229
 
3344
- /// Compare an immutable publication pin with the stable series' one compiled
3345
- /// current pointer. This is an O(current-state) check: it neither resolves the
4230
+ /// Drive actual custody synchronization until the kernel proves that the
4231
+ /// provider token verifies, the device remains enrolled, and the resolved
4232
+ /// principal is the session principal. No timer or receipt is treated as the
4233
+ /// business postcondition.
4234
+ @internal
4235
+ Future<NomosAuthenticationPosture> nmsAwaitAuthenticationReady({
4236
+ String? session,
4237
+ String? principal,
4238
+ int maximumSyncRounds = 4,
4239
+ }) async {
4240
+ final runtimeSession = session ?? _sessionName;
4241
+ final expectedPrincipal = principal ??
4242
+ this.principal ??
4243
+ (runtimeSession == null
4244
+ ? null
4245
+ : _eventPlane._sessionPrincipals[runtimeSession]) ??
4246
+ '';
4247
+ var posture = await nmsAuthenticationPosture(
4248
+ session: session,
4249
+ principal: expectedPrincipal,
4250
+ );
4251
+ if (posture.isReadyFor(expectedPrincipal)) return posture;
4252
+
4253
+ for (var round = 0; round < maximumSyncRounds; round++) {
4254
+ final result = await sync(admit: true, session: session);
4255
+ if (result.offline || result.pushFailed) {
4256
+ throw StateError(
4257
+ 'Authentication recovery could not converge custody: '
4258
+ '${result.hasPushError() ? result.pushError.message : 'workspace is offline'}',
4259
+ );
4260
+ }
4261
+ posture = await nmsAuthenticationPosture(
4262
+ session: session,
4263
+ principal: expectedPrincipal,
4264
+ );
4265
+ if (posture.isReadyFor(expectedPrincipal)) return posture;
4266
+ }
4267
+ throw StateError(
4268
+ 'Authentication recovery did not establish a valid provider token and enrolled device '
4269
+ 'for $expectedPrincipal after $maximumSyncRounds custody rounds '
4270
+ '(token: ${posture.providerToken.name}, enrolled: ${posture.enrolledDevice}, '
4271
+ 'resolved: ${posture.resolvedPrincipal})',
4272
+ );
4273
+ }
4274
+
4275
+ Future<nomos_protocol.QueryResponse> _nmsKernelHistoricQuery({
4276
+ required NomosMoment moment,
4277
+ required String? session,
4278
+ required nomos_protocol.QueryAuthority authority,
4279
+ required nomos_protocol.QueryRequest query,
4280
+ }) async {
4281
+ final runtimeSession = session ?? _sessionName;
4282
+ if (runtimeSession == null || runtimeSession.isEmpty) {
4283
+ throw StateError(
4284
+ 'A nominal historic query must be bound to a connected NomosSession');
4285
+ }
4286
+ final workspace =
4287
+ _eventPlane._workspaceBySession[runtimeSession] ?? runtimeSession;
4288
+ query
4289
+ ..target = nomos_protocol.WorkspaceTarget(
4290
+ repo: '/work/ws/$runtimeSession',
4291
+ workspace: workspace,
4292
+ branch: 'main',
4293
+ )
4294
+ ..authority = authority
4295
+ ..spans = true
4296
+ ..resultFormat =
4297
+ nomos_protocol.QueryResultFormat.QUERY_RESULT_FORMAT_NOMINAL_USDA;
4298
+ final response = await _invokeRuntimeV2(
4299
+ nomos_protocol.RuntimeRequest(
4300
+ historicQuery: nomos_protocol.HistoricQueryRequest(
4301
+ session: runtimeSession,
4302
+ moment: moment.id,
4303
+ query: query,
4304
+ ),
4305
+ ),
4306
+ );
4307
+ if (response.hasFailure()) {
4308
+ throw StateError('${response.failure.code}: ${response.failure.message}');
4309
+ }
4310
+ if (!response.hasKernelCall()) {
4311
+ throw StateError(
4312
+ 'Runtime v2 returned ${response.whichOutcome().name} for a historic query',
4313
+ );
4314
+ }
4315
+ final kernel = response.kernelCall;
4316
+ if (kernel.hasFailure()) {
4317
+ throw StateError('${kernel.failure.code}: ${kernel.failure.message}');
4318
+ }
4319
+ if (!kernel.hasQuery()) {
4320
+ throw StateError(
4321
+ 'Kernel returned ${kernel.whichOutcome().name} without a historic query result',
4322
+ );
4323
+ }
4324
+ return kernel.query;
4325
+ }
4326
+
4327
+ Future<Uint8List> _nmsKernelHistoricQueryUsda({
4328
+ required NomosMoment moment,
4329
+ required String? session,
4330
+ required nomos_protocol.QueryAuthority authority,
4331
+ required nomos_protocol.QueryRequest query,
4332
+ }) async {
4333
+ final response = await _nmsKernelHistoricQuery(
4334
+ moment: moment,
4335
+ session: session,
4336
+ authority: authority,
4337
+ query: query,
4338
+ );
4339
+ if (!response.hasResultUsda()) {
4340
+ throw StateError(
4341
+ 'Kernel returned ${response.whichResult().name} without a nominal historic USDA result',
4342
+ );
4343
+ }
4344
+ return Uint8List.fromList(response.resultUsda);
4345
+ }
4346
+
4347
+ /// A declared query's rows from the local projection. `keys` (E2E sealing) injects the reader's held
4348
+ /// scope keys so encrypted fields AEAD-open in-wasm; absent ⇒ they stay opaque envelopes (today's
4349
+ /// behavior). `principal` stamps the private-read gate (defaults to the connection's bound principal).
4350
+ /// Both are OMITTED from the wire when absent, so a public un-keyed read is byte-identical to before.
4351
+ Future<List<Map<String, dynamic>>> query(
4352
+ String queryId, [
4353
+ Map<String, Object?> params = const {},
4354
+ List<NomosScopeKey>? keys,
4355
+ String? principal,
4356
+ String? session,
4357
+ ]) async {
4358
+ return _asRows(
4359
+ await _call('query', {
4360
+ 'queryId': queryId,
4361
+ 'params': params,
4362
+ ...?_readAuthArgs(keys, principal),
4363
+ if (session != null) 'session': session,
4364
+ }),
4365
+ );
4366
+ }
4367
+
4368
+ /// Framework-private tenant read lane. The command crosses the resident host
4369
+ /// as one generated Runtime v2/Kernel v1 protobuf frame and the domain result
4370
+ /// returns as canonical USDA bytes for the generated domain decoder.
4371
+ Future<Uint8List> nmsQueryUsda(
4372
+ NmsGeneratedQuery query, [
4373
+ List<NomosScopeKey>? keys,
4374
+ String? principal,
4375
+ String? session,
4376
+ ]) =>
4377
+ _nmsKernelQueryUsda(
4378
+ session: session,
4379
+ authority: _nmsQueryAuthority(keys, principal, session),
4380
+ query: nomos_protocol.QueryRequest(
4381
+ rows: nomos_protocol.RowsQuery(
4382
+ id: query.query,
4383
+ principal: principal ?? '',
4384
+ argumentsUsda: query.canonicalArgumentsUsda,
4385
+ ),
4386
+ ),
4387
+ );
4388
+
4389
+ /// The same generated declared query at one authenticated historic moment.
4390
+ /// Runtime v2 pins custody; the kernel still owns query semantics and emits
4391
+ /// the same nominal USDA result schema as the live read.
4392
+ Future<Uint8List> nmsQueryAtUsda(
4393
+ NomosMoment moment,
4394
+ NmsGeneratedQuery query, [
4395
+ List<NomosScopeKey>? keys,
4396
+ String? principal,
4397
+ String? session,
4398
+ ]) =>
4399
+ _nmsKernelHistoricQueryUsda(
4400
+ moment: moment,
4401
+ session: session,
4402
+ authority: _nmsQueryAuthority(keys, principal, session),
4403
+ query: nomos_protocol.QueryRequest(
4404
+ rows: nomos_protocol.RowsQuery(
4405
+ id: query.query,
4406
+ principal: principal ?? '',
4407
+ argumentsUsda: query.canonicalArgumentsUsda,
4408
+ ),
4409
+ ),
4410
+ );
4411
+
4412
+ /// Reactive form of [nmsQueryUsda]. Subscription mechanics are Runtime v2
4413
+ /// protobuf; each emission is the kernel's nominal USDA result and is decoded
4414
+ /// only by the generated domain client which owns [query].
4415
+ Stream<Uint8List> nmsWatchUsda(
4416
+ NmsGeneratedQuery query, {
4417
+ List<NomosScopeKey>? keys,
4418
+ String? principal,
4419
+ String? session,
4420
+ int intervalMs = 2000,
4421
+ }) =>
4422
+ _nmsWatchQueryUsda(
4423
+ nomos_protocol.QueryRequest(
4424
+ rows: nomos_protocol.RowsQuery(
4425
+ id: query.query,
4426
+ principal: principal ?? '',
4427
+ argumentsUsda: query.canonicalArgumentsUsda,
4428
+ ),
4429
+ ),
4430
+ keys: keys,
4431
+ principal: principal,
4432
+ session: session,
4433
+ intervalMs: intervalMs,
4434
+ );
4435
+
4436
+ /// Reactive by-id read. The generated domain decoder supplies the nominal
4437
+ /// aggregate type; this framework method owns only the protobuf mechanics.
4438
+ Stream<Uint8List> nmsWatchByIdUsda(
4439
+ String aggregateType,
4440
+ String id, {
4441
+ List<NomosScopeKey>? keys,
4442
+ String? principal,
4443
+ String? session,
4444
+ int intervalMs = 2000,
4445
+ }) =>
4446
+ _nmsWatchQueryUsda(
4447
+ nomos_protocol.QueryRequest(
4448
+ byId: nomos_protocol.ByIdQuery(
4449
+ aggregateType: aggregateType,
4450
+ aggregateId: id,
4451
+ principal: principal ?? '',
4452
+ ),
4453
+ ),
4454
+ keys: keys,
4455
+ principal: principal,
4456
+ session: session,
4457
+ intervalMs: intervalMs,
4458
+ );
4459
+
4460
+ /// Reactive type-index read. The aggregate type is a dedicated protobuf
4461
+ /// field, never a magic application query name or argument bag.
4462
+ Stream<Uint8List> nmsWatchByTypeUsda(
4463
+ String aggregateType, {
4464
+ List<NomosScopeKey>? keys,
4465
+ String? principal,
4466
+ String? session,
4467
+ int intervalMs = 2000,
4468
+ }) =>
4469
+ _nmsWatchQueryUsda(
4470
+ nomos_protocol.QueryRequest(
4471
+ byType: nomos_protocol.ByTypeQuery(
4472
+ aggregateType: aggregateType,
4473
+ principal: principal ?? '',
4474
+ ),
4475
+ ),
4476
+ keys: keys,
4477
+ principal: principal,
4478
+ session: session,
4479
+ intervalMs: intervalMs,
4480
+ );
4481
+
4482
+ Stream<Uint8List> _nmsWatchQueryUsda(
4483
+ nomos_protocol.QueryRequest request, {
4484
+ List<NomosScopeKey>? keys,
4485
+ String? principal,
4486
+ String? session,
4487
+ int intervalMs = 2000,
4488
+ }) {
4489
+ final runtimeSession = session ?? _sessionName;
4490
+ if (runtimeSession == null || runtimeSession.isEmpty) {
4491
+ throw StateError(
4492
+ 'A nominal watch must be bound to a connected NomosSession');
4493
+ }
4494
+ final transport = _transport;
4495
+ if (transport is! NomosRuntimeV2EventTransport) {
4496
+ throw UnsupportedError(
4497
+ 'This host does not implement Runtime v2 event frames');
4498
+ }
4499
+ final plane = _eventPlane;
4500
+ final workspace =
4501
+ plane._workspaceBySession[runtimeSession] ?? runtimeSession;
4502
+ final subscriptionId = '$runtimeSession:watch:${++plane._seq}';
4503
+ late final StreamController<Uint8List> controller;
4504
+ controller = StreamController<Uint8List>.broadcast(
4505
+ onCancel: () async {
4506
+ plane._nominalWatchControllers.remove(subscriptionId);
4507
+ try {
4508
+ await _unwatch(subscriptionId);
4509
+ } finally {
4510
+ if (!controller.isClosed) await controller.close();
4511
+ }
4512
+ },
4513
+ );
4514
+ plane._nominalWatchControllers[subscriptionId] = controller;
4515
+ request
4516
+ ..target = nomos_protocol.WorkspaceTarget(
4517
+ repo: '/work/ws/$runtimeSession',
4518
+ workspace: workspace,
4519
+ branch: 'main',
4520
+ )
4521
+ ..authority = _nmsQueryAuthority(keys, principal, runtimeSession)
4522
+ ..spans = true
4523
+ ..resultFormat =
4524
+ nomos_protocol.QueryResultFormat.QUERY_RESULT_FORMAT_NOMINAL_USDA;
4525
+ unawaited(() async {
4526
+ try {
4527
+ final response = await _invokeRuntimeV2(
4528
+ nomos_protocol.RuntimeRequest(
4529
+ watch: nomos_protocol.WatchRequest(
4530
+ session: runtimeSession,
4531
+ subscriptionId: subscriptionId,
4532
+ kind: nomos_protocol.WatchKind.WATCH_KIND_QUERY,
4533
+ query: request,
4534
+ intervalMillis: intervalMs,
4535
+ ),
4536
+ ),
4537
+ );
4538
+ if (!response.hasWatchStarted() ||
4539
+ response.watchStarted.subscriptionId != subscriptionId) {
4540
+ throw StateError(
4541
+ 'Runtime v2 returned ${response.whichOutcome().name} for nominal watch',
4542
+ );
4543
+ }
4544
+ } catch (error, stackTrace) {
4545
+ plane._nominalWatchControllers.remove(subscriptionId);
4546
+ if (!controller.isClosed) controller.addError(error, stackTrace);
4547
+ }
4548
+ }());
4549
+ return controller.stream;
4550
+ }
4551
+
4552
+ /// One bounded query page. Unlike [query], this never requires the kernel to materialize or serialize
4553
+ /// the complete result set merely to show the first screen.
4554
+ Future<NomosPage<Map<String, dynamic>>> queryPage(
4555
+ String queryId, {
4556
+ Map<String, Object?> params = const {},
4557
+ int pageSize = 100,
4558
+ String? cursor,
4559
+ List<NomosScopeKey>? keys,
4560
+ String? principal,
4561
+ String? session,
4562
+ }) async {
4563
+ if (pageSize < 1 || pageSize > 1000) {
4564
+ throw RangeError.range(pageSize, 1, 1000, 'pageSize');
4565
+ }
4566
+ final result = _asMap(
4567
+ await _call('queryPage', {
4568
+ 'queryId': queryId,
4569
+ 'params': params,
4570
+ 'pageSize': pageSize,
4571
+ if (cursor != null) 'cursor': cursor,
4572
+ ...?_readAuthArgs(keys, principal),
4573
+ if (session != null) 'session': session,
4574
+ }),
4575
+ );
4576
+ return NomosPage<Map<String, dynamic>>(
4577
+ rows: _asRows(result),
4578
+ cursor: result['cursor'] as String?,
4579
+ done: result['done'] == true,
4580
+ );
4581
+ }
4582
+
4583
+ /// Framework-private bounded form of [nmsQueryUsda]. Page metadata lives in
4584
+ /// the same USDA result; no object envelope is reconstructed in the host.
4585
+ Future<Uint8List> nmsQueryPageUsda(
4586
+ NmsGeneratedQuery query, {
4587
+ int pageSize = 100,
4588
+ String? cursor,
4589
+ List<NomosScopeKey>? keys,
4590
+ String? principal,
4591
+ String? session,
4592
+ }) {
4593
+ if (pageSize < 1 || pageSize > 1000) {
4594
+ throw RangeError.range(pageSize, 1, 1000, 'pageSize');
4595
+ }
4596
+ return _nmsKernelQueryUsda(
4597
+ session: session,
4598
+ authority: _nmsQueryAuthority(keys, principal, session),
4599
+ query: nomos_protocol.QueryRequest(
4600
+ page: nomos_protocol.PageQuery(
4601
+ id: query.query,
4602
+ limit: pageSize,
4603
+ after: cursor ?? '',
4604
+ principal: principal ?? '',
4605
+ argumentsUsda: query.canonicalArgumentsUsda,
4606
+ ),
4607
+ ),
4608
+ );
4609
+ }
4610
+
4611
+ /// A single aggregate by id (the by-id read). `keys`/`principal`: see [query].
4612
+ Future<List<Map<String, dynamic>>> queryById(
4613
+ String id, [
4614
+ List<NomosScopeKey>? keys,
4615
+ String? principal,
4616
+ String? session,
4617
+ ]) async =>
4618
+ _asRows(
4619
+ await _call('queryById', {
4620
+ 'id': id,
4621
+ ...?_readAuthArgs(keys, principal),
4622
+ if (session != null) 'session': session,
4623
+ }),
4624
+ );
4625
+
4626
+ /// Framework-private by-id form of [nmsQueryUsda].
4627
+ Future<Uint8List> nmsQueryByIdUsda(
4628
+ String aggregateType,
4629
+ String id, [
4630
+ List<NomosScopeKey>? keys,
4631
+ String? principal,
4632
+ String? session,
4633
+ ]) =>
4634
+ _nmsKernelQueryUsda(
4635
+ session: session,
4636
+ authority: _nmsQueryAuthority(keys, principal, session),
4637
+ query: nomos_protocol.QueryRequest(
4638
+ byId: nomos_protocol.ByIdQuery(
4639
+ aggregateType: aggregateType,
4640
+ aggregateId: id,
4641
+ principal: principal ?? '',
4642
+ ),
4643
+ ),
4644
+ );
4645
+
4646
+ Future<Uint8List> nmsQueryByIdAtUsda(
4647
+ NomosMoment moment,
4648
+ String aggregateType,
4649
+ String id, [
4650
+ List<NomosScopeKey>? keys,
4651
+ String? principal,
4652
+ String? session,
4653
+ ]) =>
4654
+ _nmsKernelHistoricQueryUsda(
4655
+ moment: moment,
4656
+ session: session,
4657
+ authority: _nmsQueryAuthority(keys, principal, session),
4658
+ query: nomos_protocol.QueryRequest(
4659
+ byId: nomos_protocol.ByIdQuery(
4660
+ aggregateType: aggregateType,
4661
+ aggregateId: id,
4662
+ principal: principal ?? '',
4663
+ ),
4664
+ ),
4665
+ );
4666
+
4667
+ /// Framework-private type-index read. Aggregate type is a fixed protobuf field, not a fake
4668
+ /// application query argument.
4669
+ Future<Uint8List> nmsQueryByTypeUsda(
4670
+ String aggregateType, [
4671
+ List<NomosScopeKey>? keys,
4672
+ String? principal,
4673
+ String? session,
4674
+ ]) =>
4675
+ _nmsKernelQueryUsda(
4676
+ session: session,
4677
+ authority: _nmsQueryAuthority(keys, principal, session),
4678
+ query: nomos_protocol.QueryRequest(
4679
+ byType: nomos_protocol.ByTypeQuery(
4680
+ aggregateType: aggregateType,
4681
+ principal: principal ?? '',
4682
+ ),
4683
+ ),
4684
+ );
4685
+
4686
+ Future<Uint8List> nmsQueryByTypeAtUsda(
4687
+ NomosMoment moment,
4688
+ String aggregateType, [
4689
+ List<NomosScopeKey>? keys,
4690
+ String? principal,
4691
+ String? session,
4692
+ ]) =>
4693
+ _nmsKernelHistoricQueryUsda(
4694
+ moment: moment,
4695
+ session: session,
4696
+ authority: _nmsQueryAuthority(keys, principal, session),
4697
+ query: nomos_protocol.QueryRequest(
4698
+ byType: nomos_protocol.ByTypeQuery(
4699
+ aggregateType: aggregateType,
4700
+ principal: principal ?? '',
4701
+ ),
4702
+ ),
4703
+ );
4704
+
4705
+ /// Exact spatial-index read for generated domain clients. [spatialId] is
4706
+ /// generated from the installed DSL graph; the application supplies only
4707
+ /// the typed [bbox]. Rows remain canonical USDA until its generated decoder.
4708
+ Future<Uint8List> nmsSpatialWithinUsda(
4709
+ String spatialId,
4710
+ NomosBbox bbox, [
4711
+ String? session,
4712
+ ]) =>
4713
+ _nmsKernelQueryUsda(
4714
+ session: session,
4715
+ authority: _nmsQueryAuthority(null, null, session),
4716
+ query: nomos_protocol.QueryRequest(
4717
+ spatial: nomos_protocol.SpatialQuery(
4718
+ id: spatialId,
4719
+ minX: bbox.minX,
4720
+ minY: bbox.minY,
4721
+ maxX: bbox.maxX,
4722
+ maxY: bbox.maxY,
4723
+ ),
4724
+ ),
4725
+ );
4726
+
4727
+ /// Reactive counterpart of [nmsSpatialWithinUsda]. Runtime v2 owns the
4728
+ /// subscription; every value is still the kernel's canonical USDA image.
4729
+ Stream<Uint8List> nmsWatchSpatialUsda(
4730
+ String spatialId,
4731
+ NomosBbox bbox, {
4732
+ String? session,
4733
+ int intervalMs = 2000,
4734
+ }) =>
4735
+ _nmsWatchQueryUsda(
4736
+ nomos_protocol.QueryRequest(
4737
+ spatial: nomos_protocol.SpatialQuery(
4738
+ id: spatialId,
4739
+ minX: bbox.minX,
4740
+ minY: bbox.minY,
4741
+ maxX: bbox.maxX,
4742
+ maxY: bbox.maxY,
4743
+ ),
4744
+ ),
4745
+ session: session,
4746
+ intervalMs: intervalMs,
4747
+ );
4748
+
4749
+ Future<int> _nmsTally(
4750
+ String id,
4751
+ String? groupKey, {
4752
+ required bool sum,
4753
+ String? session,
4754
+ NomosMoment? moment,
4755
+ }) async {
4756
+ final tally = nomos_protocol.TallyQuery(
4757
+ id: id,
4758
+ groupKey: groupKey,
4759
+ );
4760
+ final query = nomos_protocol.QueryRequest();
4761
+ if (sum) {
4762
+ query.sum = tally;
4763
+ } else {
4764
+ query.count = tally;
4765
+ }
4766
+ final authority = _nmsQueryAuthority(null, null, session);
4767
+ final result = moment == null
4768
+ ? await _nmsKernelQuery(
4769
+ session: session,
4770
+ authority: authority,
4771
+ query: query,
4772
+ )
4773
+ : await _nmsKernelHistoricQuery(
4774
+ moment: moment,
4775
+ session: session,
4776
+ authority: authority,
4777
+ query: query,
4778
+ );
4779
+ if (!result.hasIntegerValue()) {
4780
+ throw StateError(
4781
+ 'Kernel returned ${result.whichResult().name} without an exact integer tally',
4782
+ );
4783
+ }
4784
+ return result.integerValue.toInt();
4785
+ }
4786
+
4787
+ Future<int> nmsCount(String countId, [String? groupKey, String? session]) =>
4788
+ _nmsTally(countId, groupKey, sum: false, session: session);
4789
+
4790
+ Future<int> nmsCountAt(
4791
+ NomosMoment moment,
4792
+ String countId, [
4793
+ String? groupKey,
4794
+ String? session,
4795
+ ]) =>
4796
+ _nmsTally(
4797
+ countId,
4798
+ groupKey,
4799
+ sum: false,
4800
+ session: session,
4801
+ moment: moment,
4802
+ );
4803
+
4804
+ Future<int> nmsSum(String sumId, [String? groupKey, String? session]) =>
4805
+ _nmsTally(sumId, groupKey, sum: true, session: session);
4806
+
4807
+ Future<int> nmsSumAt(
4808
+ NomosMoment moment,
4809
+ String sumId, [
4810
+ String? groupKey,
4811
+ String? session,
4812
+ ]) =>
4813
+ _nmsTally(
4814
+ sumId,
4815
+ groupKey,
4816
+ sum: true,
4817
+ session: session,
4818
+ moment: moment,
4819
+ );
4820
+
4821
+ /// Compare an immutable publication pin with the stable series' one compiled
4822
+ /// current pointer. This is an O(current-state) check: it neither resolves the
3346
4823
  /// pinned publication nor reconstructs aggregate history.
3347
4824
  Future<ReferenceVersionStatus<TVersion>>
3348
4825
  checkReferenceVersion<TVersion, TSeries>(
@@ -3432,39 +4909,6 @@ class NomosBridge {
3432
4909
  }),
3433
4910
  );
3434
4911
 
3435
- /// Maintained historic count/sum values at [moment]. These are the temporal counterparts of [count]/[sum].
3436
- Future<int> countAt(
3437
- NomosMoment moment,
3438
- String countId, [
3439
- String group = '',
3440
- String? session,
3441
- ]) async =>
3442
- ((_asMap(
3443
- await _call('countAt', {
3444
- 'commit': moment.id,
3445
- 'countId': countId,
3446
- 'group': group,
3447
- if (session != null) 'session': session,
3448
- }),
3449
- )['count']) as num)
3450
- .toInt();
3451
-
3452
- Future<int> sumAt(
3453
- NomosMoment moment,
3454
- String sumId, [
3455
- String group = '',
3456
- String? session,
3457
- ]) async =>
3458
- ((_asMap(
3459
- await _call('sumAt', {
3460
- 'commit': moment.id,
3461
- 'sumId': sumId,
3462
- 'group': group,
3463
- if (session != null) 'session': session,
3464
- }),
3465
- )['sum']) as num)
3466
- .toInt();
3467
-
3468
4912
  /// The read auth sidecar (private reads + E2E): `keys`/`principal` as top-level call args, or null when
3469
4913
  /// neither is present (so the wire is byte-identical to before this existed).
3470
4914
  Map<String, Object?>? _readAuthArgs(
@@ -3527,7 +4971,9 @@ class NomosBridge {
3527
4971
  epoch: (out['epoch'] as num?)?.toInt() ?? 0,
3528
4972
  key: (out['key'] ?? '').toString(),
3529
4973
  ownedKeySubject: out['ownedKeySubject']?.toString(),
3530
- ownedCustody: out['ownedCustody']?.toString(),
4974
+ ownedCustody: out['ownedCustody'] == null
4975
+ ? null
4976
+ : _nomosOwnedCustodyFromTransport(out['ownedCustody']),
3531
4977
  );
3532
4978
  }
3533
4979
 
@@ -3552,7 +4998,9 @@ class NomosBridge {
3552
4998
  epoch: (key['epoch'] as num?)?.toInt() ?? 0,
3553
4999
  key: (key['key'] ?? '').toString(),
3554
5000
  ownedKeySubject: key['ownedKeySubject']?.toString(),
3555
- ownedCustody: key['ownedCustody']?.toString(),
5001
+ ownedCustody: key['ownedCustody'] == null
5002
+ ? null
5003
+ : _nomosOwnedCustodyFromTransport(key['ownedCustody']),
3556
5004
  ),
3557
5005
  )
3558
5006
  .toList();
@@ -3589,23 +5037,95 @@ class NomosBridge {
3589
5037
  );
3590
5038
 
3591
5039
  /// Prepare the private framework payload behind generated share/unshare/transfer methods.
3592
- Future<Map<String, Object?>> nomosPrepareOwnedRecordAction<TAggregate>({
5040
+ Future<NomosOwnedRecordPreparation>
5041
+ nomosPrepareOwnedRecordAction<TAggregate>({
3593
5042
  required String action,
3594
5043
  required String scope,
3595
5044
  required NomosRef<TAggregate> record,
3596
5045
  required String peer,
3597
- }) async =>
3598
- _asMap(
3599
- await _call('cryptoOwnedPrepareRecordAction', {
3600
- 'action': action,
3601
- 'scope': scope,
3602
- 'record': {
3603
- 'type': NmsInternalNomosRefs.type(record),
3604
- 'id': NmsInternalNomosRefs.id(record),
3605
- },
3606
- 'peer': peer,
3607
- }),
3608
- );
5046
+ }) async {
5047
+ final value = _asMap(
5048
+ await _call('cryptoOwnedPrepareRecordAction', {
5049
+ 'action': action,
5050
+ 'scope': scope,
5051
+ 'record': {
5052
+ 'type': NmsInternalNomosRefs.type(record),
5053
+ 'id': NmsInternalNomosRefs.id(record),
5054
+ },
5055
+ 'peer': peer,
5056
+ }),
5057
+ );
5058
+ final ownedKeySubject = _nomosOwnedRequiredString(
5059
+ value,
5060
+ '__nomos_owned_device_key_subject_7c1f5e',
5061
+ );
5062
+ final ownedCustodyRaw = value['__nomos_owned_device_custody_7c1f5e'];
5063
+ final ownedCustody = ownedCustodyRaw == null
5064
+ ? null
5065
+ : _nomosOwnedCustodyFromTransport(ownedCustodyRaw);
5066
+ switch (action) {
5067
+ case 'share':
5068
+ final wraps = value['wraps'];
5069
+ if (wraps is! List) {
5070
+ throw const FormatException(
5071
+ 'Nomos share preparation is missing its wraps',
5072
+ );
5073
+ }
5074
+ return NomosOwnedSharePreparation(
5075
+ ownedKeySubject: ownedKeySubject,
5076
+ ownedCustody: ownedCustody,
5077
+ recordId: _nomosOwnedRequiredString(value, 'recordId'),
5078
+ peer: _nomosOwnedRequiredString(value, 'peer'),
5079
+ epoch: _nomosOwnedRequiredInt(value, 'epoch'),
5080
+ sharedAt: _nomosOwnedRequiredString(value, 'sharedAt'),
5081
+ wraps: wraps
5082
+ .map(_asMap)
5083
+ .map(NomosHistoricOwnedWrapPreparation.fromTransport)
5084
+ .toList(growable: false),
5085
+ );
5086
+ case 'unshare':
5087
+ final wraps = value['wraps'];
5088
+ if (wraps is! List) {
5089
+ throw const FormatException(
5090
+ 'Nomos unshare preparation is missing its wraps',
5091
+ );
5092
+ }
5093
+ return NomosOwnedUnsharePreparation(
5094
+ ownedKeySubject: ownedKeySubject,
5095
+ ownedCustody: ownedCustody,
5096
+ recordId: _nomosOwnedRequiredString(value, 'recordId'),
5097
+ peer: _nomosOwnedRequiredString(value, 'peer'),
5098
+ epoch: _nomosOwnedRequiredInt(value, 'epoch'),
5099
+ changedAt: _nomosOwnedRequiredString(value, 'changedAt'),
5100
+ wraps: wraps
5101
+ .map(_asMap)
5102
+ .map(NomosOwnedWrapPreparation.fromTransport)
5103
+ .toList(growable: false),
5104
+ );
5105
+ case 'transfer':
5106
+ final wraps = value['historicWraps'];
5107
+ if (wraps is! List) {
5108
+ throw const FormatException(
5109
+ 'Nomos transfer preparation is missing its historic wraps',
5110
+ );
5111
+ }
5112
+ return NomosOwnedTransferPreparation(
5113
+ ownedKeySubject: ownedKeySubject,
5114
+ ownedCustody: ownedCustody,
5115
+ recordId: _nomosOwnedRequiredString(value, 'recordId'),
5116
+ newOwner: _nomosOwnedRequiredString(value, 'newOwner'),
5117
+ previousReaders: _nomosOwnedRequiredStrings(value, 'previousReaders'),
5118
+ epoch: _nomosOwnedRequiredInt(value, 'epoch'),
5119
+ changedAt: _nomosOwnedRequiredString(value, 'changedAt'),
5120
+ historicWraps: wraps
5121
+ .map(_asMap)
5122
+ .map(NomosHistoricOwnedWrapPreparation.fromTransport)
5123
+ .toList(growable: false),
5124
+ );
5125
+ default:
5126
+ throw ArgumentError.value(action, 'action', 'unknown owned action');
5127
+ }
5128
+ }
3609
5129
 
3610
5130
  /// FRAMEWORK-INTERNAL business birth: derive the unborn child's owned key from its deterministic address.
3611
5131
  Future<NomosScopeKey> nomosOwnedBirthScopeKey(
@@ -3628,29 +5148,68 @@ class NomosBridge {
3628
5148
  epoch: (out['epoch'] as num?)?.toInt() ?? 0,
3629
5149
  key: (out['key'] ?? '').toString(),
3630
5150
  ownedKeySubject: out['ownedKeySubject']?.toString(),
3631
- ownedCustody: out['ownedCustody']?.toString(),
5151
+ ownedCustody: out['ownedCustody'] == null
5152
+ ? null
5153
+ : _nomosOwnedCustodyFromTransport(out['ownedCustody']),
3632
5154
  workspace: out['workspace']?.toString(),
3633
5155
  );
3634
5156
  }
3635
5157
 
3636
5158
  /// FRAMEWORK-INTERNAL: issue an opaque request for this session's current device.
3637
- Future<String> nomosOwnedDeviceRequest(String scope) async => (_asMap(
3638
- await _call('cryptoOwnedDeviceRequest', {'scope': scope}),
3639
- )['request'] ??
3640
- '')
3641
- .toString();
5159
+ Future<OwnedDeviceRequest> nomosOwnedDeviceRequest(String scope) async {
5160
+ final response = await _nmsKernelQuery(
5161
+ session: _sessionName,
5162
+ authority: _nmsQueryAuthority(null, null, _sessionName),
5163
+ query: nomos_protocol.QueryRequest(
5164
+ ownedDeviceRequest:
5165
+ nomos_protocol.OwnedDeviceRequestQuery(scope: scope),
5166
+ ),
5167
+ );
5168
+ if (!response.hasOwnedDeviceRequest()) {
5169
+ throw StateError(
5170
+ 'Kernel returned ${response.whichResult().name}, expected ownedDeviceRequest',
5171
+ );
5172
+ }
5173
+ return OwnedDeviceRequest.decodeUsda(
5174
+ Uint8List.fromList(response.ownedDeviceRequest.requestUsda),
5175
+ );
5176
+ }
3642
5177
 
3643
5178
  /// FRAMEWORK-INTERNAL: validate an opaque request and prepare the atomic signer + custody offer.
3644
- Future<Map<String, Object?>> nomosPrepareOwnedDeviceApproval(
3645
- String request,
5179
+ Future<NomosOwnedDeviceApprovalPreparation> nomosPrepareOwnedDeviceApproval(
5180
+ OwnedDeviceRequest request,
3646
5181
  String scope,
3647
- ) async =>
3648
- _asMap(
3649
- await _call('cryptoOwnedPrepareApproval', {
3650
- 'request': request,
3651
- 'scope': scope,
3652
- }),
5182
+ ) async {
5183
+ final response = await _nmsKernelQuery(
5184
+ session: _sessionName,
5185
+ authority: _nmsQueryAuthority(null, null, _sessionName),
5186
+ query: nomos_protocol.QueryRequest(
5187
+ ownedPrepareApproval: nomos_protocol.OwnedPrepareApprovalQuery(
5188
+ scope: scope,
5189
+ requestUsda: request.encodeUsda(),
5190
+ ),
5191
+ ),
5192
+ );
5193
+ if (!response.hasOwnedPrepareApproval()) {
5194
+ throw StateError(
5195
+ 'Kernel returned ${response.whichResult().name}, expected ownedPrepareApproval',
3653
5196
  );
5197
+ }
5198
+ final prepared = response.ownedPrepareApproval;
5199
+ return NomosOwnedDeviceApprovalPreparation(
5200
+ newKeySubject: prepared.newKeySubject,
5201
+ ownedCustody: OwnedCustody.decodeUsda(
5202
+ Uint8List.fromList(prepared.ownedCustodyUsda),
5203
+ ),
5204
+ verifiedVia: prepared.verifiedVia,
5205
+ validFromEpoch: prepared.validFromEpoch.toInt(),
5206
+ approvedAt: DateTime.fromMillisecondsSinceEpoch(
5207
+ prepared.approvedAtUnixMillis.toInt(),
5208
+ isUtc: true,
5209
+ ).toIso8601String(),
5210
+ ownedKeySubject: prepared.ownedKeySubject,
5211
+ );
5212
+ }
3654
5213
 
3655
5214
  /// Wrap a scope key TO a recipient's public key (committer side): `{hpkeEpk, ct}` base64 ciphertext.
3656
5215
  Future<Map<String, Object?>> cryptoWrapKey({
@@ -3680,94 +5239,54 @@ class NomosBridge {
3680
5239
  '')
3681
5240
  .toString();
3682
5241
 
3683
- /// THE SPATIAL READ: rows of the declared spatial index's of-type whose geometry
3684
- /// intersects [bbox] — an R*Tree probe on the local projection, same row shape as [query].
3685
- Future<List<Map<String, dynamic>>> spatialWithin(
3686
- String spatialId,
3687
- NomosBbox bbox, [
5242
+ /// Framework-filtered maintained conflicts. The kernel returns authenticated
5243
+ /// USDA; only the generated client knows the composed application field types.
5244
+ Future<Uint8List> nmsPendingConflictsUsda(
5245
+ String? aggregateType, [
3688
5246
  String? session,
3689
- ]) async =>
3690
- _asRows(
3691
- await _call('spatialWithin', {
3692
- 'spatialId': spatialId,
3693
- 'bbox': bbox.toJson(),
3694
- if (session != null) 'session': session,
3695
- }),
3696
- );
3697
-
3698
- /// A maintained O(1) count / sum.
3699
- Future<int> count(
3700
- String countId, [
3701
- String group = '',
3702
- String? session,
3703
- ]) async =>
3704
- ((_asMap(
3705
- await _call('count', {
3706
- 'countId': countId,
3707
- 'group': group,
3708
- if (session != null) 'session': session,
3709
- }),
3710
- )['count']) as num)
3711
- .toInt();
3712
- Future<int> sum(String sumId, [String group = '', String? session]) async =>
3713
- ((_asMap(
3714
- await _call('sum', {
3715
- 'sumId': sumId,
3716
- 'group': group,
3717
- if (session != null) 'session': session,
3718
- }),
3719
- )['sum']) as num)
3720
- .toInt();
3721
-
3722
- /// THE PENDING-CONFLICTS READ (Surfaced merge conflicts — the DX layer): the MAINTAINED,
3723
- /// workspace-wide "decisions needed" index (reserved id `nomosPendingConflicts`). One row per
3724
- /// (aggregate, field) whose folded state carries a Surfaced pending set of 2+ observations —
3725
- /// `{aggregateType, aggregateId, field, observations: [{value, author, occurredAt}]}`.
3726
- /// DERIVED, NOT DRAINED: a resolution write folding in removes its row by derivation.
3727
- /// Empty for a Surfaced-free workspace. (The generated typed client wraps these rows as
3728
- /// `PendingConflictRow` and pairs them with the designated `resolve…` helpers.)
3729
- Future<List<Map<String, dynamic>>> pendingConflicts([
3730
- String? session,
3731
- ]) async =>
3732
- _asRows(
3733
- await _call(
3734
- 'pendingConflicts', {if (session != null) 'session': session}),
3735
- );
3736
-
3737
- /// Framework-filtered pending conflicts for one generated aggregate type. This is the
3738
- /// primitive generated clients use; application code never filters the heterogeneous queue.
3739
- Future<List<Map<String, dynamic>>> pendingConflictsFor(
3740
- String aggregateType, [
3741
- String? session,
3742
- ]) async =>
3743
- _asRows(
3744
- await _call('pendingConflicts', {
3745
- 'aggregateType': aggregateType,
3746
- if (session != null) 'session': session,
3747
- }),
5247
+ ]) =>
5248
+ _nmsKernelQueryUsda(
5249
+ session: session,
5250
+ authority: _nmsQueryAuthority(null, null, session),
5251
+ query: nomos_protocol.QueryRequest(
5252
+ pendingConflicts: nomos_protocol.PendingConflictsQuery(
5253
+ aggregateType: aggregateType,
5254
+ ),
5255
+ ),
3748
5256
  );
3749
5257
 
3750
5258
  /// Counts-only workspace summary over the maintained conflict index.
3751
- Future<Map<String, Object?>> pendingConflictSummary([
5259
+ Future<Uint8List> nmsPendingConflictSummaryUsda([
3752
5260
  String? session,
3753
- ]) async =>
3754
- _asMap(
3755
- await _call('pendingConflictSummary', {
3756
- if (session != null) 'session': session,
3757
- }),
5261
+ ]) =>
5262
+ _nmsKernelQueryUsda(
5263
+ session: session,
5264
+ authority: _nmsQueryAuthority(null, null, session),
5265
+ query: nomos_protocol.QueryRequest(
5266
+ pendingConflictSummary: nomos_protocol.PendingConflictSummaryQuery(),
5267
+ ),
3758
5268
  );
3759
5269
 
3760
5270
  /// Push local writes → the session branch → cloud admission, and pull canonical main back.
3761
- Future<Map<String, Object?>> sync({
5271
+ Future<nomos_protocol.SyncWorkspaceResponse> sync({
3762
5272
  bool admit = true,
3763
5273
  String? session,
3764
- }) async =>
3765
- _asMap(
3766
- await _call('sync', {
3767
- 'admit': admit,
3768
- if (session != null) 'session': session,
3769
- }),
5274
+ }) async {
5275
+ final response = await _invokeRuntimeV2(
5276
+ nomos_protocol.RuntimeRequest(
5277
+ syncWorkspace: nomos_protocol.SyncWorkspaceRequest(
5278
+ session: _runtimeSession(session),
5279
+ admit: admit,
5280
+ ),
5281
+ ),
5282
+ );
5283
+ if (!response.hasSyncWorkspace()) {
5284
+ throw StateError(
5285
+ 'Runtime v2 returned ${response.whichOutcome().name} for sync',
3770
5286
  );
5287
+ }
5288
+ return response.syncWorkspace;
5289
+ }
3771
5290
 
3772
5291
  /// PERSIST: a base64 snapshot of the whole local holon (the bare nomos.git incl. un-synced writes + the
3773
5292
  /// DLQ). Store it on disk and pass it back as `connect(restoreFrom:)` next launch — the app then boots from
@@ -3789,23 +5308,46 @@ class NomosBridge {
3789
5308
  int? fallbackMs,
3790
5309
  String? signalId,
3791
5310
  }) async {
3792
- await _call('autoSync', {
3793
- if (debounceMs != null) 'debounceMs': debounceMs,
3794
- if (fallbackMs != null) 'fallbackMs': fallbackMs,
3795
- if (signalId != null) 'signalId': signalId,
3796
- });
5311
+ final response = await _invokeRuntimeV2(
5312
+ nomos_protocol.RuntimeRequest(
5313
+ startAutoSync: nomos_protocol.StartAutoSyncRequest(
5314
+ session: _runtimeSession(),
5315
+ debounceMillis: debounceMs,
5316
+ fallbackMillis: fallbackMs,
5317
+ signalId: signalId,
5318
+ ),
5319
+ ),
5320
+ );
5321
+ if (!response.hasAutoSyncStarted()) {
5322
+ throw StateError(
5323
+ 'Runtime v2 returned ${response.whichOutcome().name} for autoSync',
5324
+ );
5325
+ }
3797
5326
  final workspace = _sessionName;
3798
5327
  if (workspace != null) _eventPlane._autoSyncSessions.add(workspace);
3799
5328
  }
3800
5329
 
3801
5330
  Future<void> stopAutoSync() async {
3802
- await _call('stopAutoSync');
5331
+ final response = await _invokeRuntimeV2(
5332
+ nomos_protocol.RuntimeRequest(
5333
+ stopAutoSync: nomos_protocol.StopAutoSyncRequest(
5334
+ session: _runtimeSession(),
5335
+ ),
5336
+ ),
5337
+ );
5338
+ if (!response.hasAutoSyncStopped()) {
5339
+ throw StateError(
5340
+ 'Runtime v2 returned ${response.whichOutcome().name} for stopAutoSync',
5341
+ );
5342
+ }
3803
5343
  final workspace = _sessionName;
3804
5344
  if (workspace != null) _eventPlane._autoSyncSessions.remove(workspace);
3805
5345
  }
3806
5346
 
3807
- /// Background-sync telemetry: each autoSync round (`event:'sync'`) and doorbell socket state (`event:'link'`).
3808
- Stream<Map<String, Object?>> get syncEvents => _eventPlane._syncEvents.stream;
5347
+ /// Generated background-sync telemetry. Each item is a Runtime v2 event
5348
+ /// whose exact arm is either [SyncCompleted] or [LinkChanged].
5349
+ Stream<nomos_protocol.RuntimeEvent> get syncEvents =>
5350
+ _eventPlane._syncEvents.stream;
3809
5351
 
3810
5352
  /// LIVE pack byte-progress — the real loading bar. Each event carries `received`/`total` (the host's
3811
5353
  /// actual `Content-Length`, known before the body) + `kind` ('delta'|'full'). Emits only while a `/pack`
@@ -4185,24 +5727,6 @@ class NomosBridge {
4185
5727
  principal: principal,
4186
5728
  );
4187
5729
 
4188
- /// Reactive SPATIAL read: re-emits the R*Tree probe's rows whenever they change locally.
4189
- Stream<List<dynamic>> watchSpatial(
4190
- String spatialId,
4191
- NomosBbox bbox, [
4192
- String? session,
4193
- ]) =>
4194
- _watchStream(
4195
- kind: 'spatial',
4196
- spatialId: spatialId,
4197
- bbox: bbox,
4198
- session: session,
4199
- );
4200
-
4201
- /// Reactive PENDING-CONFLICTS read: re-emits the maintained Surfaced "decisions needed"
4202
- /// rows whenever the index changes (derived, not drained — a resolution write clears its row).
4203
- Stream<List<dynamic>> watchPendingConflicts([String? session]) =>
4204
- _watchStream(kind: 'pendingConflicts', session: session);
4205
-
4206
5730
  /// SHAPE COMPATIBILITY (#72): one-shot verdict for this client's structural [requirement]
4207
5731
  /// (its baked `…InterfaceJson`, decoded) against the workspace's ON-LEDGER offer (auto-sourced).
4208
5732
  /// `{ ok, missing[], incompatible[] }` — names the exact directive/aggregate/field, not a hash.
@@ -4340,7 +5864,7 @@ class NomosBridge {
4340
5864
  // Cancellation is a local lifecycle boundary and must complete even if the runner is slow,
4341
5865
  // disconnected, or being disposed. The remote release remains best-effort; bridge/runner disposal
4342
5866
  // also drops every outstanding watch, so awaiting this acknowledgement only creates teardown hangs.
4343
- unawaited(_call('unwatch', {'subId': subId}).catchError((_) => null));
5867
+ unawaited(_unwatch(subId).catchError((_) => null));
4344
5868
  },
4345
5869
  );
4346
5870
  _eventPlane._compatWatches[subId] = ctrl;
@@ -4352,8 +5876,6 @@ class NomosBridge {
4352
5876
  String? queryId,
4353
5877
  Map<String, Object?>? params,
4354
5878
  String? id,
4355
- String? spatialId,
4356
- NomosBbox? bbox,
4357
5879
  String? base,
4358
5880
  int? limit,
4359
5881
  String? session,
@@ -4373,8 +5895,6 @@ class NomosBridge {
4373
5895
  if (queryId != null) 'queryId': queryId,
4374
5896
  if (params != null) 'params': params,
4375
5897
  if (id != null) 'id': id,
4376
- if (spatialId != null) 'spatialId': spatialId,
4377
- if (bbox != null) 'bbox': bbox.toJson(),
4378
5898
  if (base != null) 'base': base,
4379
5899
  if (limit != null) 'limit': limit,
4380
5900
  if (principal != null) 'principal': principal,
@@ -4394,7 +5914,7 @@ class NomosBridge {
4394
5914
  _eventPlane._watches.remove(subId);
4395
5915
  // A caller owns its local subscription; it must never need a healthy transport in order to release
4396
5916
  // it. Runner-side cleanup is still requested immediately and is made total by runner disposal.
4397
- unawaited(_call('unwatch', {'subId': subId}).catchError((_) => null));
5917
+ unawaited(_unwatch(subId).catchError((_) => null));
4398
5918
  },
4399
5919
  );
4400
5920
  _eventPlane._watches[subId] = ctrl;
@@ -4407,13 +5927,24 @@ class NomosBridge {
4407
5927
  /// watches and continuous-sync loops must not survive this client.
4408
5928
  Future<void> dispose() async {
4409
5929
  try {
4410
- await _call('disposeRealm').timeout(const Duration(seconds: 10));
5930
+ final response = await _invokeRuntimeV2(
5931
+ nomos_protocol.RuntimeRequest(
5932
+ closeRealm: nomos_protocol.CloseRealmRequest(),
5933
+ ),
5934
+ ).timeout(const Duration(seconds: 10));
5935
+ if (!response.hasRealmClosed()) {
5936
+ throw StateError(
5937
+ 'Runtime v2 returned ${response.whichOutcome().name} for closeRealm',
5938
+ );
5939
+ }
4411
5940
  } catch (_) {
4412
5941
  // A dead/unstarted embedding has no reachable runner to release. The
4413
5942
  // enclosing engine host is still disposed below by NomosScope; never let
4414
5943
  // teardown strand the Flutter lifecycle on an unavailable transport.
4415
5944
  }
4416
5945
  await _sub.cancel();
5946
+ await _runtimeEventSub?.cancel();
5947
+ _runtimeEventSub = null;
4417
5948
  final pending = _pending.entries
4418
5949
  .map(
4419
5950
  (entry) => (
@@ -4426,6 +5957,7 @@ class NomosBridge {
4426
5957
  _pending.clear();
4427
5958
  _pendingOps.clear();
4428
5959
  _pendingTraces.clear();
5960
+ _pendingCallStacks.clear();
4429
5961
  for (final entry in pending) {
4430
5962
  final c = entry.completer;
4431
5963
  if (!c.isCompleted) {
@@ -4447,6 +5979,12 @@ class NomosBridge {
4447
5979
  // let its done event complete if/when that consumer resumes.
4448
5980
  if (!w.isClosed) unawaited(w.close());
4449
5981
  }
5982
+ final nominalWatches =
5983
+ _nominalWatchControllers.values.toList(growable: false);
5984
+ _nominalWatchControllers.clear();
5985
+ for (final watch in nominalWatches) {
5986
+ if (!watch.isClosed) unawaited(watch.close());
5987
+ }
4450
5988
  final compatWatches = _compatWatches.values.toList(growable: false);
4451
5989
  _compatWatches.clear();
4452
5990
  for (final w in compatWatches) {
@@ -4581,13 +6119,26 @@ class NomosSession extends NomosBridge {
4581
6119
  'Generated child model inheritance needs a bound parent session.',
4582
6120
  );
4583
6121
  }
4584
- await _call('stageGeneratedModelFromParent', {
4585
- 'sourceSession': source,
4586
- 'session': childWorkspace,
4587
- 'domain': domain,
4588
- 'candidateHash': candidateHash,
4589
- 'lawUsdz': base64Encode(lawUsdz),
4590
- });
6122
+ final response = await _invokeRuntimeV2(
6123
+ nomos_protocol.RuntimeRequest(
6124
+ modelLifecycle: nomos_protocol.ModelLifecycleRequest(
6125
+ session: childWorkspace,
6126
+ stage: nomos_protocol.StageGeneratedModelRequest(
6127
+ domain: domain,
6128
+ candidateHash: candidateHash,
6129
+ lawUsdz: lawUsdz,
6130
+ sourceSession: source,
6131
+ ),
6132
+ ),
6133
+ ),
6134
+ );
6135
+ if (!response.hasModelLifecycle() || !response.modelLifecycle.accepted) {
6136
+ throw StateError(
6137
+ response.hasModelLifecycle() && response.modelLifecycle.hasMessage()
6138
+ ? response.modelLifecycle.message
6139
+ : 'Runtime v2 refused generated child model staging',
6140
+ );
6141
+ }
4591
6142
  }
4592
6143
 
4593
6144
  /// Retain the application's typed model-decision policy for generated child