Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ jobs:
macOS) module=target/debug/libtinysecurity_module.dylib ;;
esac
cargo run -p tinysecurity-module --example verify_module -- "$module"
# Installed examples must also work outside the workspace root.
module="$(pwd)/$module"
(cd target && ./debug/examples/verify_module "$module")

- name: Verify Windows module through the restricted native copy
if: ${{ runner.os == 'Windows' }}
Expand Down Expand Up @@ -111,6 +114,14 @@ jobs:
$verifiedModule = Join-Path $verifyRoot "$libraryName.dll"
Copy-Item -LiteralPath $module -Destination $verifiedModule
cargo run --locked --package tinysecurity-module --example verify_module -- $verifiedModule
$verifier = (Resolve-Path 'target/debug/examples/verify_module.exe').Path
Push-Location $env:RUNNER_TEMP
try {
& $verifier $verifiedModule
if ($LASTEXITCODE -ne 0) { throw 'native verifier failed outside workspace' }
} finally {
Pop-Location
}

# `crates/tinysecurity-bus` exists so a host can name the payload types
# without compiling the module. That promise is invisible in a diff,
Expand Down
53 changes: 52 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ tinybus-module = { path = "vendor/tinybus/crates/tinybus-module", version = "0.1
# Derive macros for the crate-wide error type in `crates/tinysecurity-module/src/error/`.
# Every dependency entry should carry a comment like this one saying why it is
# here.
# OS entropy for opaque module-process scope references.
getrandom = "0.3"
# Conservative denial comparison on case-insensitive/normalizing filesystems.
unicode-normalization = "0.1"
thiserror = "2"
# The bus payload types are serialized into TinyBus frames.
serde = { version = "1", features = ["derive"] }
Expand Down
17 changes: 14 additions & 3 deletions MODULE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@

The artifact is `tinysecurity-module`, exporting TinyBus ABI v1. Its interface
is `ai.tinyhumans.tinysecurity.Policy`, with object
`/ai/tinyhumans/tinysecurity/Policy` and methods `Evaluate`, `Check`, `PolicyInfo`.
`/ai/tinyhumans/tinysecurity/Policy`. Its methods are `Evaluate`, `Check`,
`PolicyInfo`, `RegisterPathPolicy`, `ValidatePath`, `ValidateParent`, and `CheckPath`.
Accepted SDK reinit advances a generation and emits `PolicyChanged`; rejected
configurations retain the old policy and generation. There are no Init/Reinit
RPC methods. Configuration requires `contract_version` and `policy`. The optional
`judge` field may be omitted or set to `null` in this bootstrap.

```json
{
"contract_version": [1, 0],
"contract_version": [1, 1],
"policy": {
"enabled": false,
"action_dir": "",
Expand Down Expand Up @@ -56,12 +57,22 @@ argument-free `pwd`, `date`, `uname`, `whoami` through a trusted host executable
search path. Compound commands, expansions, heredocs, arguments, executable
paths and unrecognized commands deny conservatively on every OS.

Enabled writes deny with `approval_unavailable`; no unusable request IDs are
Enabled bootstrap Evaluate/Check writes deny with `approval_unavailable`; no unusable request IDs are
minted. Unknown tools/effects deny. Cron writes deny even with disabled policy.
Readonly callers cannot write. Decisions include generation and are uncached.
The host must reject stale generations and never retry/fallback after module
faults/timeouts. SDK faults remain latched; ordinary denials do not fault.

## Immutable filesystem scopes

Contract 1.1's filesystem members register independent immutable agent scopes.
They accept host-owned native roots and state reservations, preserve scope IDs
across SDK reinit, and authorize canonical reads or prospective writes without
requesting approval. Approval gating remains the caller's separate responsibility.
See [the scope contract](docs/specs/immutable-path-scopes.md) for configurable
floor differences from conservative bootstrap checks, registry bounds, symlink
handling and read-only grants. These members do not add command grammar parity.

## Reserved capabilities

Approval lifecycle/flow/reply, redaction/scanning, privacy/URL guard, sandbox,
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ TinySecurity supplies security policy through a native TinyBus module. Hosts
compile the transport-free `tinysecurity-bus` contract and load the isolated
`tinysecurity-module` artifact. The bootstrap implements `Evaluate`, batched
`Check`, `PolicyInfo`, validated SDK configuration and policy generations.
Contract 1.1 also supplies immutable native [filesystem scopes](docs/specs/immutable-path-scopes.md)
through `RegisterPathPolicy`, `ValidatePath`, `ValidateParent` and `CheckPath`.

The engine denies unknown tools and unsupported effects. Argument-free `pwd`,
`date`, `uname`, and `whoami` are its supported shell diagnostics. Paths pass
Expand Down
6 changes: 4 additions & 2 deletions crates/tinysecurity-bus/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
//! Versioned, transport-free `TinySecurity` wire contract.
//!
//! Hosts link this crate; the policy engine and native module stay isolated.
//! Only Evaluate, Check and `PolicyInfo` are implemented in the bootstrap.
//! Implements bootstrap checks plus immutable native filesystem scope contracts.
mod error;
mod future;
mod path_policy;
mod policy;
pub use error::*;
pub use future::*;
pub use path_policy::*;
pub use policy::*;
pub mod names;
/// Current wire vocabulary version, independent of the artifact version.
pub const CONTRACT_VERSION: (u32, u32) = (1, 0);
pub const CONTRACT_VERSION: (u32, u32) = (1, 1);
/// 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
18 changes: 17 additions & 1 deletion crates/tinysecurity-bus/src/names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ pub mod methods {
pub const CHECK: &str = "Check";
/// `PolicyInfo` member.
pub const POLICY_INFO: &str = "PolicyInfo";
/// Register an immutable filesystem policy scope.
pub const REGISTER_PATH_POLICY: &str = "RegisterPathPolicy";
/// Resolve and authorize an existing read path.
pub const VALIDATE_PATH: &str = "ValidatePath";
/// Resolve and authorize a prospective write path.
pub const VALIDATE_PARENT: &str = "ValidateParent";
/// Boolean filesystem policy query.
pub const CHECK_PATH: &str = "CheckPath";
/// Decide member.
pub const DECIDE: &str = "Decide";
/// `ListPending` member.
Expand Down Expand Up @@ -39,7 +47,15 @@ pub mod methods {
pub const AUDIT: &str = "Audit";
}
/// Bootstrap implemented methods only.
pub const METHODS: &[&str] = &[methods::EVALUATE, methods::CHECK, methods::POLICY_INFO];
pub const METHODS: &[&str] = &[
methods::EVALUATE,
methods::CHECK,
methods::REGISTER_PATH_POLICY,
methods::VALIDATE_PATH,
methods::VALIDATE_PARENT,
methods::CHECK_PATH,
methods::POLICY_INFO,
];
/// Accepted policy activation event.
pub const POLICY_CHANGED: &str = "PolicyChanged";
/// Reserved host callback identities.
Expand Down
141 changes: 141 additions & 0 deletions crates/tinysecurity-bus/src/path_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//! Immutable native filesystem policy scopes registered by the trusted host.
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// Access granted to a trusted filesystem root.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PathAccess {
/// Reading only.
ReadOnly,
/// Reading and writing.
ReadWrite,
}
/// Host-authorized root and its operation access.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PathTrustedRoot {
/// Native filesystem root.
pub path: PathBuf,
/// Allowed operations.
pub access: PathAccess,
}
/// A reserved subtree, with explicit child exceptions subject to other policy checks.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PathReservation {
/// Reserved path, including descendants.
pub path: PathBuf,
/// Reserve descendants while allowing the container directory itself.
#[serde(default)]
pub children_only: bool,
/// Subtrees excluded from this reservation. Each must resolve to a strict
/// descendant of the reserved path at registration and evaluation.
pub exceptions: Vec<PathBuf>,
}
/// A host-owned state directory family matched by its immediate child's prefix.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PathReservedPrefix {
/// Directory containing the reserved children.
pub root: PathBuf,
/// Nonempty prefix; the child must also have a nonempty suffix.
pub prefix: String,
}
/// Immutable per-agent/per-turn filesystem policy. No process environment is read.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PathPolicy {
/// Whether configurable restrictions apply; the unconditional floor always applies.
pub enabled: bool,
/// Reject absolute requests outside trusted roots.
pub workspace_only: bool,
/// Host internal-state root; also resolves relative forbidden entries.
pub workspace_dir: PathBuf,
/// Acting directory for relative requests.
pub action_dir: PathBuf,
/// Trusted home directory used to expand leading `~/`.
pub home_dir: Option<PathBuf>,
/// Configured forbidden paths; trusted roots override these only.
pub forbidden_paths: Vec<String>,
/// Explicitly granted filesystem access.
pub trusted_roots: Vec<PathTrustedRoot>,
/// Host state reservations; grants never override these.
pub reserved_paths: Vec<PathReservation>,
/// Host state directory families.
pub reserved_prefixes: Vec<PathReservedPrefix>,
/// Paths readable but never writable when policy is enabled.
pub readonly_paths: Vec<PathBuf>,
/// Reserved single-component relative filenames.
pub reserved_names: Vec<String>,
}
/// Opaque immutable registered scope identity.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PathPolicyId(pub String);
/// Path validation invocation.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PathValidationRequest {
/// Registered immutable scope.
pub policy_id: PathPolicyId,
/// User-supplied native path.
pub path: String,
}
/// Stable content-free path rejection categories.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PathErrorCategory {
/// Unconditional credential/system/traversal floor.
Protected,
/// Configurable policy restriction.
PolicyDenied,
/// Canonicalization failed or target is missing.
Resolution,
/// Input has no usable filename or parent.
InvalidPath,
}
/// Typed path validation result; no platform error text crosses the boundary.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case", deny_unknown_fields)]
pub enum PathValidationResult {
/// Validated native canonical path.
Allowed {
/// Resolved path.
resolved: PathBuf,
},
/// Rejected before the requested operation; validation may inspect filesystem metadata.
Denied {
/// Stable rejection category.
category: PathErrorCategory,
},
}
/// Boolean helper operation for host adapters that need synchronous checks.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PathCheckKind {
/// Lexical and existing-ancestor checks.
StringAllowed,
/// Resolved read containment.
ResolvedRead,
/// Resolved write containment.
ResolvedWrite,
/// Host internal-state reservation membership.
Internal,
/// Trusted read-root membership.
TrustedRead,
/// Trusted write-root membership.
TrustedWrite,
/// Unconditional floor membership.
Protected,
}
/// Boolean filesystem policy query.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PathCheckRequest {
/// Registered immutable scope.
pub policy_id: PathPolicyId,
/// Native input path.
pub path: PathBuf,
/// Query kind.
pub kind: PathCheckKind,
}
Loading
Loading