1.2.1: version bump + daemon/git/security fixes - #49
Merged
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tput + file
The workspace-config privileged-key approval printed each pending key as
KEY=VALUE to stderr, and persisted the same KEY=VALUE set to the approval
file. For credential keys (CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY, …) that
leaked the full secret VALUE — to the console, and in daemon mode to the
on-disk supervisor log (~/.sandy/sandboxes/.daemon-start-*.log). The approval
file also stored the plaintext values needlessly (only its line-1 set_hash is
ever read back).
Both now emit KEY NAMES ONLY (`${_kv%%=*}`). The set_hash still encodes the
values, so editing a value still re-prompts. Existing approval files keep
matching (only line 1 matters).
Refs: daemon-mode passive-privileged approval (1.2.1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntainer git auth)
Container-side git auth relied entirely on `gh auth login --with-token` +
`gh auth setup-git`, both of which validate the forwarded token over the
network against api.github.com and swallow every error (2>/dev/null || true).
When api.github.com returns a transient failure (observed live: authenticated
api.github.com calls returning 503 while github.com and general egress are
fine — exposed now that the base image rebuilt with gh 2.96.0, which validates
during login where older gh just stored the token), the setup silently no-ops:
no credential.helper is configured and git push/pull fails, even though a
valid GIT_TOKEN was forwarded and present in the container env.
Append a direct credential helper as a guaranteed fallback: git tries helpers
in order, so gh's multi-account helper still wins when healthy, but this one
supplies the forwarded token for github.com with NO network round-trip. Reads
$GIT_TOKEN from the container env at call time (the literal ${GIT_TOKEN} is
stored in .gitconfig, never the value). Verified end-to-end: an authenticated
`git push --dry-run` succeeds via this helper alone with gh absent.
Template mirror regenerated.
Refs: container git-auth robustness (1.2.1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… the client TTY Daemon --start forks a non-TTY supervisor (stdin=/dev/null via nohup), so the supervisor's passive-privileged approval could only ever fail closed — silently dropping workspace-config privileged keys, including a credential in a workspace .sandy/.secrets (the reported CLAUDE_CODE_OAUTH_TOKEN drop). There was no way to approve in the daemon flow. Before forking, the --start client now resolves the approval with its OWN TTY: a synchronous subprocess run from the workspace in the new env-only SANDY_APPROVE_ONLY mode reuses the full config-load + approval machinery, prompts on the terminal, and persists the approval file — then exits (it stops right after _resolve_passive_privileged_approval, before the workspace mutex, the bare-sandy busy-gate, image builds, and launch, so it touches nothing but the approval file and can't deadlock the lock or double-launch). The real supervisor then re-loads and honors the persisted approval. Gated on the client actually having a TTY (`[ -t 0 ] && [ -t 2 ]`); a non-TTY client (sandy-ui / scripted) still can't prompt, so there approval must be pre-granted (run `sandy --start` once from a terminal) or SANDY_AUTO_APPROVE_PRIVILEGED set. Verified statically (bash -n; APPROVE_ONLY exits before mutex/launch; subprocess before fork; no re-entrancy). The end-to-end prompt/persist/honor cycle is a host gate (no Docker here). Refs: daemon-mode passive-privileged approval (1.2.1) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…re zombie) (#47) Daemon PID 1 is `tail -f /dev/null`, which never exits — so when the agent itself ends (ctrl-d / /exit / crash) the tmux session dies but the container stays Up, holding the egress proxy, per-session networks, and the workspace lock, and making every later --attach return the confusing exit-5 ("container still running, but tmux session probe failed"). Reported repeatedly in daemon use; it also blocked credential refresh (a stale container never recycled). The supervisor's block-and-wait loop now also WATCHES the inner session: it polls `docker exec -u <uid> tmux has-session -t sandy` every 30s and, when the session is gone for two consecutive polls while the container is still running (the two-strike guard avoids tearing down on a transient docker-exec hiccup), breaks out and exits — firing the EXIT trap → cleanup()'s `docker rm -f` + network/lock teardown, exactly what --stop does. `docker rm -f` removes the container outright, so `--restart unless-stopped` does NOT resurrect it (only a bare exit would). --stop's SIGTERM still tears down via the trap as before, and reboot survival (D9) is unchanged: a rebooted container has no supervisor watching it and is handled by --stop/--attach via container-as-truth. Single-location change (the supervisor loop); the container-side PID 1 is untouched, so client-disconnect survival is preserved — only an AGENT exit now ends the session. Verified statically; the real stop-on-exit lifecycle is a host gate. Refs #47 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he credential helper The credential-helper string added in e9ea1f7 keeps ${GIT_TOKEN} literal on purpose — it's stored verbatim in .gitconfig and expanded by the helper's own shell at git-call time, so the value never lands in the config file. shellcheck flags exactly that with SC2016; the run-tests.sh template shellcheck gate (§48) failed on it (1/676). Add `# shellcheck disable=SC2016` in the heredoc source with a rationale comment, and regenerate templates/user-setup.sh.tmpl. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The 1.2.1 batch — version bump + the fixes found exercising 1.2.0 in real use. Five commits:
ea8bad0— bump SANDY_VERSION 1.2.0 → 1.2.1-dev.2c49fb4— security: redact secret VALUES in the passive-privileged approval prompt + approval file (they leaked credentials to the console and the on-disk daemon supervisor log).e9ea1f7— git auth: guaranteed-baseline credential helper independent of gh, so container git auth survives the transient api.github.com failures (api.github.com returns 503 on authenticated requests through the egress proxy (in-container) #50) that were silently breakinggh auth setup-git.dcefe2e— daemon approval:sandy --startgrants passive-privileged approval on the client TTY before forking the non-TTY supervisor, so workspace.sandy/.secretsprivileged keys are no longer silently dropped in daemon mode.5048899— daemon zombie (daemon session zombies on agent exit; --attach returns exit 5 instead of session-ended #47): the supervisor now watches the inner session and tears the container down when the agent exits (ctrl-d/exit/crash), instead of leaving a zombie holding the proxy/networks/lock.docker rm -fprevents--restart unless-stoppedfrom resurrecting it; reboot survival unchanged.Verified statically + (git helper) end-to-end; the daemon-approval and stop-on-exit lifecycles are host gates.
schema_versionunchanged. Fast-forward main after CI. Closes #47.🤖 Generated with Claude Code