Fix backend-switch AWS auth + add /workspace backup and terminal file drag-and-drop #8

Merged
jknapp merged 9 commits from fix/backend-switch-aws-creds into main 2026-06-30 22:20:00 +00:00
Owner

Summary

Four related changes, all on top of latest main.

1. Fix stale AWS/Bedrock auth carrying over on backend switch

Switching a project's backend (e.g. Bedrock → Anthropic) left the recreated container still authenticating against Bedrock, and SSO kept firing after switching away. Root causes + fixes:

  • Snapshot bakes env. Recreation builds the new container from a docker commit snapshot, and commit always bakes the previous container's ENV into the image (an empty commit Config does not strip it, and the commit API can't remove env). create_container now explicitly clears every managed auth key the active backend doesn't set (MANAGED_AUTH_KEYS), so create-time env overrides the stale baked-in values.
  • Persisted awsAuthRefresh. It was written to ~/.claude.json (persisted home volume) and never removed, so Claude Code kept invoking triple-c-sso-refresh after switching to a non-SSO backend. The entrypoint now deletes it when AWS_SSO_AUTH_REFRESH_CMD is unset (idempotent both ways).
  • Static creds refreshed per start. Static/session creds were baked into Config.Env at create time, so a stop/start kept stale creds. They're now written to ~/.aws/credentials (0600, secrets via exec env not argv) on every start, a stale ~/.aws/config from a prior profile/SSO session is removed, and they're dropped from the bedrock fingerprint so a key rotation refreshes in place instead of forcing a recreation.

2. "Backup" action — download a project's /workspace as .tar.gz

