@githolon/dsl 0.107.4 → 0.108.1

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 (93) hide show
  1. package/dart/CHANGELOG.md +24 -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 +10634 -6122
  8. package/dart/lib/src/runner/offline-install.mjs +4187 -2716
  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 +373 -282
  13. package/dart/lib/src/webview_bridge.dart +2146 -725
  14. package/dart/pubspec.lock +17 -1
  15. package/dart/pubspec.yaml +3 -2
  16. package/dart/test/acquire_session_credentials_test.dart +1 -1
  17. package/dart/test/acquire_session_test.dart +68 -14
  18. package/dart/test/adv_realm_interference_test.dart +6 -4
  19. package/dart/test/cluster_births_test.dart +11 -8
  20. package/dart/test/cluster_local_autosync_test.dart +1 -1
  21. package/dart/test/cluster_scale_test.dart +3 -2
  22. package/dart/test/dlq_session_surface_test.dart +52 -35
  23. package/dart/test/first_app_gauntlet_test.dart +1 -1
  24. package/dart/test/local_sync_inert_test.dart +23 -26
  25. package/dart/test/offer_authentication_proof_test.dart +193 -0
  26. package/dart/test/result_usda_test.dart +49 -0
  27. package/dart/test/session_bound_test.dart +8 -11
  28. package/dart/test/stale_bundle_test.dart +18 -3
  29. package/dart/test/test_harness_test.dart +14 -27
  30. package/dart/test/webview_bridge_test.dart +504 -83
  31. package/dart_types/CHANGELOG.md +17 -0
  32. package/dart_types/lib/nomos_kernel_protocol.dart +11 -0
  33. package/dart_types/lib/src/custody.dart +64 -19
  34. package/dart_types/lib/src/local_work_rejected.dart +30 -45
  35. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pb.dart +169 -0
  36. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pbenum.dart +11 -0
  37. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +17945 -0
  38. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +933 -0
  39. package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pb.dart +245 -0
  40. package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pbenum.dart +160 -0
  41. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +8664 -0
  42. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +369 -0
  43. package/dart_types/lib/src/surfaced.dart +30 -25
  44. package/dart_types/pubspec.lock +16 -0
  45. package/dart_types/pubspec.yaml +3 -1
  46. package/dart_types/test/surfaced_test.dart +2 -1
  47. package/package.json +1 -1
  48. package/runtime/generated-application-host.js +5760 -1879
  49. package/src/authoring.ts +3 -2
  50. package/src/birth_meter_ensure.ts +2 -0
  51. package/src/births.ts +21 -0
  52. package/src/build_package.ts +30 -0
  53. package/src/business_birth.ts +41 -35
  54. package/src/codegen_call_usda.ts +1111 -0
  55. package/src/codegen_dart.ts +207 -282
  56. package/src/codegen_dart_application.ts +195 -252
  57. package/src/codegen_dart_client.ts +1978 -593
  58. package/src/codegen_dart_framework_calls.ts +291 -0
  59. package/src/codegen_dart_scenario.ts +7 -0
  60. package/src/codegen_proof.ts +3 -4
  61. package/src/codegen_provider_dart.ts +21 -7
  62. package/src/codegen_query_usda.ts +214 -0
  63. package/src/codegen_result_usda.ts +685 -0
  64. package/src/codegen_ts.ts +3945 -1111
  65. package/src/compile_package_main.ts +392 -46
  66. package/src/directive.ts +19 -4
  67. package/src/engine_entry.ts +45 -4
  68. package/src/framework/application_home.ts +4 -0
  69. package/src/framework/auth_provider_law.ts +296 -20
  70. package/src/framework/birth_certificate_law.ts +24 -0
  71. package/src/framework/home.ts +83 -40
  72. package/src/framework/owned_custody.ts +25 -12
  73. package/src/framework/owned_custody_value.ts +51 -0
  74. package/src/framework/relations.ts +47 -21
  75. package/src/framework/signing_identity.ts +100 -17
  76. package/src/framework/workspaces.ts +39 -10
  77. package/src/framework_call_definitions.ts +53 -0
  78. package/src/framework_read_definitions.ts +45 -0
  79. package/src/identity_frontier.ts +58 -0
  80. package/src/index.ts +22 -1
  81. package/src/intent.ts +216 -16
  82. package/src/law_check.ts +19 -1
  83. package/src/law_graph.ts +57 -8
  84. package/src/law_graph_usda.ts +73 -5
  85. package/src/law_value_type.ts +106 -19
  86. package/src/read.ts +47 -0
  87. package/src/read_projection_namespace.ts +7 -0
  88. package/src/shape_compat.ts +44 -42
  89. package/src/stable_ids.ts +37 -2
  90. package/src/test_call_usda.ts +83 -0
  91. package/src/wire.ts +36 -2
  92. package/src/wire_encode.ts +45 -1
  93. package/src/workspace_sharing.ts +27 -10
@@ -20,7 +20,10 @@ 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;
26
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as nomos_protocol;
24
27
  import 'package:nomos_types/nomos_types.dart'
25
28
  show
26
29
  nomosErrorFromInfo,
@@ -44,6 +47,14 @@ import 'package:nomos_types/src/foreign_ref.dart'
44
47
 
45
48
  import 'session_bound.dart'
46
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;
47
58
 
48
59
  /// THE ENGINE (architecture/session_bound_clients.md): the multi-workspace RUNTIME — mount/connect,
49
60
  /// the session registry, sync, custody + recovery, the home ceremony, and [NomosBridge.follow]
@@ -66,150 +77,56 @@ final class NomosPage<T> {
66
77
  final bool done;
67
78
  }
68
79
 
69
- /// Typed authority evidence carried by an offer that plans exactly one child birth.
70
- /// It is offer metadata, never a tenant directive field. The bridge converts it to the
71
- /// generated protocol's JSON-shaped transport only at the WebView boundary.
72
- final class NomosBirthCertificate {
73
- const NomosBirthCertificate({
74
- required this.child,
75
- required this.founder,
76
- required this.verified,
77
- required this.parentKey,
78
- required this.parentId,
79
- required this.epoch,
80
- required this.signature,
81
- this.verifiedVia,
82
- this.idpToken,
83
- this.capturedClock,
84
- this.authProviderIssuer,
85
- this.authProviderJwks,
86
- this.authProviderAudience,
87
- });
88
-
89
- final String child;
90
- final String founder;
91
- final bool verified;
92
- final List<int> parentKey;
93
- final String parentId;
94
- final int epoch;
95
- final List<int> signature;
96
- final String? verifiedVia;
97
- final String? idpToken;
98
- final int? capturedClock;
99
- final String? authProviderIssuer;
100
- final String? authProviderJwks;
101
- final String? authProviderAudience;
102
-
103
- static String _string(
104
- Map<String, Object?> wire,
105
- String camel,
106
- String snake,
107
- ) =>
108
- (wire[camel] ?? wire[snake] ?? '').toString();
109
-
110
- static List<int> _bytes(
111
- Map<String, Object?> wire,
112
- String camel,
113
- String snake,
114
- ) =>
115
- ((wire[camel] ?? wire[snake]) as List? ?? const <Object?>[])
116
- .map((value) => (value as num).toInt())
117
- .toList(growable: false);
118
-
119
- factory NomosBirthCertificate.fromWire(
120
- Map<String, Object?> wire,
121
- ) =>
122
- NomosBirthCertificate(
123
- child: _string(wire, 'child', 'child'),
124
- founder: _string(wire, 'founder', 'founder'),
125
- verified: wire['verified'] == true,
126
- parentKey: _bytes(wire, 'parentKey', 'parent_key'),
127
- parentId: _string(wire, 'parentId', 'parent_id'),
128
- epoch: (wire['epoch'] as num?)?.toInt() ?? 0,
129
- signature: _bytes(wire, 'signature', 'signature'),
130
- verifiedVia: (wire['verifiedVia'] ?? wire['verified_via'])?.toString(),
131
- idpToken: (wire['idpToken'] ?? wire['idp_token'])?.toString(),
132
- capturedClock:
133
- ((wire['capturedClock'] ?? wire['captured_clock']) as num?)
134
- ?.toInt(),
135
- authProviderIssuer:
136
- (wire['authProviderIssuer'] ?? wire['auth_provider_issuer'])
137
- ?.toString(),
138
- authProviderJwks:
139
- (wire['authProviderJwks'] ?? wire['auth_provider_jwks'])
140
- ?.toString(),
141
- authProviderAudience:
142
- (wire['authProviderAudience'] ?? wire['auth_provider_audience'])
143
- ?.toString(),
144
- );
145
-
146
- Map<String, Object?> toWire() => <String, Object?>{
147
- 'child': child,
148
- 'founder': founder,
149
- 'verified': verified,
150
- 'parentKey': parentKey,
151
- 'parentId': parentId,
152
- 'epoch': epoch,
153
- 'signature': signature,
154
- if (verifiedVia != null) 'verifiedVia': verifiedVia,
155
- if (idpToken != null) 'idpToken': idpToken,
156
- if (capturedClock != null) 'capturedClock': capturedClock,
157
- if (authProviderIssuer != null)
158
- 'authProviderIssuer': authProviderIssuer,
159
- if (authProviderJwks != null) 'authProviderJwks': authProviderJwks,
160
- if (authProviderAudience != null)
161
- 'authProviderAudience': authProviderAudience,
162
- };
80
+ /// Typed authority evidence carried by an offer that plans exactly one child birth. The protobuf
81
+ /// declaration is the one source of truth for Rust, TypeScript and Dart; USDA is its durable state
82
+ /// projection, not another transport type. The legacy bridge envelope carries only these protobuf bytes.
83
+ typedef NomosBirthCertificate = nomos_protocol.BirthCertificate;
84
+
85
+ /// An unsigned [NomosBirthCertificate]. [NomosBridge.signBirthCert] returns the same generated message
86
+ /// with its signature populated; the signing secret is a separate argument and never enters the value.
87
+ typedef NomosBirthCertificateSpec = nomos_protocol.BirthCertificate;
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
+ }
163
116
  }
164
117
 
165
- /// Facts signed by [NomosBridge.signBirthCert]. The secret remains a separate argument and never
166
- /// enters this value, so callers cannot accidentally serialize private key material with the cert.
167
- final class NomosBirthCertificateSpec {
168
- const NomosBirthCertificateSpec({
169
- required this.child,
170
- required this.founder,
171
- required this.verified,
172
- required this.parentKey,
173
- required this.parentId,
174
- required this.epoch,
175
- this.verifiedVia,
176
- this.idpToken,
177
- this.capturedClock,
178
- this.authProviderIssuer,
179
- this.authProviderJwks,
180
- this.authProviderAudience,
181
- });
182
-
183
- final String child;
184
- final String founder;
185
- final bool verified;
186
- final String parentKey;
187
- final String parentId;
188
- final int epoch;
189
- final String? verifiedVia;
190
- final String? idpToken;
191
- final int? capturedClock;
192
- final String? authProviderIssuer;
193
- final String? authProviderJwks;
194
- final String? authProviderAudience;
195
-
196
- Map<String, Object?> toWire() => <String, Object?>{
197
- 'child': child,
198
- 'founder': founder,
199
- 'verified': verified,
200
- 'parentKey': parentKey,
201
- 'parentId': parentId,
202
- 'epoch': epoch,
203
- if (verifiedVia != null) 'verifiedVia': verifiedVia,
204
- if (idpToken != null) 'idpToken': idpToken,
205
- if (capturedClock != null) 'capturedClock': capturedClock,
206
- if (authProviderIssuer != null)
207
- 'authProviderIssuer': authProviderIssuer,
208
- if (authProviderJwks != null) 'authProviderJwks': authProviderJwks,
209
- if (authProviderAudience != null)
210
- 'authProviderAudience': authProviderAudience,
211
- };
212
- }
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
+ );
213
130
 
214
131
  /// The protocol version the runner and the bridge agree on (`webview-runner.mjs` `WEBVIEW_BRIDGE_VERSION`).
215
132
  const int kWebviewBridgeVersion = 1;
@@ -242,10 +159,10 @@ class StaleRunnerBundle implements Exception {
242
159
  'NomosBridge(..., allowStaleRunnerBundle: true) for a deliberate mixed-version setup.';
243
160
  }
244
161
 
245
- /// The platform channel to the WebView runner: send a JSON-shaped message; receive the runner's messages.
246
- /// Web impl jsifies/dartifies over `dart:js_util`; mobile impl JSON-encodes over a WebView JS-channel. The
247
- /// bridge treats messages as plain `Map<String, Object?>` — the transport owns the channel serialization.
248
- 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 {
249
166
  void send(Map<String, Object?> message);
250
167
  Stream<Map<String, Object?>> get messages;
251
168
  }
@@ -267,11 +184,17 @@ class TransferProgress {
267
184
  ? null
268
185
  : (received / total!).clamp(0.0, 1.0);
269
186
  bool get done => total != null && received >= total!;
270
- factory TransferProgress.fromMessage(Map<String, Object?> m) =>
187
+ factory TransferProgress.fromRuntime(
188
+ nomos_protocol.TransferProgress progress,
189
+ ) =>
271
190
  TransferProgress(
272
- received: _wireInteger(m['received']) ?? 0,
273
- total: _wireInteger(m['total']),
274
- 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
+ },
275
198
  );
276
199
  }
277
200
 
@@ -350,46 +273,56 @@ final class KernelProgress {
350
273
  double? get fraction =>
351
274
  (total <= 0) ? null : (completed / total).clamp(0.0, 1.0);
352
275
 
353
- factory KernelProgress.fromMessage(Map<String, Object?> message) =>
354
- KernelProgress(
355
- sequence: _wireInteger(message['sequence']) ?? 0,
356
- operation: KernelProgressOperation.values.firstWhere(
357
- (value) => value.name == message['operation'],
358
- orElse: () => KernelProgressOperation.unspecified,
359
- ),
360
- phase: KernelProgressPhase.values.firstWhere(
361
- (value) => value.name == message['phase'],
362
- orElse: () => KernelProgressPhase.unspecified,
363
- ),
364
- completed: _wireInteger(message['completed']) ?? 0,
365
- total: _wireInteger(message['total']) ?? 0,
366
- unit: KernelProgressUnit.values.firstWhere(
367
- (value) => value.name == message['unit'],
368
- orElse: () => KernelProgressUnit.unspecified,
369
- ),
370
- terminal: message['terminal'] == true,
371
- lawHydrationStep: KernelLawHydrationStep.values.firstWhere(
372
- (value) => value.name == message['lawHydrationStep'],
373
- orElse: () => KernelLawHydrationStep.unspecified,
374
- ),
375
- lawPackageSha256: message['lawPackageSha256']?.toString() ?? '',
376
- hostObservedElapsedMicros:
377
- _wireInteger(message['hostObservedElapsedMicros']) ?? 0,
378
- hostObservedPhaseElapsedMicros:
379
- _wireInteger(message['hostObservedPhaseElapsedMicros']) ?? 0,
380
- kernelAllocatorLiveBytes:
381
- _wireInteger(message['kernelAllocatorLiveBytes']) ?? 0,
382
- kernelAllocatorPeakBytes:
383
- _wireInteger(message['kernelAllocatorPeakBytes']) ?? 0,
384
- processResidentBytes:
385
- _wireInteger(message['processResidentBytes']) ?? 0,
386
- processPeakResidentBytes:
387
- _wireInteger(message['processPeakResidentBytes']) ?? 0,
388
- );
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
+ }
389
324
  }
390
325
 
391
- int? _wireInteger(Object? value) => value is num ? value.toInt() : null;
392
-
393
326
  /// One opaque point in this workspace's verified history. Applications use it as the handle passed to a
394
327
  /// generated client's `past.at(moment)`; [id] is framework identity, not a business field and need never be
395
328
  /// parsed. The remaining fields make a timeline useful without exposing commit or event-sourcing ceremony.
@@ -522,7 +455,7 @@ class NomosCreated {
522
455
  /// so the CANONICAL axes are axis-neutral (`[minX, minY] .. [maxX, maxY]`); the axes' meaning
523
456
  /// (earth coordinates, floor-plan mm, anything) is the tenant's business. The original geographic
524
457
  /// `minLng`/`minLat`/`maxLng`/`maxLat` named params remain accepted forever (era rule) — pass either
525
- /// 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.
526
459
  class NomosBbox {
527
460
  final double minX;
528
461
  final double minY;
@@ -562,18 +495,6 @@ class NomosBbox {
562
495
  'Spatial axes are generic; use maxX/maxY. Kept for source compat.',
563
496
  )
564
497
  double get maxLat => maxY;
565
-
566
- Map<String, Object?> toJson() => {
567
- 'minX': minX,
568
- 'minY': minY,
569
- 'maxX': maxX,
570
- 'maxY': maxY,
571
- // Geographic aliases so a pre-genericisation runner (which reads minLng/…) still works.
572
- 'minLng': minX,
573
- 'minLat': minY,
574
- 'maxLng': maxX,
575
- 'maxLat': maxY,
576
- };
577
498
  }
578
499
 
579
500
  /// One member's structural compat verdict — `ok`, plus the exact break [reason] when not.
@@ -661,7 +582,7 @@ class NomosScopeKey {
661
582
  final String? ownedKeySubject;
662
583
 
663
584
  /// Framework-private encrypted handoff, present only while seating the first device.
664
- final String? ownedCustody;
585
+ final OwnedCustody? ownedCustody;
665
586
 
666
587
  /// Framework-private deterministic child address, present only for an unborn owned business aggregate.
667
588
  final String? workspace;
@@ -683,14 +604,18 @@ class NomosScopeKey {
683
604
  };
684
605
  }
685
606
 
686
- /// The entire app-facing multi-device surface for owned business state. The request is deliberately an
687
- /// 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.
688
610
  class NomosDevices {
689
611
  final NomosBridge bridge;
690
612
  final String domain;
691
613
  final String domainHash;
692
614
  final String? actor;
693
615
  final String scope;
616
+ final Future<NomosOfferOutcome> Function(
617
+ NomosOwnedDeviceApprovalPreparation preparation,
618
+ ) approvePrepared;
694
619
 
695
620
  const NomosDevices({
696
621
  required this.bridge,
@@ -698,11 +623,12 @@ class NomosDevices {
698
623
  required this.domainHash,
699
624
  required this.actor,
700
625
  required this.scope,
626
+ required this.approvePrepared,
701
627
  });
702
628
 
703
- Future<String> request() => bridge.nomosOwnedDeviceRequest(scope);
629
+ Future<OwnedDeviceRequest> request() => bridge.nomosOwnedDeviceRequest(scope);
704
630
 
705
- Future<NomosOfferOutcome> approve(String request) async {
631
+ Future<NomosOfferOutcome> approve(OwnedDeviceRequest request) async {
706
632
  final principal = actor ?? bridge.principal;
707
633
  if (principal == null || principal.trim().isEmpty) {
708
634
  throw StateError('devices.approve() needs a signed-in Nomos session');
@@ -711,13 +637,7 @@ class NomosDevices {
711
637
  request,
712
638
  scope,
713
639
  );
714
- return bridge.offerOutcome(
715
- domain: domain,
716
- directiveId: 'nomos_internal_approve_owned_device_7c1f5e',
717
- payload: payload,
718
- actor: principal,
719
- domainHash: domainHash,
720
- );
640
+ return approvePrepared(payload);
721
641
  }
722
642
  }
723
643
 
@@ -874,8 +794,8 @@ class EncryptionRosterMismatch implements Exception {
874
794
  /// Without this, the app would open the child and poll `head:null` until timeout with no reason. Callers catch
875
795
  /// it to see the child name(s), the kernel [code], and the [reason]. [head] is the parent write that landed.
876
796
  class BirthIncomplete implements Exception {
877
- /// The raw failed birth outcomes (`{ workspace, status, reason, code }`), status != "born".
878
- final List<Map<String, Object?>> failedBirths;
797
+ /// The failed typed kernel outcomes, status != [NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN].
798
+ final List<NomosBirthOutcome> failedBirths;
879
799
 
880
800
  /// The parent write's local head — the parent offer itself sealed; only the child did not come up.
881
801
  final String head;
@@ -884,28 +804,25 @@ class BirthIncomplete implements Exception {
884
804
 
885
805
  /// The first failed child's kernel code (e.g. `BirthLawPackageUnavailable`), or `BirthFailed`.
886
806
  String get code =>
887
- (failedBirths.isNotEmpty
888
- ? failedBirths.first['code']?.toString()
889
- : null) ??
890
- 'BirthFailed';
807
+ failedBirths.isNotEmpty && failedBirths.first.code.isNotEmpty
808
+ ? failedBirths.first.code
809
+ : 'BirthFailed';
891
810
 
892
811
  /// The child workspace name(s) that did not come up.
893
- List<String> get workspaces => failedBirths
894
- .map((o) => (o['workspace'] ?? '').toString())
895
- .where((s) => s.isNotEmpty)
896
- .toList();
812
+ List<String> get workspaces =>
813
+ failedBirths.map((o) => o.workspace).where((s) => s.isNotEmpty).toList();
897
814
 
898
815
  @override
899
816
  String toString() {
900
817
  final list = failedBirths
901
818
  .map(
902
819
  (o) =>
903
- '${o['workspace'] ?? '?'} [${o['status']}${o['code'] != null ? ': ${o['code']}' : ''}]'
904
- '${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}' : ''}',
905
822
  )
906
823
  .join('; ');
907
824
  final lawUnavailable = failedBirths.any(
908
- (o) => o['code'] == 'BirthLawPackageUnavailable',
825
+ (o) => o.code == 'BirthLawPackageUnavailable',
909
826
  );
910
827
  return 'BirthIncomplete: ${failedBirths.length} child workspace(s) did NOT come up — $list. '
911
828
  'The parent offer admitted, but the child was not fully born, so opening it would poll head:null '
@@ -927,6 +844,254 @@ class NomosEpochResult {
927
844
  });
928
845
  }
929
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
+
930
1095
  /// THE OWNER CEREMONY surface — `<Domain>Client(bridge, actor: owner).encryption`. All three verbs open a
931
1096
  /// FRESH epoch (a fresh 32-byte secret) and fan the wraps to the resolved roster; the membership change (if
932
1097
  /// any) is the domain grant/revoke dispatched first, in the same call.
@@ -1194,6 +1359,67 @@ class NomosEncryption {
1194
1359
  /// for a `.births()` directive — the child workspace(s) the offer brought into being on this local plane.
1195
1360
  enum NomosOperationPhase { locallyAccepted, admittedToRemoteCustody, converged }
1196
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
+
1197
1423
  /// Product-facing progress for one accepted write. Transport topology and custody identifiers remain private.
1198
1424
  final class NomosOperationStatus {
1199
1425
  const NomosOperationStatus(this.phase, {this.updatedAt});
@@ -1261,8 +1487,8 @@ class NomosOfferOutcome {
1261
1487
  /// it IN-PLACE and opens it as a live sibling session — reads/writes flow into the child at once (#42).
1262
1488
  final List<String> born;
1263
1489
 
1264
- /// The raw per-child birth outcomes the holon reported (`{ workspace, genesisHead, … }`), when present.
1265
- final List<Map<String, Object?>> birthOutcomes;
1490
+ /// Typed per-child outcomes reported by the kernel, when present.
1491
+ final List<NomosBirthOutcome> birthOutcomes;
1266
1492
 
1267
1493
  /// LEGACY GATEWAY LANE ONLY: the parent gateway's verdict, when this outcome wraps a deprecated
1268
1494
  /// cross-workspace `createWorkspace` relay (the old generated-birth shape). Null on the one
@@ -1310,6 +1536,31 @@ class NomosOfferOutcome {
1310
1536
  .toList();
1311
1537
  }
1312
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
+
1313
1564
  /// OPEN A REF'S WORKSPACE DIRECTLY (co2 DX ask) — `await outcome.bornRefs.single.acquire(bridge)`. Sugar over
1314
1565
  /// [NomosBridge.acquireSession] keyed by the ref's `workspace`: an already-open session returns as-is; a
1315
1566
  /// device-resident/parked/just-born workspace opens LOCAL-ONLY in place (no cloud, no credentials); only a
@@ -1396,6 +1647,7 @@ class NomosBridge {
1396
1647
  final Map<int, Completer<Object?>> _pending = {};
1397
1648
  final Map<int, String> _pendingOps = {};
1398
1649
  final Map<int, NomosTraceContext> _pendingTraces = {};
1650
+ final Map<int, StackTrace> _pendingCallStacks = {};
1399
1651
  final Map<String, StreamController<List<dynamic>>> _watches = {};
1400
1652
  // SHAPE COMPATIBILITY (#72): compat bindings emit a verdict MAP, not a row list.
1401
1653
  final Map<String, StreamController<Map<String, Object?>>> _compatWatches = {};
@@ -1407,8 +1659,8 @@ class NomosBridge {
1407
1659
  final Map<String, StreamController<NomosCompat>> _lawChangedBySession = {};
1408
1660
  final StreamController<NomosCompat> _lawChanged =
1409
1661
  StreamController<NomosCompat>.broadcast();
1410
- final StreamController<Map<String, Object?>> _syncEvents =
1411
- StreamController<Map<String, Object?>>.broadcast();
1662
+ final StreamController<nomos_protocol.RuntimeEvent> _syncEvents =
1663
+ StreamController<nomos_protocol.RuntimeEvent>.broadcast();
1412
1664
  final StreamController<Map<String, Object?>> _signalEvents =
1413
1665
  StreamController<Map<String, Object?>>.broadcast();
1414
1666
  // CUSTODY LIFECYCLE (rebirth_and_custody_lifecycle.md): each `custody` event carries the raw map (session +
@@ -1456,13 +1708,20 @@ class NomosBridge {
1456
1708
  // workspace is a live session on it. [_sessionNames] tracks what [connect] has opened; [_sessionViews]
1457
1709
  // caches the per-workspace [NomosSession] handle so `bridge.session(ws)` is stable + cheap.
1458
1710
  final Set<String> _sessionNames = {};
1711
+ final Map<String, String> _workspaceBySession = {};
1459
1712
  final Map<String, NomosSession> _sessionViews = {};
1460
1713
  // The gate-bound identity retained from each session's auth token. Generated clients consume this so
1461
1714
  // application code never has to thread an actor into ordinary business methods.
1462
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 = {};
1463
1720
  // Device signing custody follows the application session internally. A generated application opening a
1464
1721
  // locally-born child inherits its current session's device key; tenant code never handles authorSecret.
1465
1722
  final Map<String, String> _sessionAuthorSecrets = {};
1723
+ final Map<String, int> _sessionOfferSequences = {};
1724
+ final Map<String, int> _sessionReplicas = {};
1466
1725
  // Stateful generated applications retain the refreshable cloud access that opened their primary
1467
1726
  // session. A generated read following a NomosRef can therefore mount a genuinely-absent workspace
1468
1727
  // without asking tenant code for cloud URLs, tokens, sessions or signing keys. Kept per source
@@ -1487,12 +1746,165 @@ class NomosBridge {
1487
1746
  // methods expose only the typed receipt and its product-language lifecycle.
1488
1747
  final Map<String, Map<String, NomosOperationReceipt>> _operationReceipts = {};
1489
1748
  final Map<String, NomosOperationReceipt> _birthReceipts = {};
1749
+ final Map<String, StreamController<Uint8List>> _nominalWatchControllers = {};
1490
1750
  int _seq = 0;
1751
+ late final NmsRuntimeV2Gateway _runtimeV2 = NmsRuntimeV2Gateway(_transport);
1491
1752
  final String _traceId = _newTraceId();
1492
1753
  late final StreamSubscription<Map<String, Object?>> _sub;
1754
+ StreamSubscription<nomos_protocol.RuntimeEvent>? _runtimeEventSub;
1493
1755
 
1494
1756
  NomosBridge(this._transport, {this.allowStaleRunnerBundle = false}) {
1495
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
+ }
1496
1908
  }
1497
1909
 
1498
1910
  void _onTransportError(Object error, StackTrace stackTrace) {
@@ -1501,6 +1913,7 @@ class NomosBridge {
1501
1913
  _pending.clear();
1502
1914
  _pendingOps.clear();
1503
1915
  _pendingTraces.clear();
1916
+ _pendingCallStacks.clear();
1504
1917
  for (final completer in pending) {
1505
1918
  if (!completer.isCompleted) completer.completeError(error, stackTrace);
1506
1919
  }
@@ -1643,114 +2056,6 @@ class NomosBridge {
1643
2056
  }
1644
2057
  return;
1645
2058
  }
1646
- if (event == 'sync' || event == 'link') {
1647
- // autoSync telemetry: each round { event:'sync', reason, result } and socket state { event:'link', status }.
1648
- if (!_syncEvents.isClosed) _syncEvents.add(m);
1649
- if (event == 'sync') {
1650
- final result = (m['result'] as Map?)?.cast<String, Object?>();
1651
- final session = m['session']?.toString();
1652
- if (session != null && result != null) {
1653
- _advanceOperationReceipts(session, result);
1654
- if (_syncNeedsCredentialRefresh(m, result)) {
1655
- unawaited(_refreshSessionCredential(session));
1656
- }
1657
- }
1658
- if (m['ok'] == false) {
1659
- // A rejected/failed round is not a successful sync timestamp. Keep the stable typed envelope on
1660
- // telemetry; the app-ready status needs only one legible error and an honest error phase.
1661
- final detail = m['error']?.toString() ?? 'automatic sync failed';
1662
- _updateScopedStatus(
1663
- session,
1664
- (current) => current.copyWith(
1665
- phase: SyncPhase.error,
1666
- clearTransfer: true,
1667
- lastError: detail,
1668
- ),
1669
- );
1670
- } else if (result?['offline'] == true) {
1671
- // Offline is expected transport debt. The durable local branch remains ahead and autoSync owns the
1672
- // retry; do not claim a completed sync or overwrite the previous successful timestamp.
1673
- _updateScopedStatus(
1674
- session,
1675
- (current) => current.copyWith(
1676
- phase: SyncPhase.offline,
1677
- clearTransfer: true,
1678
- lastError: (result?['pushError'] as Map?)?['error']?.toString(),
1679
- ),
1680
- );
1681
- } else if (result?['pushFailed'] == true) {
1682
- // The sync round itself completed, but shared custody refused the
1683
- // push. `stillAhead` is durable debt, not success: surface the
1684
- // cloud's typed refusal on the owning workspace so generated
1685
- // `whenCurrent()` calls fail with the real cause instead of timing
1686
- // out behind a generic saving state.
1687
- final detail = _syncPushFailureDetail(result!);
1688
- _updateScopedStatus(
1689
- session,
1690
- (current) => current.copyWith(
1691
- phase: SyncPhase.error,
1692
- clearTransfer: true,
1693
- lastError: detail,
1694
- ),
1695
- );
1696
- } else {
1697
- // A completed round → idle (the next metrics poll folds in residual ahead/behind), stamp the time.
1698
- _updateScopedStatus(
1699
- session,
1700
- (current) => current.copyWith(
1701
- phase: SyncPhase.idle,
1702
- clearTransfer: true,
1703
- clearError: true,
1704
- lastSyncedAt: DateTime.now(),
1705
- ),
1706
- );
1707
- }
1708
- } else if (m['status'] == 'closed' || m['status'] == 'offline') {
1709
- _updateScopedStatus(
1710
- m['session']?.toString(),
1711
- (current) => current.copyWith(phase: SyncPhase.offline),
1712
- );
1713
- }
1714
- return;
1715
- }
1716
- if (event == 'custody') {
1717
- // Custody lifecycle: { event:'custody', session, custody:{ kind, break, … } }. Decode + fan out so the
1718
- // app can react (kind "ask" → show a dialog then call resolveCustodyBreak; others are informational).
1719
- final ev = NomosCustodyEvent.fromMessage(m);
1720
- if (ev != null && !_custodyEvents.isClosed) _custodyEvents.add(ev);
1721
- return;
1722
- }
1723
- if (event == 'localWorkRejected') {
1724
- // LOCAL-WORK-REJECTED: { event:'localWorkRejected', session, localWorkRejected:{ workspace,
1725
- // refused, parked, resetTo, retryable, source } }. The framework already parked the refused
1726
- // intents + replayed survivors; the app's only job is the honest message (retryable ⇒ "queued,
1727
- // will retry"; terminal ⇒ "preserved for review — contact support").
1728
- final ev = NomosLocalWorkRejectedEvent.fromMessage(m);
1729
- if (ev != null && !_localWorkRejectedEvents.isClosed) {
1730
- _localWorkRejectedEvents.add(ev);
1731
- }
1732
- return;
1733
- }
1734
- if (event == 'transfer') {
1735
- // LIVE pack byte-progress { received, total, kind } — the real loading bar (host Content-Length).
1736
- final tp = TransferProgress.fromMessage(m);
1737
- if (!_transferEvents.isClosed) _transferEvents.add(tp);
1738
- _updateScopedStatus(
1739
- m['session']?.toString(),
1740
- (current) => current.copyWith(
1741
- phase: tp.done ? SyncPhase.syncing : SyncPhase.transferring,
1742
- transfer: tp,
1743
- clearTransfer: tp.done,
1744
- ),
1745
- );
1746
- return;
1747
- }
1748
- if (event == 'kernelProgress') {
1749
- if (!_kernelProgressEvents.isClosed) {
1750
- _kernelProgressEvents.add(KernelProgress.fromMessage(m));
1751
- }
1752
- return;
1753
- }
1754
2059
  if (event == 'trace') {
1755
2060
  // the realtime cloud flamepath: { event:'trace', trace:{at, oid, edge, clients} } — one per admission.
1756
2061
  if (!_flameEvents.isClosed) _flameEvents.add(m);
@@ -1782,6 +2087,7 @@ class NomosBridge {
1782
2087
  final completer = _pending.remove(id);
1783
2088
  final op = _pendingOps.remove(id);
1784
2089
  final trace = _pendingTraces.remove(id) ?? const NomosTraceContext();
2090
+ final callStack = _pendingCallStacks.remove(id) ?? StackTrace.current;
1785
2091
  if (completer == null) return;
1786
2092
  if (m['ok'] == true) {
1787
2093
  completer.complete(m['result']);
@@ -1791,14 +2097,15 @@ class NomosBridge {
1791
2097
  // [BirthIncomplete] here (apps switch on the type + entries, never scrape the prose).
1792
2098
  final failedBirths = ((m['errorInfo'] as Map?)?['failedBirths'] as List?)
1793
2099
  ?.whereType<Map>()
1794
- .map((e) => e.cast<String, Object?>())
1795
- .toList();
2100
+ .map(_birthOutcomeFromLegacyWire)
2101
+ .toList(growable: false);
1796
2102
  if (failedBirths != null && failedBirths.isNotEmpty) {
1797
2103
  completer.completeError(
1798
2104
  BirthIncomplete(
1799
2105
  failedBirths,
1800
2106
  head: ((m['errorInfo'] as Map?)?['head'] ?? '').toString(),
1801
2107
  ),
2108
+ callStack,
1802
2109
  );
1803
2110
  return;
1804
2111
  }
@@ -1808,7 +2115,7 @@ class NomosBridge {
1808
2115
  fallbackTrace: trace,
1809
2116
  fallbackMessage: 'the githolon refused the request',
1810
2117
  );
1811
- completer.completeError(nomosErrorFromInfo(info));
2118
+ completer.completeError(nomosErrorFromInfo(info), callStack);
1812
2119
  }
1813
2120
  }
1814
2121
 
@@ -1826,6 +2133,7 @@ class NomosBridge {
1826
2133
  sampled: true,
1827
2134
  );
1828
2135
  _pendingTraces[id] = trace;
2136
+ _pendingCallStacks[id] = StackTrace.current;
1829
2137
  try {
1830
2138
  _transport.send({
1831
2139
  'v': kWebviewBridgeVersion,
@@ -1845,11 +2153,49 @@ class NomosBridge {
1845
2153
  _pending.remove(id);
1846
2154
  _pendingOps.remove(id);
1847
2155
  _pendingTraces.remove(id);
2156
+ _pendingCallStacks.remove(id);
1848
2157
  completer.completeError(error, stackTrace);
1849
2158
  }
1850
2159
  return completer.future;
1851
2160
  }
1852
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
+
1853
2199
  Map<String, Object?> _asMap(Object? v) =>
1854
2200
  (v as Map?)?.cast<String, Object?>() ?? const {};
1855
2201
  List<Map<String, dynamic>> _asRows(Object? v) =>
@@ -1857,23 +2203,6 @@ class NomosBridge {
1857
2203
  .map((e) => (e as Map).cast<String, dynamic>())
1858
2204
  .toList();
1859
2205
 
1860
- String? _principalFromToken(String? token) {
1861
- if (token == null || token.isEmpty) return null;
1862
- try {
1863
- final parts = token.split('.');
1864
- if (parts.length < 2) return null;
1865
- final payload = jsonDecode(
1866
- utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))),
1867
- );
1868
- final sub = payload is Map ? payload['sub'] : null;
1869
- if (sub is! String || sub.trim().isEmpty) return null;
1870
- final value = sub.trim();
1871
- return 'user:$value';
1872
- } catch (_) {
1873
- return null;
1874
- }
1875
- }
1876
-
1877
2206
  NomosOperationReceipt _registerOperationReceipt(
1878
2207
  String workspace,
1879
2208
  String intentId,
@@ -1905,19 +2234,19 @@ class NomosBridge {
1905
2234
 
1906
2235
  void _advanceOperationReceipts(
1907
2236
  String workspace,
1908
- Map<String, Object?> result,
2237
+ nomos_protocol.SyncWorkspaceResponse result,
1909
2238
  ) {
1910
2239
  final pending = _eventPlane._operationReceipts[workspace];
1911
2240
  if (pending == null || pending.isEmpty) return;
1912
2241
 
1913
- Iterable<String> ids(String key) => ((result[key] as List?) ?? const [])
1914
- .whereType<String>()
1915
- .where((value) => value.isNotEmpty);
1916
-
1917
- for (final intentId in ids('admittedIntentIds')) {
2242
+ for (final intentId in result.admittedIntentIds.where(
2243
+ (value) => value.isNotEmpty,
2244
+ )) {
1918
2245
  pending[intentId]?._advance(NomosOperationPhase.admittedToRemoteCustody);
1919
2246
  }
1920
- for (final intentId in ids('convergedIntentIds')) {
2247
+ for (final intentId in result.convergedIntentIds.where(
2248
+ (value) => value.isNotEmpty,
2249
+ )) {
1921
2250
  final receipt = pending.remove(intentId);
1922
2251
  receipt?._advance(NomosOperationPhase.converged);
1923
2252
  }
@@ -1925,24 +2254,24 @@ class NomosBridge {
1925
2254
  }
1926
2255
 
1927
2256
  bool _syncNeedsCredentialRefresh(
1928
- Map<String, Object?> message,
1929
- Map<String, Object?> result,
2257
+ nomos_protocol.SyncCompleted event,
1930
2258
  ) {
1931
- final push = (result['pushError'] as Map?)?.cast<String, Object?>();
1932
- final status = push?['status'];
2259
+ final status =
2260
+ event.result.hasPushError() ? event.result.pushError.status : 0;
1933
2261
  if (status == 401 || status == 403) return true;
1934
- final text =
1935
- '${message['error'] ?? ''} ${push?['error'] ?? ''}'.toLowerCase();
2262
+ final text = '${event.hasFailure() ? event.failure.message : ''} '
2263
+ '${event.result.hasPushError() ? event.result.pushError.message : ''}'
2264
+ .toLowerCase();
1936
2265
  return text.contains('unauthenticated') ||
1937
2266
  text.contains('token expired') ||
1938
2267
  text.contains('invalid token');
1939
2268
  }
1940
2269
 
1941
- String _syncPushFailureDetail(Map<String, Object?> result) {
1942
- final push = (result['pushError'] as Map?)?.cast<String, Object?>();
1943
- final status = push?['status'];
1944
- final reason = push?['error'] ?? push?['body'];
1945
- 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
1946
2275
  ? 'Shared custody refused this workspace change'
1947
2276
  : 'Shared custody refused this workspace change (HTTP $status)';
1948
2277
  return reason == null || reason.toString().trim().isEmpty
@@ -2023,19 +2352,18 @@ class NomosBridge {
2023
2352
  /// call `bridge.acquireSession(ws)` / `ref.acquire(bridge)` before using it;
2024
2353
  /// • `"parked"` — custody resident but the holon is LRU-disposed; it re-mounts on demand (acquire).
2025
2354
  /// `syncPosture` is `"local-only"` (no cloud counterpart) or `"cloud-bound"`.
2026
- Future<List<Map<String, Object?>>> listWorkspaces() async {
2027
- final connected = _eventPlane._sessionNames.toSet();
2028
- return (((_asMap(await _call('listWorkspaces'))['workspaces']) as List?) ??
2029
- const [])
2030
- .map((e) => (e as Map).cast<String, Object?>())
2031
- .map((w) {
2032
- final ws = w['workspace']?.toString() ?? '';
2033
- // `connected` is the app's own connect() truth; `parked`/resident come from the runner realm.
2034
- w['state'] = (w['parked'] == true)
2035
- ? 'parked'
2036
- : (connected.contains(ws) ? 'connected' : 'resident');
2037
- return w;
2038
- }).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;
2039
2367
  }
2040
2368
 
2041
2369
  /// Framework-private plane persistence primitive. It snapshots exact local custody even when [workspace]
@@ -2109,6 +2437,7 @@ class NomosBridge {
2109
2437
  _eventPlane._sessionNames.add(
2110
2438
  ws,
2111
2439
  ); // the open succeeded — the session is real now
2440
+ _eventPlane._workspaceBySession[ws] = workspace;
2112
2441
  // Opening succeeded, but scoped metrics have not yet proved that this
2113
2442
  // workspace is current. Unknown readiness must fail closed: otherwise a
2114
2443
  // generated `whenCurrent()` can observe the default idle value and return
@@ -2118,9 +2447,13 @@ class NomosBridge {
2118
2447
  () => const SyncStatus(phase: SyncPhase.syncing),
2119
2448
  );
2120
2449
  _eventPlane._foldApplicationStatusFromSessions();
2121
- final boundPrincipal = _principalFromToken(authToken);
2122
- if (boundPrincipal != null)
2123
- _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
+ }
2124
2457
  if (authorSecret != null && authorSecret.isNotEmpty) {
2125
2458
  _eventPlane._sessionAuthorSecrets[ws] = authorSecret;
2126
2459
  }
@@ -2158,16 +2491,34 @@ class NomosBridge {
2158
2491
  );
2159
2492
 
2160
2493
  /// Continue an `ask`-policy custody break with the user's decision (adopt|reject) — the async UX lane.
2161
- Future<Map<String, Object?>> resolveCustodyBreak(
2162
- String decision, {
2494
+ Future<nomos_protocol.CustodyBreakResolved> resolveCustodyBreak(
2495
+ nomos_protocol.CustodyBreakDecision decision, {
2163
2496
  String? session,
2164
- }) async =>
2165
- _asMap(
2166
- await _call('resolveCustodyBreak', {
2167
- 'decision': decision,
2168
- if (session != null) 'session': session,
2169
- }),
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',
2505
+ );
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',
2170
2518
  );
2519
+ }
2520
+ return response.custodyBreakResolved;
2521
+ }
2171
2522
 
2172
2523
  /// The parked salvaged intents (re-offerable local work an `adopt` rescued) — metadata only.
2173
2524
  Future<List<Map<String, Object?>>> salvagedIntents({String? session}) async {
@@ -2210,42 +2561,68 @@ class NomosBridge {
2210
2561
  // parks the FULL intent durably in the workspace tree (it rides export()/restore); the app
2211
2562
  // inspects, the domain dev ships a law fix, retry unjams. ──
2212
2563
 
2213
- /// The parked dead letters — metadata only (`{id, oid, source, error, domain, directiveId, at}`);
2214
- /// the intent bytes stay in the durable queue.
2215
- Future<List<Map<String, Object?>>> deadLetters({String? session}) async {
2216
- final r = _asMap(
2217
- 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
+ ),
2218
2574
  );
2219
- return ((r['dead'] as List?) ?? const [])
2220
- .map((e) => (e as Map).cast<String, Object?>())
2221
- .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);
2222
2581
  }
2223
2582
 
2224
2583
  /// Retry one dead letter (after the law fix landed): pulls canon first (it may already be
2225
2584
  /// admitted), else re-offers the parked bytes locally. `{ok, head?|alreadyOnMain?|error?}` —
2226
2585
  /// a refusal leaves the entry parked (with the fresh error), never lost.
2227
- Future<Map<String, Object?>> retryDeadLetter(
2586
+ Future<nomos_protocol.DeadLetterRetry> retryDeadLetter(
2228
2587
  String id, {
2229
2588
  String? session,
2230
- }) async =>
2231
- _asMap(
2232
- await _call('retryDeadLetter', {
2233
- 'id': id,
2234
- if (session != null) 'session': session,
2235
- }),
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',
2236
2601
  );
2602
+ }
2603
+ return response.deadLetter.retry;
2604
+ }
2237
2605
 
2238
2606
  /// Discard one dead letter — the app's EXPLICIT choice to drop the work. `{ok}`.
2239
- Future<Map<String, Object?>> discardDeadLetter(
2607
+ Future<nomos_protocol.DeadLetterDiscard> discardDeadLetter(
2240
2608
  String id, {
2241
2609
  String? session,
2242
- }) async =>
2243
- _asMap(
2244
- await _call('discardDeadLetter', {
2245
- 'id': id,
2246
- if (session != null) 'session': session,
2247
- }),
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',
2248
2622
  );
2623
+ }
2624
+ return response.deadLetter.discard;
2625
+ }
2249
2626
 
2250
2627
  /// SALVAGE — best-effort extraction of this holon's local-only authored work (un-synced pending
2251
2628
  /// writes + the DLQ), each with its full `changeB64` bytes: `{pendingWrites, deadLetters}`. The
@@ -2327,7 +2704,8 @@ class NomosBridge {
2327
2704
  if (session != null) 'session': session,
2328
2705
  if (authorSecret != null) 'authorSecret': authorSecret,
2329
2706
  if (birthCertificate != null)
2330
- 'birthCertificate': birthCertificate.toWire(),
2707
+ 'birthCertificateProtobuf':
2708
+ base64Encode(birthCertificate.writeToBuffer()),
2331
2709
  }),
2332
2710
  );
2333
2711
  return r['head'].toString();
@@ -2367,6 +2745,164 @@ class NomosBridge {
2367
2745
  birthCertificate: birthCertificate,
2368
2746
  );
2369
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
+
2370
2906
  /// THE ONE OUTCOME-RETURNING OFFER LANE — [offerDirective] answering the full typed [NomosOfferOutcome]
2371
2907
  /// (head + law-minted `created` NomosRefs + births), for EVERY generated method (create or mutate). A pure
2372
2908
  /// mutation returns `created: []`; a `.creates(...)` plan returns the minted aggregate id(s), each already
@@ -2408,15 +2944,19 @@ class NomosBridge {
2408
2944
  if (session != null) 'session': session,
2409
2945
  if (authorSecret != null) 'authorSecret': authorSecret,
2410
2946
  if (birthCertificate != null)
2411
- 'birthCertificate': birthCertificate.toWire(),
2947
+ 'birthCertificateProtobuf':
2948
+ base64Encode(birthCertificate.writeToBuffer()),
2412
2949
  }),
2413
2950
  );
2414
- } catch (e) {
2415
- throw await _wrapDomainNotInstalled(
2416
- e,
2417
- domain: domain,
2418
- domainHash: domainHash,
2419
- 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,
2420
2960
  );
2421
2961
  }
2422
2962
  final t1 = DateTime.now().millisecondsSinceEpoch;
@@ -2447,15 +2987,18 @@ class NomosBridge {
2447
2987
  }
2448
2988
  final births = ((r['birthOutcomes'] as List?) ?? const [])
2449
2989
  .whereType<Map>()
2450
- .map((m) => m.cast<String, Object?>())
2451
- .toList();
2990
+ .map(_birthOutcomeFromLegacyWire)
2991
+ .toList(growable: false);
2452
2992
  // FAIL FAST on a birth that did NOT come up. The JS holon (index.mjs) already throws on this, so we
2453
2993
  // normally never see it here — but a runner/bundle skew (an older runner that passes birthOutcomes
2454
2994
  // through without throwing) must NOT let a silent partial birth reach the app to strand on head:null.
2455
2995
  // A `status` other than "born" is a failure; throw the typed [BirthIncomplete].
2456
2996
  final failedBirths = births
2457
- .where((m) => m['status'] != null && m['status'] != 'born')
2458
- .toList();
2997
+ .where(
2998
+ (birth) =>
2999
+ birth.status != NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN,
3000
+ )
3001
+ .toList(growable: false);
2459
3002
  if (failedBirths.isNotEmpty) {
2460
3003
  throw BirthIncomplete(failedBirths, head: (r['head'] ?? '').toString());
2461
3004
  }
@@ -2468,9 +3011,7 @@ class NomosBridge {
2468
3011
  (e) => (e is Map ? (e['workspace'] ?? '') : e).toString(),
2469
3012
  )
2470
3013
  else
2471
- ...births
2472
- .map((m) => (m['workspace'] ?? '').toString())
2473
- .where((s) => s.isNotEmpty),
3014
+ ...births.map((birth) => birth.workspace).where((s) => s.isNotEmpty),
2474
3015
  ]..removeWhere((s) => s.isEmpty);
2475
3016
  final birthPrincipal = actor ?? principal;
2476
3017
  if (birthPrincipal != null && birthPrincipal.isNotEmpty) {
@@ -2579,11 +3120,21 @@ class NomosBridge {
2579
3120
  String? directiveId,
2580
3121
  required String packageHash,
2581
3122
  }) async {
2582
- await _call('prepareDomain', {
2583
- 'domain': domain,
2584
- if (directiveId != null) 'directiveId': directiveId,
2585
- 'packageHash': packageHash,
2586
- });
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
+ }
2587
3138
  }
2588
3139
 
2589
3140
  /// FOLLOW A [NomosRef] — THE cross-workspace primitive (session_bound_clients.md ask 8): mount
@@ -2748,7 +3299,7 @@ class NomosBridge {
2748
3299
  var resident = false;
2749
3300
  try {
2750
3301
  resident = (await listWorkspaces()).any(
2751
- (w) => w['workspace']?.toString() == workspace,
3302
+ (candidate) => candidate.workspace == workspace,
2752
3303
  );
2753
3304
  } catch (_) {
2754
3305
  // a pre-connect plane has no registry yet — fall through to the credentials decision below.
@@ -2851,6 +3402,13 @@ class NomosBridge {
2851
3402
  // together. Otherwise home signs correctly after first-device enrolment
2852
3403
  // while a later catalogue/estate opens with the old empty default key.
2853
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
+ }
2854
3412
  if (session != null && authorSecret != null) {
2855
3413
  if (authorSecret.isEmpty) {
2856
3414
  _eventPlane._sessionAuthorSecrets.remove(session);
@@ -2969,12 +3527,15 @@ class NomosBridge {
2969
3527
  if (authToken != null) 'authToken': authToken,
2970
3528
  }),
2971
3529
  );
2972
- } catch (e) {
2973
- throw await _wrapDomainNotInstalled(
2974
- e,
2975
- domain: domain,
2976
- domainHash: domainHash,
2977
- 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,
2978
3539
  );
2979
3540
  }
2980
3541
  final verdict = (r['verdict'] as Map?)?.cast<String, Object?>() ?? r;
@@ -3082,20 +3643,30 @@ class NomosBridge {
3082
3643
  /// Framework-private exact-history adoption seam. Generated clients expose this as
3083
3644
  /// `model.adopt()` and, only when prior business outcomes differ, a chained
3084
3645
  /// `decision.accept(reason)`. Candidate identities, heads and receipts stay here.
3085
- Future<Map<String, Object?>> nomosFrameworkAdoptGeneratedModel({
3646
+ Future<nomos_protocol.ModelLifecycleResponse>
3647
+ nomosFrameworkAdoptGeneratedModel({
3086
3648
  required String domain,
3087
3649
  required String candidateHash,
3088
3650
  String? because,
3089
3651
  }) async {
3090
- final r = _asMap(
3091
- await _call('adoptGeneratedModel', {
3092
- 'domain': domain,
3093
- 'candidateHash': candidateHash,
3094
- if (because != null) 'because': because,
3095
- }),
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
+ ),
3096
3663
  );
3097
- return ((r['verdict'] as Map?) ?? const <String, Object?>{})
3098
- .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;
3099
3670
  }
3100
3671
 
3101
3672
  /// Framework-private generated-model staging seam. The generated client owns
@@ -3106,30 +3677,54 @@ class NomosBridge {
3106
3677
  required String candidateHash,
3107
3678
  required List<int> lawUsdz,
3108
3679
  }) async {
3109
- await _call('stageGeneratedModel', {
3110
- 'domain': domain,
3111
- 'candidateHash': candidateHash,
3112
- 'lawUsdz': base64Encode(lawUsdz),
3113
- });
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
+ }
3114
3699
  }
3115
3700
 
3116
3701
  /// Framework-private exact-history restoration seam. Generated clients expose
3117
3702
  /// `model.restorePrevious()` and a chained `decision.accept(reason)`; predecessor
3118
3703
  /// identity, derived dispositions, heads and receipts stay below this bridge.
3119
- Future<Map<String, Object?>> nomosFrameworkRestorePreviousGeneratedModel({
3704
+ Future<nomos_protocol.ModelLifecycleResponse>
3705
+ nomosFrameworkRestorePreviousGeneratedModel({
3120
3706
  required String domain,
3121
3707
  required String currentHash,
3122
3708
  String? because,
3123
3709
  }) async {
3124
- final r = _asMap(
3125
- await _call('restorePreviousGeneratedModel', {
3126
- 'domain': domain,
3127
- 'currentHash': currentHash,
3128
- if (because != null) 'because': because,
3129
- }),
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
+ ),
3130
3721
  );
3131
- return ((r['verdict'] as Map?) ?? const <String, Object?>{})
3132
- .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;
3133
3728
  }
3134
3729
 
3135
3730
  /// HISTORY of one aggregate (a todo) — the full action timeline from the ledger: every intent that touched
@@ -3202,23 +3797,23 @@ class NomosBridge {
3202
3797
  _asMap(await _call('mintDeviceKey'));
3203
3798
 
3204
3799
  /// VA ROOT-OF-TRUST: sign a BirthCert through the kernel `cert_sign` op (the SINGLE source of the cert
3205
- /// byte-layout — no Dart copy). `fields` = `{child, founder, verified, parentKey (this device's base64
3206
- /// ed25519 PUBLIC half), parentId, epoch}`; `deviceSecret` is that key's base64 SECRET half, handed to the
3207
- /// LOCAL kernel only (it never reaches a server). Returns typed authority evidence suitable for an offer.
3800
+ /// byte-layout — no Dart copy). [spec] is the generated certificate message with an empty signature;
3801
+ /// `deviceSecret` is the base64 secret half, handed to the LOCAL kernel only (it never reaches a server).
3802
+ /// Returns the same generated type with its signature populated, suitable for an offer.
3208
3803
  Future<NomosBirthCertificate> signBirthCert(
3209
3804
  NomosBirthCertificateSpec spec,
3210
3805
  String deviceSecret,
3211
- ) async =>
3212
- NomosBirthCertificate.fromWire(
3213
- _asMap(
3214
- _asMap(
3215
- await _call('signBirthCert', {
3216
- 'fields': spec.toWire(),
3217
- 'deviceSecret': deviceSecret,
3218
- }),
3219
- )['cert'],
3220
- ),
3221
- );
3806
+ ) async {
3807
+ final result = _asMap(
3808
+ await _call('signBirthCert', {
3809
+ 'certificateProtobuf': base64Encode(spec.writeToBuffer()),
3810
+ 'deviceSecret': deviceSecret,
3811
+ }),
3812
+ );
3813
+ return nomos_protocol.BirthCertificate.fromBuffer(
3814
+ base64Decode(result['certificateProtobuf']?.toString() ?? ''),
3815
+ );
3816
+ }
3222
3817
 
3223
3818
  /// MINT THE KEYLESS ENROLL-ONCE DELEGATION (`enrollFirstDevice.delegationCert`, the verified-first-device
3224
3819
  /// lane). The device SELF-signs it; its authority is the carried `idpToken` (the kernel re-verifies that to
@@ -3226,17 +3821,20 @@ class NomosBridge {
3226
3821
  /// `{user, deviceKey, childName, parentId, verifiedVia, idpToken, capturedClock, epoch}`. WITHOUT this the
3227
3822
  /// keyless home is born with a malformed cert chain (the platform_cert falls into the delegation slot →
3228
3823
  /// `home cert founder != delegation cert founder` at the home's own verify door).
3229
- Future<Map<String, Object?>> mintKeylessDelegation(
3824
+ Future<NomosBirthCertificate> mintKeylessDelegation(
3230
3825
  Map<String, Object?> spec,
3231
3826
  String deviceSecret,
3232
- ) async =>
3233
- _asMap(
3234
- await _call('mintKeylessDelegation', {
3235
- 'spec': spec,
3236
- 'deviceSecret': deviceSecret,
3237
- }),
3238
- )['cert'] as Map<String, Object?>? ??
3239
- const {};
3827
+ ) async {
3828
+ final result = _asMap(
3829
+ await _call('mintKeylessDelegation', {
3830
+ 'spec': spec,
3831
+ 'deviceSecret': deviceSecret,
3832
+ }),
3833
+ );
3834
+ return nomos_protocol.BirthCertificate.fromBuffer(
3835
+ base64Decode(result['certificateProtobuf']?.toString() ?? ''),
3836
+ );
3837
+ }
3240
3838
 
3241
3839
  /// ENROLL THIS DEVICE — the LIVE gate: bind a device key's PUBLIC half to `principal` on-chain as a
3242
3840
  /// `signer` so the holon's gate can PROVE this device's authorship on replay (and the VA chain attest it).
@@ -3361,10 +3959,62 @@ class NomosBridge {
3361
3959
 
3362
3960
  /// Framework-private receiving-group setup. Generated application hosts own this ceremony; tenant code
3363
3961
  /// receives only the resulting business application.
3364
- Future<Map<String, Object?>> nomosInternalBirthReceivingGroup(
3365
- Map<String, Object?> spec,
3366
- ) async =>
3367
- _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
+ }
3368
4018
 
3369
4019
  /// Sign a cross-custody SHARE attestation `{secret, asserterWorkspace, object, relation, subject,
3370
4020
  /// attestedAt}` → `{publicKey, signature}`. This is what drives `recordShare` into an invitee's home
@@ -3404,63 +4054,565 @@ class NomosBridge {
3404
4054
  }),
3405
4055
  );
3406
4056
 
3407
- /// A declared query's rows from the local projection. `keys` (E2E sealing) injects the reader's held
3408
- /// scope keys so encrypted fields AEAD-open in-wasm; absent ⇒ they stay opaque envelopes (today's
3409
- /// behavior). `principal` stamps the private-read gate (defaults to the connection's bound principal).
3410
- /// Both are OMITTED from the wire when absent, so a public un-keyed read is byte-identical to before.
3411
- Future<List<Map<String, dynamic>>> query(
3412
- String queryId, [
3413
- Map<String, Object?> params = const {},
4057
+ nomos_protocol.QueryAuthority _nmsQueryAuthority(
3414
4058
  List<NomosScopeKey>? keys,
3415
4059
  String? principal,
3416
4060
  String? session,
3417
- ]) async {
3418
- return _asRows(
3419
- await _call('query', {
3420
- 'queryId': queryId,
3421
- 'params': params,
3422
- ...?_readAuthArgs(keys, principal),
3423
- if (session != null) 'session': session,
3424
- }),
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
+ ),
3425
4085
  );
3426
4086
  }
3427
4087
 
3428
- /// One bounded query page. Unlike [query], this never requires the kernel to materialize or serialize
3429
- /// the complete result set merely to show the first screen.
3430
- Future<NomosPage<Map<String, dynamic>>> queryPage(
3431
- String queryId, {
3432
- Map<String, Object?> params = const {},
3433
- int pageSize = 100,
3434
- String? cursor,
3435
- List<NomosScopeKey>? keys,
3436
- String? principal,
3437
- String? session,
4088
+ Future<Uint8List> _nmsKernelQueryUsda({
4089
+ required String? session,
4090
+ required nomos_protocol.QueryAuthority authority,
4091
+ required nomos_protocol.QueryRequest query,
3438
4092
  }) async {
3439
- if (pageSize < 1 || pageSize > 1000) {
3440
- throw RangeError.range(pageSize, 1, 1000, 'pageSize');
3441
- }
3442
- final result = _asMap(
3443
- await _call('queryPage', {
3444
- 'queryId': queryId,
3445
- 'params': params,
3446
- 'pageSize': pageSize,
3447
- if (cursor != null) 'cursor': cursor,
3448
- ...?_readAuthArgs(keys, principal),
3449
- if (session != null) 'session': session,
3450
- }),
3451
- );
3452
- return NomosPage<Map<String, dynamic>>(
3453
- rows: _asRows(result),
3454
- cursor: result['cursor'] as String?,
3455
- done: result['done'] == true,
4093
+ final response = await _nmsKernelQuery(
4094
+ session: session,
4095
+ authority: authority,
4096
+ query: query,
3456
4097
  );
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);
3457
4104
  }
3458
4105
 
3459
- /// A single aggregate by id (the by-id read). `keys`/`principal`: see [query].
3460
- Future<List<Map<String, dynamic>>> queryById(
3461
- String id, [
3462
- List<NomosScopeKey>? keys,
3463
- String? principal,
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
+ ),
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;
4152
+ }
4153
+
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({
4207
+ String? session,
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',
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
+ }
4229
+
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,
3464
4616
  String? session,
3465
4617
  ]) async =>
3466
4618
  _asRows(
@@ -3471,6 +4623,201 @@ class NomosBridge {
3471
4623
  }),
3472
4624
  );
3473
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
+
3474
4821
  /// Compare an immutable publication pin with the stable series' one compiled
3475
4822
  /// current pointer. This is an O(current-state) check: it neither resolves the
3476
4823
  /// pinned publication nor reconstructs aggregate history.
@@ -3562,39 +4909,6 @@ class NomosBridge {
3562
4909
  }),
3563
4910
  );
3564
4911
 
3565
- /// Maintained historic count/sum values at [moment]. These are the temporal counterparts of [count]/[sum].
3566
- Future<int> countAt(
3567
- NomosMoment moment,
3568
- String countId, [
3569
- String group = '',
3570
- String? session,
3571
- ]) async =>
3572
- ((_asMap(
3573
- await _call('countAt', {
3574
- 'commit': moment.id,
3575
- 'countId': countId,
3576
- 'group': group,
3577
- if (session != null) 'session': session,
3578
- }),
3579
- )['count']) as num)
3580
- .toInt();
3581
-
3582
- Future<int> sumAt(
3583
- NomosMoment moment,
3584
- String sumId, [
3585
- String group = '',
3586
- String? session,
3587
- ]) async =>
3588
- ((_asMap(
3589
- await _call('sumAt', {
3590
- 'commit': moment.id,
3591
- 'sumId': sumId,
3592
- 'group': group,
3593
- if (session != null) 'session': session,
3594
- }),
3595
- )['sum']) as num)
3596
- .toInt();
3597
-
3598
4912
  /// The read auth sidecar (private reads + E2E): `keys`/`principal` as top-level call args, or null when
3599
4913
  /// neither is present (so the wire is byte-identical to before this existed).
3600
4914
  Map<String, Object?>? _readAuthArgs(
@@ -3657,7 +4971,9 @@ class NomosBridge {
3657
4971
  epoch: (out['epoch'] as num?)?.toInt() ?? 0,
3658
4972
  key: (out['key'] ?? '').toString(),
3659
4973
  ownedKeySubject: out['ownedKeySubject']?.toString(),
3660
- ownedCustody: out['ownedCustody']?.toString(),
4974
+ ownedCustody: out['ownedCustody'] == null
4975
+ ? null
4976
+ : _nomosOwnedCustodyFromTransport(out['ownedCustody']),
3661
4977
  );
3662
4978
  }
3663
4979
 
@@ -3682,7 +4998,9 @@ class NomosBridge {
3682
4998
  epoch: (key['epoch'] as num?)?.toInt() ?? 0,
3683
4999
  key: (key['key'] ?? '').toString(),
3684
5000
  ownedKeySubject: key['ownedKeySubject']?.toString(),
3685
- ownedCustody: key['ownedCustody']?.toString(),
5001
+ ownedCustody: key['ownedCustody'] == null
5002
+ ? null
5003
+ : _nomosOwnedCustodyFromTransport(key['ownedCustody']),
3686
5004
  ),
3687
5005
  )
3688
5006
  .toList();
@@ -3719,23 +5037,95 @@ class NomosBridge {
3719
5037
  );
3720
5038
 
3721
5039
  /// Prepare the private framework payload behind generated share/unshare/transfer methods.
3722
- Future<Map<String, Object?>> nomosPrepareOwnedRecordAction<TAggregate>({
5040
+ Future<NomosOwnedRecordPreparation>
5041
+ nomosPrepareOwnedRecordAction<TAggregate>({
3723
5042
  required String action,
3724
5043
  required String scope,
3725
5044
  required NomosRef<TAggregate> record,
3726
5045
  required String peer,
3727
- }) async =>
3728
- _asMap(
3729
- await _call('cryptoOwnedPrepareRecordAction', {
3730
- 'action': action,
3731
- 'scope': scope,
3732
- 'record': {
3733
- 'type': NmsInternalNomosRefs.type(record),
3734
- 'id': NmsInternalNomosRefs.id(record),
3735
- },
3736
- 'peer': peer,
3737
- }),
3738
- );
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
+ }
3739
5129
 
3740
5130
  /// FRAMEWORK-INTERNAL business birth: derive the unborn child's owned key from its deterministic address.
3741
5131
  Future<NomosScopeKey> nomosOwnedBirthScopeKey(
@@ -3758,29 +5148,68 @@ class NomosBridge {
3758
5148
  epoch: (out['epoch'] as num?)?.toInt() ?? 0,
3759
5149
  key: (out['key'] ?? '').toString(),
3760
5150
  ownedKeySubject: out['ownedKeySubject']?.toString(),
3761
- ownedCustody: out['ownedCustody']?.toString(),
5151
+ ownedCustody: out['ownedCustody'] == null
5152
+ ? null
5153
+ : _nomosOwnedCustodyFromTransport(out['ownedCustody']),
3762
5154
  workspace: out['workspace']?.toString(),
3763
5155
  );
3764
5156
  }
3765
5157
 
3766
5158
  /// FRAMEWORK-INTERNAL: issue an opaque request for this session's current device.
3767
- Future<String> nomosOwnedDeviceRequest(String scope) async => (_asMap(
3768
- await _call('cryptoOwnedDeviceRequest', {'scope': scope}),
3769
- )['request'] ??
3770
- '')
3771
- .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
+ }
3772
5177
 
3773
5178
  /// FRAMEWORK-INTERNAL: validate an opaque request and prepare the atomic signer + custody offer.
3774
- Future<Map<String, Object?>> nomosPrepareOwnedDeviceApproval(
3775
- String request,
5179
+ Future<NomosOwnedDeviceApprovalPreparation> nomosPrepareOwnedDeviceApproval(
5180
+ OwnedDeviceRequest request,
3776
5181
  String scope,
3777
- ) async =>
3778
- _asMap(
3779
- await _call('cryptoOwnedPrepareApproval', {
3780
- 'request': request,
3781
- 'scope': scope,
3782
- }),
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',
3783
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
+ }
3784
5213
 
3785
5214
  /// Wrap a scope key TO a recipient's public key (committer side): `{hpkeEpk, ct}` base64 ciphertext.
3786
5215
  Future<Map<String, Object?>> cryptoWrapKey({
@@ -3810,94 +5239,54 @@ class NomosBridge {
3810
5239
  '')
3811
5240
  .toString();
3812
5241
 
3813
- /// THE SPATIAL READ: rows of the declared spatial index's of-type whose geometry
3814
- /// intersects [bbox] — an R*Tree probe on the local projection, same row shape as [query].
3815
- Future<List<Map<String, dynamic>>> spatialWithin(
3816
- String spatialId,
3817
- NomosBbox bbox, [
3818
- String? session,
3819
- ]) async =>
3820
- _asRows(
3821
- await _call('spatialWithin', {
3822
- 'spatialId': spatialId,
3823
- 'bbox': bbox.toJson(),
3824
- if (session != null) 'session': session,
3825
- }),
3826
- );
3827
-
3828
- /// A maintained O(1) count / sum.
3829
- Future<int> count(
3830
- String countId, [
3831
- String group = '',
3832
- String? session,
3833
- ]) async =>
3834
- ((_asMap(
3835
- await _call('count', {
3836
- 'countId': countId,
3837
- 'group': group,
3838
- if (session != null) 'session': session,
3839
- }),
3840
- )['count']) as num)
3841
- .toInt();
3842
- Future<int> sum(String sumId, [String group = '', String? session]) async =>
3843
- ((_asMap(
3844
- await _call('sum', {
3845
- 'sumId': sumId,
3846
- 'group': group,
3847
- if (session != null) 'session': session,
3848
- }),
3849
- )['sum']) as num)
3850
- .toInt();
3851
-
3852
- /// THE PENDING-CONFLICTS READ (Surfaced merge conflicts — the DX layer): the MAINTAINED,
3853
- /// workspace-wide "decisions needed" index (reserved id `nomosPendingConflicts`). One row per
3854
- /// (aggregate, field) whose folded state carries a Surfaced pending set of 2+ observations —
3855
- /// `{aggregateType, aggregateId, field, observations: [{value, author, occurredAt}]}`.
3856
- /// DERIVED, NOT DRAINED: a resolution write folding in removes its row by derivation.
3857
- /// Empty for a Surfaced-free workspace. (The generated typed client wraps these rows as
3858
- /// `PendingConflictRow` and pairs them with the designated `resolve…` helpers.)
3859
- Future<List<Map<String, dynamic>>> pendingConflicts([
3860
- String? session,
3861
- ]) async =>
3862
- _asRows(
3863
- await _call(
3864
- 'pendingConflicts', {if (session != null) 'session': session}),
3865
- );
3866
-
3867
- /// Framework-filtered pending conflicts for one generated aggregate type. This is the
3868
- /// primitive generated clients use; application code never filters the heterogeneous queue.
3869
- Future<List<Map<String, dynamic>>> pendingConflictsFor(
3870
- String aggregateType, [
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, [
3871
5246
  String? session,
3872
- ]) async =>
3873
- _asRows(
3874
- await _call('pendingConflicts', {
3875
- 'aggregateType': aggregateType,
3876
- if (session != null) 'session': session,
3877
- }),
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
+ ),
3878
5256
  );
3879
5257
 
3880
5258
  /// Counts-only workspace summary over the maintained conflict index.
3881
- Future<Map<String, Object?>> pendingConflictSummary([
5259
+ Future<Uint8List> nmsPendingConflictSummaryUsda([
3882
5260
  String? session,
3883
- ]) async =>
3884
- _asMap(
3885
- await _call('pendingConflictSummary', {
3886
- if (session != null) 'session': session,
3887
- }),
5261
+ ]) =>
5262
+ _nmsKernelQueryUsda(
5263
+ session: session,
5264
+ authority: _nmsQueryAuthority(null, null, session),
5265
+ query: nomos_protocol.QueryRequest(
5266
+ pendingConflictSummary: nomos_protocol.PendingConflictSummaryQuery(),
5267
+ ),
3888
5268
  );
3889
5269
 
3890
5270
  /// Push local writes → the session branch → cloud admission, and pull canonical main back.
3891
- Future<Map<String, Object?>> sync({
5271
+ Future<nomos_protocol.SyncWorkspaceResponse> sync({
3892
5272
  bool admit = true,
3893
5273
  String? session,
3894
- }) async =>
3895
- _asMap(
3896
- await _call('sync', {
3897
- 'admit': admit,
3898
- if (session != null) 'session': session,
3899
- }),
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',
3900
5286
  );
5287
+ }
5288
+ return response.syncWorkspace;
5289
+ }
3901
5290
 
3902
5291
  /// PERSIST: a base64 snapshot of the whole local holon (the bare nomos.git incl. un-synced writes + the
3903
5292
  /// DLQ). Store it on disk and pass it back as `connect(restoreFrom:)` next launch — the app then boots from
@@ -3919,23 +5308,46 @@ class NomosBridge {
3919
5308
  int? fallbackMs,
3920
5309
  String? signalId,
3921
5310
  }) async {
3922
- await _call('autoSync', {
3923
- if (debounceMs != null) 'debounceMs': debounceMs,
3924
- if (fallbackMs != null) 'fallbackMs': fallbackMs,
3925
- if (signalId != null) 'signalId': signalId,
3926
- });
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
+ }
3927
5326
  final workspace = _sessionName;
3928
5327
  if (workspace != null) _eventPlane._autoSyncSessions.add(workspace);
3929
5328
  }
3930
5329
 
3931
5330
  Future<void> stopAutoSync() async {
3932
- 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
+ }
3933
5343
  final workspace = _sessionName;
3934
5344
  if (workspace != null) _eventPlane._autoSyncSessions.remove(workspace);
3935
5345
  }
3936
5346
 
3937
- /// Background-sync telemetry: each autoSync round (`event:'sync'`) and doorbell socket state (`event:'link'`).
3938
- 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;
3939
5351
 
3940
5352
  /// LIVE pack byte-progress — the real loading bar. Each event carries `received`/`total` (the host's
3941
5353
  /// actual `Content-Length`, known before the body) + `kind` ('delta'|'full'). Emits only while a `/pack`
@@ -4315,24 +5727,6 @@ class NomosBridge {
4315
5727
  principal: principal,
4316
5728
  );
4317
5729
 
4318
- /// Reactive SPATIAL read: re-emits the R*Tree probe's rows whenever they change locally.
4319
- Stream<List<dynamic>> watchSpatial(
4320
- String spatialId,
4321
- NomosBbox bbox, [
4322
- String? session,
4323
- ]) =>
4324
- _watchStream(
4325
- kind: 'spatial',
4326
- spatialId: spatialId,
4327
- bbox: bbox,
4328
- session: session,
4329
- );
4330
-
4331
- /// Reactive PENDING-CONFLICTS read: re-emits the maintained Surfaced "decisions needed"
4332
- /// rows whenever the index changes (derived, not drained — a resolution write clears its row).
4333
- Stream<List<dynamic>> watchPendingConflicts([String? session]) =>
4334
- _watchStream(kind: 'pendingConflicts', session: session);
4335
-
4336
5730
  /// SHAPE COMPATIBILITY (#72): one-shot verdict for this client's structural [requirement]
4337
5731
  /// (its baked `…InterfaceJson`, decoded) against the workspace's ON-LEDGER offer (auto-sourced).
4338
5732
  /// `{ ok, missing[], incompatible[] }` — names the exact directive/aggregate/field, not a hash.
@@ -4470,7 +5864,7 @@ class NomosBridge {
4470
5864
  // Cancellation is a local lifecycle boundary and must complete even if the runner is slow,
4471
5865
  // disconnected, or being disposed. The remote release remains best-effort; bridge/runner disposal
4472
5866
  // also drops every outstanding watch, so awaiting this acknowledgement only creates teardown hangs.
4473
- unawaited(_call('unwatch', {'subId': subId}).catchError((_) => null));
5867
+ unawaited(_unwatch(subId).catchError((_) => null));
4474
5868
  },
4475
5869
  );
4476
5870
  _eventPlane._compatWatches[subId] = ctrl;
@@ -4482,8 +5876,6 @@ class NomosBridge {
4482
5876
  String? queryId,
4483
5877
  Map<String, Object?>? params,
4484
5878
  String? id,
4485
- String? spatialId,
4486
- NomosBbox? bbox,
4487
5879
  String? base,
4488
5880
  int? limit,
4489
5881
  String? session,
@@ -4503,8 +5895,6 @@ class NomosBridge {
4503
5895
  if (queryId != null) 'queryId': queryId,
4504
5896
  if (params != null) 'params': params,
4505
5897
  if (id != null) 'id': id,
4506
- if (spatialId != null) 'spatialId': spatialId,
4507
- if (bbox != null) 'bbox': bbox.toJson(),
4508
5898
  if (base != null) 'base': base,
4509
5899
  if (limit != null) 'limit': limit,
4510
5900
  if (principal != null) 'principal': principal,
@@ -4524,7 +5914,7 @@ class NomosBridge {
4524
5914
  _eventPlane._watches.remove(subId);
4525
5915
  // A caller owns its local subscription; it must never need a healthy transport in order to release
4526
5916
  // it. Runner-side cleanup is still requested immediately and is made total by runner disposal.
4527
- unawaited(_call('unwatch', {'subId': subId}).catchError((_) => null));
5917
+ unawaited(_unwatch(subId).catchError((_) => null));
4528
5918
  },
4529
5919
  );
4530
5920
  _eventPlane._watches[subId] = ctrl;
@@ -4537,13 +5927,24 @@ class NomosBridge {
4537
5927
  /// watches and continuous-sync loops must not survive this client.
4538
5928
  Future<void> dispose() async {
4539
5929
  try {
4540
- 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
+ }
4541
5940
  } catch (_) {
4542
5941
  // A dead/unstarted embedding has no reachable runner to release. The
4543
5942
  // enclosing engine host is still disposed below by NomosScope; never let
4544
5943
  // teardown strand the Flutter lifecycle on an unavailable transport.
4545
5944
  }
4546
5945
  await _sub.cancel();
5946
+ await _runtimeEventSub?.cancel();
5947
+ _runtimeEventSub = null;
4547
5948
  final pending = _pending.entries
4548
5949
  .map(
4549
5950
  (entry) => (
@@ -4556,6 +5957,7 @@ class NomosBridge {
4556
5957
  _pending.clear();
4557
5958
  _pendingOps.clear();
4558
5959
  _pendingTraces.clear();
5960
+ _pendingCallStacks.clear();
4559
5961
  for (final entry in pending) {
4560
5962
  final c = entry.completer;
4561
5963
  if (!c.isCompleted) {
@@ -4577,6 +5979,12 @@ class NomosBridge {
4577
5979
  // let its done event complete if/when that consumer resumes.
4578
5980
  if (!w.isClosed) unawaited(w.close());
4579
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
+ }
4580
5988
  final compatWatches = _compatWatches.values.toList(growable: false);
4581
5989
  _compatWatches.clear();
4582
5990
  for (final w in compatWatches) {
@@ -4711,13 +6119,26 @@ class NomosSession extends NomosBridge {
4711
6119
  'Generated child model inheritance needs a bound parent session.',
4712
6120
  );
4713
6121
  }
4714
- await _call('stageGeneratedModelFromParent', {
4715
- 'sourceSession': source,
4716
- 'session': childWorkspace,
4717
- 'domain': domain,
4718
- 'candidateHash': candidateHash,
4719
- 'lawUsdz': base64Encode(lawUsdz),
4720
- });
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
+ }
4721
6142
  }
4722
6143
 
4723
6144
  /// Retain the application's typed model-decision policy for generated child