Backend:
- service_task_environments: fail closed when checkAnySessionRunning
errors so a transient DB hiccup can't bypass the running-session
guard and tear down a live container/sandbox; teardown is now truly
best-effort with errors.Join across container/sandbox/worktree, so a
failed Docker call doesn't permanently orphan the worktree on retry.
Adds tests for the running-check error path and the multi-resource
best-effort path.
- executor_execute: propagate GetTaskEnvironmentByTaskID errors instead
of silently launching a fresh environment that orphans existing
containers/worktrees; gate environment reuse on matching ExecutorType
so a profile switch doesn't pass stale PreviousExecutionID/container
IDs to the wrong backend.
- task_environment_handlers: reject malformed JSON on POST
/tasks/:id/environment/reset (still allow empty body) so a destructive
endpoint doesn't silently proceed with a bad payload.
- worktree (PushEnvironmentBranch): bound git push with a 30s context
timeout and disable interactive prompts via GIT_TERMINAL_PROMPT=0
so a stalled remote can't hang the request thread.
- manager_environment: explicitly call ensureClient() before
ContainerMgr() in DestroyContainer/GetContainerLiveStatus so a missing
Docker daemon surfaces as "docker unavailable" rather than the
generic "container manager not initialized".
- container: WorkingDir always uses the in-container /workspace path
rather than the host workspace path (Docker would happily start in an
unrelated directory in host bind-mount mode); bootstrap now logs the
prepare-script exit code so failures show up in container logs.
- docker/client: CreateContainerInteractive honors ContainerConfig.Entrypoint
so the same config struct produces consistent behavior on both creation
paths.
Frontend:
- executor-settings-button: extract useTaskEnvironment hook (component
shrinks below the 100-line limit); only treat 404 as empty env (other
errors no longer wipe a valid environment); track "loaded once" via a
ref so loadEnv doesn't depend on env state — that dep was creating a
new function identity on every successful poll, cancelling the
3-second interval and turning the popover into a tight loop hammering
/environment/live; copy-shell command uses sh instead of bash so it
works on user-built minimal images.
- task-reset-env-confirm-dialog: AlertDialogAction onClick calls
e.preventDefault() so Radix's auto-close doesn't dismiss the dialog
before the async reset completes (loading spinner now visible; close
is parent-controlled via onOpenChange).
- remote-credentials-card: track env method in selectedIds the same way
files/gh_cli_token are tracked, and let initialChoice match either
selectedIds OR envSecretId. First-time env-credential setup for an
agent that exposes both files+env was broken: clicking the env radio
never updated state, choice re-derived to "none", and the secret
picker disappeared before the user could pick anything.
- e2e/executor-reuse: select the acknowledge checkbox by label, not by
positional last(), so adding another checkbox to the dialog can't
silently target the wrong control.
Format pass on prettier-flagged files; gofmt fixed two whitespace
nits in lifecycle files.
Three independent fixes that all surface together while iterating on the
Docker executor flow:
- Remote Credentials card: the per-agent "Configured" / "Not Configured"
badge was driven by useState that only seeded once on mount. Picking a
secret in the dropdown updated the parent's `agentEnvVars` map but the
local `choice` never moved to `"env"`, so the badge stayed grey until
the page was reloaded. Derive `choice` straight from props every render
so the badge tracks live state.
- ACP session/load: when the agent process restarted between turns
(common with claude-agent-acp because its session map is in-memory),
the host's session/load call failed with JSON-RPC -32002 "Resource not
found" and the entire launch errored out. Treat that as the same kind
of "session is gone" signal as -32601 / capability=false and fall back
to session/new. Worst case the agent loses prior context; best case
the user just keeps typing.
- Executor Settings popover: dropped Execution ID and Environment ID
rows (debug-only noise users don't need), kept Container ID + the
copyable `docker exec -it … bash` line, added a live status badge
with tones for running / exited / paused / dead / error / missing,
and a 3-second polling interval so the badge follows the real
container state. New `GET /api/v1/tasks/:id/environment/live`
endpoint plumbs Docker `inspect` data through the lifecycle manager
→ task service → handler. ContainerLiveStatus mirror type at the
service layer keeps lifecycle types out of the public surface.
Make the Docker executor work with a Dockerfile users can actually build,
without depending on a maintainer-shipped image:
- The container now launches a small bootstrap (`sh -c "$KANDEV_PREPARE_SCRIPT
; exec /usr/local/bin/agentctl"`) regardless of the image's ENTRYPOINT.
The agent CLI is no longer wired through container Cmd; agentctl receives
it later via its CreateInstance HTTP API.
- Host agentctl-linux binary is mounted into the container at
/usr/local/bin/agentctl via the existing AgentctlResolver. User-built
images don't have to bake it in.
- Default Dockerfile in the executor profile UI replaced with a working
node:22-slim + git + curl image — runs npx-based ACP agents (Claude,
Codex, OpenCode, Auggie) out of the box.
- lifecycleAdapter now exposes RequiresCloneURL / ShouldApplyPreferredShell
so the orchestrator can correctly identify containerized executors and
resolve the clone URL. Without this, applyRepositoryConfig short-circuits
on `e.capabilities == nil`, the cloneURL is never resolved, and the
prepare script fails with `repository '{{repository.clone_url}}' does
not exist`.
- applyRepositoryConfig also surfaces the resolved clone URL to script
metadata so the prepare script can interpolate {{repository.clone_url}}.
- docker.ContainerConfig gains an Entrypoint field, plumbed to the docker
client.
Verified end-to-end on a local stack with the new minimal image: the
container starts, bootstrap clones the GitHub repo, agentctl launches and
hands off to claude-acp inside the container.
Two bugs:
1. The Dockerfile editor and Image Tag input on the executor profile page
stored their values in component-local useState inside DockerSections,
not in the form state. Saving the profile never wrote them back, so
"Use defaults" / typing in the editor / changing the tag never
persisted — and the Build button was the only thing that consumed
them. The same gap existed in the new-executor flow's
buildProfileConfig.
Lift `dockerfile` and `image_tag` into useProfileFormState (edit) and
useCreateProfileFormState (new), and emit them in buildSaveConfig /
buildProfileConfig so they round-trip through profile.config.
2. Even when an image_tag is set on the profile, the container builder
ignored it. imageName was always derived from the agent runtime's
hardcoded `kandev/multi-agent`, so a user who built a custom image
like `kandev/agent:latest` would still see Docker fail with
"No such image: kandev/multi-agent:latest".
Propagate `profile.config.image_tag` → metadata
(`image_tag_override`) → ExecutorCreateRequest → ContainerConfig,
and let it override `rt.Image` in buildContainerConfig when set.
Also align the frontend default image tag (kandev/multi-agent:latest)
with the backend's hardcoded runtime default so users who don't override
end up with a working build.
* fix: docker executor build/container bugs and path traversal on macOS
- Fix buildDockerImage() sending requests to Next.js instead of Go
backend by using getBackendConfig().apiBaseUrl
- Add response.ok check to prevent rendering HTML error pages in the
build log
- Fix listDockerContainers() sending labels as JSON instead of the
key=value format expected by the backend, which caused all containers
to be shown instead of only profile-scoped ones
- Add kandev.profile_id label to containers so profile-scoped filtering
works
- Fix resolveSafePath() failing on macOS when intermediate directories
don't exist (symlink mismatch: /var vs /private/var)
- Add "Use defaults" button and default Dockerfile/image tag for Docker
executor setup
Closes#98
* fix: parse Docker build JSON stream into readable output
The build log streamed raw Docker JSON lines. Parse them to show
human-readable output: extract "stream" and "status" fields, surface
errors clearly.
* fix: skip Docker aux/metadata lines in build log
* style: format docker-sections with prettier