Repository navigation
fix(sandbox): pin image version + chown /workspace + GHCR release pipeline - #72
Merged
Merged
Conversation
…eline
Three related fixes that converge on one prod symptom: workspaces failing
to come up behind HTTPS with "frame too large: 1329809696 bytes" (the
ASCII bytes "OCI " from a Docker exec error).
1. /workspace ownership (root cause of the ownership symptom)
The temps server runs as root in prod, so the host work_dir it creates
for bind-mount is root-owned. Inside the container, USER temps then
can't write anywhere in /workspace — TUIs fail, dev servers can't
open lockfiles, and any seed performed via docker exec runs as the
image's USER but inherits the host inode UID.
Fix: post-start `chown -R temps:temps /workspace` exec as root, mirror
of the existing /home/temps pattern. CHOWN+FOWNER caps are already on
the container.
2. Pinned sandbox image version (root cause of the missing-socat symptom)
`:latest` short-circuits in `ensure_image_for_runtime` because
`inspect_image` returns Ok for any cached tag. Once a host has a
stale :latest, it never re-pulls. Replace the hard-coded :latest with
`SANDBOX_IMAGE_VERSION` (currently "0.1.0") so bumping the constant
forces every host to pull the new tag.
Registry switched from Docker Hub (gotempsh/) to GHCR
(ghcr.io/gotempsh/) per ops decision.
3. Release pipeline (long-term DX so this doesn't recur)
Two new jobs in release.yml, triggered on the existing v*.*.* tags:
- build-and-push-sandbox-images: matrix over the 6 runtimes, builds
multi-arch (amd64+arm64), tags :<SANDBOX_IMAGE_VERSION>, :latest,
and :<sha>. Reads the version directly from docker.rs.
- build-and-push-preview-gateway: same shape, version comes from
Cargo.standalone.toml.
Also includes UI fix in RepositoryList.tsx (snake_case→camelCase API
field rename + updated_at→pushed_at sorting).
Workspace sessions emit URLs of shape `ws-<16hex>-<port>.<domain>` because their `public_id` is `wss_<16hex>` and `host_for()` strips the prefix. But `parse_preview_host` is synchronous (can't hit the DB) and routes any 16-hex label to `PreviewTarget::Sandbox` — the lookup then misses in the sandboxes table and returns 404 "Workspace preview not found", even though the workspace exists. Fix: add `resolve_preview_target` that runs after parsing. If the target is `Sandbox(hex)`, query `workspace_sessions` for `public_id = wss_<hex>` first; if found, swap to `WorkspaceSession(row.id)`. Otherwise leave it as a sandbox lookup. One extra unique-index hit per request — negligible. Also: the 404 body said "Workspace preview not found" for sandbox lookups too. Genericized to "Preview not found" since both targets land in the same branch. _typos.toml: allow the W3C traceparent spec example span-id used in existing test fixtures (the hex string contains "ba" which the spell checker mistakes for "by"/"be"). Repro: hit any `https://ws-<16hex>-<port>.<domain>/` URL where the hex matches a workspace session — pre-fix returns 404, post-fix returns the preview login page (or proxies through if cookie is valid).
Two follow-ups on the preview-routing fix:
1. NotConfigured outcome (was indistinguishable from NotFound)
`lookup_preview_session` previously returned `NotFound` for both
"session row doesn't exist" and "session exists but
`preview_password_hash` is NULL". Both manifested as the same cryptic
404, leaving users to guess whether the workspace was deleted or the
preview was just unconfigured.
Split into a new `PreviewSessionLookup::NotConfigured` variant that
propagates through `check_preview_auth` as
`PreviewAuthOutcome::NotConfigured`. The proxy renders a small HTML
page at 409 Conflict ("This workspace exists, but no preview password
has been set — set one in the workspace settings") instead of the
generic 404. The inline POST /__temps/preview/login handler also
returns 409 with a plaintext fallback for non-browser clients.
Sandbox targets are unaffected — sandboxes treat "no password" as
`Allow` (the unguessable hex IS the gate), so this state only applies
to workspace sessions.
2. Tests for resolve_preview_target
Four `#[tokio::test]` cases using `MockDatabase` cover:
- hex matches a workspace_sessions row → swap to WorkspaceSession(id)
- hex doesn't match → leave as Sandbox(hex)
- already a WorkspaceSession (digit URL) → pass through (no DB query)
- DB error → log + degrade to Sandbox(hex), never panic
All 4 pass; full preview_auth suite is 19/19.
Beta releases were sharing the canonical `:<version>` ref with stable, so a beta whose Dockerfile diverged could overwrite the image a stable host expected to find at the same SANDBOX_IMAGE_VERSION. Split the channels: stable (vN.N.N) -> :<ver>, :<ver>-stable, :latest, :stable, :<sha> prerelease (vN.N.N-x) -> :<ver>-beta, :beta, :<sha> (never unsuffixed) Server image (`temps`) gets the same `:beta` vs `:latest` split via the manifest-merge job; per-platform intermediate tags now use the channel name instead of a hardcoded `latest-`. Runtime: new SandboxChannel enum, default Stable. Beta hosts opt in via TEMPS_SANDBOX_CHANNEL=beta so the pull-cache path resolves to the beta image stream. hub_image_for_runtime_in_channel() is the testable seam; the env-reading wrapper stays for production callers. scripts/build-sandbox-images.sh defaults to SANDBOX_CHANNEL=beta so an accidental local push never poisons stable.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three related fixes that converge on one prod symptom: workspaces failing to come up behind HTTPS with
frame too large: 1329809696 bytes(the ASCII bytesOCIfrom a Docker exec error)./workspaceownership: post-startchown -R temps:temps /workspaceexec, mirror of the existing/home/tempspattern. Fixesroot:rootownership on bind-mounted host work_dir when the temps server runs as root.:latestwithSANDBOX_IMAGE_VERSIONconstant (currently0.1.0).:latestshort-circuits inensure_image_for_runtimebecauseinspect_imagereturns Ok for any cached tag — bumping the constant is now what forces hosts to re-pull. Registry switched from Docker Hub to GHCR (ghcr.io/gotempsh/).release.yml, triggered on the existingv*.*.*tags —build-and-push-sandbox-images(matrix over 6 runtimes, multi-arch) andbuild-and-push-preview-gateway. Both read versions from source-of-truth files so tag drift is impossible.Root cause analysis
The user reported the agent terminal failing with
frame too large: 1329809696 bytes. Decoding that integer as 4 BE bytes givesb"OCI "— the start ofOCI runtime exec failed: ...from Docker. The handler was reading Docker's error string as a u32 frame length header.The proximate cause was
socatmissing fromgotempsh/temps-sandbox-node:lateston Docker Hub. The image source hassocatin itsapt-get installline — but the published:latestwas built from an older revision, and:latestnever re-pulls once cached locally. Same root cause covers/workspacebeing owned byrootin the Hub image (image hadUSER temps, but bind-mount inherits host UID, and the platform creates the host dir as root).Fixes in detail
1.
/workspacechown (crates/temps-agents/src/sandbox/docker.rs)Mirror of the existing
/home/tempschown that runs as0:0immediately after container start. Uses theCHOWN+FOWNERcaps already added to the container.2. Pinned image version
SANDBOX_IMAGE_VERSIONconstant indocker.rsis the single source of truth. The runtime pulls images at this exact tag, so bumping the constant + shipping a release that publishes the new tag is the entire upgrade flow.3. Release workflow
Triggered only on release tags (
v*.*.*). ReadsSANDBOX_IMAGE_VERSIONdirectly fromdocker.rsfor sandbox images and fromCargo.standalone.tomlfor the preview gateway — no manual tag input, no drift between the runtime and what gets published.Also included
UI fix in
RepositoryList.tsx: snake_case → camelCase API field rename (preset_label→presetLabel) andupdated_at→pushed_atfor the recency sort.Test plan
v0.0.X) and verify the new jobs publish all 7 images toghcr.io/gotempsh/ghcr.io/gotempsh/temps-sandbox-node:0.1.0containssocatand hastemps:tempsownership when startedls -la /workspaceshowstemps:tempsand the agent terminal opens without "frame too large"cargo test --lib -p temps-agents -- sandbox::docker::still passes (verified locally: 22/22)🤖 Generated with Claude Code