diff --git a/CHANGELOG.md b/CHANGELOG.md index 298f3647..ccf0af35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -265,6 +265,13 @@ ### Fixed +- `dispatch_intent` no longer stages a witnessed suffix import against the + kernel's default worldline (#892). It ignored the request's receiving + worldline and recorded `Staged` without the bundle and basis checks the + library performs. Until comparable-basis transport exists, import intents + are refused with `NOT_SUPPORTED` and nothing is admitted, staged or + attributed. + - A tick that decides a submission no rule matched now commits and recovers under the WAL (#873). The WAL tick decision gains `NoMatchingRule` (code 4), recovery maps it to `RecoveredSubmissionPosture::DecidedNoMatchingRule`, and diff --git a/crates/warp-wasm/src/warp_kernel.rs b/crates/warp-wasm/src/warp_kernel.rs index 1c607657..78a17dec 100644 --- a/crates/warp-wasm/src/warp_kernel.rs +++ b/crates/warp-wasm/src/warp_kernel.rs @@ -987,6 +987,16 @@ impl KernelPort for WarpKernel { code: error_codes::INVALID_INTENT, message: "invalid import suffix intent envelope".into(), })?; + // Import must be judged at the request's receiving worldline and a + // comparable basis that transport establishes. Until that exists + // (flyingrobots/continuum#80), refuse instead of staging the + // import against this kernel's default worldline. + return Err(AbiError { + code: error_codes::NOT_SUPPORTED, + message: "witnessed suffix import is not supported until comparable-basis \ + transport exists; nothing was admitted" + .into(), + }); } let envelope = IngressEnvelope::local_intent( @@ -1170,13 +1180,12 @@ impl TrustedKernelControlPort for WarpKernel { mod tests { use super::*; use echo_wasm_abi::{ - decode_cbor, kernel_port::{ BuiltinObserverPlan as AbiBuiltinObserverPlan, CausalSuffixBundle as AbiCausalSuffixBundle, ControlIntentV1, GlobalTick as AbiGlobalTick, HeadEligibility as AbiHeadEligibility, HeadId as AbiHeadId, ImportSuffixRequest as AbiImportSuffixRequest, - ImportSuffixResult as AbiImportSuffixResult, ObservationAt as AbiObservationAt, + ObservationAt as AbiObservationAt, ObservationBasisPosture as AbiObservationBasisPosture, ObservationCoordinate as AbiObservationCoordinate, ObservationFrame as AbiObservationFrame, ObservationPayload as AbiObservationPayload, @@ -1191,7 +1200,6 @@ mod tests { SettlementOverlapRevalidation as AbiSettlementOverlapRevalidation, SettlementParentRevalidation as AbiSettlementParentRevalidation, SettlementRequest as AbiSettlementRequest, - WitnessedSuffixAdmissionOutcome as AbiWitnessedSuffixAdmissionOutcome, WitnessedSuffixShell as AbiWitnessedSuffixShell, WorkState, WorldlineId as AbiWorldlineId, WorldlineTick as AbiWorldlineTick, WriterHeadKey as AbiWriterHeadKey, @@ -1937,82 +1945,49 @@ mod tests { } #[test] - fn import_suffix_intent_enters_ingress_and_scheduler() { + fn import_suffix_dispatch_fails_closed_until_transport_exists() { let mut kernel = WarpKernel::new().unwrap(); let request = sample_import_suffix_request(&kernel); let intent = pack_import_suffix_intent_v1(&request).unwrap(); + let head_before = kernel.current_head().unwrap(); - let dispatch = kernel.dispatch_intent(&intent).unwrap(); - assert!(dispatch.accepted); - assert_eq!(dispatch.intent_id.len(), 32); + let refusal = kernel.dispatch_intent(&intent).unwrap_err(); + assert_eq!(refusal.code, error_codes::NOT_SUPPORTED); - let response = start_until_idle(&mut kernel, Some(1)); - let head = kernel.current_head().unwrap(); + // Nothing was admitted: no ingress to run, no tick, no staged result. + start_until_idle(&mut kernel, Some(1)); assert_eq!( - response.scheduler_status.last_run_completion, - Some(RunCompletion::Quiesced) + kernel.current_head().unwrap().worldline_tick, + head_before.worldline_tick + ); + let event_id = NodeId( + IngressEnvelope::local_intent( + IngressTarget::DefaultWriter { + worldline_id: kernel.default_worldline, + }, + make_intent_kind("echo.intent/eint-v1"), + intent.clone(), + ) + .ingress_id(), ); - assert_eq!(head.worldline_tick, AbiWorldlineTick(1)); - - let event_id = NodeId(dispatch.intent_id.as_slice().try_into().unwrap()); - let result_id = warp_core::import_suffix_result_node_id(&event_id); let frontier = kernel .runtime .worldlines() .get(&kernel.default_worldline) .unwrap(); - let frontier_state = frontier.state(); - let root_warp = frontier_state.root().warp_id; - let store = frontier_state.store(&root_warp).unwrap(); - let result_node = store.node(&result_id); - assert!(result_node.is_some()); - - let result_attachment = store - .node_attachment(&result_id) - .expect("import suffix result attachment should be recorded"); - let warp_core::AttachmentValue::Atom(atom) = result_attachment else { - panic!("import suffix result must be a typed atom"); - }; - assert_eq!( - atom.type_id, - make_type_id(warp_core::IMPORT_SUFFIX_RESULT_ATTACHMENT_TYPE) - ); - - let result: AbiImportSuffixResult = decode_cbor(atom.bytes.as_ref()).unwrap(); - assert_eq!(result.bundle_digest, request.bundle.bundle_digest); - assert_eq!( - result.admission.source_shell_digest, - request.bundle.source_suffix.witness_digest - ); - assert_eq!(result.admission.target_basis, request.target_basis); - match result.admission.outcome { - AbiWitnessedSuffixAdmissionOutcome::Staged { staged_refs, .. } => { - assert_eq!(staged_refs, request.bundle.source_suffix.source_entries); - } - other => panic!("expected staged import result, got {other:?}"), - } - - let entry = kernel - .provenance - .entry(kernel.default_worldline, WorldlineTick::ZERO) + let store = frontier + .state() + .store(&frontier.state().root().warp_id) .unwrap(); - let patch = entry.patch.expect("import tick should record a patch"); - assert!(patch.ops.iter().any(|op| { - matches!( - op, - WarpOp::UpsertNode { node, .. } if node.local_id == result_id - ) - })); - assert!(patch.ops.iter().any(|op| { - matches!( - op, - WarpOp::SetAttachment { key, .. } - if *key == AttachmentKey::node_alpha(NodeKey { - warp_id: root_warp, - local_id: result_id, - }) - ) - })); + assert!(store + .node(&warp_core::import_suffix_result_node_id(&event_id)) + .is_none()); + assert!(store.node(&event_id).is_none()); + + // A malformed import envelope is still reported as malformed. + let mut malformed = intent; + malformed.truncate(malformed.len() - 1); + assert!(kernel.dispatch_intent(&malformed).is_err()); } #[test] diff --git a/docs/architecture/continuum-transport.md b/docs/architecture/continuum-transport.md index 35f519b6..43b4249e 100644 --- a/docs/architecture/continuum-transport.md +++ b/docs/architecture/continuum-transport.md @@ -83,6 +83,14 @@ transport adapter receives bytes -> tick + receipt / witness ``` +**Current status (#892).** This flow is not wired. Until comparable-basis +transport is specified ([continuum#80](https://github.com/flyingrobots/continuum/issues/80)) +and the import is dispatched to its explicitly named receiving worldline, +`dispatch_intent` refuses import intents with `NOT_SUPPORTED` and admits, +stages and attributes nothing. It never substitutes the kernel's default +worldline. The library evaluator (`import_suffix`) is unaffected and is not +reachable from dispatch. + The runtime must: 1. Verify the source shell and bundle identities.