# Invariants and Defaults

Humanish's safety and honesty rules come in exactly two strengths. Confusing them is how
harnesses rot: a route-scoped default gets written down as if it were doctrine, then the next
legitimate use case looks like a violation and either gets blocked (capability loss) or
waved through ad hoc (safety loss). This page is the boundary.

## Invariants

True on every route, enforced in code, never overridable by config. A feature that cannot
satisfy these does not ship; a third-party extension that cannot satisfy these does not
certify (see the conformance suite).

1. **Secret values never reach PUBLISHED evidence.** No key, token, credential value, or
   session-secret may appear in any artifact that leaves the operator's machine — a committed
   file, a shared bundle, a feedback issue. Names of provisioned environment variables are
   evidence; their values never are. Harness-level errors pass through redaction before
   persisting, because SDK error strings can echo secrets. The enforcement point is the
   PUBLISH/VERIFY boundary, not capture: a local run's text artifacts are scrubbed of
   secret-shaped values unconditionally, and raw screenshots (which may render on-screen
   content) are retained locally under gitignored `.humanish/` and never emitted by a publish
   command (feedback/review carry path strings, not pixels). In this repo the CI binary-asset
   scan additionally blocks them from commit; downstream projects do not get that scan — their
   protection is the init-scaffolded `.gitignore` plus their own review. To share a bundle
   as-is, set `policies.redactScreenshots: true` (blurs
   at capture), or use `humanish export --format bundle --redact-screenshots` to create a
   separately verified copy while preserving readable local evidence. (See "the capture-vs-publish rule" below — blurring frames at *capture* was a
   default mistaken for this invariant.)
   - Scope note: the literal scrub of KNOWN provisioned values runs on harness log-tails,
     errors, AND model-authored narration (reasoning/message) before it persists; secret-SHAPED
     values are caught everywhere by pattern redaction. The text artifacts are local
     (gitignored) regardless.
2. **Actors only drive harness-minted URLs.** A model with input control (mouse, keyboard,
   terminal) is only ever pointed at a URL the harness itself issued or validated under a
   declared policy (loopback entry, provisioned subject, declared external target). Never an
   arbitrary URL from unvalidated input.
3. **Live spend requires an explicit live invocation.** No omission or fallback may
   turn a dry run, preview, reader or unsupported route into provider or sandbox
   spend. An explicitly live supported study includes the disclosed default
   post-run analysis budget unless `review.analysis: false` disables it. Analysis
   has a separate admission estimate limit, not a provider billing cap or part of
   the actor budget. Spend-bearing tests retain their explicit env gates.
4. **Evidence verifies fail-closed.** A run bundle that cannot pass verification (schema,
   redaction status, artifact presence, public-safety scan) is a failed run, even when the
   session "worked." The gate applies to the harness's own error reports.
5. **Provenance is recorded or its absence is declared.** Every bundle states what the
   subject was (commit, image, fixture, or an explicit "unpinned" marker). Evidence that
   cannot say what it measured cannot support a decision.
   - External-public plane class (#164 phase 2, 0.20.0): when a real operator-owned public
     deployment is used DIRECTLY as a concurrent shared-world plane (`source: app-url` +
     `topology: shared-world` + `allowPublicTargets` + `concurrency > 1`), provenance is the
     marker `external-public` — humanish neither provisioned nor seeded it, so it is NOT `seeded`
     and NOT `unpinned`. Attribution stays `shared-world` (N seats, ONE plane), but every strength
     claim degrades HONESTLY and is asserted-ABSENT, never silently dropped: a synthetic attestation
     is FORBIDDEN (you cannot claim synthetic on a real site — the honest analog is the
     operator-ownership attestation `subject.publicTarget`, author-trust and unverifiable); plane
     control is "operator-attested, observed-only" (`plane.publicOriginDigest`) not
     "harness-controlled" (`plane.hostDigest`); there is NO authoritative shared-state proof
     (`stateSeries` omitted); and concurrency is proven by temporal co-occupancy + observed lobby
     convergence only. Verify FAILS CLOSED if any honest-downgrade limit is missing or if a
     getHost-only claim (`exposure: synthetic`, `hostDigest`, a `seeded`/`synthetic` limit) appears
     on this class. The getHost synthetic gate is deliberately unreachable here — it exists because
     getHost is internet-reachable AND harness-owned; a public site the harness neither provisioned
     nor exposed is neither.
6. **Claims match mechanism.** A config field that is parsed but not consumed warns; a
   document that overstates behavior is a defect; an evidence artifact never claims a
   stronger evidence class than its actor and scenario can support.
7. **Reclamation is by exact created id, never account-wide enumeration.** A route that
   creates a provider resource (an E2B sandbox, for example) tracks the exact id it created and
   reclaims and PROVES cleanup against that id alone (a returned found-and-killed boolean, a
   getInfo-by-id NotFound check). No route ever calls a provider's list-everything endpoint to
   discover or verify its own cleanup, because that endpoint enumerates the whole account/team,
   not just what humanish created. This is what makes a single shared operator key safe: humanish
   only ever reaches a resource it created, never anything else the key's owner is running.
   The one narrow exception is an explicit, maintainer-run orphan sweep (never a default, never
   reachable by a normal lab run) that is itself opt-in gated (an explicit flag or env var) and
   documented as such at its call site.

## Defaults

Right most of the time, overridable with declared friction (an explicit config field, an
env gate, a warning in the run output). Overriding a default is a supported use case;
silently drifting from one is not.

| Default | Why it is the default | Legitimate override |
|---|---|---|
| Dry-run | Spend safety (invariant 3 sets the floor; dry-run keeps the floor far away) | `scenario.mode: live` |
| Post-run analysis on supported live studies | Findings accompany the recording; a separate $3 admission estimate limit is visible before execution | `review.analysis: false` disables it; an explicit mapping sets another analysis budget. Dry-run and unsupported routes never dispatch; missing default credentials produce a recorded skip |
| Per-lane worlds | Isolation, attribution, reproducibility | `subject.topology: shared-world` — N seats against ONE provisioned, mutable plane for scenarios that ARE about interaction between roles (#164). `execution.concurrency: 1` (an explicit choice) = SEQUENTIAL turns (one sandbox); higher = CONCURRENT — and since #350 an omitted concurrency fills to the seat count, so every declared seat runs live at once by default (one getHost-exposed subject sandbox + N actor sandboxes driving it at once, synthetic-subject only). The bundle declares the weaker `attributionClass: shared-world` + a verify-enforced `attributionLimits` ceiling (the concurrent set drops `sequential-only` and adds `best-effort-causal-attribution` etc.), so the looser per-role attribution is honest, not hidden. |
| External key placement | Smallest blast radius: when the keyed process (e.g. a computer-use provider loop) runs outside the sandbox, its key never enters | In-sandbox placement when the keyed process runs inside (an agent harness under test); declared per actor type, with a spend budget |
| Loopback entry URLs | Public-safety: never drive third-party sites unbidden | `policies.allowPublicTargets` for an owner-declared deployment/preview (a Vercel preview of your own app). Multi-lane public/preview fan-out needs explicit `actors[0].lanes[].target` for every lane, so the adapter-owned topology is declared rather than inferred. Provisioned clone subjects always serve in-sandbox on loopback |
| Full-fidelity screenshots, local | The common case is watching a sim of your OWN app locally; blur destroys the deliverable. Raw frames live in gitignored `.humanish/` (this repo's CI adds a binary-asset commit scan; downstream projects rely on the scaffolded `.gitignore` and their own review) | `policies.redactScreenshots: true` blurs at capture; `export --format bundle --redact-screenshots` produces a separate verified copy of a completed run |
| Synthetic, seeded state | Pinned provenance; no real user data in evidence paths | Declared external state, recorded as UNPINNED in provenance |
| Single lane | Cost + evidence simplicity | Declared fan-out where the backend supports it — `actors[0].count: N` (homogeneous), explicit `actors[0].lanes[]` (differentiated persona/device/instruction), or compact `actors[0].roster[]` groups that normalize into lanes on the computer-use E2B route (per-lane worlds, cap 16; `execution.concurrency` bounds concurrent paid lanes) |
| Stock `desktop` template | The stock E2B desktop image is right for most subjects; absent `execution.desktop.template` keeps `Sandbox.create(opts)` byte-stable | `execution.desktop.template` names a custom E2B desktop image (any name/id, no allowlist) for a subject needing baked-in runtimes the stock image lacks (e.g. node/bun/a local Postgres) — threaded to `Sandbox.create(template, opts)` on every desktop-creating route and recorded in the bundle as `desktopTemplate` (public-safe) |
| Desktop default URL opener | Preserve the route/image's historical browser/default opener behavior when unset | `execution.desktop.browser: chrome | chromium | firefox` makes hosted CUA/shared-world browser choice explicit, fail-closed, and recorded as `desktopBrowser` in the run bundle |
| Loopback-only observer serving | Run bundles are local by default (public-safety plank 3); the serve/watch surfaces bind `127.0.0.1` unconditionally, and exposing them is publishing-adjacent, so exposure is fail-closed and carries declared friction | `humanish serve --expose` behind TUNNEL-EDGE auth — ngrok `--oauth google` (`--allow-email`/`--allow-domain` allow rules) or an operator `--public-url` you secure — OR `--expose --safe` (open, gated on verify-`share_ready` runs only). `watch --expose` streams a live run behind edge auth (a live run is never `share_ready`, so `--safe` alone is refused). humanish carries no in-process auth; both print computed warnings naming exactly what became reachable |
| Generous wall-clock safety cap | `execution.timeoutMs` bounds runaway spend, not a goal; ~30 min is reasonable for open-ended "watch it play" | A session that reaches the cap AFTER productive activity is recorded as `budget_reached` (status `passed`, a NON-FAILURE completion, exit `0`), distinct from a zero-progress `timed_out` (a failure, exit `2`). Goal-directed labs set a tight `timeoutMs` and read `completionReason` to confirm the goal, not the cap |

## The placement rule (worked example)

"No environment variables enter the sandbox" was once stated as a rule. It is not one — it
is the external-placement *default* as instantiated on the computer-use route, where the
model's brain runs outside the sandbox. The underlying invariant-level principle is:

> **Keys live where the keyed process runs — and nowhere else. Names go in evidence; values
> never. Blast radius is bounded by key scoping and budgets, not by hoping.**

Consequences:

- A computer-use provider loop runs outside → forwarding env into its sandbox is rejected.
- A subject app (a real web app under test) runs inside → its declared env names are
  provisioned in, values never persisted.
- An agent harness under test runs inside with real keys → that is the point of the lab;
  the keys are presumed exfiltratable by the agent (sandboxing does not protect them — key
  scoping and spend budgets do), and the lane carries a budget/ledger.

Placement is registry metadata per actor type, enforced by the engine — not a vibe.

Two corollaries:

- **Serve commands are author-trusted.** A lab's `serve` steps execute inside the disposable
  sandbox with the declared subject env present — the same trust class as the repo's own
  package.json scripts. Run only lab configs you trust; declare only the env names the
  subject genuinely needs.
- **Pattern redaction is not enough for provisioned values.** A provisioned value (a database
  password, an arbitrary token) has no detectable "shape," so the harness scrubs every value
  it provisioned by LITERAL match before any log tail or error can persist — pattern-based
  redaction is the second pass, not the only one.

## The capture-vs-publish rule (worked example)

"Every screenshot is blurred to a 128px thumbnail" was once enforced at *capture* — the
loop never retained a usable frame. That destroyed the core deliverable for the common case
(a developer watching a sim of their OWN app locally) to defend against a leak that can only
happen at *publish*. The proof it was a default, not an invariant: the same product already
shipped raw full-resolution frames on the meta route with only a "do not publish" warning +
the `.humanish/` gitignore + the binary-asset scan. Two routes, opposite policies, identical
threat. The corrected principle:

> **A default's enforcement point belongs at the PUBLISH/VERIFY boundary, never at
> CAPTURE/RUNTIME — unless capture-time is the only physically possible point.**

Consequences:

- Screenshots are retained **raw and full-fidelity** by default, in gitignored `.humanish/`.
  `policies.redactScreenshots: true` blurs at capture for a share-as-is bundle;
  `humanish export --format bundle --redact-screenshots` blurs a separately verified
  copy of a completed raw bundle without changing the original. The
  frame sent to the provider is always raw regardless — the model must see the screen to act.
- The loopback wall and the synthetic-data stance were the same error: enforcing at
  capture/runtime (rejecting a public target outright; banning realistic local input) what
  belongs at publish (an owner-declared `allowPublicTargets`; redaction at the publish step). A genuine
  capture-time invariant — shell-injection shape checks, id-shape validation — passes the test
  because capture *is* the only point it can hold.

Litmus: for any constraint, ask "is this true everywhere, or true by default? — and if a
default, is it enforced at the boundary where the risk is actually realized?"
