Skip to content

v0.1.2

Latest

Choose a tag to compare

@github-actions github-actions released this 26 Jun 10:22
Immutable release. Only release title and notes can be modified.
2876b41

0.1.2

Released on 2026-06-26

Added

  • run: wire live IssueCapability gRPC client (FIR-186)

    Relocate the live IssueCapability call from the sidecar into firma run.
    Each run mints a per-session capability over gRPC, writes it to
    $XDG_RUNTIME_DIR/firma/capabilities/<sandbox_id>.toml, and the autostarted
    sidecar consumes it via [capability_seed]. The seed file is removed on run
    exit (kill-on-Drop). Retire the sidecar [preflight] issuance path and
    deprecate the operator-driven [capability_seed] flow.

    • firma-core: canonical CapabilitySeed on-disk seed schema
    • firma-proto: client::build_channel moved here; sidecar delegates
    • firma-run: capability::issue mint + CapabilityFileGuard
    • sidecar: remove [preflight]; deprecation warning for operator seeds
    • firma config: drop [preflight] scaffolding
    • docs + examples updated; live-mint e2e tests added

    Closes FIR-186- sidecar: present pre-shared-key credentials to Authority (#164)

    • refactor(proto): source wire contract from firma-protobuf submodule

    Add firma-protobuf as a submodule and compile the proto definitions from it
    instead of the copy vendored under crates/firma-proto/proto. Generated bindings
    are unchanged; this removes duplication so openfirma and firma-team share one
    source of truth.

    • feat(sidecar): present pre-shared-key credentials to Authority

    Add optional [authority.credentials] config so the Sidecar authenticates
    to a firma-team Authority with a workspace_id, sidecar_id, and a PSK
    resolved once at startup from an env var or file. Credentials are attached
    to IssueCapability, WatchPolicyBundle, and WatchRevocations requests, and
    omitted entirely when unconfigured so the local mini-authority dev loop
    stays keyless. The PSK is wrapped in a redacted-Debug newtype and never
    logged.

    Closes FIR-223- add path normalization in IntentNormalizer to prevent canonicalization mismatch bypass (#170)

    Summary

    Adds path normalization in the Sidecar's before policy evaluation to
    prevent canonicalization mismatch bypasses (SEC-4).

    Changes

    • Added dependency
    • Added function implementing:
      • Strip fragment ()
      • Collapse double slashes ( → )
      • Strip trailing slash (except root )
      • Apply NFC normalization to non-ASCII paths
    • Integrated normalization into - path is normalized before matching
      against the mapping table
    • Added comprehensive tests for the normalization function

    Security

    This prevents attackers from crafting requests that pass Sidecar policy
    under one path representation (e.g., ) but are treated differently by
    the origin server (e.g., ). Pairs with SEC-4 since both touch the
    path/query field.

Changed

  • proto: replace firma-protobuf submodule with crates.io crate (#175)

    Thinking Path

    • OpenFirma is a runtime enforcement boundary. Every outbound agent
      call passes through a local Sidecar that evaluates Cedar policies and
      either allows or denies it.
    • The change touches the shared wire contract used by firma-sidecar,
      firma-authority, and firma-run (the protobuf and gRPC message
      definitions).
    • Until now those definitions lived in a git submodule that the build
      compiled locally with protoc. That made checkouts and CI heavier, and
      tied the build to a private submodule.
    • The same definitions are now published as the firma-protobuf crate
      on crates.io, so the submodule is redundant.
    • This pull request removes the submodule and the local code that
      compiled it, and uses the published crate instead.
    • The benefit is a simpler checkout, simpler CI, and one shared source
      of truth for the wire contract.

    What Changed

    • Removed the firma-protobuf git submodule and the local firma-proto
      crate that compiled it.
    • Switched every part of the codebase to use the published
      firma-protobuf 0.1.1 crate from crates.io for the wire contract types.
    • Moved the helper that opens the connection to the Authority service
      into the Sidecar, since that helper was the only hand-written piece the
      old crate carried.
    • Removed the build step that generated code from the submodule, and
      removed the submodule checkout from the CI workflows.
    • Updated the project documentation and contributor templates to match.

    Verification

    cargo build --workspace
    cargo clippy --workspace --all-targets -- -D warnings
    cargo nextest run --workspace
    cargo test --doc --workspace
    dprint check
    zizmor .github/workflows/
    • cargo nextest run reports 767 passed, 9 skipped.
    • clippy, doctests, dprint, and zizmor all pass with no findings.

    Security Considerations

    The hot path (normalizer, Stage 1, Stage 2) is unchanged. No fail-closed
    behaviour changed. No network call was added or removed on the hot path.
    Trust boundaries (token verification, Cedar evaluation, audit signing)
    are untouched. Capability issuance, revocation, and scope matching are
    unchanged. This is a build and dependency change only; the message types
    and their behaviour are identical to before.

    Risks

    Low risk. The wire format is identical because the published crate is
    generated from the same protobuf definitions the submodule held. The
    change is mechanical: same types, new source.

    Model Used

    Claude, claude-opus-4-8 (Opus 4.8), extended thinking with tool use.

    Checklist

    • Thinking path traces from project context down to this specific
      change
    • make check passes locally (fmt + toml-fmt + lint + test + build
    • audit + deny)
    • No .unwrap(), .expect(), panic!(), or unsafe introduced
      outside of test code
    • All error paths are fail-closed — errors produce DENY, not silent
      ALLOW
    • No network calls added on the hot path (normalizer → Stage 1 →
      Stage 2)
    • Tests added or updated for the changed behaviour
    • If this touches the normalizer or action-class registry, mapping
      table tests are updated
    • If this changes wire format, config schema, or CLI behaviour, docs
      are updated under docs/ and docs-site/
    • Security considerations are documented above (or "None apply" is
      stated explicitly)
    • Model used is specified with version and capability details
    • I will address all reviewer comments before requesting merge

    Closes FIR-376

Documentation

  • make sandbox claims invariant-based (#189)

    at FIR-114 we decided to
    rework the sandbox boundary docs to be invariant-based and remove the
    quiet over-claim: docs should say what each backend can prove, not what
    the architecture would like all backends to become.- separate sandbox intent from shipped confinement (#190)
    still part of https://linear.app/firma-ai/issue/FIR-114- add firma-run known limits (#191)
    still part of FIR-114

    this updates the limits that matter for the current release.

Fixed

  • sidecar: bound concurrent body buffering with AtomicUsize budget

    A hostile or buggy agent could send 1000+ concurrent requests with
    near-max-size bodies, each buffering up to 4 MB, and OOM-kill the
    enforcer. Since the Sidecar is the agent's only outbound path, the
    agent dies too — but the operator sees a confusing "Sidecar
    crashed" symptom for what is really a resource-exhaustion DoS.

    Add a shared BodyBudget (AtomicUsize) with a configurable global
    ceiling (default 64 MB). Each request frame try_acquire()s budget
    byte-by-byte; on exhaustion the request is denied immediately with
    a 403 and an attributable audit trail rather than silently growing
    the heap. Budget is released after the handler completes.- sidecar: WAL compaction must always keep newest event
    The compaction walk drops lines that exceed target_bytes, but if
    ALL lines exceed the target (e.g. a single large event recycles
    through compaction repeatedly), it produces an empty WAL followed
    by a rewrite of just the incoming event. This wastes I/O and can,
    under exact boundary conditions, cause a flaky test where the
    newest event appears missing from a subsequent read.

    Change the break condition to always keep at least the first (=
    newest) line encountered during the backwards walk. This matches
    the invariant that losing the most recent audit signal is strictly
    worse than briefly exceeding the soft target.

    Tighten the compaction test to use a clear margin
    (cap = 2.5 lines) instead of an exact byte boundary, and add a
    dedicated test that verifies the always-keep-newest invariant
    when target_bytes < one_line_len.

Testing

  • e2e tests harness + simple_prompt scenario (#173)

    Summary

    Black-box E2E harness validating the OpenFirma enforcement boundary
    against real coding agents (Claude Code + Codex CLI).

    • tests/e2e/ — new test target, hooked into the firma crate via
      [[test]] (no separate workspace member).
    • Each scenario runs two phases:
    1. Baseline — agent runs directly (no firma). Confirms it can
      complete the task + reach the mock server unconfined.
    2. Enforcement — agent runs under firma run. Confirms the expected
      ALLOW/DENY outcome + correct audit events.
    • Agent traffic is intercepted with a wiremock MockServer; LLM
      provider calls are stubbed so tests need no live API.
    • Audit trail parsed from the sidecar's JSONL log and asserted via
      insta snapshots (per agent + scenario).
    • scenario_tests! macro generates one #[tokio::test] per (agent, scenario); all #[ignore] — run with --include-ignored.
    • One scenario wired: simple_prompt (greeting → LLM provider → ALLOW).
      Remaining scenarios land in the follow-up PR.

    Modules

    File Role
    setup.rs ScenarioSetup builder, FirmaConfigBuilder, git
    workspace init, mock server
    scenario.rs EnforcementScenario trait + PhaseOutput
    runner.rs run_scenario — drives baseline + enforcement phases;
    filters provider allow-traffic before assertions
    agent.rs AgentKind (Claude/Codex), spawn args, per-agent
    provider domains
    audit.rs FirmaAuditTrail — parse JSONL audit log,
    exclude_provider_allow_events, snapshot assertions
    config.rs / policy.rs firma config + Cedar policy builders for
    scenarios
    scenarios/simple_prompt.rs the simple_prompt scenario

    Supporting changes

    • e2e-tests.yml CI: matrix ubuntu-latest (bwrap) × macos-latest
      (vz) × claude + codex; on v*.*.* tags + workflow_dispatch;
      actions pinned to commit SHAs.
    • Audit snapshots drop the agent's own provider allow events
      (denials kept) before asserting. An agent must reach its provider to
      function, so that traffic is implied by a successful run and only adds
      platform-dependent noise — e.g. codex dials files.openai.com on macOS
      but not Linux. Keeps snapshots deterministic across operating systems.
    • nextest e2e profile + make e2e entry point; builds firma (debug)
      unless FIRMA_BIN is set.
    • firma-run fixes for spawned sidecar config: pin ca.dir to marker
      dir, strip TLS/ephemeral port in resolve_persisted_paths, wrap
      authority config in [authority].
    • normalizer: classify *.chatgpt.com subdomains as
      communication.external.send.

    Run

    make e2e
    
    # single agent / scenario
    cargo nextest run -p firma --test e2e --profile e2e -E 'test(claude::)'
    cargo test --test e2e -- 'claude::simple_prompt' --include-ignored

    Prerequisites

    • At least one agent on PATH: claude or codex
    • bwrap on Linux; vz sandbox on macOS (OS-provided)- add child-process escape regression target (#197)
      firma run governs a launch once, for the command it directly execs;
      any process that command spawns runs with no governance decision. A tool
      denied as the root command can therefore still run, ungoverned, as a
      child of an allowed root.

    This is a Rust integration test that drives a firma binary through a
    real bwrap sandbox, autostarted sidecar/authority, and real command
    governance.

    It asserts the secure behavior: an allowed bash root must not let a
    denied forbidden-tool child execute.

    This test is disabled until this problem is resolved.

    Thinking Path

    • OpenFirma is a runtime enforcement boundary — every outbound agent
      call passes through a local Sidecar that evaluates Cedar policies and
      either allows or denies it. The same fail-closed philosophy ext
      ends to local command execution: firma run sandboxes an agent
      (bwrap on Linux) and gates which executables may launch.
    • This change is in firma-run's command-exec governance,
      exercised end-to-end through the firma CLI crate (where the
      firma binary and its integration tests live). The governance path is
      r untime.rs::execute_runresolve_governed_executable (client-side
      allowlist) → enforce_local_command_governance (sidecar local-exec
      round-trip) → backend.start_agent (exec the root). > - The problem
      (FIR-366): both governance checks run once, for the root command
      only
      . After the root is exec'd, nothing re-enters either check for
      descendants — so a tool that is denied as the ro
      ot can still run, ungoverned, as a child of an allowed root (e.g.
      bash -c forbidden-tool). The "governance binds to the launched root"
      assumption is violated for the whole process subtree.
    • It needs attention now because the allowlist is a security control
      whose guarantee silently does not hold for child processes; the gap was
      reproduced against the real binary in spikes/fir-366/repro
      but had no committed regression coverage in the repo.
    • This pull request ports that reproducer to a proper Rust integration
      test that drives the real firma binary through a real bwrap sandbox +
      autostarted sidecar/authority, and asserts the secure b
      ehavior (an allowed bash root must not let a denied forbidden-tool
      child execute). > - The benefit is a committed, deterministic regression
      target: it fails today (documenting the gap precisely) and turns green
      when child-process governance lands, with a control case proving the
      allowli
      st still denies the same tool as a root command.

    What Changed

    • Added crates/firma/tests/child_process_escape.rs: an #[ignore]d
      integration test (child_process_escapes_run_governance) that:
    • Bootstraps a full config via firma config (authority + sidecar +
      bwrap generic profile with a read-write workspace mount), then patches
      [run.profiles.generic.sidecar_local_exec] to enforce an al
      lowlist where bash is allowed and a dropped-in forbidden-tool is
      not.
    • Stands up a tiny in-process Unix-socket allow-all endpoint speaking
      firma's exact local-exec wire protocol (newline-framed JSON request →
      {"decision":"allow"}), recording every governed executable —
      no Python dependency. - Control (passes today): asserts
      forbidden-tool as the root command is denied and never runs.
    • Feature under validation (fails today): asserts bash -c forbidden-tool does not let the child execute, and that governance was
      consulted for bash but never the child.
    • Skips cleanly (not fails) when Linux/bwrap/bash are unavailable, and
      treats a sandbox-startup failure (allowed root never ran) as
      inconclusive rather than a false pass.
    • No production code changed.

    Verification 01:33 [15/693]

    just and cargo-nextest are not installed in this environment, so I
    ran the cargo equivalents of each just check stage:

    # fmt (just fmt → dprint check), repo-wide                                                                                                                                                                  dprint check                                   # clean
    
    # lint (just lint), workspace, all targets — compiles the new test target
    cargo clippy --all-targets -- -D warnings      # Finished, 0 warnings
    
    # the new test is ignored by default → CI stays green
    cargo test -p firma --test child_process_escape
    #   test child_process_escapes_run_governance ... ignored
    #   test result: ok. 0 passed; 0 failed; 1 ignored
    
    # run it explicitly → reproduces the FIR-366 gap (the regression target)
    cargo test -p firma --test child_process_escape -- --ignored
    #   FIR-366: the forbidden-tool CHILD executed ungoverned under an allowed bash root
    #   firma run exit: Some(0)   FORBIDDEN-TOOL EXECUTED
    #   test result: FAILED. 0 passed; 1 failed
    • Not run: full just check (audit/deny and cargo test --doc) —
      toolchain (just, nextest) unavailable here. The change adds only one
      ignored test file, so dependency/audit/doctest surfaces are
      unaffected.

    Security Considerations

    • Hot path (normalizer → Stage 1 → Stage 2): not touched. This is
      firma run local command-exec governance, not the sidecar request hot
      path.
    • Fail-closed: preserved and, in fact, this test documents a gap
      in it — the local-exec allowlist fails closed for the root but does not
      bind to child processes. The test asserts the desired fail-cl
      osed (DENY) behavior for children; it adds no code that could turn an
      error into a silent ALLOW.
    • Network on the hot path: none added. The test's in-process
      endpoint is a local Unix socket used only by firma run pre-exec
      governance, off the sidecar hot path. (Per the spike, network egress for
      the subtree is already structurally contained via netns + proxy; this
      gap is local-exec only.)
    • Trust boundaries (token verification, Cedar eval, audit signing):
      unchanged.
    • Capability issuance / revocation / scope matching: unchanged.

    Risks

    • Low risk. Test-only, #[ignore]d, no production code paths
      altered; cannot affect runtime enforcement or CI green (skipped by
      default).
    • The test is expected to fail when run explicitly with --ignored
      until child-process governance lands — by design, as a regression
      target. Reviewers running --include-ignored will see one failure
      here.
    • Environmental: requires Linux + bwrap (+ user namespaces) and bash;
      it skips cleanly when those are absent or the sandbox can't start, so it
      won't produce spurious failures on unsupported hosts.

    Model Used

    • Claude Opus 4.8 (Anthropic), model ID claude-opus-4-8, via
      Claude Code (agentic CLI with tool use / code execution), extended
      thinking enabled.

    Checklist

    • Thinking path traces from project context down to this specific
      change
    • [~] just check passes locally — just/nextest unavailable in this
      environment; ran equivalents (dprint check, cargo clippy --all-targets -D warnings, cargo test/--ignored, build via clippy)
      . audit/deny/doctests not run (unaffected by a single ignored test).
    • No .unwrap(), .expect(), panic!(), or unsafe introduced
      outside of test code (all such usage is in the test, under an explicit
      #![allow(...)] with reason)
    • All error paths are fail-closed — errors produce DENY, not silent
      ALLOW (no production paths changed; the test asserts the fail-closed
      expectation)
    • No network calls added on the hot path (normalizer → Stage 1 →
      Stage 2)
    • Tests added or updated for the changed behaviour
    • If this touches the normalizer or action-class registry, mapping
      table tests are updated (N/A — does not touch the normalizer)
    • If this changes wire format, config schema, or CLI behaviour, docs
      are updated under docs/ and docs-site/ (N/A — no wire/config/CLI
      change)
    • Security considerations are documented above
    • Model used is specified with version and capability details
    • I will address all reviewer comments before requesting merge

[FIR-112]

  • macOS structural confinement strategy decision: parity path vs ESF-native model (#141)
    • docs: document macOS confinement strategy and current runtime limits

    • feat(run): macOS sandbox-exec structural network mode

    • feat(firma-run): add macOS VZ guest launch contract

    • fix(firma-run): clean up VZ guest contract CI lints

    • fix(firma-run): scope macOS DNS stub to VZ backend

    • fix(firma-run): use sandbox path semantics for macOS profiles

    • refactor(firma-run): rename network confinement proof field

    • fix(firma-run): default routing test autostart flags

    • macos-vz: fix sandbox-exec loopback selectors

    macOS sandbox-exec was failing because we were handing it a bad sandbox profile.

    the structural experiment for the VZ backend uses TrustedBSD sandbox rules:
    deny network-outbound, then allow loopback so the agent can still reach the
    host proxy bridge and DNS refusal stub.

    Apple documents sandbox failures in this area as network-outbound violations
    in the App Sandbox diagnostics page

Ai

  • Provide guidance on commit methodology to ease the work for human reviewers (#188)- Tweak guidance on how to detect the VCS used by a local clone (#196)

    More precise in practice.

Build

  • replace taplo with dprint for TOML, Markdown, and Rust formatting

    dprint becomes the single formatter for the repo, expanding coverage from
    Cargo-manifest TOML only to all TOML, Markdown, and Rust under one tool and
    one CI job. TOML behavior matches taplo: dependency tables are sorted by
    dprint's default cargo conventions.

    • add dprint.json (toml, markdown, and exec/rustfmt plugins pinned by
      version + sha256 checksum)
    • Makefile: fmt now runs 'dprint check' (covers all files); install dprint
    • ci.yml: single Format job uses dprint/check@v2.3 + rustfmt toolchain;
      drops the taplo curl install and the separate cargo fmt job
    • CLAUDE.md: document the dprint workflow
    • remove now-unused .taplo.toml

    Formatting is applied in the following revision.- run dprint format check in the pre-commit hook- switch test runner to cargo nextest
    Use cargo nextest run for unit + integration tests in CI and the make test target, with cargo test --doc kept as a separate step since nextest does not run doctests.- pin and centralize CI tooling versions, enforce SHA pins (#162)
    Pin every GitHub Actions uses: to a commit SHA with a # vX.Y.Z comment (Renovate/Dependabot still track upstream via the comment).

    Centralize all dev-tool versions (protoc, dprint, node, cross, cargo-audit/deny/nextest/fuzz/doc-md) in tool-versions.env — a single KEY=value file consumed by the Makefile (include) and by CI (a "Load tool versions" step appends it to $GITHUB_ENV). No more per-file version duplication.

    Add --locked to every cargo binstall invocation. Drop zizmors ref-pin allowance so it now requires full SHA pins (hash-pin`). OS packages installed via apt/brew remain unpinned.

Config

  • report selected config paths on load errors (#193)

    Key changes:

    • Preserves ConfigResolveError.path when mapping resolver failures in
      firma-run.
    • Preserves ConfigResolveError.path when mapping resolver failures in
      firma-stack.

    This makes operator-facing errors point at the selected config file
    instead of a placeholder like firma.toml.

Dev

  • Improve repository setup for agentic workflows (#171)

    Remove the AGENTS.md/CLAUDE.md duplication.

    Add a basic set of skills for
    building/linting/testing/verifying/documenting Rust code, as well as
    workflows for history rewrites when using Jujutsu.

    I've included symlinks to ensure the new skills are picked up from
    Claude Code too. Other coding harnesses should work out of the box with
    .skills.- Use just as command runner instead of make (#182)
    I've also broken down the monolithic Makefile (now a justfile) into
    smaller scripts to keep things manageable.
    Cargo tool installations now go through binstall, as we do in CI.- Use expect rather than allow (#184)
    To prevent stale #[allow(...)]s, this PR configures clippy to reject
    them in favour of the #[expect(..., reason="...")] attribute.

    In the process, it performs a few clean-ups:

    • All allows for panic/expect/unwrap in tests are removed. They are
      unnecessary, given our clippy.toml file:
      allow-unwrap-in-tests = true
      allow-expect-in-tests = true
      allow-panic-in-tests = true
    • (Plausible) reasons are added where they were missing
    • All lints are set to warn. No need to use deny, since we fail on
      warnings in CI.- Clippy should fail on warnings when run locally (#186)
      Align behaviour with CI.

Firma

  • wrap helpers instead of spilling columns (#165)

Firma-config

  • minimize indirection, duplication and risk of TOCTOU bugs (#187)

    Key changes:

    • To minimize the risk of time-of-check-time-of-use (TOCTOU) bugs,
      resolve_config now loads configuration files rather than just checking
      for their existence;
    • Clarified documentation to remove references to non-existing
      functionality (e.g. firma having user-level config nested under XDG
      conventional paths)
    • Removed DirProvider, since the trait had only one non-test
      implementation. Introduced a bit of machinery to test everything via
      SystemDirs directly (run_isolated and run_isolated_with_env)
    • Minimize the risk of runtime issues by computing derived state on the
      fly (e.g. config_dir) rather than storing it alongside the data it was
      derived from.
    • Various clean-ups.

Firma-run

  • add macOS VZ guest launch contract (#151)

    cherry-picking the VZ guest work from #141

    the split is useful to isolated the guest work development to match
    Apples Virtualization.framework boundaries

Interceptor

  • fix budget release race and underflow safety
    • Replace fetch_sub-based release with fetch_update + checked_sub to make
      underflow impossible (not just debug_assert! which is stripped in release)
    • Add BudgetGuard RAII wrapper: release happens in Drop, not after await,
      making budget release cancellation-safe when clients disconnect mid-request
      (Hyper drops the per-request future when the client connection is reset)
    • BudgetGuard.clone() holds a shared Arc so all guards for the
      same connection share the same budget counter

Security

  • Upgrade quinn-proto to resolve security advisory (#195)

Style

  • apply dprint formatting to TOML + Markdown + Rust