@githolon/dsl 0.108.0 → 0.108.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dart/CHANGELOG.md +5 -0
  2. package/dart/lib/src/application_framework_calls.dart +299 -0
  3. package/dart/lib/src/generated_call.dart +96 -0
  4. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  5. package/dart/lib/src/node_runner_transport.dart +77 -1
  6. package/dart/lib/src/result_usda.dart +487 -0
  7. package/dart/lib/src/runner/node-entry.mjs +6401 -2102
  8. package/dart/lib/src/runner/offline-install.mjs +1538 -163
  9. package/dart/lib/src/runtime_v2_gateway.dart +54 -0
  10. package/dart/lib/src/runtime_v2_transport.dart +117 -0
  11. package/dart/lib/src/session_bound.dart +7 -2
  12. package/dart/lib/src/test_harness.dart +193 -112
  13. package/dart/lib/src/webview_bridge.dart +2120 -569
  14. package/dart/pubspec.yaml +2 -2
  15. package/dart/test/acquire_session_credentials_test.dart +1 -1
  16. package/dart/test/acquire_session_test.dart +68 -14
  17. package/dart/test/adv_realm_interference_test.dart +6 -4
  18. package/dart/test/cluster_births_test.dart +11 -8
  19. package/dart/test/cluster_local_autosync_test.dart +1 -1
  20. package/dart/test/cluster_scale_test.dart +3 -2
  21. package/dart/test/dlq_session_surface_test.dart +52 -35
  22. package/dart/test/first_app_gauntlet_test.dart +1 -1
  23. package/dart/test/local_sync_inert_test.dart +23 -26
  24. package/dart/test/offer_authentication_proof_test.dart +193 -0
  25. package/dart/test/result_usda_test.dart +49 -0
  26. package/dart/test/session_bound_test.dart +8 -11
  27. package/dart/test/stale_bundle_test.dart +18 -3
  28. package/dart/test/test_harness_test.dart +14 -27
  29. package/dart/test/webview_bridge_test.dart +504 -83
  30. package/dart_types/CHANGELOG.md +7 -0
  31. package/dart_types/lib/nomos_kernel_protocol.dart +3 -2
  32. package/dart_types/lib/src/custody.dart +64 -19
  33. package/dart_types/lib/src/local_work_rejected.dart +30 -45
  34. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pb.dart +169 -0
  35. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pbenum.dart +11 -0
  36. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +2021 -234
  37. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +175 -8
  38. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +8664 -0
  39. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +369 -0
  40. package/dart_types/lib/src/surfaced.dart +30 -25
  41. package/dart_types/pubspec.yaml +1 -1
  42. package/dart_types/test/surfaced_test.dart +2 -1
  43. package/package.json +1 -1
  44. package/runtime/generated-application-host.js +5736 -2047
  45. package/src/authoring.ts +3 -2
  46. package/src/births.ts +21 -0
  47. package/src/build_package.ts +30 -0
  48. package/src/business_birth.ts +39 -35
  49. package/src/codegen_call_usda.ts +1111 -0
  50. package/src/codegen_dart.ts +207 -282
  51. package/src/codegen_dart_application.ts +195 -252
  52. package/src/codegen_dart_client.ts +1978 -593
  53. package/src/codegen_dart_framework_calls.ts +291 -0
  54. package/src/codegen_dart_scenario.ts +7 -0
  55. package/src/codegen_proof.ts +3 -4
  56. package/src/codegen_provider_dart.ts +21 -7
  57. package/src/codegen_query_usda.ts +214 -0
  58. package/src/codegen_result_usda.ts +685 -0
  59. package/src/codegen_ts.ts +3945 -1111
  60. package/src/compile_package_main.ts +360 -32
  61. package/src/directive.ts +19 -4
  62. package/src/engine_entry.ts +45 -4
  63. package/src/framework/application_home.ts +2 -0
  64. package/src/framework/auth_provider_law.ts +262 -20
  65. package/src/framework/home.ts +32 -28
  66. package/src/framework/owned_custody.ts +19 -11
  67. package/src/framework/owned_custody_value.ts +51 -0
  68. package/src/framework/relations.ts +57 -21
  69. package/src/framework/signing_identity.ts +34 -0
  70. package/src/framework/workspaces.ts +31 -9
  71. package/src/framework_call_definitions.ts +53 -0
  72. package/src/framework_read_definitions.ts +45 -0
  73. package/src/identity_frontier.ts +58 -0
  74. package/src/index.ts +20 -1
  75. package/src/intent.ts +216 -16
  76. package/src/law_check.ts +19 -1
  77. package/src/law_graph.ts +57 -8
  78. package/src/law_graph_usda.ts +73 -5
  79. package/src/law_value_type.ts +106 -19
  80. package/src/read.ts +47 -0
  81. package/src/read_projection_namespace.ts +7 -0
  82. package/src/shape_compat.ts +38 -39
  83. package/src/stable_ids.ts +37 -2
  84. package/src/stable_ids_for_module.ts +64 -4
  85. package/src/test_call_usda.ts +83 -0
  86. package/src/wire.ts +36 -2
  87. package/src/wire_encode.ts +45 -1
  88. package/src/workspace_sharing.ts +27 -10
  89. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbjson.dart +0 -6096
  90. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbserver.dart +0 -50
  91. package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pbjson.dart +0 -178
  92. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pb.dart +0 -828
  93. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbenum.dart +0 -465
  94. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbjson.dart +0 -573
  95. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbserver.dart +0 -50
