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