From 98d37867ebad91a1a0c83f6ff0724c8d2a293b54 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 11:23:40 +0000 Subject: [PATCH 01/15] test(tinysecurity-bus): cover abandoned execution outcome wire round-trip Extend the wire stability test to assert that the abandoned execution outcome deserializes from its wire name and serializes back to the same string. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/tinysecurity-bus/tests/wire.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/tinysecurity-bus/tests/wire.rs b/crates/tinysecurity-bus/tests/wire.rs index 4f74996..e049746 100644 --- a/crates/tinysecurity-bus/tests/wire.rs +++ b/crates/tinysecurity-bus/tests/wire.rs @@ -114,6 +114,8 @@ fn all_execution_outcome_variants_have_stable_wire_names() assert_eq!(serde_json::to_value(decoded)?, *v); } assert!(serde_json::from_str::("\"future_unknown\"").is_err()); + let abandoned: ExecutionOutcome = serde_json::from_str("\"abandoned\"")?; + assert_eq!(serde_json::to_string(&abandoned)?, "\"abandoned\""); Ok(()) } #[test] From 6d5113fd365dca8b5c03eaae7c2429decee0062f Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 11:24:14 +0000 Subject: [PATCH 02/15] feat(bus): add abandoned execution outcome Add an Abandoned variant to ExecutionOutcome for runs that were abandoned before their effects could be determined, and map the legacy "aborted" value to it instead of Cancelled. The contract version is bumped to 1.28 and the v1 fixture updated to match the new vocabulary. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/tinysecurity-bus/src/approval.rs | 2 +- crates/tinysecurity-bus/src/approval_tests.rs | 2 +- crates/tinysecurity-bus/src/device_frame_tests.rs | 2 +- crates/tinysecurity-bus/src/device_pairing_tests.rs | 2 +- crates/tinysecurity-bus/src/future.rs | 4 +++- crates/tinysecurity-bus/src/lib.rs | 2 +- crates/tinysecurity-bus/tests/fixtures/v1.json | 3 ++- 7 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/tinysecurity-bus/src/approval.rs b/crates/tinysecurity-bus/src/approval.rs index d75e7c8..79e24c3 100644 --- a/crates/tinysecurity-bus/src/approval.rs +++ b/crates/tinysecurity-bus/src/approval.rs @@ -300,7 +300,7 @@ impl ExecutionOutcome { match value { "success" => Ok(Self::Succeeded), "failure" => Ok(Self::Failed), - "aborted" => Ok(Self::Cancelled), + "aborted" => Ok(Self::Abandoned), _ => Err(crate::Error::InvalidArguments), } } diff --git a/crates/tinysecurity-bus/src/approval_tests.rs b/crates/tinysecurity-bus/src/approval_tests.rs index f4256b0..c230797 100644 --- a/crates/tinysecurity-bus/src/approval_tests.rs +++ b/crates/tinysecurity-bus/src/approval_tests.rs @@ -11,7 +11,7 @@ fn legacy_execution_outcomes_are_explicit_and_unknown_values_reject() { ); assert_eq!( ExecutionOutcome::from_legacy("aborted"), - Ok(ExecutionOutcome::Cancelled) + Ok(ExecutionOutcome::Abandoned) ); assert!(ExecutionOutcome::from_legacy("approved").is_err()); } diff --git a/crates/tinysecurity-bus/src/device_frame_tests.rs b/crates/tinysecurity-bus/src/device_frame_tests.rs index c3ed01f..1d5dedb 100644 --- a/crates/tinysecurity-bus/src/device_frame_tests.rs +++ b/crates/tinysecurity-bus/src/device_frame_tests.rs @@ -70,5 +70,5 @@ fn frame_operations_redact_both_payloads_without_changing_open_wire_shape() #[test] fn frame_member_name_is_stable_and_contract_version_is_bumped() { assert_eq!(crate::names::methods::DEVICE_FRAME2, "DeviceFrame2"); - assert_eq!(crate::CONTRACT_VERSION, (1, 27)); + assert_eq!(crate::CONTRACT_VERSION, (1, 28)); } diff --git a/crates/tinysecurity-bus/src/device_pairing_tests.rs b/crates/tinysecurity-bus/src/device_pairing_tests.rs index bde96c1..c417a30 100644 --- a/crates/tinysecurity-bus/src/device_pairing_tests.rs +++ b/crates/tinysecurity-bus/src/device_pairing_tests.rs @@ -85,7 +85,7 @@ fn refresh_and_close_are_distinct_transient_session_operations() { #[test] fn pairing_contract_is_versioned_after_the_frame_contract() { - assert_eq!(CONTRACT_VERSION, (1, 27)); + assert_eq!(CONTRACT_VERSION, (1, 28)); } #[test] diff --git a/crates/tinysecurity-bus/src/future.rs b/crates/tinysecurity-bus/src/future.rs index 6de58fa..b42dda0 100644 --- a/crates/tinysecurity-bus/src/future.rs +++ b/crates/tinysecurity-bus/src/future.rs @@ -20,7 +20,7 @@ pub enum ApprovalState { /// Executed state. Executed, } -/// `ExecutionOutcome` vocabulary, reserved until its engine is implemented. +/// Terminal execution result, including runs abandoned before their effects were known. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ExecutionOutcome { @@ -30,6 +30,8 @@ pub enum ExecutionOutcome { Failed, /// Cancelled state. Cancelled, + /// The in-flight execution was abandoned before its effects could be determined. + Abandoned, } /// `ReplyVerdict` vocabulary, reserved until its engine is implemented. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/tinysecurity-bus/src/lib.rs b/crates/tinysecurity-bus/src/lib.rs index 0933b5e..0c13ca7 100644 --- a/crates/tinysecurity-bus/src/lib.rs +++ b/crates/tinysecurity-bus/src/lib.rs @@ -65,7 +65,7 @@ pub use scoped_policy::*; pub use tool_trust::*; pub mod names; /// Current wire vocabulary version, independent of the artifact version. -pub const CONTRACT_VERSION: (u32, u32) = (1, 27); +pub const CONTRACT_VERSION: (u32, u32) = (1, 28); /// Whether a remote module's advertised version supplies this host's vocabulary. /// /// `version` is the provider version received by the host, not a host requirement diff --git a/crates/tinysecurity-bus/tests/fixtures/v1.json b/crates/tinysecurity-bus/tests/fixtures/v1.json index 17b1df3..159334a 100644 --- a/crates/tinysecurity-bus/tests/fixtures/v1.json +++ b/crates/tinysecurity-bus/tests/fixtures/v1.json @@ -590,7 +590,8 @@ "ExecutionOutcome": [ "succeeded", "failed", - "cancelled" + "cancelled", + "abandoned" ], "ReplyVerdict": [ "allow_once", From e7c851b6805fb9ce091eb2e00777bf64eca06885 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 11:24:31 +0000 Subject: [PATCH 03/15] test(approval): cover abandoned execution as immutable unknown effect Add a test asserting that recording an abandoned execution persists it as a terminal executed state with an unknown effect, that replaying the same request is idempotent, and that a later conflicting outcome is rejected. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../tinysecurity-approval/src/engine_tests.rs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/tinysecurity-approval/src/engine_tests.rs b/crates/tinysecurity-approval/src/engine_tests.rs index e5a0009..be3bc60 100644 --- a/crates/tinysecurity-approval/src/engine_tests.rs +++ b/crates/tinysecurity-approval/src/engine_tests.rs @@ -429,6 +429,32 @@ fn action(row: &ApprovalRow2, phase: u64) -> ApprovalActionRequest2 { } } #[tokio::test] +async fn abandoned_execution_is_persisted_as_an_immutable_unknown_effect() -> Result<()> { + let f = Arc::new(Fixture::default()); + let owner = engine(f, Arc::new(TestClock::default()))?; + let row = owner.begin(request()).await?; + owner.decide(decision(&row)).await?; + owner.consume(action(&row, 3)).await?; + + let terminal = RecordExecutionRequest2 { + action: action(&row, 4), + outcome: ExecutionOutcome::Abandoned, + }; + let finished = owner.record_execution(terminal.clone()).await?; + assert_eq!(finished.state, ApprovalLifecycle2::Executed); + assert_eq!(finished.outcome, Some(ExecutionOutcome::Abandoned)); + assert_eq!(owner.list(&request().scope_id).await?, vec![finished.clone()]); + assert_eq!(owner.record_execution(terminal).await?, finished); + + let mut cancelled = RecordExecutionRequest2 { + action: action(&row, 4), + outcome: ExecutionOutcome::Cancelled, + }; + cancelled.action.audit = audit(5); + assert!(owner.record_execution(cancelled).await.is_err()); + Ok(()) +} +#[tokio::test] async fn exact_human_ticket_first_winner_once_consumption_and_terminal_outcome() -> Result<()> { let f = Arc::new(Fixture::default()); let owner = engine(f, Arc::new(TestClock::default()))?; From b05b8da0c020d02c8aa7a23dc390c11d4530f1e4 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 11:24:52 +0000 Subject: [PATCH 04/15] test(bus): bump expected contract version to 1.28 Update the wire and scoped wire tests to expect contract version 1.28 instead of 1.27, and adjust the compatibility comment accordingly. The approval engine test assertion is only reformatted to satisfy rustfmt. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/tinysecurity-approval/src/engine_tests.rs | 5 ++++- crates/tinysecurity-bus/tests/scoped_wire.rs | 6 +++--- crates/tinysecurity-bus/tests/wire.rs | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/tinysecurity-approval/src/engine_tests.rs b/crates/tinysecurity-approval/src/engine_tests.rs index be3bc60..f565fe2 100644 --- a/crates/tinysecurity-approval/src/engine_tests.rs +++ b/crates/tinysecurity-approval/src/engine_tests.rs @@ -443,7 +443,10 @@ async fn abandoned_execution_is_persisted_as_an_immutable_unknown_effect() -> Re let finished = owner.record_execution(terminal.clone()).await?; assert_eq!(finished.state, ApprovalLifecycle2::Executed); assert_eq!(finished.outcome, Some(ExecutionOutcome::Abandoned)); - assert_eq!(owner.list(&request().scope_id).await?, vec![finished.clone()]); + assert_eq!( + owner.list(&request().scope_id).await?, + vec![finished.clone()] + ); assert_eq!(owner.record_execution(terminal).await?, finished); let mut cancelled = RecordExecutionRequest2 { diff --git a/crates/tinysecurity-bus/tests/scoped_wire.rs b/crates/tinysecurity-bus/tests/scoped_wire.rs index 3b9bebe..8f03e9b 100644 --- a/crates/tinysecurity-bus/tests/scoped_wire.rs +++ b/crates/tinysecurity-bus/tests/scoped_wire.rs @@ -84,9 +84,9 @@ fn new_members_are_advertised_and_named_by_contract_constants() { #[test] fn contract_bump_is_additive_for_older_hosts_and_modules() { - assert_eq!(CONTRACT_VERSION, (1, 27)); - // A host built against 1.27 accepts this module and newer minors, not a 1.26 module. - assert!(is_compatible((1, 27))); + assert_eq!(CONTRACT_VERSION, (1, 28)); + // A host built against 1.28 accepts this module and newer minors, not a 1.27 module. + assert!(is_compatible((1, 28))); assert!(!is_compatible((1, 26))); assert!(!is_compatible((1, 25))); assert!(!is_compatible((1, 24))); diff --git a/crates/tinysecurity-bus/tests/wire.rs b/crates/tinysecurity-bus/tests/wire.rs index e049746..18bc777 100644 --- a/crates/tinysecurity-bus/tests/wire.rs +++ b/crates/tinysecurity-bus/tests/wire.rs @@ -345,7 +345,7 @@ fn redactor_init_is_additive_and_secrets_never_enter_public_info() serde_json::to_value(&config)?["redactor"]["secrets"][0], "literal-private" ); - assert_eq!(CONTRACT_VERSION, (1, 27)); + assert_eq!(CONTRACT_VERSION, (1, 28)); for (mode, wire) in [ (RedactMode::Text, "text"), (RedactMode::StructuredArguments, "structured_arguments"), From bf8752dc26ac07b11ae403a7ba6f7e07e67b78bb Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 11:39:11 +0000 Subject: [PATCH 05/15] test(bus): cover contract major bump compatibility Update the scoped wire test to assert the contract version is now 2.0 and that compatibility checks accept 2.x modules while rejecting 1.x ones. The previous additive-minor expectations no longer hold now that an incompatible execution outcome requires a new major. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/tinysecurity-bus/tests/scoped_wire.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/tinysecurity-bus/tests/scoped_wire.rs b/crates/tinysecurity-bus/tests/scoped_wire.rs index 8f03e9b..789fa85 100644 --- a/crates/tinysecurity-bus/tests/scoped_wire.rs +++ b/crates/tinysecurity-bus/tests/scoped_wire.rs @@ -83,10 +83,13 @@ fn new_members_are_advertised_and_named_by_contract_constants() { } #[test] -fn contract_bump_is_additive_for_older_hosts_and_modules() { - assert_eq!(CONTRACT_VERSION, (1, 28)); - // A host built against 1.28 accepts this module and newer minors, not a 1.27 module. - assert!(is_compatible((1, 28))); +fn incompatible_execution_outcome_requires_a_new_contract_major() { + assert_eq!(CONTRACT_VERSION, (2, 0)); + // A 2.0 host accepts compatible 2.x modules, but older 1.x hosts reject this module. + assert!(is_compatible((2, 0))); + assert!(is_compatible((2, 1))); + assert!(!is_compatible((1, 28))); + assert!(!is_compatible((1, 27))); assert!(!is_compatible((1, 26))); assert!(!is_compatible((1, 25))); assert!(!is_compatible((1, 24))); From 9a3a2e8caceef898ebca9710eda465307a731c07 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 11:40:24 +0000 Subject: [PATCH 06/15] feat(bus): add abandoned execution outcome and bump contract to 2.0 Introduce `ExecutionOutcome::Abandoned` for in-flight executions whose effects could not be determined, distinct from `Cancelled` which asserts a definite cancelled result. Because the outcome enum is closed on the wire, the contract version moves to 2.0 so 1.x hosts reject 2.x modules instead of decoding a result they cannot represent. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- MODULE.md | 15 ++++++++++++++- README.md | 4 ++++ crates/tinysecurity-bus/src/device_frame_tests.rs | 2 +- .../tinysecurity-bus/src/device_pairing_tests.rs | 2 +- crates/tinysecurity-bus/src/lib.rs | 2 +- crates/tinysecurity-bus/tests/wire.rs | 2 +- docs/specs/security-module.md | 8 ++++++++ 7 files changed, 30 insertions(+), 5 deletions(-) diff --git a/MODULE.md b/MODULE.md index 28b1000..6c79fef 100644 --- a/MODULE.md +++ b/MODULE.md @@ -11,7 +11,7 @@ RPC methods. Configuration requires `contract_version` and `policy`. The optiona ```json { - "contract_version": [1, 12], + "contract_version": [2, 0], "policy": { "enabled": false, "action_dir": "", @@ -379,6 +379,19 @@ returned by `ListToolTrust2` even when the tool's display label is redacted; the module verifies the row belongs to the supplied registered scope and records the usual audited cancellation. +## Approval execution outcomes (contract 2.0) + +`ExecutionOutcome::Abandoned` means an in-flight execution was abandoned before +the host could determine its effects. It is persisted as an immutable terminal +outcome and does not claim that the tool had no effects. Use `Cancelled` only +when the host has a definite cancelled result. A host that sees the legacy +`aborted` value maps it to `Abandoned` because aborting a wait or task does not +prove that execution had no effects. + +This closed enum change is a contract-major bump: a 1.x host cannot safely +decode an `abandoned` result, so 1.x hosts reject a 2.x module during version +compatibility checks. + ### Scored prompt screening: `ScanPrompt2` `ScanPrompt2 { text }` answers `{ tier, score_milli, rule_ids }`. The tier is `allow`, `review` or diff --git a/README.md b/README.md index 540b171..9693483 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ Contract 1.25 adds `DevicePairing2`, which owns the paired-device static identity, ciphertext-only persistence, host-attested enrollment/reconnect, session refresh and revocation while preserving the existing v2/v3/v4 wire; see the [pairing lifecycle specification](docs/specs/device-pairing-contract-1.25.md). +Contract 2.0 adds the `abandoned` approval execution outcome for in-flight +executions whose effects could not be determined. Because `ExecutionOutcome` is +a closed wire enum, 1.x hosts reject 2.x modules rather than risk decoding a +result they cannot represent; see the [approval contract](MODULE.md#approval-execution-outcomes-contract-20). Contract 1.1 also supplies immutable native [filesystem scopes](docs/specs/immutable-path-scopes.md) through `RegisterPathPolicy`, `ValidatePath`, `ValidateParent` and `CheckPath`. Contract 1.3 adds native `Redact`, `ScanPrompt` and `ScanToolDefinition`, with a diff --git a/crates/tinysecurity-bus/src/device_frame_tests.rs b/crates/tinysecurity-bus/src/device_frame_tests.rs index 1d5dedb..3a1c0d5 100644 --- a/crates/tinysecurity-bus/src/device_frame_tests.rs +++ b/crates/tinysecurity-bus/src/device_frame_tests.rs @@ -70,5 +70,5 @@ fn frame_operations_redact_both_payloads_without_changing_open_wire_shape() #[test] fn frame_member_name_is_stable_and_contract_version_is_bumped() { assert_eq!(crate::names::methods::DEVICE_FRAME2, "DeviceFrame2"); - assert_eq!(crate::CONTRACT_VERSION, (1, 28)); + assert_eq!(crate::CONTRACT_VERSION, (2, 0)); } diff --git a/crates/tinysecurity-bus/src/device_pairing_tests.rs b/crates/tinysecurity-bus/src/device_pairing_tests.rs index c417a30..d1aeaea 100644 --- a/crates/tinysecurity-bus/src/device_pairing_tests.rs +++ b/crates/tinysecurity-bus/src/device_pairing_tests.rs @@ -85,7 +85,7 @@ fn refresh_and_close_are_distinct_transient_session_operations() { #[test] fn pairing_contract_is_versioned_after_the_frame_contract() { - assert_eq!(CONTRACT_VERSION, (1, 28)); + assert_eq!(CONTRACT_VERSION, (2, 0)); } #[test] diff --git a/crates/tinysecurity-bus/src/lib.rs b/crates/tinysecurity-bus/src/lib.rs index 0c13ca7..9b93517 100644 --- a/crates/tinysecurity-bus/src/lib.rs +++ b/crates/tinysecurity-bus/src/lib.rs @@ -65,7 +65,7 @@ pub use scoped_policy::*; pub use tool_trust::*; pub mod names; /// Current wire vocabulary version, independent of the artifact version. -pub const CONTRACT_VERSION: (u32, u32) = (1, 28); +pub const CONTRACT_VERSION: (u32, u32) = (2, 0); /// Whether a remote module's advertised version supplies this host's vocabulary. /// /// `version` is the provider version received by the host, not a host requirement diff --git a/crates/tinysecurity-bus/tests/wire.rs b/crates/tinysecurity-bus/tests/wire.rs index 18bc777..ec594c4 100644 --- a/crates/tinysecurity-bus/tests/wire.rs +++ b/crates/tinysecurity-bus/tests/wire.rs @@ -345,7 +345,7 @@ fn redactor_init_is_additive_and_secrets_never_enter_public_info() serde_json::to_value(&config)?["redactor"]["secrets"][0], "literal-private" ); - assert_eq!(CONTRACT_VERSION, (1, 28)); + assert_eq!(CONTRACT_VERSION, (2, 0)); for (mode, wire) in [ (RedactMode::Text, "text"), (RedactMode::StructuredArguments, "structured_arguments"), diff --git a/docs/specs/security-module.md b/docs/specs/security-module.md index 5748533..f76a0e0 100644 --- a/docs/specs/security-module.md +++ b/docs/specs/security-module.md @@ -152,6 +152,14 @@ Default TTL is ten minutes; copilot and sub-agent TTL is three minutes. permission. Cron external effects deny. SaaS has no implicit bypass: configure a verified per-user prompt channel or deny when human approval is needed. +`ExecutionOutcome::Abandoned` records an in-flight execution that was abandoned +before the host could determine its effects. This is not a cancellation and does +not claim that the tool had no effects. Use `Cancelled` only for a definite +cancelled result; map legacy `aborted` values to `Abandoned` because aborting a +wait or task does not establish the execution's outcome. The `ExecutionOutcome` +wire enum is closed, so this value requires contract 2.0: 1.x hosts must reject +2.x providers rather than admit a provider whose result they cannot decode. + Jev is an optional judge, disabled by default and enabled per origin. It can only convert `RequireApproval` to `Allow`; it cannot override any `Deny`, floor, privacy or sandbox requirement. Redacted input and independent questions test From 52bb2f4356175f43704f7e2eff067660fc87f737 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 11:41:19 +0000 Subject: [PATCH 07/15] test(tinysecurity-bus): drop incorrect compatibility assertion The scoped wire test asserted that a 2.0 module is incompatible, but a new contract major is exactly what the test expects to be accepted, so the assertion contradicted the test's intent and was removed. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/tinysecurity-bus/tests/scoped_wire.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/tinysecurity-bus/tests/scoped_wire.rs b/crates/tinysecurity-bus/tests/scoped_wire.rs index 789fa85..29342ff 100644 --- a/crates/tinysecurity-bus/tests/scoped_wire.rs +++ b/crates/tinysecurity-bus/tests/scoped_wire.rs @@ -107,7 +107,6 @@ fn incompatible_execution_outcome_requires_a_new_contract_major() { // A 1.12 module cannot supply the audited scoped members. assert!(!is_compatible((1, 12))); assert!(!is_compatible((1, 11))); - assert!(!is_compatible((2, 0))); } #[test] From 4f6c5dbcc79f3c1370193c830135b3e364d369a0 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 11:45:31 +0000 Subject: [PATCH 08/15] test(module): bump contract version in reinit test The invalid SDK reinitialization test now sets up its module config with contract version 3.0 instead of 2.0, keeping the fixture aligned with the current contract version. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- crates/tinysecurity-module/src/adapter_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tinysecurity-module/src/adapter_tests.rs b/crates/tinysecurity-module/src/adapter_tests.rs index dba11ef..5dd17ef 100644 --- a/crates/tinysecurity-module/src/adapter_tests.rs +++ b/crates/tinysecurity-module/src/adapter_tests.rs @@ -1932,7 +1932,7 @@ async fn assert_invalid_sdk_reinitializations( setup( server.clone(), ModuleConfig { - contract_version: (2, 0), + contract_version: (3, 0), ..Default::default() } ) From 9453476eeeb769af939fe36d156c5a4e2d5c8ffc Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 11:55:07 +0000 Subject: [PATCH 09/15] test: cover mode 14 and contract version 1.28 in fixtures Extend the native contract fixture so mode 14 also exercises the invalid configuration rejection path, and update the policy test to use contract version 1.28 as the incompatible value. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../examples/secret_cipher_native_contract/fixture.rs | 2 +- crates/tinysecurity-policy/tests/policy.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs b/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs index bbec78a..63fe335 100644 --- a/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs +++ b/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs @@ -2217,7 +2217,7 @@ pub(super) async fn run( let host = start_callbacks(&lifecycle, &state).await?; let config = configuration(&host, mode)?; let loaded = lifecycle.load_file_with_config(library, serde_json::to_value(&config)?); - if matches!(mode, 15 | 16) { + if matches!(mode, 14 | 15 | 16) { reject_invalid_config(&lifecycle, &host, loaded).await?; task.abort(); return Ok(()); diff --git a/crates/tinysecurity-policy/tests/policy.rs b/crates/tinysecurity-policy/tests/policy.rs index a1b6eb2..3c3dbb8 100644 --- a/crates/tinysecurity-policy/tests/policy.rs +++ b/crates/tinysecurity-policy/tests/policy.rs @@ -67,7 +67,7 @@ fn failed_reinit_preserves_old_generation_and_configuration() let mut policy = Policy::new(ModuleConfig::default())?; let old = policy.info(); let bad = ModuleConfig { - contract_version: (2, 0), + contract_version: (1, 28), ..Default::default() }; assert_eq!(policy.reinitialize(bad), Err(Error::IncompatibleVersion)); From 611590fb15145dcf4d3ce6d57ce79d45c7ac8f64 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 11:55:32 +0000 Subject: [PATCH 10/15] refactor(tinysecurity-module): use range pattern for mode check Replace the explicit alternation of modes 14, 15 and 16 with an inclusive range pattern in the secret cipher native contract fixture. The behaviour is unchanged, but the range form is clearer and less error prone if the set of modes grows. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../examples/secret_cipher_native_contract/fixture.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs b/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs index 63fe335..4237fa8 100644 --- a/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs +++ b/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs @@ -2217,7 +2217,7 @@ pub(super) async fn run( let host = start_callbacks(&lifecycle, &state).await?; let config = configuration(&host, mode)?; let loaded = lifecycle.load_file_with_config(library, serde_json::to_value(&config)?); - if matches!(mode, 14 | 15 | 16) { + if matches!(mode, 14..=16) { reject_invalid_config(&lifecycle, &host, loaded).await?; task.abort(); return Ok(()); From 35c5395f61488ba74899d6b5234b94d7fde00134 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 22:05:07 +0000 Subject: [PATCH 11/15] docs: move approval execution outcomes to a spec The approval execution outcomes section was replaced with a pointer to a new spec document, keeping the module overview concise while the detailed contract 2.0 compatibility notes live alongside the other specs. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- MODULE.md | 14 ++------------ docs/specs/approval-execution-outcomes.md | 12 ++++++++++++ 2 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 docs/specs/approval-execution-outcomes.md diff --git a/MODULE.md b/MODULE.md index cbc5d5e..cd4aa3f 100644 --- a/MODULE.md +++ b/MODULE.md @@ -382,18 +382,8 @@ returned by `ListToolTrust2` even when the tool's display label is redacted; the module verifies the row belongs to the supplied registered scope and records the usual audited cancellation. -## Approval execution outcomes (contract 2.0) - -`ExecutionOutcome::Abandoned` means an in-flight execution was abandoned before -the host could determine its effects. It is persisted as an immutable terminal -outcome and does not claim that the tool had no effects. Use `Cancelled` only -when the host has a definite cancelled result. A host that sees the legacy -`aborted` value maps it to `Abandoned` because aborting a wait or task does not -prove that execution had no effects. - -This closed enum change is a contract-major bump: a 1.x host cannot safely -decode an `abandoned` result, so 1.x hosts reject a 2.x module during version -compatibility checks. +Approval execution outcomes and the contract 2.0 compatibility boundary are +described in [`docs/specs/approval-execution-outcomes.md`](docs/specs/approval-execution-outcomes.md). ### Scored prompt screening: `ScanPrompt2` diff --git a/docs/specs/approval-execution-outcomes.md b/docs/specs/approval-execution-outcomes.md new file mode 100644 index 0000000..4ba4244 --- /dev/null +++ b/docs/specs/approval-execution-outcomes.md @@ -0,0 +1,12 @@ +# Approval execution outcomes + +Contract 2.0 adds `ExecutionOutcome::Abandoned` for an in-flight execution that +was abandoned before the host could determine its effects. It is persisted as +an immutable terminal outcome and does not claim that the tool had no effects. +Use `Cancelled` only when the host has a definite cancelled result. A host that +sees the legacy `aborted` value maps it to `Abandoned` because aborting a wait or +task does not prove that execution had no effects. + +This closed enum change is a contract-major bump: a 1.x host cannot safely +decode an `abandoned` result, so 1.x hosts reject a 2.x module during version +compatibility checks. From 9e379a76b8b4840bd47413ce1d0713fdc31d6e31 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 22:26:59 +0000 Subject: [PATCH 12/15] docs: fix approval contract link Co-authored-by: Medulla --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb0ac1e..8854ec3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ see the [pairing lifecycle specification](docs/specs/device-pairing-contract-1.2 Contract 2.0 adds the `abandoned` approval execution outcome for in-flight executions whose effects could not be determined. Because `ExecutionOutcome` is a closed wire enum, 1.x hosts reject 2.x modules rather than risk decoding a -result they cannot represent; see the [approval contract](MODULE.md#approval-execution-outcomes-contract-20). +result they cannot represent; see the [approval contract](docs/specs/approval-execution-outcomes.md). Contract 1.1 also supplies immutable native [filesystem scopes](docs/specs/immutable-path-scopes.md) through `RegisterPathPolicy`, `ValidatePath`, `ValidateParent` and `CheckPath`. Contract 1.3 adds native `Redact`, `ScanPrompt` and `ScanToolDefinition`, with a From 4c9978086bd9b12a8de18258a55f3e4b35f126e2 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 22:51:25 +0000 Subject: [PATCH 13/15] test(secret-cipher): raise fixture timeout above audit budget The native contract fixture now gives the proxy call a 35-second timeout so the outer deadline stays above the three serial audit callbacks, each of which can consume the configured 10-second deadline. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../examples/secret_cipher_native_contract/fixture.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs b/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs index 4237fa8..0046a7a 100644 --- a/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs +++ b/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs @@ -2338,7 +2338,11 @@ async fn execute( completion_audit: audit(*sequence + 2), }; *sequence += 3; + // Execution can make three serial audit callbacks, each with the configured + // 10-second deadline. Keep the fixture's outer deadline above that budget. let response = proxy + .clone() + .with_timeout(Duration::from_secs(35)) .call_confidential(names::methods::EXECUTE_SECRET_CIPHER2, (&execute,)) .await; assert!( From 7a141aa8e180869e5d8ce23c8643df6ebf5eaca7 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 23:33:04 +0000 Subject: [PATCH 14/15] refactor(examples): reuse timed proxy in secret cipher fixture Bind the proxy with the extended timeout to a local so the same instance is reused for the follow-up call instead of rebuilding it from the original proxy. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../examples/secret_cipher_native_contract/fixture.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs b/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs index 0046a7a..ac3916c 100644 --- a/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs +++ b/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs @@ -2340,13 +2340,12 @@ async fn execute( *sequence += 3; // Execution can make three serial audit callbacks, each with the configured // 10-second deadline. Keep the fixture's outer deadline above that budget. - let response = proxy - .clone() - .with_timeout(Duration::from_secs(35)) + let execute_proxy = proxy.clone().with_timeout(Duration::from_secs(35)); + let response = execute_proxy .call_confidential(names::methods::EXECUTE_SECRET_CIPHER2, (&execute,)) .await; assert!( - proxy + execute_proxy .call_confidential::( names::methods::EXECUTE_SECRET_CIPHER2, (&execute,) From 4fe3eeed53424bcb032b761d4809ed27b64ba02e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 23:51:53 +0000 Subject: [PATCH 15/15] fix(tinysecurity-module): raise native contract fixture bus timeout Windows native callbacks can exceed the default five-second bus timeout while completing the approval and audit handshake, so the outer fixture deadline is now a flat 35 seconds for every mode instead of 30 seconds only in mode 18. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../examples/secret_cipher_native_contract/fixture.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs b/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs index ac3916c..a759b4b 100644 --- a/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs +++ b/crates/tinysecurity-module/examples/secret_cipher_native_contract/fixture.rs @@ -2228,9 +2228,12 @@ pub(super) async fn run( .await? .ok_or("native module peer")?; *state.module.lock().map_err(|_| denied())? = Some(module.clone()); + // Windows native callbacks can exceed the default five-second bus timeout + // while completing the approval and audit handshake. Keep the outer + // fixture deadline above the callbacks' serial budgets for every mode. let proxy = host .proxy(module.as_str(), names::OBJECT_PATH, names::INTERFACE)? - .with_timeout(Duration::from_secs(if mode == 18 { 30 } else { 5 })); + .with_timeout(Duration::from_secs(35)); assert!(proxy.attestation().await?.is_some()); if mode == 18 { let _: PolicyInfo = proxy.call(names::methods::POLICY_INFO, ()).await?;