@@ -6,15 +6,123 @@
6
6
  import 'dart:async';
7
7
  import 'dart:convert';
8
8
  import 'dart:io';
9
+ import 'dart:typed_data';
9
10
 
10
11
  import 'package:nomos_client/src/nms_generated_runtime.dart';
12
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as protocol;
13
+ import 'package:fixnum/fixnum.dart' as fixnum;
11
14
  import 'package:test/test.dart';
12
15
 
16
+ protocol.RuntimeResponse _runtimeLifecycleResponse(
17
+ protocol.RuntimeRequest request,
18
+ ) {
19
+ final response = protocol.RuntimeResponse(
20
+ protocolVersion: 2,
21
+ requestId: request.requestId,
22
+ );
23
+ switch (request.whichCommand()) {
24
+ case protocol.RuntimeRequest_Command.listWorkspaces:
25
+ response.workspaceList = protocol.WorkspaceList();
26
+ break;
27
+ case protocol.RuntimeRequest_Command.prepareDomain:
28
+ response.domainPrepared = protocol.DomainPrepared(
29
+ domain: request.prepareDomain.domain,
30
+ packageSha256: request.prepareDomain.packageSha256,
31
+ );
32
+ break;
33
+ case protocol.RuntimeRequest_Command.resolveCustodyBreak:
34
+ response.custodyBreakResolved = protocol.CustodyBreakResolved(
35
+ session: request.resolveCustodyBreak.session,
36
+ decision: request.resolveCustodyBreak.decision,
37
+ );
38
+ break;
39
+ case protocol.RuntimeRequest_Command.startAutoSync:
40
+ response.autoSyncStarted =
41
+ protocol.AutoSyncStarted(session: request.startAutoSync.session);
42
+ break;
43
+ case protocol.RuntimeRequest_Command.stopAutoSync:
44
+ response.autoSyncStopped =
45
+ protocol.AutoSyncStopped(session: request.stopAutoSync.session);
46
+ break;
47
+ case protocol.RuntimeRequest_Command.syncWorkspace:
48
+ response.syncWorkspace = protocol.SyncWorkspaceResponse(
49
+ workspace: request.syncWorkspace.session,
50
+ );
51
+ break;
52
+ case protocol.RuntimeRequest_Command.unwatch:
53
+ response.watchStopped = protocol.WatchStopped();
54
+ break;
55
+ case protocol.RuntimeRequest_Command.closeRealm:
56
+ response.realmClosed = protocol.RealmClosed();
57
+ break;
58
+ case protocol.RuntimeRequest_Command.modelLifecycle:
59
+ final lifecycle = request.modelLifecycle;
60
+ switch (lifecycle.whichOperation()) {
61
+ case protocol.ModelLifecycleRequest_Operation.stage:
62
+ response.modelLifecycle = protocol.ModelLifecycleResponse(
63
+ stage: protocol.ModelLifecycleStage.MODEL_LIFECYCLE_STAGE_PENDING,
64
+ accepted: true,
65
+ );
66
+ break;
67
+ case protocol.ModelLifecycleRequest_Operation.adopt:
68
+ response.modelLifecycle = protocol.ModelLifecycleResponse(
69
+ stage: protocol
70
+ .ModelLifecycleStage.MODEL_LIFECYCLE_STAGE_ACCEPTANCE_REQUIRED,
71
+ accepted: false,
72
+ review: protocol.ModelReview(
73
+ checkedActions: fixnum.Int64(1),
74
+ candidateActions: fixnum.Int64(1),
75
+ sameOutcome: false,
76
+ differences: [
77
+ protocol.ModelDifference(
78
+ kind: protocol
79
+ .ModelDifferenceKind.MODEL_DIFFERENCE_KIND_OUTCOME,
80
+ action: 'registerSite',
81
+ summary: 'registerSite has a different outcome',
82
+ ),
83
+ ],
84
+ ),
85
+ );
86
+ break;
87
+ case protocol.ModelLifecycleRequest_Operation.restore:
88
+ response.modelLifecycle = protocol.ModelLifecycleResponse(
89
+ stage: protocol
90
+ .ModelLifecycleStage.MODEL_LIFECYCLE_STAGE_ACCEPTANCE_REQUIRED,
91
+ accepted: false,
92
+ review: protocol.ModelReview(
93
+ checkedActions: fixnum.Int64(1),
94
+ candidateActions: fixnum.Int64(1),
95
+ sameOutcome: false,
96
+ differences: [
97
+ protocol.ModelDifference(
98
+ kind:
99
+ protocol.ModelDifferenceKind.MODEL_DIFFERENCE_KIND_RULES,
100
+ action: 'setMood',
101
+ summary: 'setMood is governed by a different rule',
102
+ ),
103
+ ],
104
+ ),
105
+ );
106
+ break;
107
+ case protocol.ModelLifecycleRequest_Operation.notSet:
108
+ throw StateError('Model lifecycle operation is required');
109
+ }
110
+ break;
111
+ default:
112
+ throw StateError(
113
+ 'Fake lifecycle transport does not implement '
114
+ '${request.whichCommand().name}',
115
+ );
116
+ }
117
+ return response;
118
+ }
119
+
13
120
  /// A fake runner-over-a-transport: records sent requests, replies the way `webview-runner.mjs` does, and can
