diff --git a/docs/content/docs/design/_index.md b/docs/content/docs/design/_index.md index 03b2f00e..f2ae7eab 100644 --- a/docs/content/docs/design/_index.md +++ b/docs/content/docs/design/_index.md @@ -9,7 +9,7 @@ toc: true | Status | What | Where | | --- | --- | --- | -| Decided | D1 to D49, D51 and D52 (D50 is reserved, #112) | §3; new decisions are tracked in the issue that needs them (the [M0 Decisions milestone](https://github.com/wstein/workharbor/milestone/1) is closed) | +| Decided | D1 to D49, D51 to D55 and D57 to D60 (D50 is reserved, #112; D56 is reserved, #309) | §3; new decisions are tracked in the issue that needs them (the [M0 Decisions milestone](https://github.com/wstein/workharbor/milestone/1) is closed) | | Implemented | The domain (state machines, Decisions, the aggregate and its guards), the policy table, the SQLite store with redaction at ingest, the service layer and reconciler, hostgit (checkout checks, the repository cache, prepare and push, the editor copy), the runtime and agent contracts with their conformance suites, the Apple Container adapter with the egress proxy sidecar (ports 443 and 80, public addresses only, exec environment through a `0600` env file), the tool store (https only, size cap), the config file (secrets outside every root, an API key only, D40) and `make install` (development only, from independently reviewed clean current local `main`, including unpublished commits), push after approval with follow-up rounds as fast-forwards, the Claude Code adapter (`dontAsk` with an allowlist, or `manual` with host approvals over the stdio control channel), ntfy notifications, `whr version`, the `whr-shim` launcher, and the design-drift test; `whr serve` with the JSON API on a host-only socket, the CLI (D37), the web UI with passkey sign-in and step-up, previews and the usage dashboard, workspaces with named agents and the bundle export, the console with SSH certificates, workflow presets (D47), the setup wizard (D46), the launchd job, the board mirror, devcontainer environments with egress requests, release installs from drafts and the Homebrew tap workflow, and fuzz tests of the untrusted-input parsers | Packages under `internal/` and `cmd/`; what remains for the first dogfood run is the [Dogfood milestone](https://github.com/wstein/workharbor/milestone/5), the rest of release 1 the [R1 Slice](https://github.com/wstein/workharbor/milestone/2) and [R1 Complete](https://github.com/wstein/workharbor/milestone/3) milestones | | Spiked | Agent contract (Claude Code, Codex CLI, Antigravity); Apple Container; host cancel with `whr-shim`; approvals over stdio; planted Claude Code config; host reachability; bind mount against volume, and a volume over a bind mount | Issues #1, #2, #10, #7, #68, #69, #40 and #80; the [spike pages](../spikes/_index.md); results in §4.2, §4.4, §5.1 to §5.3, §5.6, §7 | | Open spikes | Signing in to the agent inside an environment (D40); what guests reach on the host with `pf` rules | Issues #82, #69 | diff --git a/docs/content/docs/design/decisions.md b/docs/content/docs/design/decisions.md index 4715d0da..93c985bb 100644 --- a/docs/content/docs/design/decisions.md +++ b/docs/content/docs/design/decisions.md @@ -32,17 +32,17 @@ toc: true | D21 | **Task state machine, amending D13** (§4.1): a run paused by `auth_expired` or `quota_exhausted` moves its task to `awaiting_guidance`; a task fails only from `running` or `awaiting_guidance`, and a lost workspace in `ready_for_review` opens a review Decision (rework or cancel) | Settles the gaps found in the review of the D13 implementation without new transitions, so the code in `internal/domain` already agrees | | D22 | **Build the supervision layer; adopt none of the agent-task supervisors** (issue #5, confirms D1). OpenHands, Vibe Kanban, Sculptor and Coder Agents were assessed from their docs and repositories (§11). Borrow: ACP as a candidate generic agent-adapter protocol (§5.5) and OpenHands' confirmation states; Sculptor's Claude control-protocol integration and editable message queue; Claude Remote Control's phone UX as a reference and a fallback for Claude | None meets the non-negotiable parts of release 1 together: Apple Container, default-deny egress per environment, approvals routed to a human for Claude Code and Codex under subscription logins, an agent that never pushes, more than one forge. Adapting one would replace its runtime, policy and forge layers, which is most of WorkHarbor. Vendor remotes cover one vendor and push to GitHub only. Desk research only: the claims marked {{< status unverified >}} in §11 were not tried | | D23 | **Decisions around pauses** (§4.2): `auth_expired` and `quota_exhausted` are blocking `question` Decisions with fixed options (re-login and resume, resume now or at the reset, cancel) and no deadline; pausing a run supersedes every open Decision the run raised, questions and approvals alike, and they are raised again when the agent asks after resuming | Nothing is permitted by a login or quota answer, so it is a question, and waiting on it is safe, so it does not fail closed. A paused agent's process is gone (D11), so an answer could only reach a dead process or the wrong request; superseding reuses the restart rule | -| D24 | **Releases are human-signed tags built by GoReleaser into draft releases, distributed through a Homebrew tap** (§13, Releases). The human pushes a signed `v*` tag; a workflow checks it and has GoReleaser build `whr`, checksums, an SBOM and a build-provenance attestation into a **draft** GitHub release; you publish it, and only then is the tap updated. Versions are `0.x` until the JSON API, the adapter `contract_version` and the database migrations are stable; the first release, `v0.1.0`, is cut when the release 1 slice demo (issue #28) passes on the Mac mini. Before it, dogfood builds are prerelease tags (`v0.1.0-alpha.N`) published as pre-releases (never to the tap): the administrator installs one with `make install-release`, which checks the checksums and the build-provenance attestation, into a prefix `whr` cannot write. The tap is a formula rendered by its own workflow, and only a published, non-prerelease release updates it. **Signatures (issue #180):** the build-provenance attestation is the release signature. The release job signs it keylessly through Sigstore (`id-token: write` in that job only), bound to the release workflow, the tag and the tagged commit, over the digest of every artifact listed in `checksums.txt`; its Sigstore bundle is also attached to the draft as a release asset, so a download can be checked offline. No GPG key and no long-lived signing key in CI, and no second cosign signature over `checksums.txt`, which the attestation already covers. Verification (`make install-release` and the manual) pins the workflow identity, not just the repository. **A development installation** (issue #261) is the one exception to the admin-owned prefix: `make install` from an independently reviewed commit into `$HOME/.local` or the developer's `PREFIX`. **Werner's development-source exception (issue #299):** the clean source tree must have `HEAD` exactly equal to the current local `refs/heads/main`; that commit may be unpublished. Equality is by commit, not branch name: a detached checkout or differently named branch at that same commit is accepted. A dirty tree or any commit differing from current local `main` (older, topic or ahead) is refused. The operator must obtain independent review of that exact commit before installing: local branch equality checks provenance, not approval. No new development install target or flag is required. The destination must be a user-owned, writable development prefix outside Git working trees and source checkouts; root, the home directory and its ancestors, and built-in managed prefixes (`/opt/whr`, `/opt/homebrew`, `/usr/local`, including their descendants and resolved aliases) are refused. A refused source install never falls back to another prefix or changes a remote-tracking ref; managed installations retain `make install-release` and its signed-tag, checksum and attestation requirements. The installed development binary is accepted only when `whr setup` and `whr doctor` are given `--dev` on that call, or when the configuration holds `development_prefix` (issue #276: built in 10bd9a5, e93d15f, b4e5ce6 and 9d69b7b, with the review fixes 3d78d78, b30999b, 3d964fc and c6bbb7b); the supervisor's own account can then replace the binary (threat model, Accepted risks), so it is for a developer's machine, never the dogfood or reference host. Its prefix is refused when it is `/`, the account's home directory or a directory above it, the home taken both from `$HOME` and from the directory service (`/usr/bin/dscl`, run by its absolute path, never found through `PATH`; a failed or empty lookup fails `--dev` closed). Directories are compared by identity, never by spelling: the prefix is `stat`ed once (`os.Stat`), a prefix that cannot be `stat`ed failing `--dev` closed, and compared with `os.SameFile` against the home and each directory above it, because the default APFS volume ignores case and `/System/Volumes/Data` reaches the same directories under another name; `development_prefix` and its file checks follow the same rule (issue #278: the lookup built in e664c8a, the absolute path and the identity comparison in e5fc9e6, with symlinked-home coverage in f205854). These checks guard against a mistake, not against the account itself, which sets its own `$HOME` and `PATH` and can replace the binary it installed anyway. **The remembered development installation** (Werner's decision on #276): `development_prefix`, a top-level key of the supervisor's configuration holding the absolute prefix, is written by `whr` only on an explicit `whr setup --dev`, shown and confirmed like any fix, and removed by `whr setup --managed` or by hand, `--managed` removing every case spelling of the key, since the JSON reader matches keys without regard to case (b30999b); run from a binary that is not an installed one (a source build, a user-writable `whr`), `whr setup --managed` refuses every step but `--only development-key`, which removes the key, and the managed-prefix check after it, so no service install and no `sudo` step runs from such a binary (3d78d78); `whr` never writes it through an environment variable (no `WORKHARBOR_DEV`, none read for it), a default, `whr doctor`, `whr serve`, `whr service`, a repair line or a workspace's or repository's file. `whr setup`, `whr doctor` and `whr service install` read it as `--dev --prefix `, an explicit `--dev` or `--prefix` wins, an explicit `--prefix` without `--dev` being a managed call that ignores the key, and `whr serve` only logs it at start. Anything running as the account can write the key too, since the file is the account's own; what limits it is that a managed installation refuses it and that the account can already replace a development installation. It loosens nothing beyond `--dev`: setup and doctor run every `--dev` prefix check again on each read; service install checks the key and its file, refuses a managed binary and requires the binary under the key's prefix, but does not run the owner, writer and home walk (c6bbb7b), and the key is refused, as a configuration error, when its value fails any check of the `--prefix` flag (absolute, no control, bidirectional or separator characters) or is a managed prefix, when `whr` runs from a built-in managed prefix (`/opt/whr` or a listed Homebrew prefix; an administrator's custom prefix is not recognised by this check, but setup, doctor and service install refuse a binary outside the key's prefix, while `whr serve` may only log a wrong warning), or when the configuration file is not a regular single-link file of the running account or root, closed to group and other writers, outside every workspace root and git working tree, checked on the opened descriptor (`O_NOFOLLOW`, then `fstat`) as the secret files are, never by path and then opened. `O_NOFOLLOW` covers only the file's last path component, so a symlinked parent directory goes through unmarked, and the workspace roots come from that same file: both checks catch a stray file, not a crafted one; the managed-prefix refusal is the control. `whr doctor` reports it as `warn` on every run, naming the key, the file and `whr setup --managed`, so the weaker mode is never silent | Tags and releases are already human-only (§6, AGENTS.md), and a signed tag is the trust anchor; the tag is also the Go module version, so there is no version file. GoReleaser covers cross-builds, checksums, SBOMs, signing and the tap in one pinned tool. The draft is where you check the assets before anyone can install them, so the tap must not point at a draft. A tap avoids notarizing a downloaded binary for now. A formula, not a cask: Homebrew does not quarantine a formula's download, while a cask of an unsigned binary would need its quarantine attribute removed (both {{< status unverified >}} until #62 installs it). A draft is downloadable only by a repository writer, which suits dogfooding: the binary the supervisor runs was built by CI from a signed tag on `main` and attested, not on the host, and an admin-owned prefix means nothing running as `whr`, an agent's escape included, can replace it. Keyless signing leaves no key to store, rotate or steal, and its transparency-log entry names the workflow and commit, which a GPG signature made from a CI secret would not; the attestation already writes that entry today, so attaching its bundle publishes nothing new. That OpenSSF Scorecard counts the attached bundle as provenance is {{< status unverified >}} until a release carries it (#180) | +| D24 | **Releases are human-signed tags built by GoReleaser into draft releases, distributed through a Homebrew tap** (§13, Releases). The human pushes a signed `v*` tag; a workflow checks it and has GoReleaser build `whr`, checksums, an SBOM and a build-provenance attestation into a **draft** GitHub release; you publish it, and only then is the tap updated. Versions are `0.x` until the JSON API, the adapter `contract_version` and the database migrations are stable; the first release, `v0.1.0`, is cut when the release 1 slice demo (issue #28) passes on the Mac mini. Before it, dogfood builds are prerelease tags (`v0.1.0-alpha.N`) published as pre-releases (never to the tap). From the release after `v0.1.0-alpha.4` a release is one archive (the macOS `whr`, the Linux guest binaries and `install.sh`) next to `checksums.txt`: the administrator checks the archive against `checksums.txt`, unpacks it and runs `sudo ./install.sh `, with only what stock macOS ships. When `checksums.txt` is next to it, as in the documented flow, `install.sh` ties the tag to the archive: the archive of that tag must be there and match its checksum line, or nothing is installed, and it installs the files of that checked archive, whose entries are owned `root:wheel` (#524); without `checksums.txt` it installs the loose files next to it and says on stderr that the tag is not checked against the archive. When `gh` is installed, as on an upgrade, the install page runs `gh attestation verify` with the workflow identity pinned before `sudo ./install.sh`; a first install has no `gh`, skips that check and trusts the checksums and TLS. `make install-release` from a clone checks the checksums and, when `gh` is present, the attestation. The prefix should be one `whr` cannot write; in the alpha `install.sh` warns about a group- or world-writable prefix, or one the running user does not own, instead of refusing it (D58, #504). The tap is a formula rendered by its own workflow, and only a published, non-prerelease release updates it. **Signatures (issue #180):** the build-provenance attestation is the release signature. The release job signs it keylessly through Sigstore (`id-token: write` in that job only), bound to the release workflow, the tag and the tagged commit, over the digest of every artifact listed in `checksums.txt`; its Sigstore bundle is also attached to the draft as a release asset, so a download can be checked offline. No GPG key and no long-lived signing key in CI, and no second cosign signature over `checksums.txt`, which the attestation already covers. Verification (`make install-release` and the manual) pins the workflow identity, not just the repository. **A development installation** (issues #261 and #299) is `make install` from source into `$HOME/.local` or the developer's `PREFIX`, meant for an independently reviewed, clean current local `main` (equal by commit, possibly unpublished); the operator obtains review of that exact commit, because local branch equality checks provenance, not approval. It never falls back to another prefix and still refuses root, `/`, the home directory and the directories above it, the source checkout and git metadata. **Narrowed by D58 (alpha, #493, #504):** a dirty tree, a `HEAD` other than local `main`, and a prefix that is managed, in another git working tree, foreign-owned or open to group and other writers only warn; `whr setup --dev`, `whr doctor --dev`, `whr setup --managed`, the `development_prefix` key (an old key in `config.json` is accepted and ignored) and their prefix, home and owner checks (#276, #278) are removed, and setup, doctor and service install accept a `whr` wherever it lies, refusing it only when it is not an executable file. The former rules are in this row's history | Tags and releases are already human-only (§6, AGENTS.md), and a signed tag is the trust anchor; the tag is also the Go module version, so there is no version file. GoReleaser covers cross-builds, checksums, SBOMs, signing and the tap in one pinned tool. The draft is where you check the assets before anyone can install them, so the tap must not point at a draft. A tap avoids notarizing a downloaded binary for now. A formula, not a cask: Homebrew does not quarantine a formula's download, while a cask of an unsigned binary would need its quarantine attribute removed (both {{< status unverified >}} until #62 installs it). A draft is downloadable only by a repository writer, which suits dogfooding: the binary the supervisor runs was built by CI from a signed tag on `main` and attested, not on the host, and an admin-owned prefix means nothing running as `whr`, an agent's escape included, can replace it (in the alpha a recommendation the doctor warns about, not a refusal: D58). Keyless signing leaves no key to store, rotate or steal, and its transparency-log entry names the workflow and commit, which a GPG signature made from a CI secret would not; the attestation already writes that entry today, so attaching its bundle publishes nothing new. That OpenSSF Scorecard counts the attached bundle as provenance is {{< status unverified >}} until a release carries it (#180) | | D25 | **Launcher and host-initiated cancel** (§5.1, D19): agent and command processes run under `whr-shim`, a small static binary in the shared tool store. The launcher starts the command in its own process group (`setpgid`) and writes the PID to a file. Cancellation is triggered from the host via `container exec /tools/whr-shim kill -pidfile -grace `, which sends `SIGINT` to the whole process group and falls back to `SIGKILL` after the grace period | Signalling the `container exec` client fails in Apple Container (`missing signal in xpc message`) and leaves processes running in the guest (spike #2; issue #10, whose cancel of a real agent is pending in #7). `whr-shim` terminates the entire process tree reliably from inside the guest without leaving orphan processes (measured in spike #10: cooperative cancel exits in ~10 ms, stubborn trees killed after 500 ms grace in ~514 ms, 0 orphans) | | D26 | **Approval channel: the stdio control protocol** (§4.2, §5.2, §12): Claude Code runs with `--permission-prompt-tool stdio` and `stream-json` in and out. Permission requests arrive as `control_request` (`can_use_tool`) messages on the `exec` stdout, and the supervisor answers with a `control_response` (allow or deny) on stdin. Whatever ends the channel, the run ends with a denial: the supervisor stops the agent with `whr-shim` (D25) and never lets a request outlive it | No network listener on the host or sidecar, no path from the internal network to the supervisor, and no approval token the guest could read. **Evidence** ({{< status verified >}}, spike #7, `spike/agent-approval` at `567b5ad`, scripts and raw streams committed): in a container on Apple Container 1.5.0, allow and deny round-trip and the tool runs or does not; a closed stdin fails closed without running the tool; a killed `container exec` client leaves the guest agent alive with its request pending and the tool unrun, and `whr-shim` reaps it in about 0.6 s with no orphans; an unanswered request holds the tool until the deadline stop; a response with an unknown `request_id` is ignored. Hence the `whr-shim` stop whatever ends the channel | | D27 | **Resume briefing** (§4.1, §4.2): when a run resumes after a pause, a cancel or an interruption, the supervisor's first message to the agent says what it knows: which tool call was running or waiting for approval when the process ended, that its effects are unknown and may be partial, and which open Decisions were superseded. The agent is told to check the workspace before repeating anything | Measured in spike #7, case 7 (branch `spike/agent-approval`): after a loop was killed 3 s into a Bash call, Claude Code's resumed session told the model *"The command was never executed … rejected before it could run"*, which was false. Left to the agent's own account, a resumed run may skip or repeat work | | D28 | **Host software through Homebrew; agent CLIs only through the tool store** (manual, host setup). The Mac mini gets Apple Container, `whr` (the tap of D24) and a VPN client from a `Brewfile` installed with `brew bundle`, with `HOMEBREW_NO_AUTO_UPDATE=1` so nothing upgrades unasked. Claude Code, Codex CLI and other agent CLIs are never installed on the host for WorkHarbor: they come from the verified, versioned tool store (D19). Nix (nix-darwin) remains possible for a host that already uses it | The host needs few packages, and D24 already distributes `whr` through a tap. Nix would be reproducible but heavy for one appliance, and it duplicates what the tool store does for agents. Whether Apple Container is packaged in nixpkgs is {{< status unverified >}} | | D29 | **The web UI listens on loopback only and a forwarder carries remote access to it; the JSON API is on a host-only socket with its API token; remote browser sessions use passkeys (D45)** (§7.5). The web UI binds to `127.0.0.1`, which no guest reaches (issue #69), and only it is forwarded. **The JSON API is never forwarded:** it is served on a unix socket in `whr`'s state directory (directory `0700`, socket `0600`), reached only by the host CLI as the `workharbor` user, and the forwarded listener serves no `/v1` route, so a leaked API token cannot answer a review, allow an egress host or enrol a passkey from the phone network (D45; review of #101). The phone reaches the web UI through a forwarder: `tailscale serve` (the default), or, for a VPN that ends on the router such as a FRITZ!Box with WireGuard, a small proxy on the Mac's LAN address admitted by a `pf` rule to the router's VPN clients only. Every local JSON API request needs the API token; browser requests use the passkey-authenticated session, with a fresh Decision-bound assertion for sensitive answers (D45). A `pf` rule blocks the container subnets from the host's own addresses, and host services that listen on all interfaces are turned off or hardened | Issue #69 (branch `spike/host-reachability`, `run.out`) measured that every guest, `--internal` included, reaches host listeners on the LAN address or on all interfaces, and none reaches a loopback-only listener; spike #2's "`--internal` blocks the host" was wrong. A forwarder on any non-loopback address is reachable by guests too, so authentication, not the address, is the guard there: the browser session and sensitive-action step-up under D45; `pf` narrows reachability. Whether a guest reaches the Tailscale address, the `pf` rules themselves, and the Application Firewall's effect are {{< status unverified >}} (issue #69) | -| D30 | **The forge board mirrors task state; the supervisor writes it** (issue #70). Through the forge adapter, as an optional capability, the supervisor keeps a project board current: a task awaiting guidance or ready for review moves its card to "Needs you" (first), running → In progress, completed → Done; Session names the agent, and the card links to the task in the web UI; a failed task moves its card to Needs you, because a human has to look, and a cancelled one back to Todo, so no card stays In progress for a task that has stopped; queued changes no card. The write is the supervisor's own `update_board` action (auto in §6), never an agent's. Whether an App's installation token can write a **user-owned** project at all is {{< status unverified >}}: the permission of D15 covers organization projects, so a user-owned board may need an organization-owned board instead; that and the real board's field names wait for the check with the real App (#73). Agents never write to the board. Starting a task by moving its card to an agent queue comes later, only through an "Accept this task?" Decision and the trust tiers (issue #71). **Amended (#513, 2026-10-09):** the human removed the board status "Ready to push" (`forge.StatusReadyToPush`), which no board used and nobody missed: a task ready for review moves its card to Needs you, because a human has to review it, and the GitHub adapter requires only the Status options Needs you, In progress and Done, while a board that still has the old option keeps working (#514, PR #516). The "Ready to push?" Decision (§4.5) keeps its name; only the board status is gone | The GitHub board is a good planning dashboard and the web UI the control surface; mirroring keeps one current view without rebuilding a board in WorkHarbor. Whether an App installation token can write fields on a **user-owned** project, and whether card-move webhooks exist for one, is {{< status unverified >}}: issue #70 tests it first, and moving the repository and project into an organization is the fallback the human decides on | +| D30 | **The forge board mirrors task state; the supervisor writes it** (issue #70). Through the forge adapter, as an optional capability, the supervisor keeps a project board current: a task awaiting guidance or ready for review moves its card to "Needs you" (first), running → In progress, completed → Done; Session names the agent, and the card links to the task in the web UI; a failed task moves its card to Needs you, because a human has to look, and a cancelled one back to Todo, so no card stays In progress for a task that has stopped; queued changes no card. The write is the supervisor's own `update_board` action (auto in §6), never an agent's. Whether an App's installation token can write a **user-owned** project at all is {{< status unverified >}}: the permission of D15 covers organization projects, so a user-owned board may need an organization-owned board instead; that and the real board's field names wait for the check with the real App (#73). Agents never write to the board. Starting a task by moving its card to an agent queue comes later, only through an "Accept this task?" Decision and the trust tiers (issue #71). **Amended (#513, 2026-10-09):** the human removed the board status "Ready to push" (`forge.StatusReadyToPush`), which the development board no longer uses: a task ready for review moves its card to Needs you, because a human has to review it, and the GitHub adapter's `CheckBoard` checks the Status options Needs you, In progress and Done (a cancelled task writes Todo and reports `ErrBoard` on a board without that option), while a board that still has the old option keeps working (#514, PR #516). The "Ready to push?" Decision (§4.5) keeps its name; only the board status is gone | The GitHub board is a good planning dashboard and the web UI the control surface; mirroring keeps one current view without rebuilding a board in WorkHarbor. Whether an App installation token can write fields on a **user-owned** project, and whether card-move webhooks exist for one, is {{< status unverified >}}: issue #70 tests it first, and moving the repository and project into an organization is the fallback the human decides on | | D31 | **GitHub is reached through its API from Go, with the App's installation token, never through `gh`** (§10). The forge adapter has a typed client for the REST API (issues, pull requests, rulesets) and the GraphQL API (Projects v2 fields), mints installation tokens from a JWT signed with the App key using the standard library, and handles rate limits and errors as typed values. `gh` stays a developer and agent-session tool for this repository, not part of the product (issue #27) | `gh` would carry the user's own broadly scoped token, the wrong identity for a bot, add a host dependency against D28, and leave errors and rate limits to output parsing. Libraries such as `go-github` and `githubv4` are added only if the hand-written client grows large enough to justify them (AGENTS.md) | | D32 | **Recommended host: Mac mini M6 with 32 GB memory and 512 GB storage; on a budget 16 GB** (§2, §8), with 512 GB, or with 256 GB plus an external SSD for repositories, workspaces and backups; 24 GB / 512 GB sits in between. Spend on memory before storage, and add an external SSD rather than paying for 1 TB internal. The M5 Pro is not worth its premium for API-backed agents | US Apple Store prices on 1 October 2026: M6 16 GB / 256 GB $899, 16 / 512 $1,099, 24 / 512 $1,299, 32 / 512 $1,499, 24 GB / 1 TB $1,599, 32 GB / 1 TB $1,799; M5 Pro 24 / 512 $1,699 (Germany: M6 from €1,049). Each memory step costs $200 and buys about four more concurrent environments; memory cannot be upgraded later, while storage can be added externally. That makes 32 GB about $150 per environment against about $215 for 24 GB and $275 for 16 / 512. Environment counts are estimates until issue #39; whether Apple Container's storage can move to an external SSD is {{< status unverified >}} (issue #54) | | D33 | **Web app previews go through a preview proxy in `whr`** (§9.3, issue #72). When an agent runs a dev server in its environment, `whr` proxies a preview of one declared port through the proxy sidecar, the only container on both networks, and `tailscale serve` (or the router-VPN forwarder of D29) carries it to the developer. Each preview has its own origin, never the web UI's; it needs a per-preview token, lives only while the environment runs, forwards only to that port, and passes WebSocket upgrades for hot reload | The developer's phone or laptop cannot reach an `--internal` environment, and should not; the supervisor already knows which task, environment and port belong together. A preview serves untrusted, agent-written code in the developer's browser, so sharing the UI's origin would let it read the session and answer Decisions. Port publishing straight to the host, Traefik or Caddy, Tailscale inside each guest, and Tailscale Funnel were rejected: they bypass the supervisor, need routing data it already has, put a key in the guest, or publish unreviewed code. That the sidecar can relay inbound traffic to the internal network is {{< status unverified >}} (issues #69, #72) | -| D34 | **Dogfood first: WorkHarbor develops WorkHarbor as early as possible** (§13). A Dogfood milestone holds the smallest set that runs one real WorkHarbor issue through `whr` end to end: `whr serve` and the core commands (#24), the Claude Code adapter in degraded mode (#25, `dontAsk` with a fixed allowlist; host approvals follow with #7), the Apple Container adapter (#26), push after approval (#27), the reconciler fixes (#66) and the adapter's permission fix (#68), on a Mac mini M4 with 16 GB (#73). The supervisor always runs an **installed binary built from an approved commit on `main`** (a dogfood draft release installed with `make install-release` until the first release, then the tap, D24; `make install` stays for a developer's own machine and may use the independently reviewed, clean current local `main` before publication, issue #299), never a topic's working tree. This development-source exception does not establish a managed dogfood installation or a measurement on the reference host. From the first green run, new issues start with `whr run`, and each manual workaround becomes an issue labelled `dogfood` | Today the human supervises three agent sessions by hand: relaying messages, pushing, ticking criteria, keeping the board, and catching duplicated work and a leaked token, which are all WorkHarbor features. Degraded mode works now and takes #7 off the critical path; the push stays human-approved (D18). Agents working on WorkHarbor edit the code that constrains them, including the policy, so the running supervisor must come from reviewed code. A host process runtime was rejected: it would be faster but would normalise unisolated agents | +| D34 | **Dogfood first: WorkHarbor develops WorkHarbor as early as possible** (§13). A Dogfood milestone holds the smallest set that runs one real WorkHarbor issue through `whr` end to end: `whr serve` and the core commands (#24), the Claude Code adapter in degraded mode (#25, `dontAsk` with a fixed allowlist; host approvals follow with #7), the Apple Container adapter (#26), push after approval (#27), the reconciler fixes (#66) and the adapter's permission fix (#68), on a Mac mini M4 with 16 GB (#73). The supervisor always runs an **installed binary built from an approved commit on `main`** (a dogfood prerelease installed from its release archive with `sudo ./install.sh ` until the first release, then the tap, D24; `make install` stays for a developer's own machine and may use the independently reviewed, clean current local `main` before publication, issue #299), never a topic's working tree. In the alpha this is the rule the sessions follow, not a check `whr` enforces: `whr` no longer refuses a binary in a git working tree (D58). This development-source exception does not establish a managed dogfood installation or a measurement on the reference host. From the first green run, new issues start with `whr run`, and each manual workaround becomes an issue labelled `dogfood` | Today the human supervises three agent sessions by hand: relaying messages, pushing, ticking criteria, keeping the board, and catching duplicated work and a leaked token, which are all WorkHarbor features. Degraded mode works now and takes #7 off the critical path; the push stays human-approved (D18). Agents working on WorkHarbor edit the code that constrains them, including the policy, so the running supervisor must come from reviewed code. A host process runtime was rejected: it would be faster but would normalise unisolated agents | | D35 | **The phone and a 12-inch tablet are the primary clients** (§9.6). The phone serves short, urgent interactions (answer, approve a tool, stop a run, glance at the harbor); the tablet replaces the laptop for reviewing a topic before push, supervising several tasks and planning with an agent. One server-rendered UI with a phone layout and a two-pane tablet layout, installed as a PWA. D45 requires passkey sign-in on either device and a fresh Decision-bound assertion for every sensitive answer: publishing (bound to its exact SHA), allowing an egress host, a policy change or an operation on a secret | The developer detaches while agents work and returns when one needs them (§1), which happens away from a desk; a 12-inch tablet with a keyboard covers the review that the phone's screen cannot. An unlocked phone can authorize sensitive actions, so D45 requires a fresh check of who is approving, bound to the Decision and, for a review, its exact SHA. That a passkey prompt works in an installed PWA on both devices is {{< status unverified >}} | | D36 | **The autonomy table's defaults and fixed floor** (§6, issue #9). Commit in the topic's checkout: `auto`; push an `agent/*` branch: `ask`, carried out by the supervisor after the "Ready to push?" approval; open or update a PR and comment on the issue: `auto`, after the push; merge, tag, release and deploy: `forbid`. Whatever a repository's table says, merge, tag, release and deploy stay `forbid` and push stays at most `ask`; an override may only tighten; an unknown action or mode is `forbid` | Implemented and tested in `internal/policy` (issues #4, #51); this row records it as decided. Sensitive actions triggered by untrusted input asking (§6) follow with the trust tiers (issue #53) | | D37 | **The CLI grammar of the dogfood slice is stable** (§9.1, issue #9): `whr serve`, `run`, `ls`, `logs -f`, `say`, `cancel`, `inbox`, `approve`, `reject` and `answer