Fix review findings: secrets in snapshots, URL spoofing, migration data loss
Adversarial review of the branch produced findings across four areas. This addresses them, plus the Windows CI environment. Secrets. commit_container_snapshot baked the container's full env into the per-project snapshot image, so the shared OAuth token — and the AWS keys, git token and gateway master key — outlived revocation and were readable via docker inspect. Verified against Engine 29.6 that a commit body's config merges over the container's: keys cannot be dropped but can be overwritten, so all of them now commit as KEY=. clear_claude_token additionally rewrites images from earlier builds and reports honestly when a tag could not be rewritten. The recommendation to move the token out of env entirely was not taken, with reasoning: apiKeyHelper is a different auth method that outranks CLAUDE_CODE_OAUTH_TOKEN rather than a transport for it, and no file-based delivery exists. The durable exposure — the image — is what is closed here. Separately noted, not fixed: entrypoint.sh captures the token into the scheduler's .env inside the persisted volume. URL spoofing. Three call sites reached openUrl with container-controlled strings, one of which the review missed (the WebLinksAddon handler). The sign-in URL was scraped from container output with a longest-match tie-break and no userinfo check, so claude.ai@evil.tld rendered as "claude.ai…" in a truncating element. There is now one sanitizer in front of every sink — scheme allowlist, no userinfo, C0/C1 and quote rejection, host allowlist for the sign-in case, first-match — and the origin renders un-truncated. The toast is keyed so a changed URL remounts, closing a bait-and-switch where the user read one URL and clicked another. Migration. The rollback pin was best-effort: a tag failure was logged and the migration continued past remove_container, after which the final commit overwrote the only copy of the old system layer. It now aborts before anything destructive and reads the tag back. /var was destroyed while the ordinary recreate path preserves it — making the "safe" alternative to Reset more destructive than Reset's alternative; data-bearing subtrees are now detected and disclosed in the pre-flight rather than copied, since tarring a live database onto a different base's packages is a corruption risk. resume_migration now verifies the migration-state label instead of reporting success for a container that never swapped. dismiss actually resolves the record rather than leaving the feature permanently refusing to migrate. Start and Reset are guarded while a migration is live. Lifecycle. The gateway no longer publishes on 0.0.0.0 — bind address and advertised URL are derived together so they cannot drift. Disabling it now stops it. App exit runs teardown concurrently under a budget with a visible shutting-down state instead of blocking for minutes. Auto-starts retry when Docker is not up yet, and the polling-recovery path now reconciles, so interrupted migrations are still recovered. Auth-bridge forwards are capped, closing a container-driven fd exhaustion. Windows CI. build-windows failed on this branch with "linker link.exe not found". The runner had no MSVC build tools and the workflow assumed a hand-provisioned machine, so a bare runner registers, accepts jobs and fails at link time after downloading the whole crate graph. The job now installs the VC++ workload when vswhere cannot find it, matching how it already conditionally installs Rust and Node. 192 Rust tests, 274 frontend tests, both builds clean, zero warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -112,9 +112,13 @@ docker exec stdout → tokio task → emit("terminal-output-{sessionId}") → li
|
||||
- `image.rs` — Image build/pull with progress streaming
|
||||
- `gateway.rs` — Optional LiteLLM sibling container giving Claude Code an Anthropic-format
|
||||
front end for providers that only speak OpenAI (see `gateway-container/`). Mirrors `stt.rs`.
|
||||
Binds `0.0.0.0` — unlike STT — because *project containers*, not the host process, consume
|
||||
it; it therefore **always** sets a LiteLLM `master_key`, since LiteLLM without one accepts
|
||||
any key.
|
||||
Its bind address is **detected, never `0.0.0.0`** — unlike STT, *project containers* consume
|
||||
it, so loopback alone is not always enough: Docker Desktop gets `127.0.0.1` (containers reach
|
||||
it via `host.docker.internal`), native Linux gets the default bridge gateway (`172.17.0.1`).
|
||||
`GatewayBinding` derives the bind address and the advertised `base_url` together so they
|
||||
cannot drift. A wildcard bind would be LAN-reachable — Docker's rules precede host firewalls —
|
||||
in front of a container config holding a billed provider key. It also **always** sets a
|
||||
LiteLLM `master_key`, since LiteLLM without one accepts any key.
|
||||
- `migration.rs` — Base-image migration: manifest capture via throwaway containers, the pure
|
||||
delta computation (dpkg-ownership filter, bind-mount exclusion, verbatim-copy set), and the
|
||||
crash-recovery state machine. See "Base-image migration" below.
|
||||
@@ -180,6 +184,25 @@ security update. Migration is the non-destructive way out; Reset is the destruct
|
||||
label plus the persisted state file let `reconcile_project_statuses` offer resume or rollback.
|
||||
- **Rollback restores the system layer only.** The volumes are never touched at any point, so work
|
||||
done in `$HOME` during a migrated session survives a rollback. Say so in any UI copy.
|
||||
- **`/var` is never copied either, and that is the one way migration is *more* destructive than
|
||||
the ordinary recreate.** A recreate builds from the project's snapshot, so `/var/lib/postgresql`
|
||||
rides along; a migration builds from the base and the apt replay hands back an empty cluster.
|
||||
Copying a live database's files onto a different base's version of the same package is a
|
||||
corruption risk, not a fix — so the answer is disclosure. `unpreserved_data()` reports
|
||||
first-level directories under `/var/lib` and `/var/www` that the base does not ship *and* that
|
||||
hold non-dpkg-owned files (which is what keeps `/var/lib/apt` and `/var/lib/dpkg` out of it),
|
||||
and the pre-flight, the banner and the finished report all name them. Do not make this silent.
|
||||
- **The rollback pin is not best-effort.** After `commit_container_snapshot` the commit is the only
|
||||
copy of the old system layer, so a `docker tag` that fails — or succeeds without the reference
|
||||
resolving — aborts the migration before `remove_container`. Same rule in reverse for
|
||||
`rollback_migration`: the image is confirmed to exist before the container is destroyed.
|
||||
- **`resume` must check the container's `triple-c.migration-state` label**, exactly as
|
||||
`reconcile_migration` does. Without it a record left behind by a failed commit "resumes" into
|
||||
the *old, unmigrated* container and commits it as migrated.
|
||||
- **Anything that stops, removes or recreates a project's container consults
|
||||
`migration_commands::is_migrating`.** The window between `remove_container` and the create that
|
||||
follows looks exactly like "no container" to Start, and Reset would delete the volumes out from
|
||||
under a live run.
|
||||
- **`/etc` is never copied**, only reported: the snapshot lineage has
|
||||
`/etc/apt/sources.list.d/nodesource.sources` where the current base has `nodesource.list`, and
|
||||
having both breaks every `apt-get update` on a duplicate source. Verified, not theoretical.
|
||||
|
||||
Reference in New Issue
Block a user