@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.
- package/dart/CHANGELOG.md +24 -0
- package/dart/lib/src/application_framework_calls.dart +299 -0
- package/dart/lib/src/generated_call.dart +96 -0
- package/dart/lib/src/nms_generated_runtime.dart +29 -0
- package/dart/lib/src/node_runner_transport.dart +77 -1
- package/dart/lib/src/result_usda.dart +487 -0
- package/dart/lib/src/runner/node-entry.mjs +10634 -6122
- package/dart/lib/src/runner/offline-install.mjs +4187 -2716
- package/dart/lib/src/runtime_v2_gateway.dart +54 -0
- package/dart/lib/src/runtime_v2_transport.dart +117 -0
- package/dart/lib/src/session_bound.dart +7 -2
- package/dart/lib/src/test_harness.dart +373 -282
- package/dart/lib/src/webview_bridge.dart +2146 -725
- package/dart/pubspec.lock +17 -1
- package/dart/pubspec.yaml +3 -2
- package/dart/test/acquire_session_credentials_test.dart +1 -1
- package/dart/test/acquire_session_test.dart +68 -14
- package/dart/test/adv_realm_interference_test.dart +6 -4
- package/dart/test/cluster_births_test.dart +11 -8
- package/dart/test/cluster_local_autosync_test.dart +1 -1
- package/dart/test/cluster_scale_test.dart +3 -2
- package/dart/test/dlq_session_surface_test.dart +52 -35
- package/dart/test/first_app_gauntlet_test.dart +1 -1
- package/dart/test/local_sync_inert_test.dart +23 -26
- package/dart/test/offer_authentication_proof_test.dart +193 -0
- package/dart/test/result_usda_test.dart +49 -0
- package/dart/test/session_bound_test.dart +8 -11
- package/dart/test/stale_bundle_test.dart +18 -3
- package/dart/test/test_harness_test.dart +14 -27
- package/dart/test/webview_bridge_test.dart +504 -83
- package/dart_types/CHANGELOG.md +17 -0
- package/dart_types/lib/nomos_kernel_protocol.dart +11 -0
- package/dart_types/lib/src/custody.dart +64 -19
- package/dart_types/lib/src/local_work_rejected.dart +30 -45
- package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pb.dart +169 -0
- package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pbenum.dart +11 -0
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +17945 -0
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +933 -0
- package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pb.dart +245 -0
- package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pbenum.dart +160 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +8664 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +369 -0
- package/dart_types/lib/src/surfaced.dart +30 -25
- package/dart_types/pubspec.lock +16 -0
- package/dart_types/pubspec.yaml +3 -1
- package/dart_types/test/surfaced_test.dart +2 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +5760 -1879
- package/src/authoring.ts +3 -2
- package/src/birth_meter_ensure.ts +2 -0
- package/src/births.ts +21 -0
- package/src/build_package.ts +30 -0
- package/src/business_birth.ts +41 -35
- package/src/codegen_call_usda.ts +1111 -0
- package/src/codegen_dart.ts +207 -282
- package/src/codegen_dart_application.ts +195 -252
- package/src/codegen_dart_client.ts +1978 -593
- package/src/codegen_dart_framework_calls.ts +291 -0
- package/src/codegen_dart_scenario.ts +7 -0
- package/src/codegen_proof.ts +3 -4
- package/src/codegen_provider_dart.ts +21 -7
- package/src/codegen_query_usda.ts +214 -0
- package/src/codegen_result_usda.ts +685 -0
- package/src/codegen_ts.ts +3945 -1111
- package/src/compile_package_main.ts +392 -46
- package/src/directive.ts +19 -4
- package/src/engine_entry.ts +45 -4
- package/src/framework/application_home.ts +4 -0
- package/src/framework/auth_provider_law.ts +296 -20
- package/src/framework/birth_certificate_law.ts +24 -0
- package/src/framework/home.ts +83 -40
- package/src/framework/owned_custody.ts +25 -12
- package/src/framework/owned_custody_value.ts +51 -0
- package/src/framework/relations.ts +47 -21
- package/src/framework/signing_identity.ts +100 -17
- package/src/framework/workspaces.ts +39 -10
- package/src/framework_call_definitions.ts +53 -0
- package/src/framework_read_definitions.ts +45 -0
- package/src/identity_frontier.ts +58 -0
- package/src/index.ts +22 -1
- package/src/intent.ts +216 -16
- package/src/law_check.ts +19 -1
- package/src/law_graph.ts +57 -8
- package/src/law_graph_usda.ts +73 -5
- package/src/law_value_type.ts +106 -19
- package/src/read.ts +47 -0
- package/src/read_projection_namespace.ts +7 -0
- package/src/shape_compat.ts +44 -42
- package/src/stable_ids.ts +37 -2
- package/src/test_call_usda.ts +83 -0
- package/src/wire.ts +36 -2
- package/src/wire_encode.ts +45 -1
- package/src/workspace_sharing.ts +27 -10
|
@@ -41,13 +41,17 @@ import 'dart:convert';
|
|
|
41
41
|
import 'dart:io';
|
|
42
42
|
|
|
43
43
|
import 'package:crypto/crypto.dart' show sha256;
|
|
44
|
+
import 'package:fixnum/fixnum.dart' show Int64;
|
|
45
|
+
import 'package:nomos_types/nomos_kernel_protocol.dart'
|
|
46
|
+
show BirthCertificate, BirthOutcomeStatus, SyncWorkspaceResponse;
|
|
47
|
+
import 'package:nomos_types/nomos_types.dart' show Sha256Digest;
|
|
44
48
|
import 'package:package_config/package_config.dart' show loadPackageConfigUri;
|
|
45
49
|
|
|
46
50
|
import 'dev_cloud.dart';
|
|
51
|
+
import 'application_framework_calls.dart';
|
|
47
52
|
import 'node_runner_transport.dart';
|
|
48
53
|
import 'session_bound.dart' show DomainNotInstalledForSession;
|
|
49
|
-
import 'webview_bridge.dart'
|
|
50
|
-
show NomosBirthCertificateSpec, NomosBridge, NomosSession;
|
|
54
|
+
import 'webview_bridge.dart' show NomosBridge, NomosSession;
|
|
51
55
|
|
|
52
56
|
enum _KernelRuntimeArtifact {
|
|
53
57
|
executable('holon.wasm'),
|
|
@@ -68,6 +72,30 @@ enum _CandidateKernelBinary {
|
|
|
68
72
|
final String fileName;
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
/// Lower the generated protobuf certificate into the framework law's USDA-authored value shape. This is
|
|
76
|
+
/// deliberately not a second certificate type: callers cannot construct it independently, and every field
|
|
77
|
+
/// is read from the compiler-generated message.
|
|
78
|
+
Map<String, Object?> _birthCertificateLawValue(BirthCertificate certificate) =>
|
|
79
|
+
<String, Object?>{
|
|
80
|
+
'child': certificate.child,
|
|
81
|
+
'founder': certificate.founder,
|
|
82
|
+
'verified': certificate.verified,
|
|
83
|
+
'parentKey': base64Encode(certificate.parentKey),
|
|
84
|
+
'parentId': certificate.parentId,
|
|
85
|
+
'epoch': certificate.epoch.toInt(),
|
|
86
|
+
'signature': base64Encode(certificate.signature),
|
|
87
|
+
if (certificate.hasVerifiedVia()) 'verifiedVia': certificate.verifiedVia,
|
|
88
|
+
if (certificate.hasIdpToken()) 'idpToken': certificate.idpToken,
|
|
89
|
+
if (certificate.hasCapturedClock())
|
|
90
|
+
'capturedClock': certificate.capturedClock.toInt(),
|
|
91
|
+
if (certificate.hasAuthProviderIssuer())
|
|
92
|
+
'authProviderIssuer': certificate.authProviderIssuer,
|
|
93
|
+
if (certificate.hasAuthProviderJwks())
|
|
94
|
+
'authProviderJwks': certificate.authProviderJwks,
|
|
95
|
+
if (certificate.hasAuthProviderAudience())
|
|
96
|
+
'authProviderAudience': certificate.authProviderAudience,
|
|
97
|
+
};
|
|
98
|
+
|
|
71
99
|
/// A source checkout is itself a coordinated release candidate: compiler, framework packages and kernels
|
|
72
100
|
/// move together. Generated scenarios must not combine those source-generated laws with a recently-written
|
|
73
101
|
/// but older global runtime cache. Published packages have no release/ directory and keep the normal
|
|
@@ -104,18 +132,21 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
|
|
|
104
132
|
if (host == null) {
|
|
105
133
|
throw NomosTestHarnessException('rustc did not report its host target');
|
|
106
134
|
}
|
|
107
|
-
final result = await Process.run(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
final result = await Process.run(
|
|
136
|
+
'cargo',
|
|
137
|
+
[
|
|
138
|
+
'run',
|
|
139
|
+
'--quiet',
|
|
140
|
+
'--target',
|
|
141
|
+
host,
|
|
142
|
+
'--manifest-path',
|
|
143
|
+
'${sourceRoot.path}/tools/kernel-resource/Cargo.toml',
|
|
144
|
+
'--',
|
|
145
|
+
'read',
|
|
146
|
+
'${sourceRoot.path}/release/kernel-candidate.usda',
|
|
147
|
+
field,
|
|
148
|
+
],
|
|
149
|
+
workingDirectory: sourceRoot.path);
|
|
119
150
|
if (result.exitCode != 0) {
|
|
120
151
|
throw NomosTestHarnessException(
|
|
121
152
|
'could not read typed kernel candidate: ${result.stderr}',
|
|
@@ -222,36 +253,13 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
|
|
|
222
253
|
/// Framework-internal classifier for a sync result that can never become converged by retrying. Generated
|
|
223
254
|
/// scenarios use it through [NomosTestHarness.converge]; it is public only inside `package:nomos_client/src`
|
|
224
255
|
/// so the false-green contract has a cheap regression test without booting a cloud.
|
|
225
|
-
String? nmsPermanentConvergenceFailure(
|
|
226
|
-
final
|
|
227
|
-
|
|
228
|
-
? admission['sessions'] as List
|
|
229
|
-
: const [];
|
|
230
|
-
final rejected = sessions
|
|
231
|
-
.whereType<Map>()
|
|
232
|
-
.expand(
|
|
233
|
-
(session) => session['rejected'] is List
|
|
234
|
-
? session['rejected'] as List
|
|
235
|
-
: const [],
|
|
236
|
-
)
|
|
237
|
-
.toList();
|
|
238
|
-
final deadLetters = admission is Map && admission['deadLetters'] is List
|
|
239
|
-
? admission['deadLetters'] as List
|
|
240
|
-
: const [];
|
|
241
|
-
final converged = result['converged'];
|
|
242
|
-
final replayDeadLetters =
|
|
243
|
-
converged is Map && converged['deadLettered'] is List
|
|
244
|
-
? converged['deadLettered'] as List
|
|
245
|
-
: const [];
|
|
246
|
-
final pushError = result['pushError'];
|
|
247
|
-
final refused = pushError is Map && pushError['refused'] == true;
|
|
248
|
-
if (rejected.isEmpty &&
|
|
249
|
-
deadLetters.isEmpty &&
|
|
250
|
-
replayDeadLetters.isEmpty &&
|
|
251
|
-
!refused) {
|
|
256
|
+
String? nmsPermanentConvergenceFailure(SyncWorkspaceResponse result) {
|
|
257
|
+
final transportRefused = result.hasPushError() && result.pushError.refused;
|
|
258
|
+
if (result.refused == 0 && !transportRefused) {
|
|
252
259
|
return null;
|
|
253
260
|
}
|
|
254
|
-
return
|
|
261
|
+
return 'refused=${result.refused}, '
|
|
262
|
+
'push=${result.hasPushError() ? result.pushError.message : "none"}';
|
|
255
263
|
}
|
|
256
264
|
|
|
257
265
|
/// The offline-install lane configuration a harness keeps so the CUSTODY DIAGNOSTIC helpers
|
|
@@ -311,10 +319,10 @@ class HarnessPhaseTimeout extends NomosTestHarnessException {
|
|
|
311
319
|
required this.awaiting,
|
|
312
320
|
this.likelyCauses = const [],
|
|
313
321
|
}) : super(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
322
|
+
'phase "$phase"${workspace != null ? ' (workspace "$workspace")' : ''} '
|
|
323
|
+
'did not complete within ${elapsed.inSeconds}s while awaiting `$awaiting`'
|
|
324
|
+
'${likelyCauses.isEmpty ? '' : ' — likely causes: ${likelyCauses.map((c) => '\n • $c').join()}'}',
|
|
325
|
+
);
|
|
318
326
|
@override
|
|
319
327
|
String toString() => 'HarnessPhaseTimeout: $message';
|
|
320
328
|
}
|
|
@@ -432,16 +440,16 @@ class NomosTestHarness {
|
|
|
432
440
|
_CloudRestartLane? cloudRestartLane,
|
|
433
441
|
this.testCloud,
|
|
434
442
|
this.homeWorkspace,
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
443
|
+
}) : _sessionNames = List.of(
|
|
444
|
+
sessionNames,
|
|
445
|
+
), // GROWABLE — restoreWorkspace may add a fresh session
|
|
446
|
+
_devicePublics = devicePublics,
|
|
447
|
+
_deviceSecrets = deviceSecrets,
|
|
448
|
+
_lawHashes = Map.of(lawHashes),
|
|
449
|
+
_snapshots = Map.of(snapshots),
|
|
450
|
+
_installLane = installLane,
|
|
451
|
+
_phaseTimeout = phaseTimeout,
|
|
452
|
+
_cloudRestartLane = cloudRestartLane;
|
|
445
453
|
|
|
446
454
|
// ── THE "NO SILENT HANGS" MACHINERY — every lifecycle await runs as a named, deadlined PHASE ──
|
|
447
455
|
|
|
@@ -587,6 +595,7 @@ class NomosTestHarness {
|
|
|
587
595
|
/// [autoTearDown] — when true (default) and [registerTearDown] is given, registers [dispose] as a teardown.
|
|
588
596
|
static Future<NomosTestHarness> ephemeral({
|
|
589
597
|
required List<String> lawUsdzPaths,
|
|
598
|
+
NmsApplicationFrameworkCalls? applicationFrameworkCalls,
|
|
590
599
|
int? applicationMaxChildWorkspacesPerUser,
|
|
591
600
|
String? ownerSubject,
|
|
592
601
|
String workspace = 'test-ws',
|
|
@@ -609,11 +618,9 @@ class NomosTestHarness {
|
|
|
609
618
|
'no lawUsdzPaths — pass the build/<name>.law.usdz `npx githolon compile` emits',
|
|
610
619
|
);
|
|
611
620
|
}
|
|
612
|
-
final entry =
|
|
613
|
-
nodeEntry ??
|
|
621
|
+
final entry = nodeEntry ??
|
|
614
622
|
await _resolveEntry(node, resolveFrom ?? Directory.current.path);
|
|
615
|
-
final effectiveRuntimeDir =
|
|
616
|
-
runtimeDir ??
|
|
623
|
+
final effectiveRuntimeDir = runtimeDir ??
|
|
617
624
|
Platform.environment['NOMOS_CANDIDATE_RUNTIME'] ??
|
|
618
625
|
Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
619
626
|
Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
|
|
@@ -626,21 +633,25 @@ class NomosTestHarness {
|
|
|
626
633
|
// (default `npx githolon` when the in-repo helper is absent; the test forces this lane by passing
|
|
627
634
|
// e.g. `installHelper: 'node /abs/cli/dist/cli.mjs'`).
|
|
628
635
|
final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
|
|
629
|
-
final resolvedHelper =
|
|
630
|
-
installHelper ??
|
|
636
|
+
final resolvedHelper = installHelper ??
|
|
631
637
|
(File(adjacentInstaller).existsSync()
|
|
632
638
|
? adjacentInstaller
|
|
633
639
|
: '${File(entry).parent.path}/offline-install.ts');
|
|
634
640
|
final useInRepoScript =
|
|
635
641
|
(resolvedHelper.endsWith('.ts') || resolvedHelper.endsWith('.mjs')) &&
|
|
636
|
-
|
|
642
|
+
File(resolvedHelper).existsSync();
|
|
637
643
|
final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
|
|
638
644
|
|
|
639
645
|
final owner = ownerSubject?.trim();
|
|
640
|
-
final birthsApplicationHome =
|
|
641
|
-
owner != null &&
|
|
646
|
+
final birthsApplicationHome = owner != null &&
|
|
642
647
|
owner.isNotEmpty &&
|
|
643
648
|
workspace == _receivingGroupNameOf(owner);
|
|
649
|
+
if (birthsApplicationHome && applicationFrameworkCalls == null) {
|
|
650
|
+
throw NomosTestHarnessException(
|
|
651
|
+
'a generated application scenario must pass its compiler-generated framework calls; '
|
|
652
|
+
'the harness cannot construct domain payloads',
|
|
653
|
+
);
|
|
654
|
+
}
|
|
644
655
|
// A receiving-group address is not an arbitrary law-install target. It is valid only when a parent
|
|
645
656
|
// births it with the attestation, signer seating and warrant ordered into genesis. Generated scenarios
|
|
646
657
|
// therefore install their package on a private throwaway application parent and exercise the same local
|
|
@@ -662,9 +673,8 @@ class NomosTestHarness {
|
|
|
662
673
|
final info = await _phased(
|
|
663
674
|
phase: '1/4: offline law install (${lawUsdzPaths.length} package(s))',
|
|
664
675
|
workspace: installWorkspace,
|
|
665
|
-
awaiting:
|
|
666
|
-
? 'offline-install.ts'
|
|
667
|
-
: 'githolon harness-install',
|
|
676
|
+
awaiting:
|
|
677
|
+
useInRepoScript ? 'offline-install.ts' : 'githolon harness-install',
|
|
668
678
|
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
669
679
|
onDiagnostic: onDiagnostic,
|
|
670
680
|
likelyCauses: const [
|
|
@@ -781,53 +791,151 @@ class NomosTestHarness {
|
|
|
781
791
|
// scenario carries the value from the same package declaration that compiled the
|
|
782
792
|
// receiving-group plan; the harness does not choose or default it. The ordinary
|
|
783
793
|
// seed offer crosses the kernel author gate before the first birth is attempted.
|
|
784
|
-
run: () => plane
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
794
|
+
run: () => plane.session(installWorkspace).offerCallUsdaOutcome(
|
|
795
|
+
call: applicationFrameworkCalls!.seedBirthPolicy(
|
|
796
|
+
domain: '__nomos_application_home_v1',
|
|
797
|
+
workspace: installWorkspace,
|
|
798
|
+
maxBirthsPerPrincipal: maxChildWorkspaces,
|
|
799
|
+
setBy: owner,
|
|
800
|
+
setAt: bornAt,
|
|
801
|
+
),
|
|
802
|
+
actor: owner,
|
|
803
|
+
),
|
|
804
|
+
);
|
|
805
|
+
final keyHash = sha256.convert(base64Decode(devicePublic)).toString();
|
|
806
|
+
await _phased(
|
|
807
|
+
phase: 'application: seat the verified parent signer',
|
|
808
|
+
workspace: installWorkspace,
|
|
809
|
+
awaiting: 'law-declared enrollSigner offer',
|
|
810
|
+
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
811
|
+
onDiagnostic: onDiagnostic,
|
|
812
|
+
run: () => plane.session(installWorkspace).offerCallUsdaOutcome(
|
|
813
|
+
call: applicationFrameworkCalls!.enrollSigner(
|
|
814
|
+
domain: '__nomos_application_home_v1',
|
|
815
|
+
workspace: installWorkspace,
|
|
816
|
+
keyHash: keyHash,
|
|
817
|
+
grantedBy: owner,
|
|
818
|
+
grantedAt: 1,
|
|
819
|
+
verifiedVia: 'nomos:test-mint',
|
|
820
|
+
),
|
|
789
821
|
actor: owner,
|
|
790
|
-
authorSecret: deviceSecret,
|
|
791
|
-
payload: <String, Object?>{
|
|
792
|
-
'maxBirthsPerPrincipal': maxChildWorkspaces,
|
|
793
|
-
'setBy': owner,
|
|
794
|
-
'setAt': bornAt,
|
|
795
|
-
},
|
|
796
822
|
),
|
|
797
823
|
);
|
|
824
|
+
final delegationCert =
|
|
825
|
+
await plane.session(installWorkspace).signBirthCert(
|
|
826
|
+
BirthCertificate(
|
|
827
|
+
child: workspace,
|
|
828
|
+
founder: owner,
|
|
829
|
+
verified: true,
|
|
830
|
+
parentKey: base64Decode(devicePublic),
|
|
831
|
+
parentId: 'root',
|
|
832
|
+
epoch: Int64.ZERO,
|
|
833
|
+
),
|
|
834
|
+
deviceSecret,
|
|
835
|
+
);
|
|
836
|
+
await _phased(
|
|
837
|
+
phase: 'application: record the parent-signed device delegation',
|
|
838
|
+
workspace: installWorkspace,
|
|
839
|
+
awaiting: 'law-declared enrollDelegation offer',
|
|
840
|
+
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
841
|
+
onDiagnostic: onDiagnostic,
|
|
842
|
+
run: () => plane.session(installWorkspace).offerCallUsdaOutcome(
|
|
843
|
+
call: applicationFrameworkCalls!.enrollDelegation(
|
|
844
|
+
domain: '__nomos_application_home_v1',
|
|
845
|
+
workspace: installWorkspace,
|
|
846
|
+
user: owner,
|
|
847
|
+
deviceKey: devicePublic,
|
|
848
|
+
certificate: delegationCert,
|
|
849
|
+
enrolledAt: '1970-01-01T00:00:00.000Z',
|
|
850
|
+
),
|
|
851
|
+
actor: owner,
|
|
852
|
+
),
|
|
853
|
+
);
|
|
854
|
+
final parentCert = await plane.session(installWorkspace).signBirthCert(
|
|
855
|
+
BirthCertificate(
|
|
856
|
+
child: installWorkspace,
|
|
857
|
+
founder: owner,
|
|
858
|
+
verified: true,
|
|
859
|
+
parentKey: base64Decode(devicePublic),
|
|
860
|
+
parentId: 'root',
|
|
861
|
+
epoch: Int64.ZERO,
|
|
862
|
+
),
|
|
863
|
+
deviceSecret,
|
|
864
|
+
);
|
|
865
|
+
await _phased(
|
|
866
|
+
phase: 'application: attest and warrant the application parent',
|
|
867
|
+
workspace: installWorkspace,
|
|
868
|
+
awaiting: 'typed birth attestation and genesis offers',
|
|
869
|
+
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
870
|
+
onDiagnostic: onDiagnostic,
|
|
871
|
+
run: () async {
|
|
872
|
+
await plane.session(installWorkspace).offerCallUsdaOutcome(
|
|
873
|
+
call: applicationFrameworkCalls!.seedBirthAttestation(
|
|
874
|
+
domain: '__nomos_application_home_v1',
|
|
875
|
+
workspace: installWorkspace,
|
|
876
|
+
child: installWorkspace,
|
|
877
|
+
founder: owner,
|
|
878
|
+
verified: 'true',
|
|
879
|
+
parentId: 'root',
|
|
880
|
+
parentKey: devicePublic,
|
|
881
|
+
epoch: 0,
|
|
882
|
+
links: <BirthCertificate>[parentCert],
|
|
883
|
+
),
|
|
884
|
+
actor: owner,
|
|
885
|
+
);
|
|
886
|
+
await plane.session(installWorkspace).offerCallUsdaOutcome(
|
|
887
|
+
call: applicationFrameworkCalls.seedGenesis(
|
|
888
|
+
domain: '__nomos_application_home_v1',
|
|
889
|
+
workspace: installWorkspace,
|
|
890
|
+
principal: owner,
|
|
891
|
+
workspaceId: installWorkspace,
|
|
892
|
+
bornAt: '1970-01-01T00:00:00.000Z',
|
|
893
|
+
),
|
|
894
|
+
actor: owner,
|
|
895
|
+
);
|
|
896
|
+
},
|
|
897
|
+
);
|
|
898
|
+
final homeCert = await plane.session(installWorkspace).signBirthCert(
|
|
899
|
+
BirthCertificate(
|
|
900
|
+
child: workspace,
|
|
901
|
+
founder: owner,
|
|
902
|
+
verified: true,
|
|
903
|
+
parentKey: base64Decode(devicePublic),
|
|
904
|
+
parentId: installWorkspace,
|
|
905
|
+
epoch: Int64.ZERO,
|
|
906
|
+
),
|
|
907
|
+
deviceSecret,
|
|
908
|
+
);
|
|
798
909
|
final outcome = await _phased(
|
|
799
910
|
phase: 'application: birth the signed-in application',
|
|
800
911
|
workspace: workspace,
|
|
801
912
|
awaiting: 'attested receiving-group birth',
|
|
802
913
|
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
803
914
|
onDiagnostic: onDiagnostic,
|
|
804
|
-
run: () => plane
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
915
|
+
run: () => plane.session(installWorkspace).offerCallUsdaOutcome(
|
|
916
|
+
call: applicationFrameworkCalls!.birthReceivingGroup(
|
|
917
|
+
domain: '__nomos_application_home_v1',
|
|
918
|
+
workspace: installWorkspace,
|
|
919
|
+
owner: owner,
|
|
920
|
+
frameworkHash: Sha256Digest(framework),
|
|
921
|
+
lawHash: Sha256Digest(laws.last),
|
|
922
|
+
bornAt: bornAt,
|
|
923
|
+
),
|
|
809
924
|
actor: owner,
|
|
810
925
|
authorSecret: deviceSecret,
|
|
811
|
-
|
|
812
|
-
'owner': owner,
|
|
813
|
-
'frameworkHash': framework,
|
|
814
|
-
// Ordered application deployment is reference packages first, deferred child payload last.
|
|
815
|
-
// The parent serves the references; the receiving group is born with only its payload law.
|
|
816
|
-
'lawHash': laws.last,
|
|
817
|
-
'bornAt': bornAt,
|
|
818
|
-
},
|
|
926
|
+
birthCertificate: homeCert,
|
|
819
927
|
),
|
|
820
928
|
);
|
|
821
929
|
String? bornStatus;
|
|
822
930
|
for (final entry in outcome.birthOutcomes) {
|
|
823
|
-
if (entry
|
|
824
|
-
bornStatus = entry
|
|
931
|
+
if (entry.workspace == workspace) {
|
|
932
|
+
bornStatus = entry.status.name;
|
|
825
933
|
break;
|
|
826
934
|
}
|
|
827
935
|
}
|
|
828
936
|
if (outcome.born.length != 1 ||
|
|
829
937
|
outcome.born.single != workspace ||
|
|
830
|
-
bornStatus != '
|
|
938
|
+
bornStatus != 'BIRTH_OUTCOME_STATUS_BORN') {
|
|
831
939
|
throw NomosTestHarnessException(
|
|
832
940
|
'the offline application parent did not birth "$workspace" cleanly '
|
|
833
941
|
'(born=${outcome.born}, birthOutcomes=${outcome.birthOutcomes})',
|
|
@@ -870,19 +978,16 @@ class NomosTestHarness {
|
|
|
870
978
|
// harness device through the installed framework law with the same verified provenance
|
|
871
979
|
// that the private test root later attests. Production clients never manufacture this
|
|
872
980
|
// marker; it exists only inside the offline test-mint ceremony.
|
|
873
|
-
run: () => plane
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
981
|
+
run: () => plane.session(workspace).offerCallUsdaOutcome(
|
|
982
|
+
call: applicationFrameworkCalls!.enrollSigner(
|
|
983
|
+
domain: '__nomos_application_home_v1',
|
|
984
|
+
workspace: workspace,
|
|
985
|
+
keyHash: keyHash,
|
|
986
|
+
grantedBy: owner,
|
|
987
|
+
grantedAt: 1,
|
|
988
|
+
verifiedVia: 'nomos:test-mint',
|
|
989
|
+
),
|
|
878
990
|
actor: owner,
|
|
879
|
-
authorSecret: deviceSecret,
|
|
880
|
-
payload: <String, Object?>{
|
|
881
|
-
'keyHash': keyHash,
|
|
882
|
-
'grantedBy': owner,
|
|
883
|
-
'grantedAt': 1,
|
|
884
|
-
'verifiedVia': 'nomos:test-mint',
|
|
885
|
-
},
|
|
886
991
|
),
|
|
887
992
|
);
|
|
888
993
|
} catch (error) {
|
|
@@ -890,8 +995,8 @@ class NomosTestHarness {
|
|
|
890
995
|
// signer route. Preserve their old harness behaviour, while keeping
|
|
891
996
|
// every refusal from a current package fatal.
|
|
892
997
|
if (!error.toString().contains(
|
|
893
|
-
|
|
894
|
-
|
|
998
|
+
"no active installed law declares domain '__nomos_application_home_v1'",
|
|
999
|
+
)) {
|
|
895
1000
|
rethrow;
|
|
896
1001
|
}
|
|
897
1002
|
}
|
|
@@ -904,36 +1009,34 @@ class NomosTestHarness {
|
|
|
904
1009
|
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
905
1010
|
onDiagnostic: onDiagnostic,
|
|
906
1011
|
run: () async {
|
|
907
|
-
await plane
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1012
|
+
await plane.session(workspace).offerCallUsdaOutcome(
|
|
1013
|
+
call: applicationFrameworkCalls!.seedReceivingGroupPolicy(
|
|
1014
|
+
domain: '__nomos_application_home_v1',
|
|
1015
|
+
workspace: workspace,
|
|
1016
|
+
setAt: bornAt,
|
|
1017
|
+
),
|
|
912
1018
|
actor: 'host-birth',
|
|
913
|
-
payload: <String, Object?>{'setAt': bornAt},
|
|
914
1019
|
);
|
|
915
|
-
await plane
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1020
|
+
await plane.session(workspace).offerCallUsdaOutcome(
|
|
1021
|
+
call: applicationFrameworkCalls.seedReceivingGroup(
|
|
1022
|
+
domain: '__nomos_application_home_v1',
|
|
1023
|
+
workspace: workspace,
|
|
1024
|
+
principal: owner,
|
|
1025
|
+
workspaceName: _receivingGroupNameOf(owner),
|
|
1026
|
+
bornAt: bornAt,
|
|
1027
|
+
),
|
|
920
1028
|
actor: 'host-birth',
|
|
921
|
-
payload: <String, Object?>{
|
|
922
|
-
'principal': owner,
|
|
923
|
-
'workspaceName': _receivingGroupNameOf(owner),
|
|
924
|
-
'bornAt': bornAt,
|
|
925
|
-
},
|
|
926
1029
|
);
|
|
927
1030
|
},
|
|
928
1031
|
);
|
|
929
1032
|
final parentCert = await plane.signBirthCert(
|
|
930
|
-
|
|
1033
|
+
BirthCertificate(
|
|
931
1034
|
child: workspace,
|
|
932
1035
|
founder: owner,
|
|
933
1036
|
verified: true,
|
|
934
|
-
parentKey: devicePublic,
|
|
1037
|
+
parentKey: base64Decode(devicePublic),
|
|
935
1038
|
parentId: 'root',
|
|
936
|
-
epoch:
|
|
1039
|
+
epoch: Int64.ZERO,
|
|
937
1040
|
),
|
|
938
1041
|
deviceSecret,
|
|
939
1042
|
);
|
|
@@ -944,38 +1047,36 @@ class NomosTestHarness {
|
|
|
944
1047
|
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
945
1048
|
onDiagnostic: onDiagnostic,
|
|
946
1049
|
run: () async {
|
|
947
|
-
await plane
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
await plane
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
},
|
|
978
|
-
);
|
|
1050
|
+
await plane.session(workspace).offerOutcome(
|
|
1051
|
+
domain: '__nomos_application_home_v1',
|
|
1052
|
+
directiveId: 'seedBirthAttestation',
|
|
1053
|
+
actor: owner,
|
|
1054
|
+
authorSecret: deviceSecret,
|
|
1055
|
+
payload: <String, Object?>{
|
|
1056
|
+
'child': workspace,
|
|
1057
|
+
'founder': owner,
|
|
1058
|
+
'verified': 'true',
|
|
1059
|
+
'parentId': 'root',
|
|
1060
|
+
'parentKey': devicePublic,
|
|
1061
|
+
'epoch': 0,
|
|
1062
|
+
'certChain': jsonEncode(<String, Object?>{
|
|
1063
|
+
'links': <Object?>[
|
|
1064
|
+
_birthCertificateLawValue(parentCert),
|
|
1065
|
+
],
|
|
1066
|
+
}),
|
|
1067
|
+
},
|
|
1068
|
+
);
|
|
1069
|
+
await plane.session(workspace).offerOutcome(
|
|
1070
|
+
domain: '__nomos_application_home_v1',
|
|
1071
|
+
directiveId: 'seedGenesis',
|
|
1072
|
+
actor: owner,
|
|
1073
|
+
authorSecret: deviceSecret,
|
|
1074
|
+
payload: <String, Object?>{
|
|
1075
|
+
'principal': owner,
|
|
1076
|
+
'workspaceId': workspace,
|
|
1077
|
+
'bornAt': '2000-01-01T00:00:00.000Z',
|
|
1078
|
+
},
|
|
1079
|
+
);
|
|
979
1080
|
},
|
|
980
1081
|
);
|
|
981
1082
|
} on DomainNotInstalledForSession {
|
|
@@ -993,9 +1094,9 @@ class NomosTestHarness {
|
|
|
993
1094
|
final sessionLaw = installedHashes.isEmpty
|
|
994
1095
|
? 'installed'
|
|
995
1096
|
: (birthsApplicationHome
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1097
|
+
? installedHashes.last
|
|
1098
|
+
: installedHashes.first)
|
|
1099
|
+
.substring(0, 12);
|
|
999
1100
|
phase('ready — session "$workspace" connected (law $sessionLaw)');
|
|
1000
1101
|
// THE DECOY SESSION (multi-workspace by default in tests): sole-session inference is how
|
|
1001
1102
|
// single-workspace assumptions ship — with one session connected, an op that forgot its session
|
|
@@ -1125,24 +1226,21 @@ class NomosTestHarness {
|
|
|
1125
1226
|
);
|
|
1126
1227
|
}
|
|
1127
1228
|
}
|
|
1128
|
-
final entry =
|
|
1129
|
-
nodeEntry ??
|
|
1229
|
+
final entry = nodeEntry ??
|
|
1130
1230
|
await _resolveEntry(node, resolveFrom ?? Directory.current.path);
|
|
1131
|
-
final effectiveRuntimeDir =
|
|
1132
|
-
runtimeDir ??
|
|
1231
|
+
final effectiveRuntimeDir = runtimeDir ??
|
|
1133
1232
|
Platform.environment['NOMOS_CANDIDATE_RUNTIME'] ??
|
|
1134
1233
|
Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
1135
1234
|
Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
|
|
1136
1235
|
await _sourceCandidateRuntime(entry);
|
|
1137
1236
|
final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
|
|
1138
|
-
final resolvedHelper =
|
|
1139
|
-
installHelper ??
|
|
1237
|
+
final resolvedHelper = installHelper ??
|
|
1140
1238
|
(File(adjacentInstaller).existsSync()
|
|
1141
1239
|
? adjacentInstaller
|
|
1142
1240
|
: '${File(entry).parent.path}/offline-install.ts');
|
|
1143
1241
|
final useInRepoScript =
|
|
1144
1242
|
(resolvedHelper.endsWith('.ts') || resolvedHelper.endsWith('.mjs')) &&
|
|
1145
|
-
|
|
1243
|
+
File(resolvedHelper).existsSync();
|
|
1146
1244
|
final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
|
|
1147
1245
|
|
|
1148
1246
|
final workdir = await Directory.systemTemp.createTemp('nomos-cluster-');
|
|
@@ -1402,12 +1500,10 @@ class NomosTestHarness {
|
|
|
1402
1500
|
'no lawUsdzPaths — pass the build/<name>.law.usdz `npx githolon compile` emits',
|
|
1403
1501
|
);
|
|
1404
1502
|
}
|
|
1405
|
-
final entry =
|
|
1406
|
-
nodeEntry ??
|
|
1503
|
+
final entry = nodeEntry ??
|
|
1407
1504
|
await _resolveEntry(node, resolveFrom ?? Directory.current.path);
|
|
1408
|
-
final bareUid =
|
|
1409
|
-
? subject.substring(5)
|
|
1410
|
-
: subject;
|
|
1505
|
+
final bareUid =
|
|
1506
|
+
subject.startsWith('user:') ? subject.substring(5) : subject;
|
|
1411
1507
|
final http = HttpClient();
|
|
1412
1508
|
Future<Map<String, Object?>> post(
|
|
1413
1509
|
String path,
|
|
@@ -1481,8 +1577,8 @@ class NomosTestHarness {
|
|
|
1481
1577
|
}
|
|
1482
1578
|
rethrow;
|
|
1483
1579
|
}
|
|
1484
|
-
frameworkHash =
|
|
1485
|
-
?.toString();
|
|
1580
|
+
frameworkHash =
|
|
1581
|
+
((created['controller'] as Map?)?['domainHash'])?.toString();
|
|
1486
1582
|
final existing = await get(
|
|
1487
1583
|
'/v2/workspaces/$platformWorkspace/domains',
|
|
1488
1584
|
);
|
|
@@ -1571,7 +1667,7 @@ class NomosTestHarness {
|
|
|
1571
1667
|
);
|
|
1572
1668
|
final currentLaw =
|
|
1573
1669
|
(parentDomains['currentLaw'] as Map?)?.cast<String, Object?>() ??
|
|
1574
|
-
|
|
1670
|
+
const <String, Object?>{};
|
|
1575
1671
|
final controlHomeLawHash =
|
|
1576
1672
|
currentLaw[applicationHomeDomain]?.toString() ?? '';
|
|
1577
1673
|
if (controlHomeLawHash.isEmpty) {
|
|
@@ -1609,8 +1705,7 @@ class NomosTestHarness {
|
|
|
1609
1705
|
// restart while offline must not fetch any member before it can replay the persisted ledger. Release CI
|
|
1610
1706
|
// supplies the exact candidate bundle. An ordinary local test pins all three artifacts advertised by its
|
|
1611
1707
|
// ephemeral cloud while still online; never combine a candidate kernel with stale framework law.
|
|
1612
|
-
final candidateRuntimeDir =
|
|
1613
|
-
Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
1708
|
+
final candidateRuntimeDir = Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
1614
1709
|
Platform.environment['NOMOS_OFFLINE_RUNTIME'];
|
|
1615
1710
|
final localRuntime = await Directory(
|
|
1616
1711
|
'${workdir.path}/client-runtime',
|
|
@@ -1630,8 +1725,8 @@ class NomosTestHarness {
|
|
|
1630
1725
|
_KernelRuntimeArtifact.controllerLaw:
|
|
1631
1726
|
'$candidateRuntimeDir/${_KernelRuntimeArtifact.controllerLaw.fileName}',
|
|
1632
1727
|
};
|
|
1633
|
-
final hasCandidateBundle =
|
|
1634
|
-
|
|
1728
|
+
final hasCandidateBundle = candidateArtifacts.length ==
|
|
1729
|
+
_KernelRuntimeArtifact.values.length &&
|
|
1635
1730
|
candidateArtifacts.values.every((path) => File(path).existsSync());
|
|
1636
1731
|
if (hasCandidateBundle) {
|
|
1637
1732
|
for (final artifact in candidateArtifacts.entries) {
|
|
@@ -1785,17 +1880,17 @@ class NomosTestHarness {
|
|
|
1785
1880
|
'validFromEpoch': 0,
|
|
1786
1881
|
'grantedAt': DateTime.now().toUtc().toIso8601String(),
|
|
1787
1882
|
'idpToken': idpToken,
|
|
1788
|
-
|
|
1883
|
+
// The framework law owns this USDA object shape. Keep the
|
|
1884
|
+
// compiler-generated protobuf certificate structured while
|
|
1885
|
+
// lowering it into that domain value; never resurrect the
|
|
1886
|
+
// retired JSON string envelope (`delegationCert`).
|
|
1887
|
+
'delegationCertificate':
|
|
1888
|
+
_birthCertificateLawValue(delegationCert),
|
|
1789
1889
|
},
|
|
1790
1890
|
);
|
|
1791
|
-
|
|
1891
|
+
await plane.sync(
|
|
1792
1892
|
session: platformAuthorSession,
|
|
1793
1893
|
);
|
|
1794
|
-
if (enrollmentSync['ok'] == false) {
|
|
1795
|
-
throw NomosTestHarnessException(
|
|
1796
|
-
'cloudBound: sealed enrollFirstDevice offer did not converge: $enrollmentSync',
|
|
1797
|
-
);
|
|
1798
|
-
}
|
|
1799
1894
|
return (
|
|
1800
1895
|
publicB64: publicB64,
|
|
1801
1896
|
secret: secret,
|
|
@@ -1821,36 +1916,34 @@ class NomosTestHarness {
|
|
|
1821
1916
|
// authenticated platform holon mounted. Bind this parent certificate operation to the
|
|
1822
1917
|
// platform session explicitly; relying on the single-session convenience API becomes
|
|
1823
1918
|
// ambiguous as soon as the ceremony correctly opens the target workspace in-kernel.
|
|
1824
|
-
final homeCert =
|
|
1825
|
-
.session('$clientId-platform-author')
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
birthCertificate: homeCert,
|
|
1853
|
-
);
|
|
1919
|
+
final homeCert =
|
|
1920
|
+
await plane.session('$clientId-platform-author').signBirthCert(
|
|
1921
|
+
BirthCertificate(
|
|
1922
|
+
child: homeName,
|
|
1923
|
+
founder: subject,
|
|
1924
|
+
// The cert's `verified` must MATCH the gate verdict (CAS-as-law): the offer carries
|
|
1925
|
+
// the dev-IdP token, so the gate resolves a VERIFIED founder.
|
|
1926
|
+
verified: true,
|
|
1927
|
+
parentKey: base64Decode(device.publicB64),
|
|
1928
|
+
parentId: platformWorkspace,
|
|
1929
|
+
epoch: Int64.ZERO,
|
|
1930
|
+
),
|
|
1931
|
+
device.secret,
|
|
1932
|
+
);
|
|
1933
|
+
final localBirth =
|
|
1934
|
+
await plane.session('$clientId-platform-author').offerOutcome(
|
|
1935
|
+
domain: applicationHomeDomain,
|
|
1936
|
+
directiveId: 'nomosInternalBirthReceivingGroup7c1f5e',
|
|
1937
|
+
actor: subject,
|
|
1938
|
+
authorSecret: device.secret,
|
|
1939
|
+
payload: <String, Object?>{
|
|
1940
|
+
'owner': subject,
|
|
1941
|
+
'frameworkHash': frameworkHash,
|
|
1942
|
+
'lawHash': applicationHomeLawHash,
|
|
1943
|
+
'bornAt': DateTime.now().toUtc().toIso8601String(),
|
|
1944
|
+
},
|
|
1945
|
+
birthCertificate: homeCert,
|
|
1946
|
+
);
|
|
1854
1947
|
if (!localBirth.born.contains(homeName)) {
|
|
1855
1948
|
throw NomosTestHarnessException(
|
|
1856
1949
|
'cloudBound: the local parent kernel admitted the receiving-group offer but did not birth '
|
|
@@ -1860,27 +1953,24 @@ class NomosTestHarness {
|
|
|
1860
1953
|
final birthSync = await plane.sync(
|
|
1861
1954
|
session: '$clientId-platform-author',
|
|
1862
1955
|
);
|
|
1863
|
-
if (birthSync
|
|
1864
|
-
birthSync
|
|
1865
|
-
birthSync
|
|
1956
|
+
if (birthSync.pushFailed ||
|
|
1957
|
+
birthSync.diverged ||
|
|
1958
|
+
birthSync.stillAhead) {
|
|
1866
1959
|
throw NomosTestHarnessException(
|
|
1867
1960
|
'cloudBound: the sealed receiving-group offer did not converge: $birthSync',
|
|
1868
1961
|
);
|
|
1869
1962
|
}
|
|
1870
|
-
final remoteOutcomes =
|
|
1871
|
-
((birthSync['birthOutcomes'] as List?) ?? const [])
|
|
1872
|
-
.whereType<Map>()
|
|
1873
|
-
.map((entry) => entry.cast<String, Object?>())
|
|
1874
|
-
.toList();
|
|
1963
|
+
final remoteOutcomes = birthSync.birthOutcomes;
|
|
1875
1964
|
final failedRemoteBirth = remoteOutcomes.where(
|
|
1876
1965
|
(entry) =>
|
|
1877
|
-
entry
|
|
1878
|
-
entry
|
|
1966
|
+
entry.hasWorkspace() &&
|
|
1967
|
+
entry.workspace == homeName &&
|
|
1968
|
+
entry.status != BirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN,
|
|
1879
1969
|
);
|
|
1880
1970
|
if (failedRemoteBirth.isNotEmpty) {
|
|
1881
1971
|
throw NomosTestHarnessException(
|
|
1882
1972
|
'cloudBound: the remote parent replay refused the receiving-group birth: '
|
|
1883
|
-
'${
|
|
1973
|
+
'${failedRemoteBirth.toList()}',
|
|
1884
1974
|
);
|
|
1885
1975
|
}
|
|
1886
1976
|
home = homeName;
|
|
@@ -1977,7 +2067,7 @@ class NomosTestHarness {
|
|
|
1977
2067
|
/// each locally-born child's remote custody BEFORE that child's own sync converges against it. Repeats
|
|
1978
2068
|
/// until every session reports neither `pushFailed` nor `stillAhead`, or [timeout] (then throws with the
|
|
1979
2069
|
/// per-workspace last verdicts). Returns the final sync result per workspace.
|
|
1980
|
-
Future<Map<String,
|
|
2070
|
+
Future<Map<String, SyncWorkspaceResponse>> converge({
|
|
1981
2071
|
Duration timeout = const Duration(seconds: 120),
|
|
1982
2072
|
List<String>? workspaces,
|
|
1983
2073
|
}) async {
|
|
@@ -1988,37 +2078,35 @@ class NomosTestHarness {
|
|
|
1988
2078
|
final hw = homeWorkspace;
|
|
1989
2079
|
if (hw != null && live.remove(hw)) live.insert(0, hw); // the parent leads
|
|
1990
2080
|
final targets = workspaces ?? live;
|
|
1991
|
-
final last = <String,
|
|
2081
|
+
final last = <String, SyncWorkspaceResponse>{};
|
|
2082
|
+
final lastErrors = <String, String>{};
|
|
1992
2083
|
final pending = List.of(targets);
|
|
1993
2084
|
final deadline = DateTime.now().add(timeout);
|
|
1994
2085
|
while (pending.isNotEmpty) {
|
|
1995
2086
|
for (final ws in List.of(pending)) {
|
|
1996
2087
|
try {
|
|
1997
|
-
final r =
|
|
1998
|
-
.cast<String, Object?>();
|
|
2088
|
+
final r = await bridge.session(ws).sync(admit: true);
|
|
1999
2089
|
last[ws] = r;
|
|
2090
|
+
lastErrors.remove(ws);
|
|
2000
2091
|
final permanentFailure = nmsPermanentConvergenceFailure(r);
|
|
2001
2092
|
if (permanentFailure != null) {
|
|
2002
2093
|
throw NomosTestHarnessException(
|
|
2003
2094
|
'converge: $ws rejected or dead-lettered business work — $permanentFailure',
|
|
2004
2095
|
);
|
|
2005
2096
|
}
|
|
2006
|
-
if (r
|
|
2007
|
-
r['stillAhead'] != true &&
|
|
2008
|
-
r['offline'] != true &&
|
|
2009
|
-
r['deferred'] != true)
|
|
2097
|
+
if (!r.pushFailed && !r.stillAhead && !r.offline && r.deferred == 0)
|
|
2010
2098
|
pending.remove(ws);
|
|
2011
2099
|
} on NomosTestHarnessException {
|
|
2012
2100
|
rethrow;
|
|
2013
2101
|
} catch (e) {
|
|
2014
|
-
|
|
2102
|
+
lastErrors[ws] = e.toString();
|
|
2015
2103
|
}
|
|
2016
2104
|
}
|
|
2017
2105
|
if (pending.isEmpty) break;
|
|
2018
2106
|
if (DateTime.now().isAfter(deadline)) {
|
|
2019
2107
|
throw NomosTestHarnessException(
|
|
2020
2108
|
'converge: ${pending.length} workspace(s) did not converge within ${timeout.inSeconds}s — '
|
|
2021
|
-
'${pending.map((w) => '$w: ${
|
|
2109
|
+
'${pending.map((w) => '$w: ${lastErrors[w] ?? last[w]?.toString() ?? "no result"}').join('; ')}',
|
|
2022
2110
|
);
|
|
2023
2111
|
}
|
|
2024
2112
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
|
@@ -2418,14 +2506,14 @@ class NomosTestHarness {
|
|
|
2418
2506
|
// transport first makes its incoming stream wait on consumers that can no longer receive their
|
|
2419
2507
|
// unwatch/done messages—a deterministic teardown deadlock in reactive Flutter tests.
|
|
2420
2508
|
await _plane.dispose().timeout(
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2509
|
+
_phaseTimeout ?? _kOtherDeadline,
|
|
2510
|
+
onTimeout: () => throw HarnessPhaseTimeout(
|
|
2511
|
+
phase: 'dispose: close bridge watches',
|
|
2512
|
+
workspace: workspace,
|
|
2513
|
+
elapsed: _phaseTimeout ?? _kOtherDeadline,
|
|
2514
|
+
awaiting: 'NomosBridge.dispose',
|
|
2515
|
+
),
|
|
2516
|
+
);
|
|
2429
2517
|
} catch (e) {
|
|
2430
2518
|
_diag(
|
|
2431
2519
|
'[nomos-test-harness] bridge dispose error (continuing to runner cleanup): $e',
|
|
@@ -2433,14 +2521,14 @@ class NomosTestHarness {
|
|
|
2433
2521
|
}
|
|
2434
2522
|
try {
|
|
2435
2523
|
await _transport.dispose().timeout(
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2524
|
+
_phaseTimeout ?? _kOtherDeadline,
|
|
2525
|
+
onTimeout: () => throw HarnessPhaseTimeout(
|
|
2526
|
+
phase: 'dispose: stop the node runner',
|
|
2527
|
+
workspace: workspace,
|
|
2528
|
+
elapsed: _phaseTimeout ?? _kOtherDeadline,
|
|
2529
|
+
awaiting: 'NodeRunnerTransport.dispose',
|
|
2530
|
+
),
|
|
2531
|
+
);
|
|
2444
2532
|
} catch (e) {
|
|
2445
2533
|
_diag(
|
|
2446
2534
|
'[nomos-test-harness] transport dispose error (continuing to temp-dir cleanup): $e',
|
|
@@ -2574,10 +2662,13 @@ class NomosTestHarness {
|
|
|
2574
2662
|
'@githolon/client/runner/node-entry.mjs',
|
|
2575
2663
|
'@githolon/client/runner/node-entry.ts',
|
|
2576
2664
|
]) {
|
|
2577
|
-
final r = await Process.run(
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2665
|
+
final r = await Process.run(
|
|
2666
|
+
node,
|
|
2667
|
+
[
|
|
2668
|
+
'-p',
|
|
2669
|
+
"require.resolve('$entry')",
|
|
2670
|
+
],
|
|
2671
|
+
workingDirectory: from);
|
|
2581
2672
|
final out = (r.stdout as String).trim();
|
|
2582
2673
|
if (r.exitCode == 0 && out.isNotEmpty) return out;
|
|
2583
2674
|
}
|
|
@@ -2610,21 +2701,20 @@ class WorkspaceSnapshot {
|
|
|
2610
2701
|
|
|
2611
2702
|
/// JSON round-trip so a staged snapshot persists to disk verbatim (the bytes + provenance).
|
|
2612
2703
|
Map<String, Object?> toJson() => {
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2704
|
+
'workspace': workspace,
|
|
2705
|
+
'snapshotB64': snapshotB64,
|
|
2706
|
+
'head': head,
|
|
2707
|
+
'lawHashes': lawHashes,
|
|
2708
|
+
'capturedAt': capturedAt.toUtc().toIso8601String(),
|
|
2709
|
+
};
|
|
2619
2710
|
|
|
2620
2711
|
factory WorkspaceSnapshot.fromJson(Map<String, Object?> j) =>
|
|
2621
2712
|
WorkspaceSnapshot(
|
|
2622
2713
|
workspace: j['workspace'] as String,
|
|
2623
2714
|
snapshotB64: j['snapshotB64'] as String,
|
|
2624
2715
|
head: (j['head'] ?? '') as String,
|
|
2625
|
-
lawHashes:
|
|
2626
|
-
.map((e) => '$e')
|
|
2627
|
-
.toList(),
|
|
2716
|
+
lawHashes:
|
|
2717
|
+
((j['lawHashes'] as List?) ?? const []).map((e) => '$e').toList(),
|
|
2628
2718
|
capturedAt: DateTime.parse(
|
|
2629
2719
|
(j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String())
|
|
2630
2720
|
as String,
|
|
@@ -2646,19 +2736,20 @@ class PlaneSnapshot {
|
|
|
2646
2736
|
const PlaneSnapshot({required this.workspaces, required this.capturedAt});
|
|
2647
2737
|
|
|
2648
2738
|
Map<String, Object?> toJson() => {
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2739
|
+
'workspaces': workspaces.map((k, v) => MapEntry(k, v.toJson())),
|
|
2740
|
+
'capturedAt': capturedAt.toUtc().toIso8601String(),
|
|
2741
|
+
};
|
|
2652
2742
|
|
|
2653
2743
|
factory PlaneSnapshot.fromJson(Map<String, Object?> j) => PlaneSnapshot(
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2744
|
+
workspaces: ((j['workspaces'] as Map?) ?? const {}).map(
|
|
2745
|
+
(k, v) => MapEntry(
|
|
2746
|
+
'$k',
|
|
2747
|
+
WorkspaceSnapshot.fromJson((v as Map).cast<String, Object?>()),
|
|
2748
|
+
),
|
|
2749
|
+
),
|
|
2750
|
+
capturedAt: DateTime.parse(
|
|
2751
|
+
(j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String())
|
|
2752
|
+
as String,
|
|
2753
|
+
),
|
|
2754
|
+
);
|
|
2664
2755
|
}
|