4d1a5a2417bdb9703051c35e9d3dd23316ec2464
68
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4d1a5a2417 |
Remove list_sibling_containers, which nothing called
It listed every container on the daemon — including the user's unrelated postgres, mysql and other work — and handed the summaries to the webview. It had a registration, a command, a docker-layer helper, a typed frontend wrapper and a `SiblingContainer` type, and zero call sites. An audit named it as step one of an escalation chain: enumerate the daemon's containers, then point `update_project`'s unvalidated `container_id` at one and read its files through the file-command surface. The second half of that chain is closed now, but a command that exposes the user's unrelated containers and serves no feature is surface with no upside. Found by the registration test added in the previous commit, which is the answer to "why test something the compiler already checks": the compiler is perfectly happy with a command nobody calls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc |
||
|
|
913aa85805 |
Stop the pre-commit scrub running with its guards silently absent
Two fail-open gaps, both latent on the shipped image and both live on an ImageSource::Custom one. `--one-file-system` is the only bound on the scrub's `rm -rf`, and it was probed into `$rmopt` with `rm --one-file-system --help`. BusyBox answers that with `unrecognized option` and exits 1, so on any busybox- or toybox-derived image the variable was empty and the delete ran as root across mount boundaries — reported as a completed `Reclaimed(n)`. Measured in `busybox:latest` with a named volume one level below the match at /tmp/claude-x/inner: emptied, `###TRIPLE-C-SCRUBBED 102423`. It is a prerequisite now, probed by `rm --one-file-system -f -- ''` — the code path that matters, with the one operand no filesystem can name — and an image without it prints `###TRIPLE-C-SCRUB-UNAVAILABLE` and deletes nothing. That costs Alpine and busybox images their scrub, which is the cheaper half of the trade: declining costs disk, proceeding costs the mount. The second is that resetting `PATH` was never the whole of command lookup. `bash` builds functions out of `BASH_FUNC_<name>%%` environment variables, function lookup precedes `PATH` entirely, and `command -v` reports a function as found — so a planted `stat` passed the prerequisite probe and then answered the containment checks. Every in-shell answer is itself importable: `unset`, `command`, and — the point that settles it — `[`, `pwd` and `cd`, which are checks 0 to 2 rather than merely the tools. So the script is no longer run by the shell that read the environment. The exec's argv is a bootstrap using only reserved words, parameter expansions and command words containing a `/` (which `bash` refuses to import a function for), and it hands the script as `$1` to a second `/bin/sh` started by `env -i`. Measured on `/bin/sh -> bash` with `BASH_FUNC_stat%%` set on the container and a volume mounted at the match: the old invocation emptied it and printed 306565, the bootstrap left it intact and printed 65536. `/usr/bin/env` then `/bin/env`, because H3's lesson about hardcoded coreutils locations applies to `env` too. The comment claiming the `PATH` reset "defeats it just as completely as spelling /usr/bin/stat out" is corrected, as is the one claiming a sudo-written /usr/bin/stat does not survive a container restart — it lands in the writable layer, which is exactly what the commit this runs in front of captures. Also here: a stored project path row with an empty host_path or mount_name no longer becomes a mount. The first sends `field Source must not be empty` back for the whole create, so the project cannot start at all until the row is gone, and no amount of save-time validation reaches a record already on disk; the second mounts over /workspace itself and the daemon then creates the other rows' mount points inside the user's real folder. And in migration_commands, the deferred-reconcile claim is RAII rather than a trailing statement — a panic inside `reconcile_migration_now` stranded it for the rest of the process — and `await_release` looks before it sleeps, so a project released a moment later no longer costs a full twenty seconds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc |
||
|
|
00128f9b1a |
Make the scrub work off /usr/bin, and stop a log level deciding whether it runs
H3 — the pre-commit scrub was a silent no-op on any base image whose coreutils
are not under /usr/bin. Hardening against a PATH-planted `stat` shim by naming
every tool absolutely bought nothing the `PATH` reset on the first line had not
already bought — the shim lives in the persisted home volume, and uid 1000
cannot write /usr/bin or /bin — and it cost the whole feature on Alpine, which
Settings -> Docker -> Custom accepts. Measured on one seeded tree in a real
container: the absolute-path script printed `###TRIPLE-C-SCRUBBED 0` and left
every planted file in place; the PATH-resolved one reclaims 77824 bytes. The
default image is unchanged at 521038.
It was silent three times over, and all three are fixed:
* The script now probes for all six things it needs (`command -v` for the five
tools, plus the root device id reading back as a number) and, if any is
missing, prints `###TRIPLE-C-SCRUB-UNAVAILABLE <what>` and no total at all.
* `scrub_writable_layer` reads that marker first and returns a new
`ScrubOutcome::Unavailable`, warning with what the image is missing; a
genuine `Reclaimed(0)` now leaves a debug line rather than nothing.
* `commit_log_suffix` renders `Reclaimed(0)` as "ran and found nothing to drop"
rather than "0.00 MB dropped", which is what a scrub that could not run used
to look like.
Verified in real containers: the mount-at-the-match defence still holds with a
home-volume `stat` shim first on PATH (the volume survives; deleting the PATH
reset from the same script empties it, so the harness can tell the difference).
H2 — the pre-migration scrub had been folded into `log::info!`'s argument list
to satisfy `#[must_use]`. `log::info!` expands to `if Info <= max_level() { … }`,
so the awaited scrub lived inside the level check, and `logging::init`
tolerates `dispatch.apply()` failing — which returns before `set_max_level` and
leaves the process at `Off`. In that state the scrub never ran and the layer
was committed into the longest-lived snapshot the app takes. The outcome is
bound first now, `logging::init` restores the level on failure and says so on
stderr, and a test scans all four files for an `.await` inside any `log::*!`
argument list.
Also:
* `reconcile_migration` deferred a held project instead of dropping it. Its
only caller fires once per "Docker became available", so a project held at
that instant was never revisited for the session — phase un-normalised, no
resume or rollback offered, pin left `Claimed`. It now waits for the holder
to let go (20s x 90, one waiter per project) and reconciles then.
* The scrub's byte total counts what a partly failed `rm` removed, by
re-measuring rather than dropping the whole subtree on a non-zero exit —
which was exactly the `--one-file-system` case.
* The scrub exec blanks `LD_PRELOAD`, `LD_AUDIT` and `LD_LIBRARY_PATH`.
`LD_PRELOAD` is in none of the reserved env families, so a project's custom
env var reached a root exec and injected code into every tool the scrub runs,
`PATH` reset or not. Verified against Engine 29.7 that `docker exec -e` wins.
* The device test is described honestly: it is a mount test under `overlay2`
and not under `vfs`, where checks 1 and 2 are what still hold.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
|
||
|
|
ed91423666 |
Hold back the Disk panel and OS drag-out from the ship branch
This is a scope reduction, not an abandonment. Both subsystems are
preserved in full on `hold/disk-and-dragout` and are intended to come
back once they have been hardened separately. Nothing here is a
judgement that the features are unwanted — three successive
audit-and-fix cycles each closed a critical defect in these two areas
and each opened a new one, so the rest of the round ships now and these
two get their own cycle rather than holding it up.
Removed: the Disk settings panel and its whole reclaim / destroy /
compaction surface — `DiskSettings`, `DiskProjectTable`, `useDiskUsage`,
`docker/disk.rs`, `disk_tests.rs`, the disk commands in
`docker_commands.rs`, and their `generate_handler!` entries. Dropping
the IPC entries is the point: a UI-only removal would have left five
commands callable by a compromised webview, one of them a verified
arbitrary-DELETE primitive. `sweep_orphaned_snapshots`'s *command* goes
with them (the panel was its only caller); the sweep itself stays.
Removed: OS drag-out from the Files tab — `stage_container_file_for_drag`
and its host staging lifecycle, the pointer gesture and `dragPreview`,
`stageForDrag` / `isStagedHostPath`, the `tauri-plugin-drag` and
`@crabnebula/tauri-plugin-drag` dependencies, and the
`drag:allow-start-drag` capability grant, which could not be scoped.
The capability test's expected list is updated; its `*:default` and
`store:*` assertions are untouched.
Kept, deliberately: drag-and-drop *into* the app (Files pane and
terminal) and "Save to host…", which is now the only route out of a
container. The prevention work is untouched — the pre-commit scrub and
`SNAPSHOT_SCRUB_PATHS`, capped container logs, the `triple-c.base` /
`triple-c.managed` labels, `sweep_orphaned_snapshots` and the startup
housekeeping, the migration pin/probe reapers, scheduler log pruning,
`formatBytes.ts`, and `project_lock.rs` in full with every acquisition
site outside `disk.rs`.
Entanglements, resolved rather than deleted blind:
* `container.rs`'s `a_compaction_runs_this_module_s_scrub_script_byte_for_byte`
pinned the compaction Dockerfile against `snapshot_scrub_script()`.
Dropped — it existed only for compaction. `snapshot_scrub_script` and
its containment tests are untouched.
* `lib.rs`'s startup reap of `:compacting` tags and `triple-c-compact-*`
containers is dropped: nothing on this branch creates them.
* `project_lock`'s `Compaction` / `CacheClear` variants and
`any_held_excluding`, `migration_commands::is_migrating`, and
`formatBytes{Delta,Ceiling}` lose their last production caller but are
kept and still tested, annotated with why.
* `projects_store::corrupt_since` and `migration_store::peek_ownerless_since`
were read only by the disk survey and are removed. The corrupt-load
marker and `.bak` are still written.
Verified: `npm run test` 611 passing, `npx tsc --noEmit` clean,
`npm run build` green; `cargo test` 419 passed / 2 ignored,
`cargo build` 0 warnings. Every test removed belongs to a removed
feature — no kept-behaviour test was weakened or deleted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
|
||
|
|
6a8972980d |
Stop an untouched secret field from deleting the stored credential
Making `null` actually clear a secret — which it had to, since a blanked
token was previously never revoked — turned the config editors into a
credential shredder. Secrets are `#[serde(skip_serializing)]`, so the
inputs always render empty whether or not one is stored, and the blur
handlers sent `value || null` unconditionally. Focusing the Git token
field and tabbing away deleted it, with nothing shown and no undo.
`useSecretField` encodes the rule: only a field the user typed in may
speak about a secret. Untouched, `patch()` contributes no key at all, and
Rust already distinguishes an absent key from an explicit null.
`withoutUntouchedSecrets` covers the structural half. `saveBedrock`
spreads `{ ...bedrock, ...patch }`, and when that falls back to
DEFAULT_BEDROCK_CONFIG the literal spells every secret out as `null` — so
editing the AWS region would have wiped the credentials as a side effect.
Also here: `WorkspaceSection` no longer saves a half-filled folder row,
which `update_project`'s new validation would refuse on every keystroke
between the two inputs; `snapshots_skipped` is declared on the wire type
rather than widened locally; and `#[must_use]` on `ProjectGuard` and
`ScrubOutcome` — which immediately caught the migration path discarding
its scrub outcome, the one scrub whose silence is expensive because the
layer it declined to clean is about to be committed.
The capability test reads the real file and is mutation-verified: adding
`core:default` back makes it fail. That grant pulls in an unscoped
`std::fs::read` of any host path and went unnoticed for months, because
nothing in the suite read the file at all.
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
|
||
|
|
a76f2c0a17 |
Close the cross-stream gaps the parallel fix round left open
Three items each of which fell between two agents' file lists.
`scrub_secrets_from_snapshots` was the third unsynchronised writer of
`triple-c-snapshot-{id}:latest`, after a recreate's commit and a
compaction. It has the same read-modify-write shape — create a scratch
container from the snapshot, commit back over the same tag — and loses
the same race, which here means re-baking the very credential it exists
to remove. It now takes the project's claim, under a new
`ProjectOp::SecretScrub`, and reports a snapshot it had to skip rather
than rewriting it unsafely.
Its scratch container also now carries `triple-c.scrub=true`, so the
Disk panel's reclaim bucket discriminates by label and by the live claim
rather than by a clock. The 15-minute age gate stays as the backstop for
the cross-process case the claim cannot see.
The store plugin is unregistered and its dependency dropped. Its
capability grants were removed as a host-file-write primitive; the
registration without a grant was unreachable but dead.
Finally, container.rs's fold test was pinning a fold that no longer
exists — disk.rs now emits the JSON exec form. It asserts the stronger
property instead: the script a compaction runs is byte-for-byte the one
snapshot_scrub_script() produces, so the two files cannot drift silently.
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 |
||
|
|
0003793abb | Merge branch 'feat/disk-ui' into integration/round-1 | ||
|
|
77ef2291d7 |
Add a Disk section: see where the bytes went, and get them back
Every recreation runs `docker commit`, which stacks a layer and never rewrites one, and 24 conditions in `container_needs_recreation` trigger a recreation. Prevention landed earlier on this branch; this is the half a user can act on. The per-project table leads with the two numbers that explain the mechanism rather than just the total: how many commit layers a snapshot has stacked above its base, and what the container's writable layer will add at the next commit. Backend (`docker/disk.rs`, commands in `docker_commands.rs`): - `get_docker_disk_usage` — one `df()` joined against the project store, behind an explicit Scan button because it walks the whole daemon. - `list_reclaimable` / `reclaim` — classified buckets with measured bytes, planned off the existing report so re-planning costs no second scan. - `destroy_project_disk_object` — one object, typed confirmation. - `sweep_orphaned_snapshots` — exposed, so its report is finally visible. Safety is structural: `reclaim` takes `ReclaimTarget`, which has no variant that can name a live project's data. Destructive work is a separate type reached only through `destroy`. No unfiltered prune is called anywhere, and nothing outside a `triple-c*` name or `triple-c.*` label is touched. Orphan detection subtracts ids from the project store and consults nothing else. From the daemon's side an idle live project and a deleted one are indistinguishable — volumes present, no container, no image — so inferring from container or image absence would offer a live project's credentials and transcripts for deletion. A store that loaded empty from an existing `projects.json` is treated as a failed load, not as "no projects", because `ProjectsStore::new()` recovers from a corrupt file by starting empty. Three things verified against a live Docker 29.7.2 rather than assumed: - Compaction is a two-stage build (`FROM scratch` + `COPY --from`), which keeps every byte inside the daemon; bollard's import buffers a whole image into memory. uid/gid and setuid survive; a 192.6 MB/4-layer synthetic came out 45.7 MB/1 layer. Image config does not survive, so it is replayed via create+commit, which round-trips a multi-line env var that a Dockerfile `ENV` could not. - Flattening breaks base-layer sharing, so the result carries its own copy of the base. Eight of ten real projects had a 0.10–1.32 GB delta over a 4.72 GB shared base — compacting those costs ~4 GB. The bound now subtracts that penalty, such projects are not offered at all, and the run compares unique bytes and abandons a rewrite that would grow. - `docker builder prune` reports `Total:`, not `Total reclaimed space:`, so the first parser scored every prune as freeing nothing. The Windows/WSL2 note is mandatory and its copy lives in Rust beside the tests that pin it: pruning frees space inside `ext4.vhdx`, which never shrinks on its own, so C: does not change until the disk is compacted. Also adds `lib/formatBytes.ts` — the app had four disagreeing copies, and `projects/home/format.ts` and `migrationCopy.ts` now delegate to it with byte-identical output. Base 1000 by default, matching what Docker prints. Tests: 502 frontend (was 453), 365 Rust (was 322). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc |
||
|
|
0a022dfcf0 |
Let a project turn a globally-enabled Claude Code setting back off
The six boolean settings were plain `bool`s merged with
`if p.x { true } else { g.x }`, so a project could only ever add to the
global set. There was no project value that produced `false` — turning a
switch off at project level simply fell through to the global value and
the control did nothing.
Widen them to `Option<bool>`. `None` means "not set at this level":
inherit the global on a project, leave Claude Code's own default alone
globally. `Some(false)` is a deliberate off and wins outright.
The fingerprint now formats with `{:?}` rather than `{}` — `None` and
`Some(false)` mean different things, and conflating them would leave the
container un-recreated when a project switched from inherit to off.
The project editor grows a third "Global" state per switch; the global
editor has nothing to inherit from, so it stays a plain toggle and keeps
collapsing to null at the default. Its three existing tests passed
unchanged and caught a first attempt that rendered unset as off, which
would have told every user their session recap was disabled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
|
||
|
|
bb41275cea |
Keep the managed settings payload safe for un-migrated projects
The null-means-delete convention is only understood by the entrypoint.sh shipped alongside this code. An existing project recreates from its own snapshot image, which carries whatever entrypoint it was built with, and an older one merges with a plain `.[0] * .[1]` — so the literal nulls would land in the user's settings.json rather than clearing the keys. Verified against jq: that produces `"tui": null, "effortLevel": null, "viewMode": null, "awaySummaryEnabled": null`, risking the whole file being rejected and taking the user's own `model` and `statusLine` with it. Split the payload instead. CLAUDE_CODE_SETTINGS_JSON now carries only keys that have a value and is safe under either merge; the new CLAUDE_CODE_SETTINGS_CLEAR carries the key names to delete and is ignored by an entrypoint that predates it. Such a project keeps the old sticky behaviour until it is migrated or Reset, which is the pre-existing state rather than a regression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc |
||
|
|
2ca86bb5d8 |
Scrub the writable layer on the migration path too, and de-duplicate CLAUDE_JSON
Two integration fixes after merging the three feature branches. `scrub_writable_layer` is a `docker exec`, so it only works while the container runs. `migrate_project_to_base` stops its container one line before the pre-swap commit, which meant the single largest snapshot Triple-C ever takes was the one path that committed unscrubbed. Call the scrub explicitly before the stop instead of relying on the call inside `commit_container_snapshot`. Also drop a duplicate `CLAUDE_JSON=` assignment in entrypoint.sh. The Shift+Enter block re-declared it defensively to avoid a merge conflict with the awsAuthRefresh block; the conflict did not materialise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc |
||
|
|
dd2894cc60 |
Stop Docker disk growth, and fix the Claude Code settings that never worked
Two independent sets of fixes.
## Disk: stop the growth, no UI this round
The dangling-snapshot sweep was already correct and was never the leak. The
leak is that every `docker commit` **stacks** a layer and nothing compacts one:
a file deleted after it has been committed becomes a whiteout, not free bytes.
24 conditions trigger recreation+commit, so changing one settings field costs a
multi-gigabyte layer for the life of the project. One project was measured with
14 stacked commit layers, ~5.1 GB above its base.
* **Scrub the writable layer before every commit** (`docker/container.rs`,
`SNAPSHOT_SCRUB_PATHS` / `scrub_writable_layer`). The one moment those bytes
are still free to drop is before the commit that captures them. Measured on
one container's 4.48 GB pending layer: 3.0 GB of agent scratchpad under
`/tmp/claude-*`, the terminal drag-drop staging area (256 MiB per file, with
no `rm` for it anywhere in the repo), a PNG per pasted image, and the apt
lists/cache/logs that `browser_view/install.rs` and `triple-c-playwright-heal`
leave behind with no `apt-get clean`. A hardcoded list, never a heuristic:
`/workspace/{mount_name}` is a host bind mount and nothing here may reach one,
and the three `/tmp` globs cannot select the read-only `.host-ca`/`.host-aws`
mounts. Failure is a log line — a scrub must never block a snapshot.
* **Cap container logs** (`capped_log_config`). There was no `LogConfig`
anywhere, so containers ran on the daemon's unbounded `json-file` default.
Deliberately *not* wired into `container_needs_recreation`: participating
would recreate every project once, and a recreation costs a commit, which is
the thing being fixed. Picked up on the next natural recreation.
* **Make superseded base images sweepable** (`container/Dockerfile`). It carried
no `LABEL` at all, so `orphan_sweep_filters`' `dangling` + `triple-c.managed`
pair provably could not match one — ~11.9 GB observed stranded. Stamping
`triple-c.managed=true` is the whole fix; the sweep needed no change.
`create_container` writes the new `triple-c.base` key explicitly empty, or
Docker's label inheritance plus `docker commit` would make every snapshot
claim to be a base image. `force: false` stays, and now says why.
* **Sweep at startup** (`lib.rs`), not only after recreation: probes first
(a probe pins an image the unforced sweep then refuses), pins second, sweep
last. `sweep_orphaned_snapshots_logged` exists because all three callers threw
the report away — `reclaimed_bytes`, `failed` and `unavailable` included.
* **Reap migration leftovers.** `rollback_migration` retagged and orphaned the
migrated snapshot with no sweep. Stale `pre-migration-*` pins are now
age-reaped by scanning the tag pattern rather than trusting the state file —
`migration_store::load` reports an unparseable record as absent, which
stranded a 4-12 GB pin nothing could name again; `load` now moves a corrupt
record aside so `has_record` is trustworthy. A pin whose migration is still
awaiting confirmation is never reaped at any age. The probe container's
removal was a plain statement after an await, so a dropped future (an app quit
mid-migration) leaked a container pinning a multi-gigabyte image; it is a
`Drop` guard now, with `reap_probe_containers` for the case where the process
itself dies.
* **Prune scheduler logs.** `remove` deleted a task's JSON but never its log
directory, and the task runner appended uncapped `claude -p` output.
* **Fix the delete copy.** It said "the container, config volume, and stored
credentials"; it removes *both* volumes and the snapshot image.
No prune UI, and no unfiltered `prune_images`/`prune_volumes` anywhere — the
daemon is shared with the user's unrelated work.
## Claude Code settings: two invented keys, one inverted default, one sticky bug
Verified against code.claude.com/docs/en/settings-reference.md and env-vars.md.
* `effort` -> **`effortLevel`**, the key Claude Code actually reads; the old one
was written and silently ignored. `xhigh` added to the dropdown.
* `focusMode` -> **`viewMode: "focus"`**. `focusMode` was invented. The real key
does exactly what the existing UI hint already described.
* **Session recap was inverted.** Claude Code's recap is on by default, so
`CLAUDE_CODE_ENABLE_AWAY_SUMMARY=1`-when-enabled was a no-op and the control
could never turn the recap *off*. The field is renamed to
`session_recap_disabled` rather than reused: reusing the name with the
opposite meaning would have read every stored `enable_session_recap: false` —
which is every project that never touched the control — as "the user turned
this off".
* **The stickiness, which is the important one.** Keys were emitted only when
non-default, and the entrypoint *merges* into a settings.json on a persisted
volume, so switching a setting off omitted its key, the merge preserved the
stale on-value, and the setting stayed on until a destructive Reset. The fix
already existed in the same file — the sandbox block is emitted
unconditionally for exactly this reason — and is now applied to all five keys.
A key whose neutral state is *unset* (`tui`, `effortLevel`, `viewMode`,
`awaySummaryEnabled`) is emitted as JSON `null` and the entrypoint deletes it,
because a stand-in value is not neutral: `tui: "default"` pins the classic
renderer where unset lets Claude Code choose, and `viewMode: "default"`
overrides the user's own sticky `/focus` choice.
* The same stickiness existed, unnoticed, in the **env vars**: `docker commit`
bakes container env into the snapshot image, so a `=1` written once rode it
forever. All four are now emitted on every create, extracted into
`claude_code_env_vars` and unit tested. Two use an empty value for "off"
rather than `0`, because they outrank a setting the user can change from
inside their own container and Triple-C's default must not overrule a
`/config` choice it never asked about.
* TUI mode is now a genuine three-way choice (automatic / classic / fullscreen),
which the always-emitted key makes both necessary and possible.
`merge_claude_code_settings` is untouched by choice: a project-level OFF still
cannot override a globally-ON setting.
Tests: 364 frontend (+5), 308 Rust (+23), covering the scrub path list and
script, log rotation, pin reaping, and that toggling a setting off actually
clears a previously-set ON value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
|
||
|
|
dcb13d23ea |
Fix what review found in the skill: five real defects
Adversarial review of #29 found bugs I confirmed by reproducing each one. **Every hand-written error message was unreachable.** `tok=$(curl ...)` is a plain assignment, so `set -e` acts on the command substitution before the following `|| die` can run. A wrong password produced exit 22 and no output at all — the most likely way this gets used wrongly, and the least explained. All four captures now go through a `run` helper that takes a *description* rather than echoing the command, because one of them carries the account password in `-u`. **`up` was not idempotent, and the second run destroyed DNS.** The resolv.conf backup was copied unconditionally, so `up --full` twice overwrote the good backup with PIA's own resolvers; the later `down` then "restored" those and left the container with no working DNS and no way back. `up` now runs `down` first. Verified: two `up --full` runs, then `down`, and the backup still holds the original 192.168.65.7. **An empty gateway produced total connectivity loss, reported as healthy.** `$gw` was never validated and `add_route` swallowed every failure to /dev/null. The two half-routes need no gateway and would succeed, so the tunnel captured everything while the exclusions keeping DNS and the Docker host reachable silently did not exist — and `status` still printed "full tunnel". Routes are now fatal on failure, and a via-less default (`$3` is the literal "eth0") is rejected. **The PIA session token was in the process arguments** — confirmed in `ps` and /proc/*/cmdline, a ~24h bearer credential for the account readable by anything in the container. It now goes to curl on stdin as a config. Verified: 60 polls across a full `up`, zero sightings. **The preflight diagnosed the wrong kernel module.** It checked /dev/net/tun and blamed the tun module, but kernel WireGuard is a netlink interface and does not use it — verified by creating one with NET_ADMIN and no tun device. The check is dropped (the container could not have started without the device anyway) and `ip link add` now reports the real dependency. Also: a full tunnel with no DNS servers from PIA used to warn and carry on, which is a tunnel leaking every lookup while reporting itself healthy — now fatal. `down` validates the backup before restoring it, so a truncated one cannot leave the container with no resolver at all. `wg.priv` is shredded on teardown and created under umask 077, because /run rides `docker commit` into the snapshot image. A mistyped `up --ful` is rejected instead of silently giving a test route. entrypoint: `install_feature_skill` gets `local`, a blank-name guard (the disabled branch would otherwise `rm -rf` the whole skills directory under a persisted volume), `-e`/`-L` so a leftover *file* at the destination is cleaned up, and a chown of the parent so `claude` can still add skills of their own when Mission Control is off. When the base image predates the skill it now says so instead of returning silently — and `/opt/triple-c-skills` joins FEATURE_PROBES so the migration pre-flight reports it. Docs corrected to match: neither half reaches an existing project without a migration. `vpn_env_var` extracted and tested, pinning the property the whole removal path rests on — that the variable is emitted as 0 rather than omitted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3bd3caa101 |
Ship a pia-vpn skill with the VPN support toggle
The toggle grants CAP_NET_ADMIN and /dev/net/tun and stops there, which users reasonably read as "turn the VPN on" — the gap between the two is the reported bug that the default network does not route through a VPN. Close it by giving the container an agent-usable way to build the tunnel, rather than leaving each project to rediscover it. container/skills/ is baked to /opt/triple-c-skills and installed into ~/.claude/skills/ by entrypoint.sh from VPN_SUPPORT_ENABLED, mirroring how Mission Control installs its own. Staged under /opt because ~/.claude is a volume mount that would mask an image copy from first start. Three details that are not incidental: - The variable is sent as 0 rather than omitted when off, because ~/.claude persists: entrypoint has to be *told* to remove a skill left by an earlier run with the toggle on, and an absent variable cannot say that. A stale skill is worse than none, since it instructs an agent to use a capability the container no longer has. - It is reserved in RESERVED_ENV_EXACT alongside MISSION_CONTROL_ENABLED, or a custom env var of the same name could claim the skill without the capability behind it. Covered by a test. - The skill is re-copied on every start, rm -rf'd first, so fixes reach existing projects and files dropped from a later version do not linger. The skill itself carries the three things that are easy to get wrong: that a full tunnel captures the Docker resolver and takes DNS down with it, that an IP-literal health check cannot see a dead resolver, and that no tunnel survives a restart while /run state riding the snapshot makes it look as though one did. It also states what it deliberately does not do — no killswitch, no autostart — so an agent proposes those as decisions rather than improvising them. pia-wg.sh preflights CAP_NET_ADMIN by capability bit rather than letting the first `ip` call fail with a bare EPERM that points nowhere near the setting that needs changing. Credentials stay in a file (~/pia-creds, PIA_CREDS to override) rather than the environment, where docker inspect and every process in the container would see them. Tested: install/refresh/remove/no-op paths of install_feature_skill against the real function; preflight with and without the capability; and a full up --full / down round trip, confirming DNS via PIA's resolvers, api.anthropic.com reachable through the exit, and routes and resolv.conf restored on teardown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2b35aa8c16 |
Explain a missing tun device where the failure actually happens
Build App (Preview) / compute-version (pull_request) Successful in 3s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m37s
Build App (Preview) / build-linux (pull_request) Successful in 5m30s
Build App (Preview) / build-windows (pull_request) Successful in 5m55s
Build App (Preview) / prune-previews (pull_request) Successful in 3s
Review caught that the device guard was wired to the wrong call. The
daemon does not resolve `--device` at create: verified against Docker
29.7, `docker create --device /dev/does-not-exist` succeeds and prints an
id, and runc only resolves the device — and validates sysctls — when it
builds the container. So on a host with no tun module the create returns
fine and `start` fails, which means the explanation never ran and the
user saw the raw daemon string naming a path they would go looking for on
the wrong machine. The unit tests fed the create-side string straight in,
so they confirmed a function no real failure could reach.
Move the guard onto `start_container`, covering create as well in case a
future daemon checks earlier. It no longer takes `vpn_support_enabled` —
`start_container` has a container id and no project, and nothing else in
Triple-C ever requests a device, so an error naming /dev/net/tun is
unambiguous on its own. The test now uses the daemon's verbatim message
via bollard's real Display format.
Also from review:
* Soften the security claim. Docker does not enable user-namespace
remapping by default, so this is a real CAP_NET_ADMIN in the initial
user namespace with only the network namespace confining it. It
cannot touch host interfaces, but "confers no authority outside the
container" was too strong: within its namespace it can set
promiscuous mode and add addresses, routes and NAT on the shared
docker0 segment, which puts sibling containers — the LiteLLM gateway
among them — within ARP-spoofing reach, and it can flush netfilter
rules sandbox mode may rely on. Said plainly in the code, CLAUDE.md
and HOW-TO-USE.
* Drop Tailscale from the list of clients needing this. Its
--tun=userspace-networking mode needs neither the capability nor the
device, and listing it invites granting NET_ADMIN for nothing.
* Say in the toggle's own hint that changing it recreates the
container, matching how every other recreation-triggering setting is
labelled. The tab's generic "stop the container first" chip does not
tell the user what is about to happen.
* Add RuntimeSection tests: saves on, saves off explicitly rather than
dropping the key, reflects state, is disabled while running, and
carries the recreation warning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
65a3d4eb29 |
Let a project's container run a VPN client
Build App (Preview) / compute-version (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m39s
Build App (Preview) / build-linux (pull_request) Successful in 7m10s
Build App (Preview) / build-windows (pull_request) Successful in 6m33s
Build App (Preview) / prune-previews (pull_request) Successful in 4s
A VPN client installed in a container today starts, runs, and then hangs
until its connection times out. Nothing reports an error: a default
container has no /dev/net/tun to open and no CAP_NET_ADMIN to add an
interface or a route with, and clients surface that as a generic timeout
rather than a permissions failure.
Add an opt-in per-project "VPN support" switch granting the three things
a tunnel needs. They are useless individually, which is why
vpn_host_config() defines the set in one place and the tests assert all
of it:
* CAP_NET_ADMIN — Docker's default bounding set has net_raw but not
net_admin, so a client can ping but never connect.
* /dev/net/tun — passed through from the host so the kernel's tun
module backs it, rather than mknod-ed inside.
* net.ipv4.conf.all.src_valid_mark — WireGuard's wg-quick sets this and
cannot from inside a container, /proc/sys being read-only, so its
handshakes are dropped by reverse-path filtering.
Off by default and deliberately opt-in: NET_ADMIN lets anything in the
container reconfigure that container's network stack. It is namespaced —
no authority over the host's interfaces or any other container.
Capabilities and devices are fixed when a container is created, so this
is container state and takes the label-and-compare treatment.
triple-c.vpn-support is written unconditionally, false included, for the
usual docker commit reason: a true stamped once would ride the snapshot
image into every future container and make the switch impossible to turn
back off. A missing label reads as false and off is byte-identical to
today, so no existing project is churned.
Requesting the device fails at creation when the host kernel has no tun
module, which would otherwise surface as a project that simply refuses to
start. explain_create_failure() rewrites that one error to name the
switch and the Docker-Desktop-VM-versus-your-machine distinction, and
leaves every other failure untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
88f2e73474 |
Merge branch 'main' into fix/scheduler-home-clobber
Build App (Preview) / compute-version (pull_request) Successful in 5s
Build Container / build-container (pull_request) Successful in 34s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-linux (pull_request) Canceled after 0s
Build App (Preview) / prune-previews (pull_request) Canceled after 0s
Build App (Preview) / build-macos (pull_request) Canceled after 21s
Build App (Preview) / build-windows (pull_request) Canceled after 22s
|
||
|
|
fa4940dd7d |
Say when a scheduled task is running
Build App (Preview) / compute-version (pull_request) Successful in 7s
Build Container / build-container (pull_request) Successful in 2m53s
Build App (Preview) / create-release (pull_request) Successful in 5s
Build App (Preview) / build-macos (pull_request) Successful in 2m37s
Build App (Preview) / build-windows (pull_request) Successful in 6m2s
Build App (Preview) / build-linux (pull_request) Successful in 6m53s
Build App (Preview) / prune-previews (pull_request) Successful in 2s
A run is detached — cron has no terminal, and the app fires it as a detached exec — so triggering one and watching the log was indistinguishable from triggering one that died. Worse, `claude -p` writes its answer in a single burst at the end, so a healthy run shows nothing but its log header for as long as it is thinking. The honest reading of the old UI was "it stalled". triple-c-task-runner now publishes a state file per run (pid, start time, log path) and removes it from an EXIT trap. flock remains what actually prevents overlapping runs; this is purely observability, so every reader verifies the pid rather than trusting the file — a container stopped mid-run cannot fire a trap, and a task stuck on "running" forever would be a worse lie than no indicator at all. Stale files are cleared on read. On top of that: - `list` grows a status column: "running 4m12s" or "idle". - `status [--id] [--watch]` answers "is it still going?" directly, with elapsed time and the tail of the log when there is any output yet. - `run` streams the log instead of blocking silently, and refuses to start a task that is already running. - The Automation tab marks a running task, disables its Run now button, and polls while anything is in flight — including the second or two between firing a run and the runner registering it, which is the exact window that used to read as dead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5f990dd28b |
Sweep the snapshot commits recreation leaves behind
Build App (Preview) / compute-version (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m40s
Build App (Preview) / build-linux (pull_request) Successful in 5m37s
Build App (Preview) / build-windows (pull_request) Successful in 6m16s
Build App (Preview) / prune-previews (pull_request) Successful in 5s
Every recreation commits the container to triple-c-snapshot-{id}:latest
and moves that tag; the image it pointed at keeps its layers and loses
its name. Nothing deleted those, so they accumulate — measured on one
real host, 7 orphans holding 7.4 GB, three of them from a single day's
work.
`sweep_orphaned_snapshots` removes them, under two conditions that are
the whole safety argument. Untagged: every image the app depends on
carries a tag, so a project's live `:latest` and a migration's
`pre-migration-*` rollback pin cannot match the filter at all. And
labelled `triple-c.managed=true`, which `docker commit` copies from the
container onto the image — the user's own dangling images are not ours
to delete. Removal is unforced on top of that, so Docker refuses while
any container is still built from the image, including the stopped
containers of projects that are not running; those are counted and left
for the next sweep.
It runs after a recreation, which is when the orphan it just made
becomes removable, and after a migration is accepted, which is the
moment dropping the pin turns the pre-migration snapshot into an orphan.
Both detached: this is housekeeping, and a full disk beats a project
that will not start. Each sweep clears every orphan it finds, so
recreations that predate it are cleaned up too.
The label string is now a constant rather than four literals, and a test
pins both filter conditions in place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9b2f4fe79f |
Give the env var its value box back, and stop labelling the secret
Build App (Preview) / compute-version (pull_request) Successful in 7s
Build App (Preview) / create-release (pull_request) Successful in 3s
Build App (Preview) / build-macos (pull_request) Successful in 2m56s
Build App (Preview) / build-windows (pull_request) Successful in 5m33s
Build App (Preview) / build-linux (pull_request) Successful in 6m47s
Build App (Preview) / prune-previews (pull_request) Successful in 4s
Two separate faults, both reachable from one screenshot of the Global Environment Variables editor. The value input was collapsed to a sliver, so a variable looked like it had lost its value. `inputClass` carries `w-full`, and the `w-2/5` on the key input did not beat it — class-attribute order is not what resolves that conflict, stylesheet order is. The key therefore asked for the whole row, and the value input, whose `flex-1` gives it a basis of 0 and only the leftover space, got almost nothing. Widths now live on wrapper divs, where nothing competes with them. The fingerprint that detects custom-env changes was a plaintext `KEY=VALUE` join, and it is written as the `triple-c.custom-env-fingerprint` label. Labels are readable by anything on the host via `docker inspect`, `docker commit` copies them onto the project's snapshot image, and the recreation check logs both sides on a mismatch — so an API token set as a custom variable was published to all three. It is hashed now, exactly as `triple-c.git-token-hash` already was. Empty stays empty, so "nothing configured" still reads as an empty label. Changing the fingerprint format means every project's label mismatches once: expect a single container recreation per project on next start. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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 |
||
|
|
2de00b3c55 |
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> |
||
|
|
d42b741337 |
Migrate a project onto a new base image without losing its volumes
Projects were pinned to the image they were first created from. Both create paths preferred triple-c-snapshot-<id>:latest whenever it existed, and container_needs_recreation compared the container's live image against the triple-c.image label — which create_container wrote from the same image it created from. A tautology that could never fire. The only escape was Reset, which calls remove_project_volumes and destroys the login, skills and transcripts. Measured consequences on this host: real projects are missing socat (so the auth bridge cannot tunnel) and bubblewrap (so sandbox mode does not work), plus Mission Control and triple-c-sso-refresh, and sit 61 packages behind the base including ca-certificates, openssl and curl. Detection. create_container now writes triple-c.base-image-id (the image ID, not RepoDigests, which local-built and custom images do not have) and triple-c.create-image. container_needs_recreation takes the expected create-image and compares against the latter, so the check means something. base-image-id is deliberately NOT compared: a base bump would otherwise silently recreate from the snapshot, consuming the "you should migrate" signal without migrating. Staleness is surfaced, never acted on automatically. Migration keeps the volumes. /home/claude and ~/.claude are volumes and the image's copy is seed-only — permanently masked after first mount — so the login, ~/.claude.json, skills, transcripts, scheduler tasks, SSH keys, cargo, uv, ruff and Claude Code itself re-attach untouched. Only root-level state is rebuilt: apt packages are replayed against the new base rather than copied, so no stale libc is dragged forward, and /usr/local, /opt and the non-bind-mounted parts of /workspace are copied verbatim with tar --skip-old-files so they can never clobber a newer base binary. docker diff is not used: on a snapshot-derived container it reports only changes since the last commit. Raw image-vs-image diffing is filtered through dpkg ownership because it otherwise lies — 8,677 raw path differences on a real project reduced to 2 genuinely user-authored files, both loose /workspace-root files. Crash safety. snapshot:latest keeps pointing at the old image until the final commit, so any crash before it self-heals on next start. Later crashes are caught by reconcile_project_statuses. The rollback pin is a docker tag: 0.057s and 0 bytes. Rollback restores the system layer only — volumes are never touched — and the UI says so rather than implying a time machine. Fixes an infinite recreation loop shipped with the MCP removal. docker commit propagates labels to the image, so a container created from a snapshot inherited its non-empty triple-c.mcp-fingerprint and the one-shot shim recreated it again on every start, forever. Lineage labels are now always written explicitly. Documents the second, separate bug this uncovered: Dockerfile changes under /home/claude never reach an existing project, migration or not, because the volume masks them. Anything that must stay upgradable belongs in /usr/local/bin or /opt, or must be seeded by entrypoint.sh. 145 Rust tests, 227 frontend tests, both builds clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cc5f691677 |
Add llama.cpp backend, model gateway, URL relay and browser view
Four features, plus a latent bug fix.
llama.cpp backend. Claude Code only ever speaks the Anthropic Messages
API — confirmed empirically by pointing it at a logging server, which
received POST /v1/messages?beta=true. llama-server implements that
natively (verified in its README, alongside --port default 8080), so
this is a plain base-URL backend with no translation shim, the same
shape as Ollama. Its --api-key defaults to none, so the auth token is a
placeholder Claude Code requires and llama-server ignores.
Model alias fix. ANTHROPIC_DEFAULT_HAIKU_MODEL is documented as "also
used for background functionality", and Triple-C set none of the alias
vars. So on every custom-endpoint backend, Claude Code resolved `haiku`
to an Anthropic model id and sent it to a local server that does not
have it — background features failed silently. All four
ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU,FABLE}_MODEL vars are now pinned to
the backend's configured model, with an optional Haiku override, and
blanked for Anthropic and Bedrock so those keep Claude Code's defaults.
The deprecated ANTHROPIC_SMALL_FAST_MODEL is never emitted. Existing
Ollama and OpenAI-Compatible containers are recreated once so the new
env reaches them; the snapshot is preserved.
Model gateway. Optional LiteLLM sibling container, off by default,
mirroring stt.rs — this is what makes real OpenAI usable, since
api.openai.com has no /v1/messages. Pinned to v1.96.0 by tag and digest:
the 1.82.7/1.82.8 malware was PyPI-only and never affected the official
images, which is precisely why this builds FROM the image rather than
pip-installing, but 1.84.0 is still the floor for proxy CVEs (API-key
SQLi, Host-header auth bypass, MCP auth bypass). Binds 0.0.0.0 because
project containers consume it, and therefore always sets a master_key —
LiteLLM without one accepts any key. The provider key lives in the OS
keychain and is uploaded into a volume, never an image layer or label.
URL relay. A container-side xdg-open/BROWSER shim opens URLs in the
host's browser. Uses an OSC sequence to /dev/tty rather than a printed
sentinel, because the shim usually runs as a grandchild of a process
capturing its children's output. Degrades to printing the URL when no
terminal is attached, so scheduled tasks do not hang. Only http/https,
with control characters rejected before new URL() — which strips
newlines, so java\nscript: would otherwise parse as javascript:. Nothing
auto-opens; the user confirms. The web terminal shows a tap-to-open
banner instead, since that browser may be a phone across a tunnel.
Browser view. A Project Home tab that watches and takes over the browser
Claude drives with Playwright, using Playwright's own dashboard. Zero
image cost — Playwright stays user-installed. It does not reuse the auth
bridge's PortForward, which binds an unauthenticated port: correct for a
throwaway OAuth listener, wrong for mouse and keyboard control of a
browser in a passwordless-sudo container. Instead a token-gated loopback
proxy checks Host, then token or a forbidden-header origin signal,
before a byte reaches the container. Host ports are confined to
47820..=47827 so CSP frame-src can enumerate them rather than widening
to a wildcard, with a test asserting the two agree.
188 frontend tests, 107 Rust tests, both builds clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
01a2f6aec8 |
Add Project Home, Auth Bridge, shared auth token, and Tier-1 polish
Project Home (DESIGN-REVIEW §B2): the project is promoted from a 280px
sidebar card to a first-class main-area view. ProjectCard.tsx (1,257
lines) is replaced by a select-only ProjectRow plus tabs for Overview,
Sessions, Automation, Config and Files. The PortMappings, FileManager
and ContainerProgress modals are absorbed rather than reimplemented.
Config gains a Saved/Saving/Failed indicator — save-on-blur failures
previously reached only console.error.
Tier-1 polish (DESIGN-REVIEW §A): new elevation, muted-accent, disabled
and focus-ring tokens; a global :focus-visible ring with every
focus:outline-none removed; filled buttons moved to --accent-emphasis
and white-on-success toggles retired, fixing three WCAG AA failures
(2.1:1, 2.5:1, 2.4:1); a shared Modal primitive with role="dialog",
focus trap and restore, adopted by all remaining modals; status
indicators that carry a glyph and word rather than colour alone.
Ctrl+Shift+W closes a tab, deliberately not Ctrl+W — that is readline's
kill-word, used constantly in the terminal this app is built around.
Auth Bridge: a general loopback-callback bridge so browser logins run
inside a container (aws sso login, Concourse fly login, claude login)
can complete against the host browser. Listeners are discovered from
/proc/net/tcp{,6} — ss/netstat/lsof are absent from the image — bound on
host 127.0.0.1 only, and tunnelled in over the Docker API via socat,
which keeps working on Docker Desktop where container IPs are not
routable. Falls back to [::1] because Node resolves localhost to IPv6
first, so claude login often binds ::1 alone. Opt-in per project.
This extracts create_attached_exec() and moves the existing terminal
session path onto it, so there is one attached-exec implementation
rather than two.
Shared auth token: `claude setup-token` is run in a container, the token
is stored in the OS keychain and injected as CLAUDE_CODE_OAUTH_TOKEN
into Anthropic-backend projects. Contrary to the initial design note,
setup-token uses an Anthropic-hosted redirect and blocks on a stdin
paste prompt rather than a loopback callback, so a stdin command is
required for the flow to complete.
The token is never logged, never returned to the frontend, and is
redacted from the streamed output with a stateful matcher that withholds
any tail that could still grow into a secret. Change detection uses a
random rotation id rather than a hash, since a hash in a docker-inspect
readable label would be an offline verification oracle.
Frontend 33 -> 51 tests; Rust 34 tests. Both builds clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
0ac4e5030c |
Add permission modes and container introspection backend
Permission modes: replaces the binary full_permissions flag with a PermissionMode enum (Plan/Default/AcceptEdits/Bypass). Flag mapping is defined once in PermissionMode::cli_args() and used by the terminal, the web terminal, and the scheduler: Plan -> --permission-mode plan Default -> (no flag) AcceptEdits -> --permission-mode acceptEdits Bypass -> --dangerously-skip-permissions Choices verified against `claude --permission-mode` on 2.1.226. full_permissions is retained and effective_permission_mode() falls back to it, so existing projects.json needs no migration. Bug fix: triple-c-task-runner ran `claude -p ... --dangerously-skip- permissions` unconditionally, ignoring the project's setting entirely. It now reads TRIPLE_C_PERMISSION_MODE, which is injected into the container, added to the reserved env blocklist, propagated through the entrypoint's cron env filter, and tracked by a new triple-c.permission-mode label so a change forces recreation. Introspection: new commands/inspect_commands.rs exposes read-only views into the container over docker exec — Claude sessions (parsed from ~/.claude/projects/<cwd>/<uuid>.jsonl), installed capabilities (skills, agents, commands, hooks, plugins, natively-configured MCP servers), and the triple-c-scheduler task list, logs and notifications. Task/session ids are validated against a strict allowlist and every parameterized call runs as a bare argv vector via bollard, so no shell is involved. Stopped containers return empty results rather than errors. No UI yet; that lands with the Project Home view. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d0bb631d4d |
Remove MCP backend, entrypoint injection, and docs; add migration shim
Completes the removal begun in the previous commit. Backend: deletes models/mcp_server.rs, storage/mcp_store.rs and commands/mcp_commands.rs, the McpStore on AppState, the four IPC handlers, Project::enabled_mcp_servers, build_mcp_servers_json(), compute_mcp_fingerprint(), the MCP_SERVERS_JSON env injection, the mcp-fingerprint label, and the whole MCP container lifecycle. create_container() and container_needs_recreation() lose their mcp_servers/network_name parameters. Container: entrypoint.sh no longer merges MCP_SERVERS_JSON into ~/.claude.json. MCP_SERVERS_JSON stays in the reserved env blocklist. Security: the Docker socket is no longer auto-mounted for stdio+Docker MCP servers — it now mounts only when allow_docker_access is set. Migration: old containers were created with network_mode=triple-c-net-<projectId> and refuse to start once that network is gone. docker/network.rs becomes docker/legacy_cleanup.rs with label-driven, best-effort removal of leftover MCP containers and the per-project network, called on both delete and recreate. container_needs_recreation() now forces a rebuild for any container carrying a non-empty triple-c.mcp-fingerprint label or attached to a triple-c-net-* network, moving it onto the default bridge. Both can be dropped a release later. Docs: drops the MCP sections from README/HOW-TO-USE/TECHNICAL and adds a short note pointing at Claude Code's native `claude mcp` / `/mcp` / .mcp.json instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0945e21eb1 |
Address second review pass: fix start-time race + minor cleanups
Build App / compute-version (pull_request) Successful in 9s
Build Container / build-container (pull_request) Successful in 1m48s
Build App / build-macos (pull_request) Successful in 2m16s
Build App / build-windows (pull_request) Successful in 3m11s
Build App / build-linux (pull_request) Successful in 4m51s
Build App / create-tag (pull_request) Has been skipped
Build App / sync-to-github (pull_request) Has been skipped
- M1 (race): don't mount the host AWS dir for static-credential Bedrock. sync_bedrock_credentials() is the sole writer of ~/.aws/credentials in that mode, and mounting /tmp/.host-aws let the entrypoint's `rm -rf ~/.aws; cp -a` race that write at startup (only when a global aws_config_path was also set). Static keys + AWS_REGION env are self-sufficient and don't need the host config, so skipping the mount removes the dual-writer entirely. - L-a: exit codes are now read via wait_for_exec_exit(), which polls inspect_exec until the exec reports finished, so a non-zero tar/cred exit isn't missed by reading exit_code too early. The backup only fails on a definitively non-zero code (falls back to the empty-output check if undeterminable). - L-b: fixed two comments that referenced the old write_bedrock_static_credentials name (now sync_bedrock_credentials). - L-c: entrypoint only rewrites ~/.claude.json when awsAuthRefresh is actually present, avoiding a needless jq reformat on every non-SSO start. - L-d: backup script traps EXIT to remove its mktemp staging dir even when tar fails, so failed backups don't accumulate temp dirs (with the sanitized config copy) in the container. L-e (drop routing) is a non-issue: the layout is tabbed, so only one terminal pane is ever visible; the active-guard routing is correct. Verified the race fix, trap cleanup, grep guard, and exit-code polling. cargo check / tsc / vitest all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d65872dc94 |
Address remaining review items: L4/M1 + L2/L5 cleanup
Build App / compute-version (pull_request) Successful in 3s
Build Container / build-container (pull_request) Successful in 57s
Build App / build-macos (pull_request) Successful in 2m15s
Build App / build-windows (pull_request) Successful in 2m54s
Build App / build-linux (pull_request) Successful in 5m44s
Build App / create-tag (pull_request) Has been skipped
Build App / sync-to-github (pull_request) Has been skipped
- L4: sync_bedrock_credentials (renamed from write_bedrock_static_ credentials) now also clears a stale ~/.aws/credentials when the project no longer uses static-credential Bedrock, so static keys don't linger unused in the persistent home volume after switching backends. Skipped when /tmp/.host-aws is mounted (host-managed ~/.aws). HOME is also set explicitly on the exec env for robustness. - M1: the Backup button now has a tooltip and the success toast notes that the archive includes MCP/config which may contain MCP-embedded API keys (OAuth tokens are excluded) — keep it private. - L2: backup now uses async file IO (tokio::fs::File + AsyncWriteExt, tokio::fs::remove_file) instead of blocking std::fs between awaits; dropped-file reads use tokio::fs::metadata/read. - L5: upload_host_file_to_terminal explicitly `mkdir -p`s /tmp/triple-c-drops instead of relying on Docker's tar extractor to create the parent dir. Verified L4 cleanup guard, L5 mkdir, async IO, and exit-code paths against real containers. cargo check / tsc / vitest all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
edf0698774 |
Address PR review: backup correctness/security + drop hardening
Build App / compute-version (pull_request) Successful in 8s
Build Container / build-container (pull_request) Successful in 51s
Build App / build-macos (pull_request) Successful in 2m15s
Build App / build-windows (pull_request) Successful in 2m52s
Build App / build-linux (pull_request) Successful in 6m5s
Build App / create-tag (pull_request) Has been skipped
Build App / sync-to-github (pull_request) Has been skipped
Fixes from the code review of this branch: - Backup requires a running container (it runs via `docker exec`, which can't run on a stopped one). Removed the misleading "Backup" button from the stopped-project actions, added an explicit running check with a clear error, and corrected the doc comment. (H1) - jq sanitization fallback no longer leaks secrets: if ~/.claude.json can't be parsed, the backup substitutes an empty object and warns to stderr instead of copying the raw file (which held primaryApiKey / oauthAccount). Verified the raw key never reaches the archive. (H2) - Dropped-file paths typed into the terminal are now always single-quoted (with '\'' escaping), not only when they contain whitespace — a name like `foo$(whoami).txt` was previously sent raw into the shell. (M2) - write_bedrock_static_credentials checks the exec exit code via the new exec_oneshot_env_status and fails loudly on a write/chmod error instead of silently reporting success. exec_oneshot keeps its ignore-exit-code behavior so list_container_files is unaffected. (M4) - Backup removes a partial/truncated archive on any stream error and treats a non-zero tar exit code as failure (a truncated gzip was previously reported as success). (L1) - Dropped files are capped at 256 MiB to avoid ballooning host RAM (the file is read fully into memory then re-tarred). (M3) - Stopped excluding .git/objects from the backup so git history, including unpushed commits, is preserved faithfully. (L3) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
424ab04ca8 |
Fix stale AWS/Bedrock auth carrying over on backend switch
When a project switched backends (e.g. Bedrock -> Anthropic), the recreated container kept authenticating against Bedrock, and SSO kept firing after switching away. Three root causes, all fixed: 1. Recreation builds the new container from a `docker commit` snapshot. commit always bakes the previous container's full ENV into the image (an empty commit Config does NOT strip it, and the commit API cannot remove env). So CLAUDE_CODE_USE_BEDROCK=1 / AWS_* survived into the new container. Fix: create_container now explicitly clears every managed auth key the active backend does not set (MANAGED_AUTH_KEYS), so create-time env overrides the stale baked-in values. 2. awsAuthRefresh was written into ~/.claude.json (persisted home volume) and never removed, so Claude Code kept invoking triple-c-sso-refresh after switching to a non-SSO backend. Fix: entrypoint now deletes awsAuthRefresh when AWS_SSO_AUTH_REFRESH_CMD is unset, idempotent both ways. 3. Static/session AWS creds were baked into Config.Env at create time, so a stop/start kept stale creds and rotated keys never refreshed without a full recreation. Fix: static creds are no longer injected as env vars; write_bedrock_static_credentials() writes ~/.aws/credentials (0600, secrets via exec env not argv) on every start, and removes a stale ~/.aws/config left from a prior profile/SSO session. Static creds also dropped from the bedrock fingerprint so a key rotation refreshes in place instead of forcing recreation. Adds exec_oneshot_env() for env-carrying one-shot execs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5b1c801cf1 |
Add global backend defaults with runtime fallback
New fields: GlobalAwsSettings.default_model_id, plus GlobalOllamaSettings and GlobalOpenAiCompatibleSettings (base_url + default_model_id each). When a per-project base_url or model_id is blank, the container env vars and config fingerprints fall back to the global value. Container recreation is triggered whenever the resolved value changes, so editing a global default updates existing projects on next start. UI: added the new fields to AwsSettings and two new global settings components, slotted into the Backends accordion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5974347913 |
Add per-project sandbox mode and Bedrock service-tier
Build App / compute-version (pull_request) Successful in 2s
Build App / build-macos (pull_request) Successful in 2m31s
Build App / build-windows (pull_request) Successful in 8m1s
Build Container / build-container (pull_request) Successful in 8m11s
Build App / build-linux (pull_request) Failing after 1m53s
Build App / create-tag (pull_request) Has been skipped
Build App / sync-to-github (pull_request) Has been skipped
Sandbox mode: new per-project toggle that turns on Claude Code's bash sandbox inside the container. Adds `bubblewrap` and `socat` to the Dockerfile (the two Linux deps required by the sandbox), and emits a managed `sandbox` block into `~/.claude/settings.json` via the existing CLAUDE_CODE_SETTINGS_JSON entrypoint merge: - `enabled` mirrors the Triple-C toggle and is always emitted, so the entrypoint's recursive jq merge clears any prior on-state from the persisted named volume — Triple-C is authoritative. - `enableWeakerNestedSandbox: true` because we run inside Docker without privileged user namespaces. - `allowUnsandboxedCommands: false` to disable the `dangerouslyDisableSandbox` escape hatch — opting into the sandbox shouldn't come with a runtime bypass. When sandbox is on, a SANDBOX_INSTRUCTIONS section is appended to CLAUDE_INSTRUCTIONS so Claude can guide users through allowing extra paths/domains, excluding `docker *`/`watchman *` from the sandbox, and the rule that `sandbox.enabled` is owned by Triple-C. The Claude-Code settings fingerprint includes sandbox state (only when on, to avoid spuriously flagging existing containers for recreation on upgrade). Bedrock service tier: new optional field on the per-project Bedrock config. When set, exported as ANTHROPIC_BEDROCK_SERVICE_TIER (added in Claude Code 2.1.122) and included in the Bedrock fingerprint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d6ac3ae6c6 |
Add Claude Code settings infrastructure, TUI mode, session naming, and global defaults
Adds first-class support for Claude Code CLI features (2.1.71-2.1.110): - New ClaudeCodeSettings struct with per-project and global defaults for TUI mode, effort level, focus mode, thinking summaries, session recap, auto-scroll, env scrub, and 1-hour prompt caching - Settings injected as env vars (CLAUDE_CODE_NO_FLICKER, etc.) and ~/.claude/settings.json entries via entrypoint.sh merge block - New ClaudeCodeSettingsModal component for configuring settings - Session naming support (-n flag passed to claude CLI, shown in tabs) - Relaxed reserved prefix filter: CLAUDE_CODE_* env vars now allowed in custom env vars UI for power users - Global SSH key path, git name, and git email now used as fallbacks when per-project values are not set, with UI in SettingsPanel - Fingerprint-based change detection triggers container recreation when Claude Code settings change - Updated README, HOW-TO-USE, and CLAUDE.md documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
2dffef0767 |
Bundle mission-control into Triple-C instead of cloning from GitHub
Build App / compute-version (push) Successful in 2s
Build App / build-macos (push) Successful in 2m47s
Build Container / build-container (push) Successful in 9m0s
Build App / build-linux (push) Successful in 4m41s
Build App / build-windows (push) Successful in 5m33s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 10s
The mission-control (Flight Control) project is being closed upstream. This embeds the project files directly in the repo under container/mission-control/, bakes them into the Docker image at /opt/mission-control, and copies them into place at container startup instead of git cloning from GitHub. Also adds missing osc52-clipboard, audio-shim, and triple-c-sso-refresh to the programmatic Docker build context in image.rs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
13038989b8 |
Fix spurious container snapshot on every start for projects with env vars
Build App / compute-version (push) Successful in 3s
Build App / build-macos (push) Successful in 2m23s
Build App / build-windows (push) Successful in 4m7s
Build App / build-linux (push) Successful in 4m58s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 11s
Migrate env-var-based checks in container_needs_recreation() to label-based checks. When a container snapshot is committed, Docker merges the image's env vars with the container's, causing get_env() to return stale values and triggering an infinite snapshot→recreate loop. Labels are immutable after creation and immune to this merging behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
d7d7a83aec |
Rename LiteLLM backend to OpenAI Compatible
Build App / compute-version (push) Successful in 8s
Build App / build-macos (push) Successful in 2m25s
Build App / build-windows (push) Successful in 4m0s
Build App / build-linux (push) Successful in 4m47s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 12s
Reflects that this backend works with any OpenAI API-compatible endpoint (LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, etc.), not just LiteLLM. Includes serde aliases for backward compatibility with existing projects.json files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
38082059a5 |
Rename AuthMode to Backend, fix LiteLLM variant typo, add image update alerts, clean up Settings
Build App / compute-version (push) Successful in 6s
Build App / build-macos (push) Successful in 2m21s
Build App / build-windows (push) Successful in 3m28s
Build App / build-linux (push) Successful in 5m14s
Build App / create-tag (push) Successful in 2s
Build App / sync-to-github (push) Successful in 10s
- Fix serde deserialization error: TypeScript sent "lit_llm" but Rust expected "lite_llm" - Rename AuthMode enum to Backend across Rust and TypeScript (with serde alias for backward compat) - Add container image update checking via registry digest comparison - Improve Settings page: fix image address display spacing, remove per-project auth section - Update UI labels from "Auth" to "Backend" throughout Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
b6fd8a557e |
Clean up compiler warnings and document Ollama/LiteLLM backends
Remove unused `any_docker_mcp()` function, add `#[allow(unused_imports)]` and `#[allow(dead_code)]` annotations to suppress false-positive warnings. Update README.md and HOW-TO-USE.md with Ollama and LiteLLM auth backend documentation including best-effort compatibility notices. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
93deab68a7 |
Add Ollama and LiteLLM backend support (v0.2.0)
Add two new auth modes for projects alongside Anthropic and Bedrock: - Ollama: connect to local or remote Ollama servers via ANTHROPIC_BASE_URL - LiteLLM: connect through a LiteLLM proxy gateway to 100+ model providers Both modes inject ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN env vars into the container, with optional model override via ANTHROPIC_MODEL. LiteLLM API keys are stored securely in the OS keychain. Config changes trigger automatic container recreation via fingerprinting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
2dce2993cc |
Fix AWS SSO for Bedrock profile auth in containers
Build App / build-macos (push) Successful in 2m29s
Build App / build-windows (push) Successful in 3m56s
Build App / build-linux (push) Successful in 4m42s
Build Container / build-container (push) Successful in 54s
Build App / sync-to-github (push) Successful in 10s
SSO login was broken in containers due to three issues: the sso_session indirection format not being resolved by Claude Code's AWS SDK, SSO detection only checking sso_start_url (missing sso_session), and the OAuth callback port not being accessible from inside the container. This fix runs SSO login on the host OS (where the browser and ports work natively) by having the container emit a marker that the Tauri app detects in terminal output, triggering host-side `aws sso login`. The entrypoint also inlines sso_session properties into profile sections and injects awsAuthRefresh into Claude Code config for mid-session refresh. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
b7585420ef |
Reconcile project statuses against Docker on startup, update docs and CI
Build App / build-macos (push) Successful in 2m40s
Build App / build-windows (push) Successful in 4m12s
Build App / build-linux (push) Successful in 5m4s
Build Container / build-container (push) Successful in 2m41s
Build App / sync-to-github (push) Successful in 10s
- Add reconcile_project_statuses command that checks actual Docker container state on startup, preserving Running status for containers that are genuinely still running and resetting stale statuses to Stopped - Add is_container_running helper using Docker inspect API - Frontend calls reconciliation after Docker is confirmed available - Update TECHNICAL.md project structure, auth modes, and file listings to match current codebase - Update README.md and HOW-TO-USE.md with MCP servers, Mission Control, file manager, bash shells, clipboard/audio shims, and progress modal docs - Add workflow file self-triggers to CI path filters for build-app.yml and build.yml - Install Mission Control skills to ~/.claude/skills/ in entrypoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
429acd2fb5 |
Add Mission Control integration with per-project toggle
Build App / build-macos (push) Successful in 2m49s
Build App / build-windows (push) Successful in 3m32s
Build App / build-linux (push) Successful in 4m29s
Build Container / build-container (push) Successful in 56s
Build App / sync-to-github (push) Successful in 9s
When enabled, the entrypoint clones mission-control into ~/mission-control (persisted on the home volume) and symlinks it to /workspace/mission-control. Flight Control global and project instructions are programmatically appended to CLAUDE.md. Container recreation is triggered on toggle change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
d56c6e3845 |
fix: validate AWS SSO session before launching Claude for Bedrock Profile auth
Build App / build-macos (push) Successful in 2m20s
Build App / build-windows (push) Successful in 3m21s
Build App / build-linux (push) Successful in 5m41s
Build Container / build-container (push) Successful in 1m27s
Build App / sync-to-github (push) Successful in 12s
When using AWS Profile auth (SSO) with Bedrock, expired SSO sessions caused Claude Code to spin indefinitely. Three root causes fixed: 1. Mount host .aws at /tmp/.host-aws (read-only) and copy to /home/claude/.aws in entrypoint, mirroring the SSH key pattern. This gives AWS CLI writable sso/cache and cli/cache directories. 2. For Bedrock Profile projects, wrap the claude command in a bash script that validates credentials via `aws sts get-caller-identity` before launch. If SSO session is expired, runs `aws sso login` with the auth URL visible and clickable in the terminal. 3. Non-SSO profiles with bad creds get a warning but Claude still starts. Non-Bedrock projects are unaffected. Note: existing containers need a rebuild to pick up the new mount path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
e07c0e6150 |
fix: use SHA-256 for container fingerprints instead of DefaultHasher
DefaultHasher is not stable across Rust compiler versions or binary rebuilds, causing unnecessary container recreations on every app update. Replace with SHA-256 for deterministic, cross-build-stable fingerprints. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
20a07c84f2 |
feat: upgrade MCP to Docker-based architecture (Beta)
Each MCP server can now run as its own Docker container on a dedicated per-project bridge network, enabling proper isolation and lifecycle management. SSE transport is removed (deprecated per MCP spec) with backward-compatible serde alias. Docker socket access is auto-enabled when stdio+Docker MCP servers are configured. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
625d48a6ed |
feat: add MCP server support with global library and per-project toggles
Build App / build-macos (push) Successful in 2m20s
Build App / build-windows (push) Successful in 3m21s
Build App / build-linux (push) Successful in 5m8s
Build Container / build-container (push) Successful in 1m4s
Sync Release to GitHub / sync-release (release) Successful in 2s
Add Model Context Protocol (MCP) server configuration support. Users can define MCP servers globally (new sidebar tab) and enable them per-project. Enabled servers are injected into containers as MCP_SERVERS_JSON env var and merged into ~/.claude.json by the entrypoint. Backend: McpServer model, McpStore (JSON + atomic writes), 4 CRUD commands, container injection with fingerprint-based recreation detection. Frontend: MCP sidebar tab, McpPanel/McpServerCard components, useMcpServers hook, per-project MCP checkboxes in ProjectCard config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
eb86aa95b7 |
fix: persist full container state across stop/start and config-change recreation
- Add home volume (triple-c-home-{id}) for /home/claude to persist
.claude.json, .local, and other user-level state across restarts
- Add docker commit before recreation: when container_needs_recreation()
triggers, snapshot the container to preserve system-level changes
(apt/pip/npm installs), then create the new container from that snapshot
- On Reset/removal: delete snapshot image + both volumes for clean slate
- Remove commit from stop_project_container (stop/start preserves the
writable layer naturally; no commit needed)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|