Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
98d3786
test(tinysecurity-bus): cover abandoned execution outcome wire round-…
senamakel Oct 22, 2026
6d5113f
feat(bus): add abandoned execution outcome
senamakel Oct 22, 2026
e7c851b
test(approval): cover abandoned execution as immutable unknown effect
senamakel Oct 22, 2026
b05b8da
test(bus): bump expected contract version to 1.28
senamakel Oct 22, 2026
bf8752d
test(bus): cover contract major bump compatibility
senamakel Oct 22, 2026
9a3a2e8
feat(bus): add abandoned execution outcome and bump contract to 2.0
senamakel Oct 22, 2026
52bb2f4
test(tinysecurity-bus): drop incorrect compatibility assertion
senamakel Oct 22, 2026
e47934a
Merge remote-tracking branch 'origin/main' into fix/abandoned-approva…
senamakel Oct 22, 2026
4f6c5db
test(module): bump contract version in reinit test
senamakel Oct 22, 2026
9453476
test: cover mode 14 and contract version 1.28 in fixtures
senamakel Oct 22, 2026
611590f
refactor(tinysecurity-module): use range pattern for mode check
senamakel Oct 22, 2026
7592cb4
Merge upstream main into abandoned approval outcome
senamakel Oct 22, 2026
35c5395
docs: move approval execution outcomes to a spec
senamakel Oct 22, 2026
9e379a7
docs: fix approval contract link
senamakel Oct 22, 2026
4c99780
test(secret-cipher): raise fixture timeout above audit budget
senamakel Oct 22, 2026
7a141aa
refactor(examples): reuse timed proxy in secret cipher fixture
senamakel Oct 22, 2026
4fe3eee
fix(tinysecurity-module): raise native contract fixture bus timeout
senamakel Oct 22, 2026
c49f856
Merge remote-tracking branch 'origin/main' into fix/abandoned-approva…
senamakel Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion MODULE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down Expand Up @@ -385,6 +385,9 @@ 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 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`

`ScanPrompt2 { text }` answers `{ tier, score_milli, rule_ids }`. The tier is `allow`, `review` or
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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](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
Expand Down
29 changes: 29 additions & 0 deletions crates/tinysecurity-approval/src/engine_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,35 @@ 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()))?;
Expand Down
2 changes: 1 addition & 1 deletion crates/tinysecurity-bus/src/approval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/tinysecurity-bus/src/approval_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
2 changes: 1 addition & 1 deletion crates/tinysecurity-bus/src/device_frame_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, 29));
assert_eq!(crate::CONTRACT_VERSION, (2, 0));
}
2 changes: 1 addition & 1 deletion crates/tinysecurity-bus/src/device_pairing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, 29));
assert_eq!(CONTRACT_VERSION, (2, 0));
}

#[test]
Expand Down
4 changes: 3 additions & 1 deletion crates/tinysecurity-bus/src/future.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -30,6 +30,8 @@ pub enum ExecutionOutcome {
Failed,
/// Cancelled state.
Cancelled,
/// The in-flight execution was abandoned before its effects could be determined.
Abandoned,
Comment thread
senamakel marked this conversation as resolved.
}
/// `ReplyVerdict` vocabulary, reserved until its engine is implemented.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
Expand Down
2 changes: 1 addition & 1 deletion crates/tinysecurity-bus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, 29);
pub const CONTRACT_VERSION: (u32, u32) = (2, 0);
Comment thread
senamakel marked this conversation as resolved.
/// 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
Expand Down
3 changes: 2 additions & 1 deletion crates/tinysecurity-bus/tests/fixtures/v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,8 @@
"ExecutionOutcome": [
"succeeded",
"failed",
"cancelled"
"cancelled",
"abandoned"
],
"ReplyVerdict": [
"allow_once",
Expand Down
14 changes: 8 additions & 6 deletions crates/tinysecurity-bus/tests/scoped_wire.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Registered subject policy wire shapes and the 1.29 compatibility boundary.
//! Registered subject policy wire shapes and the contract compatibility boundary.
use serde_json::json;
use tinysecurity_bus::*;

Expand Down Expand Up @@ -87,11 +87,14 @@ 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, 29));
// A host built against 1.29 accepts this module and newer minors, not a 1.28 module.
assert!(is_compatible((1, 29)));
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, 29)));
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)));
Expand All @@ -109,7 +112,6 @@ fn contract_bump_is_additive_for_older_hosts_and_modules() {
// 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]
Expand Down
4 changes: 3 additions & 1 deletion crates/tinysecurity-bus/tests/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<ExecutionOutcome>("\"future_unknown\"").is_err());
let abandoned: ExecutionOutcome = serde_json::from_str("\"abandoned\"")?;
assert_eq!(serde_json::to_string(&abandoned)?, "\"abandoned\"");
Ok(())
}
#[test]
Expand Down Expand Up @@ -343,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, 29));
assert_eq!(CONTRACT_VERSION, (2, 0));
for (mode, wire) in [
(RedactMode::Text, "text"),
(RedactMode::StructuredArguments, "structured_arguments"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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..=16) {
reject_invalid_config(&lifecycle, &host, loaded).await?;
task.abort();
return Ok(());
Expand All @@ -2228,6 +2228,9 @@ 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)?
// Let the module's own callback deadline settle pending work. In
Expand Down Expand Up @@ -2341,11 +2344,14 @@ async fn execute(
completion_audit: audit(*sequence + 2),
};
*sequence += 3;
let response = proxy
// Execution can make three serial audit callbacks, each with the configured
// 10-second deadline. Keep the fixture's outer deadline above that budget.
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::<SecretCipherResponse2>(
names::methods::EXECUTE_SECRET_CIPHER2,
(&execute,)
Expand Down
2 changes: 1 addition & 1 deletion crates/tinysecurity-module/src/adapter_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1990,7 +1990,7 @@ async fn assert_invalid_sdk_reinitializations(
setup(
server.clone(),
ModuleConfig {
contract_version: (2, 0),
contract_version: (3, 0),
..Default::default()
}
)
Expand Down
6 changes: 3 additions & 3 deletions crates/tinysecurity-policy/src/engine_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ fn verdict(policy: &Policy, request: &EvaluateRequest) -> Result<Verdict> {
}

#[test]
fn additive_contract_minor_accepts_an_older_same_major_host() {
fn incompatible_contract_major_rejects_an_older_host() {
let config = ModuleConfig {
contract_version: (1, 28),
contract_version: (1, 29),
..Default::default()
};
assert!(Policy::new(config).is_ok());
assert!(Policy::new(config).is_err());
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/tinysecurity-policy/tests/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, 29),
..Default::default()
};
assert_eq!(policy.reinitialize(bad), Err(Error::IncompatibleVersion));
Expand Down
12 changes: 12 additions & 0 deletions docs/specs/approval-execution-outcomes.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions docs/specs/security-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading