# 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 relay — 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 | **One shape, mapped once.** The envelope's `data` is mapped at the single egress onto the common event shape (`src/core/envelope/normalize.rs::to_common`), so no consumer carries a parser per agent. The mapping matches on payload SHAPE, never on `source` — a `match source` answers `None` for the fourth agent and for every fork of one. A payload that had to be rewritten carries its original verbatim under `raw_agent_payload`; one already in the common shape is not touched and carries no copy of itself. `source` and `type` stay open strings, so an unknown agent or event is valid and round-trips. 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 (`[model_relay] 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 relay 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
olbox --version # 0.2.0 or newer for Rust paths and command lookup
cargo install --path tools/sandbox --locked --force # install or update; --locked is not optional
olbox exec <name> -- <cmd> # or `olbox enter <name>` for a shell
```
### Sandbox binary provenance
**A provenance-sensitive `olbox` run never uses the host's `openlatch`
binary.** For any sandbox that starts a daemon, installs hooks, or drives an
agent, build both binaries from
the task's target client checkout *inside the sandbox environment*, which sets
`CARGO_TARGET_DIR="$OPENLATCH_DIR/build"`.
Its `PATH` searches that build's `debug` then `release` directories before the
inherited host path, and its `OPENLATCH_BIN` / `OPENLATCH_HOOK_BIN` defaults
select one matching profile. For provenance-sensitive runs, verify those values
resolve to `$OPENLATCH_DIR/build/<profile>/openlatch` and the sibling
`openlatch-hook` before invoking them. Never mix a daemon from one checkout
with a hook from another. The retained host `PATH` is a convenience fallback,
not process confinement or valid provenance evidence.
Record the checkout commit and both `--version` values before exercising the
agent; refuse the run if they differ. This deliberately also applies to a
clean checkout: an old `cargo install` is not a valid stand-in for current
source. The only exception is a test explicitly validating a published client
artifact; it must install and verify that one wrapper/binary pair instead.
### `olbox` versioning
`tools/sandbox` is a standalone, `publish = false` crate with no release
workflow, but its package version is still the version developers see and use
to decide whether a local `olbox` is current. Bump `tools/sandbox/Cargo.toml`
with every behavior, command, or environment-contract change: use a minor bump
for new behavior/commands and a patch bump for fixes. Keep the package entry in
`tools/sandbox/Cargo.lock` synchronized and verify with `--locked`. The runtime
freshness notice compares these semver versions and tells developers how to
install the newer one; the Git revision remains build identity, not a substitute
for a version bump. Update the documented minimum `olbox` version when a feature
requires it, and keep the N-1 test asserting the `A new version of olbox
(vX.Y.Z) is available` message and install command. This is local source
versioning; it does not require tags or a separate release cycle.