* fix(executor): re-clone provider repos when local path is missing or not a git repo
Previously the re-clone guard only fired when local_path was empty, so a
set-but-invalid path (e.g. deleted checkout, or relocated by the provider
clone-path/identity change) was fed straight to the worktree preparer,
which hard-failed with "repository is not a git repository" instead of
recovering.
Two-part fix:
- resolveTaskRepoInfo now also re-clones when local_path exists but isn't
a valid git checkout (new isLocalGitRepo helper), self-healing on next
launch.
- validateWorktreeRequest now fails the "Validate repository" step with an
honest error naming the offending path when it truly isn't a git repo,
instead of passing and cascading into a misleading "sync interrupted"
failure downstream.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(executor): gate stale-path reclone to provider repos and cover resume + multi-repo
Restrict the stale-local-path re-clone to genuinely provider-backed repos
(SourceType != "local") so a local checkout that's temporarily missing is
never silently overwritten. Centralize the re-clone guard into a shared
ensureRepoLocalPath helper and call it from the single-repo resume path
(applyResumeRepoConfig) so resume self-heals like a fresh launch. Apply the
same non-git-path validation used by the single-repo prepare path to the
multi-repo prepareOneRepo path so a stale secondary repo fails fast with a
clear message instead of surfacing worktree.Manager's ErrRepoNotGit.
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: prevent command injection via untrusted git branch names
Untrusted git branch names (e.g. a fork PR head branch) were substituted
unescaped into executor prepare scripts and run via `eval` in a shell,
yielding command injection (CRITICAL RCE) in the Docker/Sprites/remote
executors. git check-ref-format permits `;`, `|`, `$`, backticks and
`()`, so a branch like `$(touch pwned)` or `main;touch pwned` executed.
- scriptengine: shell-single-quote every data placeholder that lands in
shell text (repository.path/name/branch/clone_url/ssh_url,
workspace.path, worktree.base_path/path/branch/base_branch). Script
fragments (setup_script, git.identity_setup, github.auth_setup,
kandev.*) are left untouched.
- default_scripts: wrap each data placeholder in single quotes in the
postlude, clone lines, and the Sprites echo (double quotes still allow
$(...) substitution). `'<escaped>'` round-trips to the literal.
- orchestrator: defense-in-depth — reject a PR head branch that fails
securityutil.IsValidBranchName before it reaches CheckoutBranch, while
still creating the review task without a checkout.
- tests: PoC-derived regression asserting the injected command no longer
runs through the real sh -c/eval sink, provider escaping unit tests,
and ingestion-layer validation coverage.
* fix: harden branch-name escaping for legacy stored prepare scripts
Automated review (Codex, CodeRabbit) flagged that shell-single-quoting the
value while relying on the templates to add the surrounding quotes only
protects the templates we ship. Profiles snapshot the default prepare
script at creation time, so an OLD stored script referencing a placeholder
bare (e.g. `git clone --depth=1 --branch {{repository.branch}} ...`) still
resolved with an unquoted, injectable branch value.
- scriptengine: add shellQuote, which emits a fully self-contained
single-quoted shell token (surrounding quotes included). Every data
placeholder (repository.path/name/branch/clone_url/ssh_url,
workspace.path, worktree.base_path/path/branch/base_branch) now carries
its own quoting, so a bare `{{placeholder}}` in any template — including
legacy stored scripts — is safe. shellSingleQuote is kept for the git
identity fragment, which supplies its own quotes.
- default_scripts: reference data placeholders bare (the value self-quotes);
the Sprites echo becomes printf with the values as separate args so a
hostile URL/branch can't expand inside a double-quoted string.
- tests: assert providers emit fully-quoted tokens that round-trip through a
real shell; cover base_path/path/repository.path/name/ssh_url; add a
regression proving a legacy stored script with bare placeholders no longer
executes injected commands.
* test: strengthen branch-injection regression assertions
Address Greptile review feedback on the new tests.
- assertNoUnquotedPayload now tracks real single-quote state char-by-char
instead of "some quote before and after", so a payload in an unquoted
context on a line that happens to contain other quotes is caught.
- Drop the unused wantHasRepoRow table field; repo-row presence is already
asserted via len(got) != 1.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* fix(lifecycle): don't split streaming messages on subagent tool calls
A subagent's internal tool calls (ParentToolCallID set) stream on the same
session concurrently with the parent agent's own text. Flushing the message
buffer on those events closed the parent's in-flight streaming message, so
each subagent tool call started a new message row — splitting one logical
message mid-sentence and breaking markdown (e.g. **bold** spans rendered as
literal asterisks) across the fragments.
Only flush on top-level tool calls; subagent-internal ones leave the parent's
streaming segment open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(lifecycle): drop dead flush-text branch in tool call handler
flushMessageBuffer always publishes remaining content through the streaming
path and returns "", so the event.Text / history-append branch could never
execute. Call it directly instead of through a helper carrying dead code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* perf(backend): cap cumulative diff bytes and file count during large rebases
* perf(web): coalesce cumulative-diff refetches and skip redundant status compares
* feat(web): warn when the changes panel hides files from a capped diff
* fix(backend): enforce cumulative diff total byte budget strictly
* fix(web): keep coalesced refetch timer shared across envKey subscribers
* fix(web): drop unsound git-status SHA fast-path that staled live diffs
* fix(web): cancel coalesced timer on mid-flight drain to avoid duplicate refetch
* refactor(web): move changes-panel banner and helper to a sibling file
* docs(backend): clarify cumulative diff truncated-files-count semantics
* fix(web): tighten useSelectedFileKey mode param to the all|file union
* fix(backend): prefer local copilot CLI over npx at launch
IsInstalled probed the copilot binary for the UI badge but launch always
ran `npx -y @github/copilot`, paying an npm registry round-trip every
start — minutes behind a private registry. Reuse the env-correct binary
check (standalone LookPath, SSH preflight probe) so when copilot is on
PATH the lifecycle runs it directly; containerized runtimes keep npx.
* fix(backend): make SSH native-binary probe non-fatal on transport error
The native-binary probe is an optimisation, not a requirement. A transient
SSH transport error (MaxSessions, reset) made "Verifying agent binary"
abort the launch, blaming copilot before npx was tried. Treat probe
transport errors as a miss and fall through to the required default-command
probe, which reports failures with proper context. Also call
NativeBinaryName() once.
* fix(backend): clear stale native-binary metadata before SSH probe
A pre-existing or stale MetadataKeyNativeBinary in req.Metadata could make
the command builder prefer copilot --acp on an npx-only host even when this
probe misses. Clear it up front so only a positive probe re-sets it.
* refactor(backend): use resolved RuntimeName for native-binary check
buildExecutionFromInstance re-derived the runtime from req.ExecutorType
while promoteWorkspaceExecution read execution.RuntimeName. Use the already
resolved execution.RuntimeName (set from rt.Name()) in both, so the two call
sites are consistent.
* fix(backend): reflect local copilot CLI in profile command preview
The profile setup command preview always rendered `npx -y @github/copilot`
because PreviewAgentCommand built the command without the native-binary
probe. The preview is not bound to an executor, so assume the default
standalone host and probe PATH (mirroring the standalone launch path): show
`copilot --acp` when the CLI is installed, npx otherwise.