preview-307ea07
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b49dddab45 |
Make a blanked project credential actually clear
Handoff from the credential change on `r2/sec`, which landed `secure::store_or_clear_project_secret` and left a `TODO` naming this call site. `store_secrets_for_project` could only ever write: a token blanked in the UI stayed in the keychain, `load_secrets_for_project` read it straight back onto the project, and the container went on receiving a credential the user had revoked. **Not the mechanical switch the handoff describes, deliberately.** Every secret field is `#[serde(skip_serializing)]`, so the project object the frontend holds carries no `git_token` key at all — a save from the Workspace, Runtime or Model section sends the field *absent*, while the editor that owns it sends `git_token: null` when the user blanks it. `Option<String>` maps both to `None`, so clearing on `None` would delete every project secret each time an unrelated setting was changed. `update_project` therefore takes the raw payload, records which secret fields arrived as an explicit `null`, and clears exactly those; absent still means "not mine to touch". **This commit does not build on `r2/scrub` alone** — it calls `secure::store_or_clear_project_secret`, which exists only on `r2/sec`. Verified green against that file: 464 tests pass with `r2/sec`'s `storage/secure.rs` in place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc |
||
|
|
6d27f924ff |
Stop the scrub emptying a mount that is itself a glob match
The parent checks in `snapshot_scrub_script` validate the directory a
pattern is anchored to, and `rm --one-file-system` compares against its
own command-line argument's device — so with the mount planted *at* the
match there was nothing between the scrub and the mounted filesystem.
Verified against a live daemon with the byte-identical generated script:
mounted at the parent (`/var/log/apt`) refused, mounted one level below
(`/tmp/claude-x/inner`) refused, mounted as the match (`/tmp/claude-x`)
came back with the volume empty. The same run against a host directory
bound at `/workspace/../tmp/claude-x` — the target the daemon builds from
a mount name of `../tmp/claude-x`, confirmed through the API bollard uses
— emptied the host directory.
Each match is now checked against the root's device too, which for a
directory whose parent has already been validated is exactly a
"not a mount point" test. A symlinked match still reports the link's own
device, so `rm -rf -- link` goes on unlinking it and stopping.
The tools are also named absolutely and `PATH` is reset. The image's
`PATH` starts with three directories inside the container's persisted
home volume, and a three-line `stat` shim planted in the first of them
made the previously-refused `/var/log/apt` mount delete its contents.
"Missing `stat` fails closed" was true and beside the point.
`update_project` validated nothing while `add_project` validated its
folder list, so the mount name that reaches all of this was one
save-on-blur away from anything at all. Both now share
`validate_project_paths`, which also refuses `..`, a filesystem root as a
host path, and a half-filled row; `container_id`, `status` and
`created_at` are no longer writable through a project save.
`remove_project` was the only writer of a container, a snapshot image or
a volume that took no project claim (H-2), so removing a project during
a compaction let `restore_image_config` commit a flat image back over
`triple-c-snapshot-{id}:latest` for a project that no longer exists —
invisible to every reclaim path. It takes `ProjectOp::Destroy` for the
whole removal; the sidebar row survives a refusal because it is only
dropped after the command resolves.
Finally, a commit whose scrub was skipped no longer logs "0.00 MB
dropped", which every migration did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
|
||
|
|
ae3ca8cda4 |
Fix root scrub deleting host files through symlinked parents (C1)
SNAPSHOT_SCRUB_PATHS is expanded by /bin/sh inside the container as root. For an entry ending `/*` the parent is a path *component*, resolved by both the glob expansion and the `rm -rf`. The agent has passwordless sudo, so `ln -s /workspace/myproject /var/log/apt` turned the next commit into a recursive delete of the user's real files on the host — reproduced end to end against a live container. snapshot_scrub_script now routes every deletion through one `scrub_in` function that validates the parent before touching anything inside it: `cd -P` for a TOCTOU-free handle, `pwd -P` equality to reject a symlinked component, a hardcoded containment allowlist that is deliberately not derived from the path list, and an st_dev comparison against `/` so a bind mount or a volume is refused even though it is not a symlink. It fails closed when `stat` is missing. Also: - /tmp/triple-c-drops/* and /tmp/clipboard_*.png are age-limited to 14 days instead of scrubbed unconditionally. They hold the user's own files, and scrubbing them meant "drop a file, change a setting, lose it silently"; removing them from the list would restore unbounded growth instead. - M11: scrub_writable_layer returns a ScrubOutcome and skips cleanly when the container is not running, so a migration no longer logs "could not run … committing anyway" on every run. - M12: the scrub exec is bounded by a 120s timeout; on expiry it logs and lets the commit proceed. - The script is now fold-safe (self-terminating lines, no `#` comments). disk.rs folds it onto one `RUN` line and the previous form was a `"do" unexpected` syntax error there, so compaction had been scrubbing nothing at all. Tests: the substring check on the script text is replaced by a behavioural test that runs the real generated script with a real symlink planted in a throwaway tree, plus structural tests over each containment construct and a `sh -n` check of the folded form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc |
||
|
|
7a5823cb2b |
Inject the corporate CA certificate into containers
Build App / compute-version (pull_request) Successful in 6s
Build App / build-macos (pull_request) Successful in 2m30s
Build App / build-windows (pull_request) Successful in 5m16s
Build Container / build-container (pull_request) Successful in 10m15s
Build App / build-linux (pull_request) Successful in 6m35s
Build App / create-tag (pull_request) Skipped
Build App / sync-to-github (pull_request) Skipped
Behind a TLS-terminating corporate proxy every HTTPS call inside a container fails — npm, pip, git, curl, the browser-view pane, and Claude Code's own API requests. There was no mechanism at all: installing the certificate by hand inside a container is lost on Reset and had to be repeated per project. A global CA path in AppSettings with a per-project override on Project, taking either a single certificate file or a directory. It is bind-mounted read-only at /tmp/.host-ca (mirroring /tmp/.host-ssh and /tmp/.host-aws) and applied by entrypoint.sh on every start, so it survives recreation, migration and Reset. Four things this gets right that are easy to get wrong: * update-ca-certificates globs *.crt case-sensitively, so a .pem that is merely copied in is ignored in silence. Certificates are renamed, by container_cert_name() in Rust and a mirrored few lines of shell. * The system store only serves curl/git/apt. Node — and so Claude Code itself — needs NODE_EXTRA_CA_CERTS, Python needs REQUESTS_CA_BUNDLE/SSL_CERT_FILE, and Chromium reads neither: it wants ~/.pki/nssdb, seeded with certutil (libnss3-tools, added to the image). * Those vars are set from Rust at creation, never exported by the entrypoint — a terminal is a docker exec and sees nothing the entrypoint exported. They are emitted empty when no CA is configured, since docker commit bakes env into the snapshot image. * triple-c.ca-fingerprint hashes the certificate bytes as well as the path, so a CA rotated in at the same location still forces a recreation. Verified end to end against a real container and a self-signed CA: curl, node, python and git all complete a TLS handshake against a server signed by it and all three fail in the same container without it; the env vars are visible from a docker exec session; the store is cleaned when the setting is cleared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k |