14
121
  /// push reactive `watch` events. Lets us drive [NomosBridge] without a real WebView.
15
122
  class FakeRunner implements BridgeTransport {
16
123
  final _incoming = StreamController<Map<String, Object?>>.broadcast();
17
124
  final List<Map<String, Object?>> sent = [];
125
+ final List<protocol.RuntimeRequest> runtimeRequests = [];
18
126
  String? lastWatchSubId;
19
127
  bool acknowledgeWatch = true;
20
128
  bool acknowledgeUnwatch = true;
@@ -31,6 +139,29 @@ class FakeRunner implements BridgeTransport {
31
139
  @override
32
140
  Stream<Map<String, Object?>> get messages => _incoming.stream;
33
141
 
142
+ @override
143
+ Future<protocol.RuntimeResponse> invokeRuntime(
144
+ protocol.RuntimeRequest request,
145
+ ) async {
146
+ runtimeRequests.add(request.deepCopy());
147
+ if (!acknowledgeUnwatch && request.hasUnwatch()) {
148
+ return Completer<protocol.RuntimeResponse>().future;
149
+ }
150
+ if (request.hasSyncWorkspace() && !request.syncWorkspace.admit) {
151
+ return protocol.RuntimeResponse(
152
+ protocolVersion: 2,
153
+ requestId: request.requestId,
154
+ failure: protocol.RuntimeFailure(
155
+ code: 'nomos.sync.refused',
156
+ message: 'typed sync failed',
157
+ component: 'test.runner',
158
+ workspace: request.syncWorkspace.session,
159
+ ),
160
+ );
161
+ }
162
+ return _runtimeLifecycleResponse(request);
163
+ }
164
+
34
165
  @override
35
166
  void send(Map<String, Object?> message) {
36
167
  sent.add(message);
@@ -193,15 +324,33 @@ class FakeRunner implements BridgeTransport {
193
324
  }));
194
325
  break;
195
326
  case 'cryptoOwnedDeviceRequest':
196
- _incoming.add(reply({'request': 'opaque-owned-device-request'}));
327
+ _incoming.add(reply({
328
+ 'request': {
329
+ 'v': 1,
330
+ 'workspace': 'notes',
331
+ 'principal': 'user:alice',
332
+ 'scope': 'nomos_internal_owned_read_7c1f5e',
333
+ 'signingPublic': 'AA==',
334
+ 'wrappingPublic': 'AQ==',
335
+ 'signature': 'Ag==',
336
+ },
337
+ }));
197
338
  break;
198
339
  case 'cryptoOwnedPrepareApproval':
199
340
  _incoming.add(reply({
200
341
  'newKeySubject': 'key:${'a' * 64}',
201
- 'ownedCustody': 'opaque-owned-custody',
342
+ 'ownedCustody': {
343
+ 'v': 1,
344
+ 'request': args['request'],
345
+ 'hpkeEpk': 'Aw==',
346
+ 'ct': 'BA==',
347
+ 'keyCommitment': '0' * 64,
348
+ 'principalWrappingPublic': 'BQ==',
349
+ },
202
350
  'verifiedVia': 'nomos:test-root',
203
351
  'validFromEpoch': 0,
204
352
  'approvedAt': '2026-07-18T14:20:00.000Z',
353
+ '__nomos_owned_device_key_subject_7c1f5e': 'key:${'b' * 64}',
205
354
  }));
206
355
  break;
207
356
  case 'cryptoOwnedScopeKeys':
@@ -220,9 +369,11 @@ class FakeRunner implements BridgeTransport {
220
369
  break;
221
370
  case 'cryptoOwnedPrepareRecordAction':
222
371
  _incoming.add(reply({
372
+ '__nomos_owned_device_key_subject_7c1f5e': 'key:${'c' * 64}',
223
373
  'recordId': 'note-1',
224
374
  'peer': 'user:bob',
225
375
  'epoch': 2,
376
+ 'sharedAt': '2026-07-18T14:25:00.000Z',
226
377
  'wraps': <Object?>[],
227
378
  }));
228
379
  break;
@@ -351,12 +502,160 @@ class FakeRunner implements BridgeTransport {
351
502
  });
352
503
  }
353
504
 
