Skip to content

Build tinysecurity: one bus module for policy, approvals, auto-approve, audit, redaction and sandbox planning #1

Description

@senamakel

Tracker in OpenHuman: tinyhumansai/openhuman#7328 · Sandbox execution side: tinyhumansai/tinybox#27

Goal

Turn this repo (currently the unrenamed Rust template) into tinysecurity: one TinyBus module that owns every policy, approval, auto-approve, approval-log, redaction/privacy, audit and sandbox-planning decision OpenHuman makes today. OpenHuman currently spreads this over crates/openhuman-core/src/security/ (~21.5k LOC non-test), sandbox/, three or four tool-policy layers, about eight redactors and four SSRF checks, with several config fields that nothing enforces. Reference designs: tinywallet (contract crate + module cdylib), tinysearch (MODULE.md, credentials via init/reinit config), and NVIDIA OpenShell (declarative versioned policy, policy-change review, credentials injected only at approved endpoints).

Non-negotiable delivery rule: bus-only

tinysecurity is compiled and loaded as a TinyBus native module. Its dependencies are not shared with the host.

  • The host (OpenHuman) depends on exactly one crate from this repo: tinysecurity-bus (names, payload types, errors, contract version; sync, no I/O, no runtime, no transport). CI asserts the bus crate stays transport-free (template check) and OpenHuman adds a manifest check that no other tinysecurity-* crate is a dependency.
  • Every other crate here is publish = false and internal to the cdylib: regex, SQLite, HTTP client, tinyinference-decisions, etc. are compiled into the module and never unified with the host graph.
  • Rust traits cannot cross the cdylib boundary, so "seams" are bus contracts: host-served callbacks (approval persistence on the host storage backend, approval prompt/await, audit forwarding, keyring consent; the memory_host.rs pattern) and init/reinit config (policy config; the Jev judge's endpoint and credential, like tinysearch credentials; never in call arguments).
  • Futures and task-locals do not cross the bus: requests carry origin/thread/flow/agent context. RequireApproval(request_id) returns to the host, which parks the call and prompts; Decide resumes it. The module owns the state machine, TTLs and log.
  • Fail closed everywhere. Module absent, faulted or timed out ⇒ external-effect calls are denied; the module is never retried after a fault in-process.

Workspace layout

crates/
  tinysecurity-bus/        contract (the only crate hosts see)
  tinysecurity-policy/     rules, command/path classification, floor, rate limit, Decision
  tinysecurity-approval/   request state machine, TTL, decisions, flow trust, approval log
  tinysecurity-auto/       auto-approve engine + Jev judge
  tinysecurity-redact/     one redactor, PII, URL/args, prompt-injection
  tinysecurity-egress/     privacy modes, descriptors, SSRF/private-host guard
  tinysecurity-sandbox/    SandboxPlan resolution (no execution)
  tinysecurity-audit/      audit event model, sinks, rotation
  tinysecurity-crypto/     (phase 3) password crypto, keyring backends, device crypto, pairing
  tinysecurity/            umbrella lib (internal)
  tinysecurity-module/     cdylib, ABI v1 exports, bus adapter

Bus: ai.tinyhumans.tinysecurity.Policy, object /ai/tinyhumans/tinysecurity/Policy. Follow tinywallet's layout, lints (unsafe_code = "forbid", clippy pedantic, no unwrap/panic in library code), mod.rs/types.rs/*_tests.rs structure, and copy ci.yml/release.yml from tinywallet.

Step-by-step plan

Phase 0: Scaffold (T0)

  1. Rename crates/template* → names above; set description, repository, keywords; update MODULE.md (bus name, members, contract version, credentials-in-init-config statement, fail-closed statement), AGENTS.md project-structure section, SECURITY.md contact, license check.
  2. Replace ROADMAP.md with the real phases; add docs/specs/, docs/plans/, ADRs: 0002 bus-only delivery, 0003 fail-closed semantics, 0004 policy-vs-sandbox split (plan here, execute in tinybox), 0005 Jev judge safety rules.
  3. Keep vendor/tinybus pinned. Remove the greeting placeholder from both crates.

Phase 1: Contract tinysecurity-bus (T1)

Members (all request/response types versioned, #[non_exhaustive] where evolution is expected):

  • Init/Reinit config: PolicyConfig (autonomy level, enabled, workspace/trusted roots, forbidden paths, allowed commands, rate limit, cost cap, privacy mode, approval TTLs, sandbox defaults, auto-approve config incl. judge endpoint/credential/thresholds, audit config).
  • Evaluate(ToolCall{tool, args, class, origin, thread, flow, agent, call_id, tier}) → Decision {Allow, RequireApproval{request_id, reason}, Deny{reason}}; Check (batched path/command/url checks for in-tool use); PolicyChanged generation counter so hosts cache by (generation, fingerprint).
  • Approvals: Decide, ListPending, ListDecisions, RecordExecution, PreauthorizeFlow, ParseReply.
  • Redact, ScanPrompt, ScanToolDefinition, EgressCheck, UrlGuard.
  • Plan(SandboxRequest) → SandboxPlan.
  • Audit append/query.
  • Host-served callback interfaces: ApprovalStore, ApprovalPrompt, AuditSink, KeyringConsent.
  • Contract version and the host bind rule. Latency budget documented per member (hot path: Evaluate, Check).

Acceptance: contract crate compiles with only serde + thiserror; round-trip serde tests for every type; golden wire fixtures committed.

Phase 2: Pure engines (each its own PR, with tests ported from OpenHuman and then deleted there)

  • T2 policy. Port security/policy/{types,command_checks,path_checks,enforcement}.rs minus config binding. Build the rule model on tinytools ToolRules/ApprovalDirective (single tinytools copy via the vendored tinyagents; do not add a second). Absorb tinybox_core::shell::{classify,scan,env_guard} here as command classification (policy, not sandboxing). Keep is_always_forbidden (credential stores, system roots, .., NUL) as an unconditional floor even when autonomy is disabled. Implement max_cost_per_day_cents for real or drop the field (decide in the ADR). Add the action-rate limiter.
  • T3 redact. One pattern registry replacing: security/core.rs, util/redact.rs, approval/redact.rs, security/scrub.rs (tinymemory safety policy; decide to wrap or absorb), pii, core/log_redaction.rs, tools/registry/denials.rs, integrations/composio/tools/redact.rs, the vendor credential_scrub pattern list. Modes: secret-token, PII/identifiers, URL/endpoint, home-path, key-based args, free text. Absorb the prompt-injection detector and scan_tool_definition. Property tests: idempotence, no-secret-survives corpus, no catastrophic regex backtracking (fuzz).
  • T4 egress. Privacy modes (LocalOnly, Standard, Sensitive; define Sensitive for real or delete it), EgressDescriptor/DataKind/IdentificationRisk, local-only enforcement, SSRF guard: link-local 169.254.0.0/16 (cloud metadata), CGNAT, IPv6 ULA/link-local, IPv4-mapped IPv6, decimal/octal/hex host forms, and a resolved-IP check (resolve, validate every address, pin the connection) to defeat DNS rebinding. Domain allowlists (http_request, browser).
  • T5 sandbox planning. SandboxRequest{command class, source trust tier (user/agent/mcp/skill/download), saas, env override, agent mode, config, needs_network, grants} → SandboxPlan{backend (None/Jail/Namespace/MicroVm/Docker), grants, network policy, resources, reason}. Rules: SaaS always sandboxed and OPENHUMAN_SANDBOX=off never applies under SaaS; untrusted tiers fail closed (no silent Noop); credential-store floor excluded from every grant; host-network requires approval. The module does not execute: the host maps the plan onto tinybox. See Make tinybox sandbox-only: capability honesty, resource limits, network policy, events, bus contract (Linux/macOS/Windows) tinybox#27.
  • T6 audit. Event model for approvals, shell, sandbox decisions, policy changes, judge verdicts; JSONL sink with rotation; callback forwarding to the host AuditSink; one log, configurable (enabled, path, max size), covering more than shell. auto_approve_all must still write audit rows.

Phase 3: Approval and auto-approve

  • T7 approval. Port the ApprovalGate state machine (parked → decided/expired/executed), TTLs (default 10 min; copilot and sub-agent 3 min), decisions approve_once/approve_always_for_tool/approve_always_for_flow/deny, flow trust, parse_approval_reply, restart-survival through the host-served ApprovalStore (the module does not own SQLite on the host's behalf), redaction of everything persisted/broadcast, terminal execution-outcome rows. Origin rules (cron: read-only tools only, external effects denied).
  • T8 auto (Jev). Rule layer: allowlist, flow trust, origin policy. Judge layer on tinyinference-decisions (Client, EvaluationRequest::jev, Question::{Choice, Score, Noul}): send redacted call state with 2–3 independent questions (does this match the user's request, is it reversible, does it exfiltrate data). That crate deliberately does not infer permission from a probability, so thresholds live here, per origin and class. Hard rules: Jev can only convert RequireApproval → Allow; never overrides Deny or the floor; timeout/error/low confidence/unreachable ⇒ human approval; every verdict logged with question, scores, threshold (auto:jev); judge input goes through the redactor first; off by default, per-origin opt-in; judge endpoint/credential only via init config; cache + budget cap to keep it cheap. Tests with a local stub judge on all three OSes (no real service).

Phase 4: Module and release

  • T9 module. tinysecurity-module cdylib with ABI v1 exports, provides/methods, host-callback client, init/reinit handling, single-fault policy (no retries after a fault), attestation. Release workflow per tinywallet (11 host keys, checksum.toml), release smoke test that loads the published archive through TinyBus and calls Evaluate for both an allow and a deny.
  • T10 policy-change review ("prover" light). Versioned declarative policy file + schema; diff two policies; flag changes that grant new reach (new path, host, tool, class, auto-approve origin, judge enablement) and require human approval before activation. PolicyChanged bumps the generation.
  • T11 crypto (last). Move encryption/core (Argon2id + AES-256-GCM), keyring backends and crypto, devices/crypto (X25519/HKDF tunnel), pairing guard. Unify three crypto stacks into one; keep keychain consent callbacks host-side.

T12 Quality gates

Conformance suite over the bus contract; fuzz targets (command parser, path normalizer, redactor, URL parser); property tests; 90% per-file coverage gate; cargo deny; MSRV build; rustdoc -D warnings.

Cross-OS end-to-end tests (required)

Every phase ships Linux, macOS and Windows e2e tests on real runners (no mocking of OS behavior). Matrix: ubuntu-24.04, ubuntu-22.04, macos-15/macos-26 (Intel + Apple Silicon where the release matrix has them), windows-2025/windows-11-arm. Skips are listed explicitly per OS in-repo with a reason, never silent.

Area Linux macOS Windows
Path policy ~/.ssh/~/.aws/~/.gnupg floor; symlink, .. and NUL escapes; case-sensitive FS case-insensitive APFS; /private/var, /var, /tmp symlinks; firmlinks; /Users/x drive letters, UNC, \\?\, 8.3 short names, reserved names (CON, NUL), alternate data streams, case-insensitivity, C:\Users\x
Command classification sh/bash/dash, $(…), heredocs (quoted = data, unquoted = scanned) zsh default, sh as bash PowerShell and cmd.exe quoting, ^ escapes, &/&&, .exe/.bat resolution
Redaction /home/x strip, env var forms /Users/x C:\Users\x, %USERPROFILE%, backslash JSON escaping
SSRF guard resolve + pin; IPv6 same (system resolver differences) same (Windows resolver, IPv6 zone IDs)
Sandbox planning Landlock/namespace/microvm/docker selection by capability Seatbelt/docker; namespace/microvm ⇒ clean Unsupported AppContainer/docker; namespace/microvm ⇒ clean Unsupported; untrusted tier fails closed without a real jail
Audit log rotation, permissions 0600 same ACL-restricted file, file locking during rotation, long paths
Approval lifecycle park → decide → TTL → restart survival → execution row same same (file-lock and path differences)
Module load .so + digest admission .dylib + digest admission (+ codesign/quarantine notes) .dll + digest admission; locked-file replacement
Crypto/keyring Secret Service / encrypted file Keychain Credential Manager
Jev judge (stub) timeout/error ⇒ human same same

Definition of done

  • Contract published; golden fixtures; latency budget documented and benchmarked (p50/p99 Evaluate/Check).
  • Every crate has tests ported from OpenHuman plus new ones; three-OS e2e green in CI.
  • Released archive loads through TinyBus on all release-matrix hosts; checksum.toml published.
  • OpenHuman has migrated (see tracker) and the old code and tests there are deleted.
  • Documentation: MODULE.md, specs, ADRs, an OpenHuman gitbook page.

Activity

  1. tinysweeper commented on Oct 10, 2026

    @tinysweeper

    Large roadmap-style proposal to build a new tinysecurity bus module consolidating policy, approvals, redaction, egress, sandbox planning, audit and crypto from OpenHuman into this repo; this is new functionality/architecture work, not a defect.

    Labelled priority: p3.

  2. senamakel commented on Oct 15, 2026

    @senamakel
    MemberAuthor

    Host tracking: merged PRs #7, #8, #9, #10, #11, #12, #13, #14, #15, #16, #17, #18 are released through v0.7.0, which openhuman PR tinyhumansai/openhuman#7331 pins. Host migration is still in progress (tracker tinyhumansai/openhuman#7328).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions