* fix(worktree): export executor-profile env into repo setup script
Executor-profile environment variables (with secrets revealed) were merged
into the launch request env and reached the agent subprocess, but the
repository setup script — run inside worktree.Manager.Create, where
`npm install` typically runs — was built with only the install-managed env
(GOCACHE). The profile env was never injected, so tokens like an npm auth
token were unavailable during install. This affected both single-repo and
multi-repo tasks, since both run their setup script through Create.
Thread the resolved profile env into the setup script at the point both
paths converge: add CreateRequest.ScriptEnv (transient, never persisted so
secrets stay out of the DB), set it from req.Env in buildWorktreeCreateRequest
(used by single-repo and per-repo in multi-repo), and merge it over the
managed env in runWorktreeSetupScript. Managed GOCACHE keeps precedence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(worktree): avoid overflow-flagged alloc size, clarify env precedence
Address PR review: drop the len(a)+len(b) capacity hint in mergeScriptEnv
(CodeQL flagged the addition as a possible allocation-size overflow) and
reword the ScriptEnv doc comment to state that install-managed values such
as GOCACHE take precedence over profile values on key collisions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(worktree): round out mergeScriptEnv edge cases
Add profile-only and managed-only assertions to TestMergeScriptEnv per PR
review, making the table exhaustive alongside the existing (nil,nil) and
(profile+managed) cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: harden submodule init against malicious .gitmodules
initSubmodules ran a bare `git submodule update --init --recursive` in a
worktree whose HEAD is attacker-controlled during a fork-PR review task,
with no protocol restriction, no hooks disable, and no cmd.Env. Attacker
`.gitmodules` could pick a command-executing transport (`ext::sh -c`,
CVE-2018-17456), a local `file://` path, or a plain-http URL (cloud
metadata SSRF).
Harden the single sink so all call sites benefit:
- protocol.allow=never default-denies every transport; only https/ssh are
re-enabled (blocks file://, git://, plain http:// SSRF).
- protocol.ext.allow=never hard-pins the RCE transport off; on the command
line it outranks any ambient protocol.ext.allow=always.
- core.hooksPath=/dev/null neutralizes attacker-planted hooks.
- GIT_TERMINAL_PROMPT=0 stops attacker URLs from hanging on a prompt.
Residual: attacker-chosen https/ssh URLs are still fetched (transports real
submodules require); fully closing that needs gating submodule init off for
untrusted worktrees, tracked separately.
* fix: address submodule hardening review feedback
- Reuse newNonInteractiveGitCmd so submodule init inherits the full
non-interactive env set (GIT_SSH_COMMAND=ssh -oBatchMode=yes, SSH/GCM
askpass suppression, WaitDelay). Prevents an attacker ssh:// submodule
URL from hanging the review runner on host-key/passphrase prompts until
the context deadline (GIT_TERMINAL_PROMPT=0 alone only covers git's HTTP
prompt, not SSH).
- Correct the file:// comment: command-line -c outranks repo/global git
config, so ambient protocol.file.allow=always does NOT re-enable file
submodules here. This is intentional for untrusted trees; a trusted
opt-in must be an explicit per-repo -c, not implicit host config.
- Add a file:// URL variant to the transport-block test alongside the
bare-path form (CVE-2022-39253 classification differs by git version).
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* fix: validate destination parent chain in copyfiles byte copy
copyState.copyFile wrote into the target worktree via os.MkdirAll +
os.Create with no destination-parent symlink validation, unlike the
sibling paths that were hardened for exactly this: symlinkMatch calls
validateParentChain and remote writeOneEntry calls secureDestination.
A worktree whose HEAD ships a directory segment as a symlink (a fork-PR
head can set a tree entry to mode 120000 pointing at an absolute host
path) let MkdirAll follow the link and os.Create write the source file's
bytes outside the worktree as the backend user — a zero-click,
content-driven arbitrary-file-write reachable on the PR-review path
(copyConfiguredFiles -> copyfiles.Copy during worktree creation).
Route the byte copy through the same secureDestination hardening: it
runs validateParentChain, creates the parent dirs, and re-confirms
containment via EvalSymlinks before returning a destination guaranteed
inside the worktree. A rejected destination emits a non-fatal warning,
mirroring symlinkMatch. The leaf os.Lstat skip-if-exists behavior is
preserved (only a symlinked parent was the escape vector).
Add byte-copy regression tests: a symlinked parent is rejected and
nothing is written outside the worktree, a leaf symlink is still
skipped, and a clean nested copy still works.
* fix: canonicalize copyfiles target root before containment checks
copyFile passed the raw s.targetDir into secureDestination, which
EvalSymlinks-resolves the created parent and then Rel-checks it against
that root. When the worktree is reached through a symlinked ancestor
(macOS /tmp -> /private/tmp, or a user-symlinked tasks dir) the canonical
parent looks like it escapes the non-canonical root, so every legitimate
byte copy is silently rejected.
Resolve targetDir once in Copy (falling back to the raw path when it does
not exist yet), mirroring the EvalSymlinks WriteEntries already performs.
Add a regression test that routes the target through a symlinked ancestor
and asserts the copy still lands at the real location.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* feat: unarchive tasks with worktree branch recovery
Archived tasks can now be restored. Unarchive works for manual/legacy
archives (empty cascade id, root-only) and cascade archives. Archive
deletes the local branch, so worktree recreate now falls back to
fetching the branch from origin, and unarchive writes the old branch
back as checkout_branch so a fresh session resumes the prior work.
Unrecoverable branches (never pushed) surface a warning. WS archive now
routes through the handoff cascade so it stays stamped and reversible.
UI: unarchive action on /tasks archived rows and the task top bar.
* fix: address PR review feedback on unarchive recovery
Forward PRNumber to the recreate fetch so fork-PR heads (only reachable
via pull/<N>/head) are recoverable, publish task.updated per task in the
cascade unarchive loop so the UI restores without a reload, and
re-publish task.updated after a checkout_branch restore since the
unarchive event goes out with the pre-restore repository rows.
* fix: harden unarchive recovery per review feedback
Propagate branch-probe errors instead of misreporting recoverable work
as gone, and reserve ErrBranchUnrecoverable for a confirmed-missing
remote ref (transient fetch failures keep their own error). Validate
the archive root exists so WS/HTTP archive no longer reports success
for unknown task IDs, CAS-guard manual unarchive on an empty cascade
stamp, detach branch recovery from the request context, and skip
checkout_branch restore for multi-branch repos. UI: in-flight guard on
the list unarchive button, pluralized unrecoverable-branch toast.
* docs: document CAS guard on UnarchiveTask interface contract
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>