505
+ /// Proves that generated application reads bypass the legacy operation bus.
506
+ /// The ordinary bridge remains present for lifecycle setup, but a query can
507
+ /// only succeed through the generated Runtime v2 kernel-call capability.
508
+ class FakeTypedRunner extends FakeRunner
509
+ implements NomosKernelCallTransport, NomosRuntimeV2Transport {
510
+ protocol.CallRequest? lastKernelCall;
511
+ String? lastKernelSession;
512
+ @override
513
+ Future<protocol.CallResponse> callKernel({
514
+ required String session,
515
+ required protocol.CallRequest request,
516
+ }) async {
517
+ lastKernelSession = session;
518
+ lastKernelCall = request;
519
+ if (request.hasQuery() && request.query.hasOwnedDeviceRequest()) {
520
+ final value = OwnedDeviceRequest(
521
+ v: 1,
522
+ workspace: 'home',
523
+ principal: 'user:alice',
524
+ scope: request.query.ownedDeviceRequest.scope,
525
+ signingPublic: 'AA==',
526
+ wrappingPublic: 'AQ==',
527
+ signature: 'Ag==',
528
+ );
529
+ return protocol.CallResponse(
530
+ protocolVersion: 1,
531
+ requestId: request.requestId,
532
+ query: protocol.QueryResponse(
533
+ ownedDeviceRequest: protocol.OwnedDeviceRequestResponse(
534
+ requestUsda: value.encodeUsda(),
535
+ ),
536
+ ),
537
+ );
538
+ }
539
+ if (request.hasQuery() && request.query.hasOwnedPrepareApproval()) {
540
+ final device = OwnedDeviceRequest.decodeUsda(
541
+ Uint8List.fromList(
542
+ request.query.ownedPrepareApproval.requestUsda,
543
+ ),
544
+ );
545
+ final custody = OwnedCustody(
546
+ v: 1,
547
+ request: device,
548
+ hpkeEpk: 'Aw==',
549
+ ct: 'BA==',
550
+ keyCommitment: '0' * 64,
551
+ principalWrappingPublic: 'BQ==',
552
+ );
553
+ return protocol.CallResponse(
554
+ protocolVersion: 1,
555
+ requestId: request.requestId,
556
+ query: protocol.QueryResponse(
557
+ ownedPrepareApproval: protocol.OwnedPrepareApprovalResponse(
558
+ newKeySubject: 'key:${'a' * 64}',
559
+ ownedCustodyUsda: custody.encodeUsda(),
560
+ verifiedVia: 'nomos:test-root',
561
+ validFromEpoch: fixnum.Int64.ZERO,
562
+ approvedAtUnixMillis: fixnum.Int64(1787062800000),
563
+ ownedKeySubject: 'key:${'b' * 64}',
564
+ ),
565
+ ),
566
+ );
567
+ }
568
+ return protocol.CallResponse(
569
+ protocolVersion: 1,
570
+ requestId: request.requestId,
571
+ query: protocol.QueryResponse(
572
+ resultUsda: utf8.encode('''#usda 1.0
573
+ (
574
+ defaultPrim = "Result"
575
+ )
576
+ def NomosReadResult "Result"
577
+ {
578
+ def NomosListValue "Rows"
579
+ {
580
+ }
581
+ }
582
+ '''),
583
+ ),
584
+ );
585
+ }
586
+
587
+ @override
588
+ Future<protocol.RuntimeResponse> invokeRuntime(
589
+ protocol.RuntimeRequest request) async {
590
+ runtimeRequests.add(request.deepCopy());
591
+ final response = protocol.RuntimeResponse(
592
+ protocolVersion: 2,
593
+ requestId: request.requestId,
594
+ );
595
+ switch (request.whichCommand()) {
596
+ case protocol.RuntimeRequest_Command.listWorkspaces:
597
+ response.workspaceList = protocol.WorkspaceList(workspaces: [
598
+ protocol.WorkspaceSummary(
599
+ session: 'home',
600
+ workspace: 'home',
601
+ residency: protocol.WorkspaceResidency.WORKSPACE_RESIDENCY_MOUNTED,
602
+ syncPosture: protocol
603
+ .WorkspaceSyncPosture.WORKSPACE_SYNC_POSTURE_CLOUD_BOUND,
604
+ ),
605
+ ]);
606
+ break;
607
+ case protocol.RuntimeRequest_Command.prepareDomain:
608
+ response.domainPrepared = protocol.DomainPrepared(
609
+ domain: request.prepareDomain.domain,
610
+ packageSha256: request.prepareDomain.packageSha256,
611
+ );
612
+ break;
613
+ case protocol.RuntimeRequest_Command.resolveCustodyBreak:
614
+ response.custodyBreakResolved = protocol.CustodyBreakResolved(
615
+ session: request.resolveCustodyBreak.session,
616
+ decision: request.resolveCustodyBreak.decision,
617
+ );
618
+ break;
619
+ case protocol.RuntimeRequest_Command.startAutoSync:
620
+ response.autoSyncStarted =
621
+ protocol.AutoSyncStarted(session: request.startAutoSync.session);
622
+ break;
623
+ case protocol.RuntimeRequest_Command.stopAutoSync:
624
+ response.autoSyncStopped =
625
+ protocol.AutoSyncStopped(session: request.stopAutoSync.session);
626
+ break;
627
+ case protocol.RuntimeRequest_Command.syncWorkspace:
628
+ response.syncWorkspace = protocol.SyncWorkspaceResponse(
629
+ workspace: request.syncWorkspace.session,
630
+ );
631
+ break;
632
+ case protocol.RuntimeRequest_Command.closeRealm:
633
+ response.realmClosed = protocol.RealmClosed();
634
+ break;
635
+ case protocol.RuntimeRequest_Command.modelLifecycle:
636
+ response.modelLifecycle =
637
+ _runtimeLifecycleResponse(request).modelLifecycle;
638
+ break;
639
+ default:
640
+ throw StateError(
641
+ 'FakeTypedRunner does not implement ${request.whichCommand().name}');
642
+ }
643
+ return response;
644
+ }
645
+ }
646
+
354
647
  class ErrorRunner implements BridgeTransport {
355
648
  final _incoming = StreamController<Map<String, Object?>>.broadcast();
356
649
 
357
650
  @override
358
651
  Stream<Map<String, Object?>> get messages => _incoming.stream;
359
652
 
653
+ @override
654
+ Future<protocol.RuntimeResponse> invokeRuntime(
655
+ protocol.RuntimeRequest request,
656
+ ) async =>
657
+ _runtimeLifecycleResponse(request);
658
+
360
659
  @override
361
660
  void send(Map<String, Object?> message) {}
362
661
 
@@ -380,6 +679,11 @@ class ReplayTransport implements BridgeTransport {
380
679
  @override
381
680
  Stream<Map<String, Object?>> get messages => _incoming.stream;
382
681
  @override
682
+ Future<protocol.RuntimeResponse> invokeRuntime(
683
+ protocol.RuntimeRequest request,
684
+ ) async =>
685
+ _runtimeLifecycleResponse(request);
686
+ @override
383
687
  void send(Map<String, Object?> message) {
384
688
  lastRequest = message;
385
689
  final id = message['id'];
@@ -459,6 +763,7 @@ Object? _expectedTyped(String op, Map<String, Object?> result) {
459
763
  case 'sum':
460
764
  return result['sum'];
461
765
  case 'sync':
766
+ return isA<protocol.SyncWorkspaceResponse>();
462
767
  case 'lawStatus':
463
768
  return result;
464
769
  default:
@@ -492,6 +797,8 @@ void main() {
492
797
  () => expect(corpus['version'], kWebviewBridgeVersion));
493
798
  for (final c in cases) {
494
799
  final op = c['op'] as String;
800
+ if (op == 'sync')
801
+ continue; // Runtime v2 has its own protobuf conformance tests.
495
802
  final args = (c['args'] as Map).cast<String, Object?>();
496
803
  final result = (c['result'] as Map).cast<String, Object?>();
497
804
  test("'$op' request args + parsed result match the corpus", () async {
@@ -636,6 +943,93 @@ void main() {
636
943
  await bridge.ready.timeout(const Duration(seconds: 1));
637
944
  });
638
945
 
946
+ test('nominal queries use Runtime v2 protobuf and return USDA bytes',
947
+ () async {
948
+ final typedRunner = FakeTypedRunner();
949
+ final typedBridge = NomosBridge(typedRunner);
950
+ addTearDown(typedBridge.dispose);
951
+ await typedBridge.ready;
952
+ await typedBridge.connect(
953
+ cloud: 'https://c',
954
+ workspace: 'home',
955
+ session: 'home',
956
+ );
957
+ typedRunner.sent.clear();
958
+
959
+ final result = await typedBridge.nmsQueryUsda(
960
+ NmsInternalGeneratedQueries.create(
961
+ domain: 'entries',
962
+ query: 'entriesByOwner',
963
+ canonicalArgumentsUsda: Uint8List.fromList(utf8.encode('''#usda 1.0
964
+ (
965
+ defaultPrim = "Arguments"
966
+ )
967
+ def NomosQueryArguments "Arguments"
968
+ {
969
+ custom string nomos:owner = "user:ada"
970
+ }
971
+ ''')),
972
+ ),
973
+ null,
974
+ 'user:ada',
975
+ 'home',
976
+ );
977
+
978
+ expect(result, isA<Uint8List>());
979
+ expect(utf8.decode(result), startsWith('#usda 1.0'));
980
+ expect(typedRunner.sent, isEmpty,
981
+ reason: 'the JSON-shaped BridgeTransport must not carry the query');
982
+ expect(typedRunner.lastKernelSession, 'home');
983
+ expect(typedRunner.lastKernelCall!.hasQuery(), isTrue);
984
+ expect(typedRunner.lastKernelCall!.query.rows.id, 'entriesByOwner');
985
+ expect(
986
+ typedRunner.lastKernelCall!.query.resultFormat,
987
+ protocol.QueryResultFormat.QUERY_RESULT_FORMAT_NOMINAL_USDA,
988
+ );
989
+ });
990
+
991
+ test('steady-state lifecycle uses generated Runtime v2 commands', () async {
992
+ final typedRunner = FakeTypedRunner();
993
+ final typedBridge = NomosBridge(typedRunner);
994
+ await typedBridge.ready;
995
+ await typedBridge.connect(
996
+ cloud: 'https://c',
997
+ workspace: 'home',
998
+ session: 'home',
999
+ );
1000
+ typedRunner.sent.clear();
1001
+
1002
+ final workspaces = await typedBridge.listWorkspaces();
1003
+ await typedBridge.session('home').prepareDomain(
1004
+ domain: 'guestbook',
1005
+ packageHash: 'a' * 64,
1006
+ );
1007
+ await typedBridge.session('home').autoSync(fallbackMs: 60000);
1008
+ await typedBridge.session('home').stopAutoSync();
1009
+ final custody = await typedBridge.session('home').resolveCustodyBreak(
1010
+ protocol.CustodyBreakDecision.CUSTODY_BREAK_DECISION_ADOPT,
1011
+ );
1012
+
1013
+ expect(workspaces.single.workspace, 'home');
1014
+ expect(
1015
+ custody.decision,
1016
+ protocol.CustodyBreakDecision.CUSTODY_BREAK_DECISION_ADOPT,
1017
+ );
1018
+ expect(
1019
+ typedRunner.runtimeRequests.map((request) => request.whichCommand()),
1020
+ [
1021
+ protocol.RuntimeRequest_Command.listWorkspaces,
1022
+ protocol.RuntimeRequest_Command.prepareDomain,
1023
+ protocol.RuntimeRequest_Command.startAutoSync,
1024
+ protocol.RuntimeRequest_Command.stopAutoSync,
1025
+ protocol.RuntimeRequest_Command.resolveCustodyBreak,
1026
+ ],
1027
+ );
1028
+ expect(typedRunner.sent, isEmpty,
1029
+ reason: 'no JSON command envelope carries steady-state lifecycle');
1030
+ await typedBridge.dispose();
1031
+ });
1032
+
639
1033
  test('a local durability span remains workspace/head specific', () async {
640
1034
  await bridge.ready;
641
1035
  bridge.noteLocalSpan(
@@ -750,28 +1144,35 @@ void main() {
750
1144
  expect(bridge.principal, 'user:ada');
751
1145
  });
752
1146
 
753
- test(
754
- 'generated model restoration crosses one framework-private bridge operation',
1147
+ test('generated model restoration uses one exact Runtime v2 command',
755
1148
  () async {
756
1149
  await bridge.ready;
1150
+ await bridge.connect(cloud: 'https://c', workspace: 'evo');
1151
+ runner.runtimeRequests.clear();
757
1152
  final verdict = await bridge.nomosFrameworkRestorePreviousGeneratedModel(
758
1153
  domain: 'evo',
759
1154
  currentHash: 'current-law-hash',
760
1155
  because: 'the previous operating policy is required',
761
1156
  );
762
1157
 
763
- expect(verdict['ok'], isFalse);
764
- expect(verdict['stage'], 'acceptance');
765
- expect(((verdict['report'] as Map)['divergences'] as List).single,
766
- containsPair('directiveId', 'setMood'));
767
- final request = runner.sent.firstWhere(
768
- (message) => message['op'] == 'restorePreviousGeneratedModel',
1158
+ expect(verdict.accepted, isFalse);
1159
+ expect(
1160
+ verdict.stage,
1161
+ protocol.ModelLifecycleStage.MODEL_LIFECYCLE_STAGE_ACCEPTANCE_REQUIRED,
769
1162
  );
770
- expect((request['args'] as Map).cast<String, Object?>(), {
771
- 'domain': 'evo',
772
- 'currentHash': 'current-law-hash',
773
- 'because': 'the previous operating policy is required',
774
- });
1163
+ expect(verdict.review.differences.single.action, 'setMood');
1164
+ final request = runner.runtimeRequests.singleWhere(
1165
+ (request) =>
1166
+ request.hasModelLifecycle() && request.modelLifecycle.hasRestore(),
1167
+ );
1168
+ expect(request.modelLifecycle.restore.domain, 'evo');
1169
+ expect(request.modelLifecycle.restore.currentHash, 'current-law-hash');
1170
+ expect(request.modelLifecycle.restore.acceptanceReason,
1171
+ 'the previous operating policy is required');
1172
+ expect(
1173
+ runner.sent.where(
1174
+ (message) => message['op'] == 'restorePreviousGeneratedModel'),
1175
+ isEmpty);
775
1176
  });
776
1177
 
777
1178
  test(
@@ -788,7 +1189,7 @@ void main() {
788
1189
  expect(page.done, isFalse);
789
1190
  expect((await bridge.queryById('Entry_1')).first['id'], 'Entry_1');
790
1191
  expect(await bridge.count('entries'), 7);
791
- expect(await bridge.sync(), {'pushed': true});
1192
+ expect((await bridge.sync()).workspace, 'ws');
792
1193
  expect(
793
1194
  await bridge.lawStatus('abc'), {'installed': true, 'drifted': false});
794
1195
  });
@@ -868,50 +1269,53 @@ void main() {
868
1269
  await readBridge.dispose();
869
1270
  });
870
1271
 
871
- test('devices traffic one opaque request and one atomic framework approval',
1272
+ test(
1273
+ 'devices retain nominal request and custody types through one atomic approval',
872
1274
  () async {
873
- await bridge.ready;
1275
+ final typedRunner = FakeTypedRunner();
1276
+ final typedBridge = NomosBridge(typedRunner);
1277
+ addTearDown(typedBridge.dispose);
1278
+ await typedBridge.ready;
1279
+ await typedBridge.connect(
1280
+ cloud: 'https://c',
1281
+ workspace: 'home',
1282
+ session: 'home',
1283
+ authorSecret: base64Encode(List<int>.filled(32, 7)),
1284
+ authToken: 'e30.eyJzdWIiOiJhbGljZSJ9.',
1285
+ );
1286
+ NomosOwnedDeviceApprovalPreparation? approved;
874
1287
  final devices = NomosDevices(
875
- bridge: bridge,
1288
+ bridge: typedBridge.session('home'),
876
1289
  domain: 'notes',
877
1290
  domainHash: 'a' * 64,
878
1291
  actor: 'user:alice',
879
1292
  scope: 'nomos_internal_owned_read_7c1f5e',
1293
+ approvePrepared: (preparation) async {
1294
+ approved = preparation;
1295
+ return const NomosOfferOutcome(
1296
+ head: 'HEAD-owned-approval',
1297
+ created: [],
1298
+ workspace: 'notes',
1299
+ );
1300
+ },
880
1301
  );
881
1302
 
882
1303
  final request = await devices.request();
883
- expect(request, 'opaque-owned-device-request');
884
- final requestCall = runner.sent.firstWhere(
885
- (message) => message['op'] == 'cryptoOwnedDeviceRequest',
886
- );
887
- expect((requestCall['args'] as Map)['scope'],
888
- 'nomos_internal_owned_read_7c1f5e');
1304
+ expect(request.workspace, 'home');
1305
+ expect(request.principal, 'user:alice');
1306
+ expect(request.scope, 'nomos_internal_owned_read_7c1f5e');
1307
+ expect(typedRunner.lastKernelCall!.query.hasOwnedDeviceRequest(), isTrue);
889
1308
 
890
1309
  final outcome = await devices.approve(request);
891
1310
  expect(outcome.head, 'HEAD-owned-approval');
892
- final prepareCall = runner.sent.firstWhere(
893
- (message) => message['op'] == 'cryptoOwnedPrepareApproval',
894
- );
895
- expect((prepareCall['args'] as Map).cast<String, Object?>(), {
896
- 'request': 'opaque-owned-device-request',
897
- 'scope': 'nomos_internal_owned_read_7c1f5e',
898
- });
899
- final offerCall = runner.sent.firstWhere(
900
- (message) => message['op'] == 'offerDirective',
901
- );
902
- final offerArgs = (offerCall['args'] as Map).cast<String, Object?>();
903
- expect(offerArgs['domain'], 'notes');
904
- expect(
905
- offerArgs['directiveId'], 'nomos_internal_approve_owned_device_7c1f5e');
906
- expect(offerArgs['actor'], 'user:alice');
907
- expect(offerArgs['domainHash'], 'a' * 64);
908
- expect(offerArgs['payload'], {
909
- 'newKeySubject': 'key:${'a' * 64}',
910
- 'ownedCustody': 'opaque-owned-custody',
911
- 'verifiedVia': 'nomos:test-root',
912
- 'validFromEpoch': 0,
913
- 'approvedAt': '2026-07-18T14:20:00.000Z',
914
- });
1311
+ expect(typedRunner.lastKernelCall!.query.hasOwnedPrepareApproval(), isTrue);
1312
+ expect(approved?.newKeySubject, 'key:${'a' * 64}');
1313
+ expect(approved?.ownedCustody.request.workspace, 'home');
1314
+ expect(approved?.ownedCustody.keyCommitment, '0' * 64);
1315
+ expect(approved?.verifiedVia, 'nomos:test-root');
1316
+ expect(approved?.validFromEpoch, 0);
1317
+ expect(approved?.approvedAt, '2026-08-18T14:20:00.000Z');
1318
+ expect(approved?.ownedKeySubject, 'key:${'b' * 64}');
915
1319
  });
916
1320
 
917
1321
  test(
@@ -941,7 +1345,9 @@ void main() {
941
1345
  ),
942
1346
  peer: 'user:bob',
943
1347
  );
944
- expect(payload, containsPair('recordId', 'note-1'));
1348
+ expect(payload, isA<NomosOwnedSharePreparation>());
1349
+ expect((payload as NomosOwnedSharePreparation).recordId, 'note-1');
1350
+ expect(payload.ownedKeySubject, 'key:${'c' * 64}');
945
1351
 
946
1352
  final keyCall = runner.sent.firstWhere(
947
1353
  (message) => message['op'] == 'cryptoOwnedScopeKeys',
@@ -969,48 +1375,57 @@ void main() {
969
1375
  });
970
1376
  });
971
1377
 
972
- test(
973
- 'generated model adoption crosses one framework-private bridge operation',
974
- () async {
1378
+ test('generated model adoption uses one exact Runtime v2 command', () async {
975
1379
  await bridge.ready;
1380
+ await bridge.connect(cloud: 'https://c', workspace: 'evo');
1381
+ runner.runtimeRequests.clear();
976
1382
  final verdict = await bridge.nomosFrameworkAdoptGeneratedModel(
977
1383
  domain: 'evo',
978
1384
  candidateHash: 'candidate-law-hash',
979
1385
  because: 'the renamed business action is intentional',
980
1386
  );
981
1387
 
982
- expect(verdict['ok'], isFalse);
983
- expect(verdict['stage'], 'acceptance');
984
- expect(((verdict['report'] as Map)['divergences'] as List).single,
985
- containsPair('directiveId', 'registerSite'));
986
- final request = runner.sent.firstWhere(
987
- (message) => message['op'] == 'adoptGeneratedModel',
1388
+ expect(verdict.accepted, isFalse);
1389
+ expect(
1390
+ verdict.stage,
1391
+ protocol.ModelLifecycleStage.MODEL_LIFECYCLE_STAGE_ACCEPTANCE_REQUIRED,
988
1392
  );
989
- expect((request['args'] as Map).cast<String, Object?>(), {
990
- 'domain': 'evo',
991
- 'candidateHash': 'candidate-law-hash',
992
- 'because': 'the renamed business action is intentional',
993
- });
1393
+ expect(verdict.review.differences.single.action, 'registerSite');
1394
+ final request = runner.runtimeRequests.singleWhere(
1395
+ (request) =>
1396
+ request.hasModelLifecycle() && request.modelLifecycle.hasAdopt(),
1397
+ );
1398
+ expect(request.modelLifecycle.adopt.domain, 'evo');
1399
+ expect(request.modelLifecycle.adopt.candidateHash, 'candidate-law-hash');
1400
+ expect(request.modelLifecycle.adopt.acceptanceReason,
1401
+ 'the renamed business action is intentional');
1402
+ expect(
1403
+ runner.sent.where((message) => message['op'] == 'adoptGeneratedModel'),
1404
+ isEmpty);
994
1405
  });
995
1406
 
996
1407
  test(
997
- 'generated model staging carries opaque USDA through one framework-private operation',
1408
+ 'generated model staging carries USDA bytes in one exact Runtime v2 command',
998
1409
  () async {
999
1410
  await bridge.ready;
1411
+ await bridge.connect(cloud: 'https://c', workspace: 'evo');
1412
+ runner.runtimeRequests.clear();
1000
1413
  await bridge.nomosInternalStageGeneratedModel(
1001
1414
  domain: 'evo',
1002
1415
  candidateHash: 'candidate-law-hash',
1003
1416
  lawUsdz: const [1, 2, 3],
1004
1417
  );
1005
1418
 
1006
- final request = runner.sent.firstWhere(
1007
- (message) => message['op'] == 'stageGeneratedModel',
1419
+ final request = runner.runtimeRequests.singleWhere(
1420
+ (request) =>
1421
+ request.hasModelLifecycle() && request.modelLifecycle.hasStage(),
1008
1422
  );
1009
- expect((request['args'] as Map).cast<String, Object?>(), {
1010
- 'domain': 'evo',
1011
- 'candidateHash': 'candidate-law-hash',
1012
- 'lawUsdz': 'AQID',
1013
- });
1423
+ expect(request.modelLifecycle.stage.domain, 'evo');
1424
+ expect(request.modelLifecycle.stage.candidateHash, 'candidate-law-hash');
1425
+ expect(request.modelLifecycle.stage.lawUsdz, [1, 2, 3]);
1426
+ expect(
1427
+ runner.sent.where((message) => message['op'] == 'stageGeneratedModel'),
1428
+ isEmpty);
1014
1429
  });
1015
1430
 
1016
1431
  test(
@@ -1036,7 +1451,8 @@ void main() {
1036
1451
  ]
1037
1452
  ]);
1038
1453
  await sub.cancel();
1039
- expect(runner.sent.any((m) => m['op'] == 'unwatch'), isTrue);
1454
+ expect(
1455
+ runner.runtimeRequests.any((request) => request.hasUnwatch()), isTrue);
1040
1456
  });
1041
1457
 
1042
1458
  test('watch cancellation is local and never waits for the runner', () async {
@@ -1046,7 +1462,8 @@ void main() {
1046
1462
  await Future<void>.delayed(Duration.zero);
1047
1463
 
1048
1464
  await sub.cancel().timeout(const Duration(milliseconds: 100));
1049
- expect(runner.sent.any((message) => message['op'] == 'unwatch'), isTrue);
1465
+ expect(
1466
+ runner.runtimeRequests.any((request) => request.hasUnwatch()), isTrue);
1050
1467
  });
1051
1468
 
1052
1469
  test('signal events are forwarded on their own stream', () async {
@@ -1071,9 +1488,10 @@ void main() {
1071
1488
 
1072
1489
  test('autoSync forwards a scoped signal id', () async {
1073
1490
  await bridge.ready;
1074
- await bridge.autoSync(signalId: 'signal:ws:list:main');
1075
- final req = runner.sent.firstWhere((m) => m['op'] == 'autoSync');
1076
- expect((req['args'] as Map)['signalId'], 'signal:ws:list:main');
1491
+ await bridge.session('ws').autoSync(signalId: 'signal:ws:list:main');
1492
+ final request = runner.runtimeRequests
1493
+ .firstWhere((request) => request.hasStartAutoSync());
1494
+ expect(request.startAutoSync.signalId, 'signal:ws:list:main');
1077
1495
  });
1078
1496
 
1079
1497
  test(
@@ -1463,10 +1881,13 @@ void main() {
1463
1881
  expect(e.message, contains('shallow-insufficient'));
1464
1882
  });
1465
1883
 
1466
- test(
1467
- 'legacy string-only runner errors still get operation and trace metadata',
1468
- () async {
1884
+ test('Runtime v2 failures preserve their generated error identity', () async {
1469
1885
  await bridge.ready;
1886
+ await bridge.connect(
1887
+ cloud: 'https://c',
1888
+ workspace: 'ws-1',
1889
+ session: 'ws-1',
1890
+ );
1470
1891
  Object? err;
1471
1892
  try {
1472
1893
  await bridge.sync(admit: false);
@@ -1475,10 +1896,10 @@ void main() {
1475
1896
  }
1476
1897
  expect(err, isA<NomosBridgeError>());
1477
1898
  final e = err as NomosBridgeError;
1478
- expect(e.message, 'legacy sync failed');
1479
- expect(e.info.code, 'nomos.bridge.error');
1480
- expect(e.info.operation, 'sync');
1481
- expect(e.info.trace.traceId, isNotEmpty);
1899
+ expect(e.message, 'typed sync failed');
1900
+ expect(e.info.code, 'nomos.sync.refused');
1901
+ expect(e.info.component, 'test.runner');
1902
+ expect(e.info.workspace, 'ws-1');
1482
1903
  });
1483
1904
 
1484
1905
  test('typed kernel progress preserves the measured verification fraction',