Manual button on each project card (next to Start/Reset when stopped, next to Files when running). Runs tar czf - inside the container (excludes + gzip happen in-container, so a large workspace isn't streamed in full), excluding regenerable node_modules/target/.git/objects, and streams to a host file via the native save dialog.

3. Backup also captures sanitized home config

So MCP servers, settings, and skills set up directly via Claude Code (stored in ~/.claude.json / ~/.claude, on the volumes a Reset wipes) survive a backup/restore. Secrets are stripped: ~/.claude.json is filtered through jq to drop primaryApiKey/oauthAccount/customApiKeyResponses (mcpServers/settings kept), and ~/.claude/.credentials.json (OAuth tokens) is omitted. Staged under home-claude/ in the archive.

4. Terminal file drag-and-drop

Drop files onto a terminal pane → copied into the container at /tmp/triple-c-drops/<name> and their paths typed into the prompt for Claude Code to read (mirrors image-paste). Uses Tauri's webview onDragDropEvent, guarded by the pane's active flag + a bounds hit-test so a drop only affects the terminal it landed on.

Verification

  • cargo check , tsc --noEmit , vite build , vitest 33/33 (7 pre-existing unhandled-error logs, unchanged).
  • Docker commit/env-precedence, tar excludes + GNU-tar/jq behavior, sanitized home config, and credential-file precedence all verified against real containers.
  • Drag-drop is verified by build/types only (no display in CI) — worth a manual smoke test in tauri dev. If core:default doesn't grant the webview drag-drop event in this setup, it's a one-line capability add.

🤖 Generated with Claude Code

## Summary Four related changes, all on top of latest `main`. ### 1. Fix stale AWS/Bedrock auth carrying over on backend switch Switching a project's backend (e.g. Bedrock → Anthropic) left the recreated container still authenticating against Bedrock, and SSO kept firing after switching away. Root causes + fixes: - **Snapshot bakes env.** Recreation builds the new container from a `docker commit` snapshot, and commit always bakes the previous container's ENV into the image (an empty commit Config does **not** strip it, and the commit API can't remove env). `create_container` now explicitly clears every managed auth key the active backend doesn't set (`MANAGED_AUTH_KEYS`), so create-time env overrides the stale baked-in values. - **Persisted `awsAuthRefresh`.** It was written to `~/.claude.json` (persisted home volume) and never removed, so Claude Code kept invoking `triple-c-sso-refresh` after switching to a non-SSO backend. The entrypoint now deletes it when `AWS_SSO_AUTH_REFRESH_CMD` is unset (idempotent both ways). - **Static creds refreshed per start.** Static/session creds were baked into `Config.Env` at create time, so a stop/start kept stale creds. They're now written to `~/.aws/credentials` (0600, secrets via exec env not argv) on every start, a stale `~/.aws/config` from a prior profile/SSO session is removed, and they're dropped from the bedrock fingerprint so a key rotation refreshes in place instead of forcing a recreation. ### 2. "Backup" action — download a project's /workspace as .tar.gz Manual button on each project card (next to Start/Reset when stopped, next to Files when running). Runs `tar czf -` inside the container (excludes + gzip happen in-container, so a large workspace isn't streamed in full), excluding regenerable `node_modules`/`target`/`.git/objects`, and streams to a host file via the native save dialog. ### 3. Backup also captures sanitized home config So MCP servers, settings, and skills set up directly via Claude Code (stored in `~/.claude.json` / `~/.claude`, on the volumes a Reset wipes) survive a backup/restore. Secrets are stripped: `~/.claude.json` is filtered through jq to drop `primaryApiKey`/`oauthAccount`/`customApiKeyResponses` (mcpServers/settings kept), and `~/.claude/.credentials.json` (OAuth tokens) is omitted. Staged under `home-claude/` in the archive. ### 4. Terminal file drag-and-drop Drop files onto a terminal pane → copied into the container at `/tmp/triple-c-drops/<name>` and their paths typed into the prompt for Claude Code to read (mirrors image-paste). Uses Tauri's webview `onDragDropEvent`, guarded by the pane's `active` flag + a bounds hit-test so a drop only affects the terminal it landed on. ## Verification - `cargo check` ✅, `tsc --noEmit` ✅, `vite build` ✅, vitest 33/33 ✅ (7 pre-existing unhandled-error logs, unchanged). - Docker `commit`/env-precedence, `tar` excludes + GNU-tar/jq behavior, sanitized home config, and credential-file precedence all verified against real containers. - Drag-drop is verified by build/types only (no display in CI) — worth a manual smoke test in `tauri dev`. If `core:default` doesn't grant the webview drag-drop event in this setup, it's a one-line capability add. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jknapp added 4 commits 2026-06-30 21:15:40 +00:00
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>
Adds a manual backup button on each project card (next to Start/Reset
when stopped, and next to Files when running) that saves a gzipped
tarball of the container's /workspace to a host path via the native save
dialog.

Backend: download_container_backup runs `tar czf -` inside the container
(so excludes + compression happen there rather than streaming a 16 GB
workspace) and pipes stdout straight to the chosen file. Regenerable
build artifacts (node_modules, target, .git/objects) are excluded so the
archive stays restore-sized. Returns bytes written; stderr is captured
for error reporting and a zero-byte result is treated as failure.

Works whether the container is running or stopped (only requires that it
exists). Verified on the Ubuntu/GNU-tar container base.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends download_container_backup to also capture the container's home
config so MCP servers, settings, and skills set up directly via Claude
Code (stored in ~/.claude.json / ~/.claude, on the home/config volumes
that a Reset wipes) survive a backup/restore cycle.

Secrets are stripped per the "exclude secrets" choice: ~/.claude.json is
filtered through jq to drop primaryApiKey/oauthAccount/customApiKeyResponses
(mcpServers and settings are kept), and ~/.claude/.credentials.json (the
OAuth tokens) is omitted. Staged config is archived under home-claude/ in
the tarball. Verified on the Ubuntu/jq container base.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add file drag-and-drop onto the terminal
Build App / compute-version (pull_request) Successful in 6s
Build Container / build-container (pull_request) Successful in 1m25s
Build App / build-macos (pull_request) Successful in 2m31s
Build App / build-windows (pull_request) Successful in 4m14s
Build App / build-linux (pull_request) Successful in 4m54s
Build App / create-tag (pull_request) Has been skipped
Build App / sync-to-github (pull_request) Has been skipped
84e0bdf7b4
Drop files onto a terminal pane and they're copied into the container and
their in-container paths typed into the prompt, so Claude Code can read
them for reference — mirroring the existing image-paste flow.

Backend: upload_host_file_to_terminal reads the dropped host file and
writes it under /tmp/triple-c-drops/<name> in the session's container,
returning that path. Rejects directories and unreadable paths.

Frontend: TerminalView subscribes to Tauri's webview onDragDropEvent
(OS file drops are intercepted at the webview level, so HTML5 ondrop
wouldn't expose paths). The window-wide event is guarded by the pane's
`active` flag plus a bounds hit-test so a drop only affects the terminal
it landed on; multiple files are uploaded and their paths inserted
space-separated (quoted when they contain spaces).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jknapp added 1 commit 2026-06-30 21:28:03 +00:00
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
edf0698774
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>
jknapp added 1 commit 2026-06-30 21:37:36 +00:00
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
d65872dc94
- 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>
jknapp added 1 commit 2026-06-30 21:59:52 +00:00
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
0945e21eb1
- 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>
jknapp added 1 commit 2026-06-30 22:05:29 +00:00
Apply remaining review findings (L-e, L-f)
Build App / compute-version (pull_request) Successful in 3s
Build Container / build-container (pull_request) Successful in 1m30s
Build App / build-macos (pull_request) Successful in 2m16s
Build App / build-windows (pull_request) Successful in 3m6s
Build App / build-linux (pull_request) Successful in 6m11s
Build App / create-tag (pull_request) Has been skipped
Build App / sync-to-github (pull_request) Has been skipped
01a8f5c503
- L-e: route terminal file drops purely by a bounds hit-test instead of
  the `active` flag. Inactive panes are display:none (zero-size rect) so
  they never match; a zero-size guard makes that explicit. Correct for
  the current tabbed layout and future-proof for split panes, where a
  drop on a visible-but-unfocused pane previously matched no handler.
- L-f: stream the dropped file straight into the upload tar inside a
  blocking task (new exec::upload_host_file_to_container) instead of
  reading the whole file into a Vec and then re-packing it. Peak memory
  drops from ~2x to ~1x the file size, and the synchronous file IO no
  longer runs on the async worker.

cargo check / tsc / vitest all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jknapp added 1 commit 2026-06-30 22:16:45 +00:00
Third review pass: fix tar TOCTOU + transient backup status
Build App / compute-version (pull_request) Successful in 3s
Build Container / build-container (pull_request) Successful in 31s
Build App / build-macos (pull_request) Successful in 2m16s
Build App / build-windows (pull_request) Successful in 3m9s
Build App / build-linux (pull_request) Successful in 5m53s
Build App / create-tag (pull_request) Has been skipped
Build App / sync-to-github (pull_request) Has been skipped
3e2e3f231b
- F2: upload_host_file_to_container now reads the dropped file into a Vec
  inside the blocking task and sizes the tar entry from those exact bytes,
  rather than stat-then-stream where a file changing size between the
  stat and the read could desync the tar header and silently corrupt the
  archive. Still runs off the async worker; memory stays bounded by the
  256 MiB drop cap.
- F4: the "Backup saved" confirmation now auto-clears after 8s (guarded
  against clobbering a newer status message) instead of lingering in the
  project card's status line indefinitely.

F1 (claimed AWS CLI regression from empty-env neutralization) was a false
positive: verified against aws-cli 2.35 that an empty AWS_ACCESS_KEY_ID is
treated as absent and botocore falls through to ~/.aws/credentials (the
call reached AWS and returned InvalidClientTokenId for the file's key, not
PartialCredentialsError). No change needed.

cargo check / tsc / vitest all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jknapp merged commit dc253e8da0 into main 2026-06-30 22:20:00 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: CyberCoveLLC/Triple-C#8