Skip to content

Commit 40ce807

Browse files
senamakelmedullabot
andcommitted
fix(audit): bind permission decisions to exact durable events
Co-authored-by: Medulla <medulla@tinyhumans.ai>
1 parent d618792 commit 40ce807

17 files changed

Lines changed: 763 additions & 31 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,11 @@ jobs:
7272
macOS) module=target/debug/libtinysecurity_module.dylib ;;
7373
esac
7474
cargo run -p tinysecurity-module --example verify_module -- "$module"
75+
cargo run -p tinysecurity-module --example audit_native_contract -- "$module"
7576
# Installed examples must also work outside the workspace root.
7677
module="$(pwd)/$module"
7778
(cd target && ./debug/examples/verify_module "$module")
79+
(cd target && ./debug/examples/audit_native_contract "$module")
7880
7981
- name: Verify Windows module through the restricted native copy
8082
if: ${{ runner.os == 'Windows' }}
@@ -114,11 +116,15 @@ jobs:
114116
$verifiedModule = Join-Path $verifyRoot "$libraryName.dll"
115117
Copy-Item -LiteralPath $module -Destination $verifiedModule
116118
cargo run --locked --package tinysecurity-module --example verify_module -- $verifiedModule
119+
cargo run --locked --package tinysecurity-module --example audit_native_contract -- $verifiedModule
117120
$verifier = (Resolve-Path 'target/debug/examples/verify_module.exe').Path
121+
$auditVerifier = (Resolve-Path 'target/debug/examples/audit_native_contract.exe').Path
118122
Push-Location $env:RUNNER_TEMP
119123
try {
120124
& $verifier $verifiedModule
121125
if ($LASTEXITCODE -ne 0) { throw 'native verifier failed outside workspace' }
126+
& $auditVerifier $verifiedModule
127+
if ($LASTEXITCODE -ne 0) { throw 'native audit verifier failed outside workspace' }
122128
} finally {
123129
Pop-Location
124130
}

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎README.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ Blocked original content denies; suspicious content denies with
1212
approval-unavailable until the durable approval engine exists. Scan verdicts
1313
never grant permission, and redaction never clears an existing scan finding.
1414

