openlatch-client 0.3.3

OpenLatch runtime enforcement node — the capture-and-enforce adapter that evaluates every covered action against a coding agent's Autonomy Zone before it runs
# AGENTS.md — what openlatch-client is, and what it may not become

Macro decisions and product perimeter only. Granular, fast-moving detail lives in
path-scoped rules under `.claude/rules/`, loaded automatically when you touch the
code they cover. Anything a CI gate already enforces is deliberately absent here —
the gate is the instruction, and restating it just gives it a second copy to drift
away from.

## What this is

The on-host node of a runtime control plane for AI coding agents. It captures hook
events from the agent, wraps them in a CloudEvents envelope, evaluates a resident
policy bundle **locally** to return a verdict before the action runs, and forwards
the event to the platform asynchronously.

It also carries a model boundary — a loopback listener the agent's provider traffic
is routed through — and a configuration monitor that reports changes to agent
config files.

## Perimeter — what it does not do

| Perimeter Boundary | Rule & Architectural Constraint |
|---|---|
| Policy Authoring | **Platform-only.** The cloud writes and serves the rules; the client evaluates them. Everything past deterministic rule matching (correlation, cross-host analysis, scanning) stays cloud-side. |
| Event Normalization | **Opaque data.** The envelope's `data` is the agent's raw payload: opaque, never rewritten. `source` and `type` are open strings, so an unknown agent or event is valid and round-trips verbatim. Never add a path that rejects one. |
| Network on Verdict Path | **Zero remote calls.** Evaluation reads an in-memory handle and returns. A hook event that needs a round trip to decide is a design error, not a slow path. |
| Provider Traffic Mutation | **Default off.** The forward path sends the original bytes, with one narrow and default-off exception (`[boundary] transforms_act`). |
| Enforcement Reach | **Requires agent translator.** Enforcement reaches an agent only once a translator exists in `src/hook_output/` to express a deny in that agent's own hook-output shape. Without one the deny is discarded before the developer ever sees it. |
| Developer Tooling | **Standalone crates.** No sandbox subcommand or debug-only flags in client binaries. Internal tools live in `tools/` as standalone crates that link no client code. |

## Shape

Two binaries from one crate. `openlatch` is the full CLI and daemon. `openlatch-hook`
is the lean binary the agent invokes on every tool call, and it must stay lean — the
whole `core` module is feature-gated so the hook never links it. A dependency that
reaches the hook path is a decision, not a detail. `cargo test --lib
--no-default-features` is what proves it — **plain `cargo test` links everything and
passes on a violation.**

That binary runs on the developer's critical path, spawned fresh per event. Anything
costing a round trip there — a keychain read, a network call, a large
deserialization — is paid on every tool call they make, all day.

Verification gates:
- Feature isolation: `cargo test --lib --no-default-features`
- Hot-path dependencies: Frozen against `ci/baselines/hook-deps.txt`
- Binary size limits: ~1.14 MB baseline (`ci/baselines/hook-size.txt`), <20 MB hard CI ceiling for `openlatch-hook`, <30 MB for `openlatch` (enforced by `.github/workflows/pr-checks.yml`)

## Invariants no gate catches

- **Fail-static, not fail-open.** A resident bundle keeps enforcing offline, forever,
  with no expiry; a failed refresh keeps the last known good. Genuine fail-open
  exists in exactly two places — no bundle was ever fetched, and the hook cannot
  reach the daemon. Do not add a third.
- **Everything fails toward the original bytes.** In the model boundary a panic, an
  error or a failed validation forwards what the agent sent. Never toward a modified
  request, never toward a block.
- **Off is never a pass.** In any diagnostic, a subsystem switched off is a warning
  carrying a code and an actionable remedy. Green means enabled *and* proven working.
  The three narrow `NotApplicable` carve-outs are named in the CLI rules (`.claude/rules/cli-output-contract.md`: telemetry and crash reporting opt-outs) and isolated-instance rules (`.claude/rules/isolated-instances.md`: persistence in sandboxes); **there is no fourth**.
- **One question, one set of detectors.** Every command answering "is this host
  healthy?" renders from the same detectors (`crate::cli::commands::doctor::run_all_checks`).
- **An explicit credential wins.** Lookup runs environment → OS keychain → encrypted
  file (the hook's HMAC key is the one documented departure, reading disk first), so an explicit value
  overrides a stale keychain entry and a headless caller never trips a keychain dialog.
- **A supervised task's factory produces a fresh future on every call.** A task that
  owns a receiver parks it behind a shared lock (`Arc<Mutex<_>>`) and re-acquires per run; otherwise a
  panic takes the queued messages with it.
- **Platform differences live in one place.** Use the existing owner-only file helper
  (`src/core/fs_secure.rs`) and the path-comparison helpers (`src/core/path_compat.rs`) instead of re-deriving a `cfg` branch at a new call
  site.

## Working on it

Anything that starts a daemon, installs hooks or writes agent config runs inside an
isolated instance. The client supports the seams, `tools/sandbox` derives them from a
name, and the E2E harness refuses to run without them. Never point a run at your own
install — the failure mode is rewiring every live agent session on the machine.

```bash
cargo install --path tools/sandbox --locked   # once; --locked is not optional
olbox exec <name> -- <cmd>                    # or `olbox enter <name>` for a shell
```