# Threat model ## Protected assets - DeepSeek credentials and credential-bearing headers/URLs. - Source checkout and unrelated worktrees. - Controlling checklist integrity. - Git remote state, releases, packages, PRs, and deployments. - Correct ownership of running/stale worker processes. ## Enforced controls - Realpath-based write allowlist; traversal and escaping symlinks/junctions fail closed. Ordinary reads may inspect the worktree except the controlling checklist; publication-conflict workers remain exact-path for both reads and writes. - Checklist denied by file tools and verified by digest after every worker. - Exact argv tool; dynamic eval, remote clients, `git push`, `gh`, publication/deploy, integration, and worktree commands denied. Git uses a positive read-only verb allowlist; mutation is exclusive to `leppy_commit`. PowerShell is accepted only with `-File` targeting a repo-local `.ps1`, never command/eval strings. - Ordinary commands wrapped by the official `workspace-write` sandbox with no escalation path. - Narrow commit capability validates every changed path and stages only the exact changed-path set. Ignored untracked files are discovered only beneath explicitly declared task scopes before a narrow force-add, so a scoped versioned migration is permitted without sweeping unrelated ignored material. A completed clean-tree ordinary attempt with zero commits may retry once. Terminal Agent errors observed in SDK notifications are classified before this branch, so provider overload cannot masquerade as a completed no-commit task. The controller never manufactures an empty commit: it accepts a repeated zero-commit result only with one exact final already-satisfied evidence marker from the independent retry on a clean unchanged branch, then records a checklist-only closure commit; dirty WIP or missing evidence still fails. - A normal SDK turn is not success authority. One final structured `LEPPY_OUTCOME` is mandatory; completion requires passed validation evidence, and explicit blocked/failed/contradictory prose fails closed before Git adoption. Three identical failed tool calls or eight failures total close one worker turn. Durable same-signature/blocked/unavailable failure state suppresses automatic lifecycle follow-ups until conditions change or the human intervenes. A clean exact-scope commit with validation not failed is bound to an HMAC-authenticated active/pending identity, including ignored paths and bytes. Its isolated verifier receives no write/edit/commit/delete tools; package managers, repository scripts, shells and interpreter frontends are denied, and a bare executable must resolve from authenticated root `node_modules/.bin`. Commands run only in a detached disposable worktree. Adoption requires unchanged detached HEAD/index/tracked/untracked state, unchanged durable HEAD/checklist/tree/ignored digest, and a structured passed report embedded atomically before an exact checklist-only amend. A generated physical `.npm-cache` is moved with its bytes into identity-bound private quarantine before state advances. - On Windows, Playwright's nested libuv IPC/browser pipes are an intentional `WRITE_RESTRICTED` sandbox boundary, not a reason to escalate. The ordinary task worker records `LEPPY_WINDOWS_NAMED_PIPE_UNAVAILABLE` only for one authenticated direct-Playwright call so a clean exact commit can enter pending validation. Schema-v2 recovery requires a synchronously persisted HMAC-bound validation-unavailable terminal receipt; failed/missing receipts cannot be promoted. Only the detached read-only verification worker may route `playwright test` into an explicitly configured WSL2 capsule. The controller authenticates candidate root plus pending commit OID and archives that exact object, may copy only configured Host-generated `seedPaths` ignored by tracked `.gitignore` authority and proven untracked in both candidate and exact source `repoRoot` after stable handle-bound bounded regular-file/no-link/source-authority validation, and materializes seeds before candidate archive extraction while rejecting every tracked non-directory destination ancestor. It passes only allowlisted names from a physically contained private regular Host env file, denies canonical snapshot-update options, resolves staging with the selected distro's `wslpath`, separates outer/bootstrap infrastructure from candidate setup/prepare/test failure with a Host-only phase receipt, redacts secrets before output truncation, installs with lifecycle disabled, authenticates canonical-registry lock/package/dependency edges plus the direct Playwright entrypoint, and mounts dependencies read-only except exact private Vite cache overlays before candidate-authorized preparation. Bubblewrap constructs a minimal root from read-only `/usr`, toolchain libraries and `/etc`, authenticates the live mount table and masks known WSL submounts so arbitrary distro/Host mounts, homes, mutable distro state and WSL interop are absent; only private workspace/home/temp/cache are writable under fresh user/mount/pid/ipc/uts namespaces, awaited process-tree cancellation, and no Host-unconfined fallback. The WSL distribution and bubblewrap installation remain Host prerequisites. Network is deliberately shared under the existing network non-goal. Machine-local `.leppy-loop.local.json` must be untracked Host authority and takes precedence over a portable tracked `.leppy-loop.json`; neither file may contain secret values. - General ignored artifacts use a separate attempt-scoped signed baseline and private quarantine protocol. Reconciliation preserves unchanged baseline WIP, accepts baseline paths only when they became tracked in the candidate, and rejects changed/deleted/replaced baseline entries before any move. New regular-file leaves are moved only after an HMAC-bound transaction reference is persisted in active state and a complete preflight proves source/destination exclusivity, physical private containment, unchanged device/inode/link-count/type/content identity, and one filesystem. Symlinks, junction escape, hardlinks, special files, receipt loss/tamper, unexpected additions and recreated sources fail closed. Legacy state without a baseline is recoverable only from the canonical authenticated empty digest or a stable bounded current fingerprint subset whose ordered-list SHA-256 exactly matches the authenticated non-empty digest. The last baseline-only capability may include every ordinary untracked path in that proof set, but never in quarantine classification: unproven ordinary paths stay in place and block the clean-tree gate. Base-rule checks execute in an isolated Git dir with empty repository, global and system excludes, while a single content budget spans every proof class in each snapshot. Circuit bypass for legacy inference requires the authenticated active task/attempt/baseline identity. A digest-bound condition/attempt admission is lock-protected and HMAC-persisted as `prepared` before the controller job starts. Exact preparation binds its normalized request digest and lifecycle epoch/transition and is idempotent across authority/job-start failure; a target receipt already persisted at the budget boundary is reused only for that prepared job registration; runner entry requires it and promotes it to `consumed` before reconciliation. Only consumed state blocks duplicate command admission. Four-removal inference is confined to the predecessor terminal-error envelope of at most 39 entries. Its canonical terminal admits one further promotion-aware transition whose added-path candidates must be new since the authenticated base, remain ignored, and reconstruct the exact digest. That terminal admits one base-ignore transition for current ordinary untracked paths matched only through regular mode-preserving, object-bound, byte-exact base ignore blobs and strict binary path protocols. The base-ignore terminal admits one final transition for newly tracked paths matched by those immutable base rules even when current rules no longer ignore them; `--no-renames` prevents exact rename destinations from escaping Added classification. Symlink/gitlink rules, checkout transforms, invalid UTF-8, POSIX backslash relocation and Windows-noncanonical paths reject. The tracked-and-untracked base-ignore terminal cannot replay. Wider snapshots retain three removals/10,000 candidates, fingerprints are capped at 128 KiB UTF-8, aggregate candidate serialization/hashing aborts above 512 MiB, and file content is streamed under independent 512 MiB budgets through discovery, stable preflight, reconciliation, move and retry before receipts or adoption. Worker hosts sign only a real OS start identity before child spawn; recovery never signals a reusable PID and advances only after definitive absence or positive identity change, while inspection errors and a persistent identity fail closed. - The selected provider credential is resolved by Harness or its isolated credential store and remains inside the model runtime; model-facing tools never expose it, and their subprocess environments are scrubbed. - Recursive redaction of sensitive names, known values, headers, and URL userinfo. - Atomic state, per-common-dir lock, HMAC ownership proofs and leases, PID plus process-start identity checks, and tree-scoped termination. The Host rejects concurrent jobs for the same canonical repository across Agents; the runner acquires the repository lock before terminating any authenticated worker lease. Lock files carry PID, real OS process-start identity and random token, treat identity-inspection errors as live, compare before deletion, and use a reclaim marker to recover a provably dead crash owner or aged partial write without letting an old disposer remove a replacement lock. Every throwing path after lock acquisition releases it before propagating. Exact recovery may ignore the receiving source checkout's branch/dirty state only after ownership authentication and preserved worktree/branch verification; fresh runs still require a clean tracked source controller. - Gate command fingerprint is denied in workers. Gates never retry without fresh direct-human authority after failure/crash. A repeated fingerprint requires a transition reserved from the active human lifecycle permit bound to the exact session, repository and authenticated run; each repair transition consumes at most three cycles, cumulative lifecycle transitions are bounded, model arguments cannot widen repair scope, and every attempt receives a durable receipt. Repair fails closed on a dirty worktree, reopens only the immediately preceding completed closure through a controller commit, and gives a bounded prior receipt to a fresh worker. Direct-human `--repair-path` additions are allowed only with exact-run repair authority, must canonicalize to existing paths inside the authenticated worktree, cannot target the controller, are durably recorded, and remain absent from the model tool; commit validation uses the union of original and explicitly added scopes. - The Web slash interface accepts only simple intent and rejects technical flags. One invocation returns after issuing a lifecycle permit and enqueueing a short resolver turn. Global `leppy_loop_control` reserves one state transition at a time before transfer into the owner-fenced Harness job registry; start still requires that direct slash permit. Once bound, authority uses an immutable HMAC required-marker, chained receipts, an authenticated local head, and append-only per-run high-water anchors under Host-owned `DSH_HOME`; admissions persist before job start, direct local-only downgrade before slash acknowledgment, and Stop revocation before kill. It may rehydrate only for the same session after Host restart; mutable `run.json` cannot grant authority, coordinated local head/prefix rollback fails against the external high-water mark, and corrupt modern state is quarantined rather than reset as legacy. The permit expires, rejects concurrent transitions, retains its publication restriction, and cannot cross session/repository/run. Its cumulative sixteen-transition epoch can advance only after exact exhaustion and a fresh direct-human command; live Host jobs, repository locks, or signed worker leases block rollover, while stale prior-epoch Host memory cannot carry consumption forward. Every continuation re-inspects the HMAC-owned controller and exact checklist/branch/worktree/base facts. Existing-run resolver turns inspect read-only status before one transition; status returns before grant hydration/reservation and returns a job ID only for a live owner-fenced registry record; stale durable `running` state becomes `orphaned`, never a guessed ID. Direct-human stop is exact-run and owner-fenced; stop remains absent from the model tool schema. - A clean checkout may supply tracked `.leppy-loop.json` `customInstructions`. Parsing, shape and byte caps fail closed before a worker starts; the text is treated as repository-authored instruction under the same trusted-repository boundary as `AGENTS.md`, `CLAUDE.md`, scripts and hooks. - The controller prepares npm dependencies before worker release. It copies only a structurally current equal-lock source tree or runs the Host Node installation's exact `npm-cli.js` for a non-workspace lock whose package entries carry credential-free HTTPS origins and supported integrity digests. An `inBundle` entry without its own origin is accepted only when the lock's direct parent names that child and the recursive bundle chain terminates at such an integrity-pinned tarball. The install uses private empty configs/cache, an environment allowlist, disabled lifecycle scripts/audit/funding, live file/byte/depth quotas and process-tree cancellation. Complete receipt/package/shim/link/hardlink validation precedes no-replace publication; an unresolved prior dependency miss stalls in setup without releasing a worker even when legacy hydration state is absent. - The root `/leppy-loop` lifecycle authorizes controller-owned branch push and PR delivery unless direct human local-only/negated language removes that authority immutably; merge and deploy remain absent. The private tool may request publication only from such a permit and only after no open checklist row remains. Background completion can enqueue another resolver turn under the same bounded run permit rather than minting authority. A new publish grant authenticates `head-name`, `onto`, `orig-head`, and backend before aborting any interrupted prior rebase, so partial/manual resolution never crosses jobs. In one live job, rebase conflicts expose only exact Git unmerged file paths to fresh recovery workers, bounded by the controller-side grant limit. Conflict workers receive no commit or exec capability and cannot touch the checklist, index, HEAD, Git sequencer, gate, push, or `gh`. The controller freezes authenticated rebase identity, HEAD and the complete staged index; it rejects index drift, a changed unmerged set, untracked paths, or edits outside the exact conflicts. Only the controller stages those resolved paths and continues the rebase, preserving clean paths already staged by Git. Before mutation it derives one repository from matching fetch/push GitHub URLs and reconciles only an exact same-owner PR at the authenticated head and requested base; MERGED state additionally requires its reported merge commit to remain an ancestor of the live base OID. Otherwise `fetch --prune` plus `ls-remote` supplies live base/head OIDs; deleted bases fail closed, and a private replacement requires a durable prior target with ancestry proof. It rebases onto the target OID, reruns the final gate, rechecks unchanged clean HEAD/base/remote lease, pushes only with an exact expected-OID `force-with-lease`, verifies the remote head, and re-lists before one explicit-`--repo` PR create. - Task progress uses paired, deterministic command-lifecycle records carrying only bounded checklist text, a human-facing per-row attempt, counts, and an elapsed-time baseline. The per-row ordinal never replaces or resets the cumulative global attempt used by leases, receipts, events, recovery and lifecycle bounds. The long label alone elides while status/attempt/timer remain separate. General controller status/cancel uses owner-fenced job snapshots plus a direct human stop command. Browser ticks create no durable per-second writes; progress remains outside model history and excludes worker output, credentials, gate output, and mutable repository data. ## Explicit non-goals The Harness filesystem sandbox does not confine network. Existing repository scripts may access network, invoke native code, exploit the OS account, or hide behavior behind an otherwise permitted test command. The current boundary assumes the repository and its existing scripts are trusted. Controller-owned npm materialization is narrower than worker execution but still deliberately permits only the integrity-pinned HTTPS origins named by an accepted lock. The narrow Git commit bridge writes shared Git metadata. Validation prevents it from staging out-of-scope worktree paths, but a hostile repository with crafted Git configuration/hooks could affect commit execution. Leppy sets no blanket claim against malicious Git hooks; use clean trusted repositories and review local Git configuration. This project does not merge PRs, mutate issues/releases/packages, deploy, or clean preserved WIP. Its only remote mutation is the lifecycle-authorized controller-owned branch push and PR creation described above; direct local-only language disables both. ## Reporting Follow [SECURITY.md](SECURITY.md). Do not include real credentials or private repository data in a report.