044b76bbd56ab6ea249ac3dc9de53e9bee2ff3c1
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
39c574bd57 |
fix(ssh): make the end-to-end launch path actually work
A pass of issues uncovered by running the SSH e2e suite locally
(KANDEV_E2E_CONTAINERS=1 against the real sshd container). After this
commit all 15 non-launch SSH e2e tests pass (UI form validation,
test-connection contract, auth-methods, error-surfacing, sessions-card
empty state) and the launch path runs through prepare -> upload ->
agentctl -> instance -> forward, with WebSocket streams sometimes
disconnecting partway through (tracked below for follow-up).
Image changes (apps/web/e2e/fixtures/ssh-image.ts):
- Install curl so the SSH executor can POST to the in-container
agentctl control server for instance creation.
- Install gcompat + libstdc++ so the dynamically-linked glibc
agentctl binary can exec on musl-libc Alpine. Without it the
kernel returns ENOENT for /lib64/ld-linux-x86-64.so.2.
- Install shadow + 'usermod -p *' kandev so sshd doesn't refuse
pubkey auth for the otherwise-locked user.
- Drop in 'AllowTcpForwarding yes' and 'MaxSessions 100' via
/etc/ssh/sshd_config.d/. Alpine's defaults are 'no' and 10, both
of which crater the SSH executor's port-forward path.
- chown /home/kandev/.kandev to kandev on entrypoint so a host
bind-mount of that path (used in this commit for log persistence)
doesn't make agentctl unable to write its log file.
Image helper (apps/web/e2e/helpers/ssh.ts):
- Bind-mount the host mock-agent-linux-amd64 into the container at
the same absolute path. kandev's orchestrator builds the agent
command from KANDEV_MOCK_AGENT_LINUX_BINARY (a host path), so the
remote needs the binary at that exact path; bake-into-image isn't
enough because the absolute host path is what kandev sends.
- Bind-mount /home/kandev/.kandev so per-session agentctl logs
survive --rm teardown and the test runner can inspect them on
failure.
- Replace the broken CPU-spin sleep helper with a real sleep
subprocess and a /dev/tcp TCP-open probe so timing-dependent
helpers stop racing the container start.
- Retry the host fingerprint scan with stderr capture; the first
scan can race the sshd accept loop on Alpine.
Backend SSH executor (apps/backend/internal/agent/lifecycle):
- executor_ssh_operations.go startRemoteAgentctl: agentctl does not
accept --port-file or --pid-file flags (it reads AGENTCTL_PORT
and writes its own pid). Rewrite the launch wrapper to inject
AGENTCTL_PORT via env, capture $! as the pid, and tail the
on-remote agentctl.log for 'HTTP server bound successfully'
instead of waiting on imaginary side-files.
- executor_ssh_operations.go createRemoteAgentInstance: new helper
that POSTs /api/v1/instances on the in-container control server
via SSH-exec'd curl, returning the per-instance port the executor
should forward to. Mirrors what executor_sprites_operations.go
does inside its sprite. Without this step the agent stream
returns 'configure request failed with status 404' because the
SSH executor was talking to the control server, not the
per-session sub-server.
- executor_ssh.go startAndForwardAgentctl: chains the new
create-instance step in between agentctl launch and port-forward,
and forwards the *instance* port (not the control port).
- executor_ssh.go: drop the per-host SSH connection pool (it was
introducing a subtle aliasing issue between
CreateInstance/StopInstance/GetRemoteStatus that made the SSH
client unreliable across same-host sessions). Each session now
holds its own ssh.Client directly on sshSessionState and
ResumeRemoteInstance dials a fresh client too.
- executor_ssh_operations.go SSHPortForwarder: serialize the
client.Dial calls with a per-forwarder mutex. golang.org/x/crypto/
ssh.Client.Dial is documented as concurrency-safe but in practice
parallel opens from the kandev stream-manager occasionally
produce io.EOF as if the channel-open response was never
received.
Test fix (apps/web/e2e/tests/ssh/error-surfacing.spec.ts):
- Q1 ('TCP refused') was using identity_source='agent' with no
SSH_AUTH_SOCK in the backend env, so it failed at
buildAuthMethods before reaching the TCP-refused path. Switch to
identity_source='file' with the seed key so the test exercises
the failure it claims to.
Known remaining issue (to investigate next): the launch path now
reaches 'agent process started' + 'ACP initialize response
received' when run in isolation, but the WebSocket updates stream
sometimes disconnects with 1006 abnormal closure shortly after. A
standalone Go reproduction of concurrent direct-tcpip channels
against the same image works without issue, so the trigger is
something specific to the kandev backend's stream-manager + my
forwarder interaction. Tracked for the next iteration.
|
||
|
|
c5d678a37a |
test(e2e): add SSH executor Playwright suite (17 specs, ~75 cases)
Every spec drives the real sshd container set up by the ssh-test-base fixture; no mocks. Specs are grouped by concern (file per group), mirroring the docker/ directory style. Each one maps 1:1 to a row in docs/specs/ssh-executor/e2e-plan.md. UI surface: - connection-form.spec.ts — field rendering, save/test gating, identity-source toggle, default port. - test-result.spec.ts — successful + failed Test Connection paths, per-step badges, fingerprint surfacing, cached/will-upload copy. - trust-gate.spec.ts — save disabled until trust ticked, edits reset result + trust, fingerprint-change warning when pinned != observed. - sessions-card.spec.ts — empty state, row rendering, manual refresh, status badges, truncation, polling pickup. - persistence.spec.ts — fingerprint persists across reload, new form starts blank, executor list label, partial state doesn't leak. API contract: - test-endpoint.spec.ts — POST /api/v1/ssh/test step ordering, invalid body, host-required, handshake short-circuit, cached/uploaded states. - sessions-endpoint.spec.ts — GET /api/v1/ssh/executors/:id/sessions filters to ssh-runtime rows, populates metadata, monotonic uptime. - executor-crud.spec.ts — type=ssh persists fingerprint, profile workdir round-trip, edit-with-running-sessions keeps snapshots. Real-SSH integration: - launch-task.spec.ts — full smoke: agentctl upload + sha256 cache hit on second launch, task/session dir layout, stop cleans session dir. - recovery.spec.ts — backend restart reconnects, dead pid surfaces failure, persisted metadata keys present. - auth-methods.spec.ts — key + valid, missing key, passphrase-protected rejection, agent without SSH_AUTH_SOCK, wrong-key auth failure. - concurrency.spec.ts — two sessions share task dir distinct ports, two tasks distinct dirs, traffic drop + restore exercises eviction + reconnect. - hostkey-rotation.spec.ts — regenerate host key surfaces new fingerprint, stale pinned fingerprint blocks launch verbatim, re-trust restores function. - config-inheritance.spec.ts — ~/.ssh/config HostName / Port / IdentityFile / ProxyJump inheritance with form-overrides-config rules. - workdir-per-profile.spec.ts — per-profile ssh_workdir_root routes to separate remote workdir trees; default falls back to ~/.kandev. - proxy-jump.spec.ts — 2-container bastion topology asserts target is unreachable directly, reachable via ProxyJump, chained ProxyJump fails. - error-surfacing.spec.ts — TCP refused, auth failed, bastion unreachable surface verbatim; backend 400 stays a 400 not a synthesised failure. Also adds an AGENTS.md (CLAUDE.md symlink) line pointing at the e2e/README.md so future agents discover the containers project + KANDEV_E2E_CONTAINERS env var without grepping playwright.config.ts. |