15+
Contract 1.6 supplies `Audit2`, `EvaluateAudited2` and `CheckAudited2` with
16+
authenticated exact audit callbacks and a bounded [native journal](crates/tinysecurity-audit/README.md).
17+
The audited permission methods derive events from the actual original request,
18+
computed decision and current generation. Required audit disables unbound
19+
`Evaluate`/`Check` permission and other unbound path/command/egress authorization;
20+
exact durable evidence precedes an audited allow. Scope registration and
21+
descriptive classification remain available.
22+
Windows local journal support remains unavailable at this checkpoint.
23+
1524
The engine denies unknown tools and unsupported effects. Argument-free `pwd`,
1625
`date`, `uname`, and `whoami` are its supported shell diagnostics. Paths pass
1726
an unconditional credential/system/traversal floor and native ancestor
@@ -22,7 +31,7 @@ diagnostics work with disabled autonomy, which is the default.
2231
This is bootstrap infrastructure for [OpenHuman #7328](https://github.com/tinyhumansai/openhuman/issues/7328)
2332
and [TinySecurity #1](https://github.com/tinyhumansai/tinysecurity/issues/1).
2433
It does **not** complete the migration. Approval persistence,
25-
network connection pinning, sandbox planning, audit, judge and crypto engines
34+
network connection pinning, sandbox planning, judge and crypto engines
2635
remain unavailable. Reserved wire types do not advertise callable handlers.
2736
There is no production host migration until an upstream release and checksum
2837
manifest exist.
@@ -34,6 +43,7 @@ manifest exist.
3443
| `tinysecurity-bus` | Wire types, errors, names and compatibility; normal dependencies only serde and thiserror |
3544
| `tinysecurity-policy` | Native conservative policy engine |
3645
| `tinysecurity-redact` | Internal immutable redaction registry and semantic intent scanner |
46+
| `tinysecurity-audit` | Internal bounded authoritative journal and sanitized exact receipts |
3747
| `tinysecurity` | Internal implementation re-export |
3848
| `tinysecurity-module` | SDK initialization, bus dispatch and ABI cdylib |
3949

‎crates/tinysecurity-audit/README.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,10 @@ The module can forward exact version-two batches to the host unique connection
4242
bound at native initialization. Callback unavailability, malformed receipts and
4343
required audit loss deny permission; optional loss reports degraded health and
4444
never fabricates a receipt. Scope and sanitation failures deny even when audit is
45-
optional. An execution host must consume this audit permission before executing;
46-
OpenHuman host migration is separate work.
45+
optional. `EvaluateAudited2`/`CheckAudited2` derive the pre-execution event from
46+
the original permission request and computed verdict, using the trusted init
47+
scope/stream. Hosts provide only stable event identity, tail and expected policy
48+
generation. Required audit disables unbound `Evaluate`/`Check` allows. A policy
49+
change during callback denies permission while preserving any exact committed
50+
receipt; a receipt never revives a stale generation. Terminal `Audit2` events are
51+
a separate phase. OpenHuman host migration is separate work.

‎crates/tinysecurity-audit/src/sink_tests.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use super::*;
22
#[cfg(unix)]
33
use std::os::unix::fs::{PermissionsExt, symlink};
4+
#[cfg(unix)]
45
use tinysecurity_bus::{AuditEvent2, AuditKind2, SubjectScope};
6+
#[cfg(unix)]
57
fn batch(tail: u64) -> AuditSinkRequest2 {
68
AuditSinkRequest2 {
79
scope: SubjectScope {

‎crates/tinysecurity-bus/src/audit_tests.rs‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,45 @@
11
use super::*;
2+
#[test]
3+
fn diagnostic_formatting_never_exposes_nested_audit_payloads_or_storage_locations() {
4+
let mut request = request();
5+
request.events[0].arguments_json = "private-secret".into();
6+
request.events[0].attribution = "private-actor".into();
7+
let receipt = AuditSinkResponse2 {
8+
scope: request.scope.clone(),
9+
stream_id: request.stream_id.clone(),
10+
events: vec![],
11+
new_tail: 0,
12+
};
13+
let response = AuditResponse2 {
14+
permitted: true,
15+
committed: true,
16+
health: AuditHealth2::Healthy,
17+
receipt: Some(receipt.clone()),
18+
};
19+
let config = AuditConfig2 {
20+
scope: request.scope.clone(),
21+
stream_id: request.stream_id.clone(),
22+
timeout_ms: 100,
23+
required: true,
24+
native_path: Some("private-file".into()),
25+
max_bytes: 1000,
26+
max_archives: 2,
27+
journal_max_bytes: 10000,
28+
journal_max_events: 10,
29+
};
30+
for diagnostic in [
31+
format!("{:?}", request.events[0]),
32+
format!("{request:?}"),
33+
format!("{receipt:?}"),
34+
format!("{response:?}"),
35+
format!("{config:?}"),
36+
] {
37+
assert!(diagnostic.contains("REDACTED"));
38+
for sensitive in ["private-", "safe-user", "safe-workspace", "safe-agent"] {
39+
assert!(!diagnostic.contains(sensitive));
40+
}
41+
}
42+
}
243
fn request() -> AuditSinkRequest2 {
344
AuditSinkRequest2 {
445
scope: SubjectScope {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//! Permission requests bound to module-derived pre-execution audit evidence.
2+
use serde::{Deserialize, Serialize};
3+
4+
/// Stable retry metadata; the module supplies scope, stream, payload and verdict.
5+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
6+
#[serde(deny_unknown_fields)]
7+
pub struct AuditCorrelation2 {
8+
/// Opaque 32-hex event identity allocated once by the trusted host.
9+
pub event_id: String,
10+
/// Exact previously committed stream tail; adding one must not overflow.
11+
pub expected_tail: u64,
12+
/// Expected policy generation, retained unchanged for an indeterminate retry.
13+
pub generation: u64,
14+
}
15+
/// Original permission request and stable audit correlation, without caller verdicts.
16+
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
17+
#[serde(deny_unknown_fields)]
18+
pub struct AuditedRequest2<T> {
19+
/// Actual original request evaluated by the module.
20+
pub request: T,
21+
/// Stable event correlation for exact reconciliation.
22+
pub audit: AuditCorrelation2,
23+
}
24+
impl<T> std::fmt::Debug for AuditedRequest2<T> {
25+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26+
f.write_str("AuditedRequest2([REDACTED])")
27+
}
28+
}
29+
/// Computed permission and its independent audit durability evidence.
30+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
31+
#[serde(deny_unknown_fields)]
32+
pub struct AuditedResponse2<T> {
33+
/// Computed permission, denied on required loss or stale generation.
34+
pub decision: T,
35+
/// Exact audit evidence; a stale policy does not erase a committed receipt.
36+
pub audit: crate::AuditResponse2,
37+
}
38+
/// Module-evaluated operation with pre-execution audit.
39+
pub type EvaluateAuditedRequest2 = AuditedRequest2<crate::EvaluateRequest>;
40+
/// Computed operation permission with audit evidence.
41+
pub type EvaluateAuditedResponse2 = AuditedResponse2<crate::Decision>;
42+
/// Module-evaluated ordered checks with one pre-execution audit event.
43+
pub type CheckAuditedRequest2 = AuditedRequest2<crate::CheckRequest>;
44+
/// Computed ordered permissions with audit evidence.
45+
pub type CheckAuditedResponse2 = AuditedResponse2<crate::CheckResponse>;
46+
#[cfg(test)]
47+
#[path = "audited_tests.rs"]
48+
mod tests;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use super::*;
2+
#[test]
3+
fn audited_permission_wire_carries_original_request_and_correlation_without_caller_verdicts()
4+
-> std::result::Result<(), Box<dyn std::error::Error>> {
5+
let wire = serde_json::json!({
6+
"request": { "caller": {
7+
"agent": "private-agent", "thread": null, "flow": null,
8+
"call_id": "private-call", "origin": "operator", "tier": "full"
9+
}, "checks": [{"kind": "local_diagnostic"}] },
10+
"audit": { "event_id": "a".repeat(32), "expected_tail": 0, "generation": 1 }
11+
});
12+
let request: CheckAuditedRequest2 = serde_json::from_value(wire.clone())?;
13+
assert_eq!(serde_json::to_value(&request)?, wire);
14+
assert!(!format!("{request:?}").contains("private-"));
15+
for field in ["verdict", "arguments_json", "committed", "original_binding"] {
16+
let mut forged = wire.clone();
17+
forged[field] = true.into();
18+
assert!(serde_json::from_value::<CheckAuditedRequest2>(forged).is_err());
19+
}
20+
let mut forged = wire;
21+
forged["audit"]["scope"] = "other-user".into();
22+
assert!(serde_json::from_value::<CheckAuditedRequest2>(forged).is_err());
23+
let response = AuditedResponse2 {
24+
decision: crate::CheckResponse {
25+
generation: 1,
26+
decisions: vec![],
27+
},
28+
audit: crate::AuditResponse2 {
29+
permitted: false,
30+
committed: false,
31+
health: crate::AuditHealth2::Denied,
32+
receipt: None,
33+
},
34+
};
35+
assert_eq!(
36+
serde_json::from_value::<CheckAuditedResponse2>(serde_json::to_value(&response)?)?,
37+
response
38+
);
39+
Ok(())
40+
}

‎crates/tinysecurity-bus/src/lib.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! Hosts link this crate; the policy engine and native module stay isolated.
44
//! Implements bootstrap checks plus immutable native filesystem scope contracts.
55
mod audit;
6+
mod audited;
67
mod callbacks;
78
mod command_policy;
89
mod egress;
@@ -13,6 +14,7 @@ mod policy;
1314
mod prompt;
1415
mod redact;
1516
pub use audit::*;
17+
pub use audited::*;
1618
pub use callbacks::*;
1719
pub use command_policy::*;
1820
pub use egress::*;
@@ -24,7 +26,7 @@ pub use prompt::*;
2426
pub use redact::RedactorConfig;
2527
pub mod names;
2628
/// Current wire vocabulary version, independent of the artifact version.
27-
pub const CONTRACT_VERSION: (u32, u32) = (1, 5);
29+
pub const CONTRACT_VERSION: (u32, u32) = (1, 6);
2830
/// Whether a remote module's advertised version supplies this host's vocabulary.
2931
///
3032
/// `version` is the provider version received by the host, not a host requirement

‎crates/tinysecurity-bus/src/names.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,19 @@ pub mod methods {
5959
pub const PLAN: &str = "Plan";
6060
/// Exact durable version-two audit member.
6161
pub const AUDIT2: &str = "Audit2";
62+
/// Original operation evaluation bound to exact pre-execution audit.
63+
pub const EVALUATE_AUDITED2: &str = "EvaluateAudited2";
64+
/// Ordered check evaluation bound to exact pre-execution audit.
65+
pub const CHECK_AUDITED2: &str = "CheckAudited2";
6266
/// Audit member.
6367
pub const AUDIT: &str = "Audit";
6468
}
6569
/// Bootstrap implemented methods only.
6670
pub const METHODS: &[&str] = &[
6771
methods::EVALUATE,
6872
methods::CHECK,
73+
methods::EVALUATE_AUDITED2,
74+
methods::CHECK_AUDITED2,
6975
methods::REGISTER_PATH_POLICY,
7076
methods::VALIDATE_PATH,
7177
methods::VALIDATE_PARENT,

0 commit comments

Comments
 (0)