feat: support remote macos ssh hosts#1534
Conversation
📝 WalkthroughWalkthroughThe SSH executor is extended from Linux/amd64-only to support macOS (darwin/arm64, darwin/amd64) remote hosts. This involves cross-compiling platform-specific ChangesmacOS remote SSH executor support
Sequence Diagram(s)sequenceDiagram
participant Client as Web Client
participant SSHHandlers as ssh/handlers.go
participant SSHExecutor as SSHExecutor.CreateInstance
participant detectRemoteInfo
participant AgentctlResolver
participant RemoteHost as Remote SSH Host
Client->>SSHHandlers: POST /ssh/test
SSHHandlers->>detectRemoteInfo: uname -m, uname -s
detectRemoteInfo->>RemoteHost: run uname commands
RemoteHost-->>detectRemoteInfo: OS/arch strings
detectRemoteInfo-->>SSHHandlers: SSHRemotePlatform (e.g. darwin/arm64)
SSHHandlers->>SSHHandlers: requireSupportedRemotePlatform
SSHHandlers-->>Client: TestResult{OS, Platform}
Client->>SSHExecutor: CreateInstance
SSHExecutor->>detectRemoteInfo: probe platform
detectRemoteInfo-->>SSHExecutor: SSHRemotePlatform
SSHExecutor->>AgentctlResolver: ResolveRemoteBinary(platform)
AgentctlResolver-->>SSHExecutor: agentctl-darwin-arm64 path
SSHExecutor->>RemoteHost: upload + forward agentctl (zsh shell on darwin)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Claude finished @carlosflorencio's task in 5s —— View job I'll analyze this and get back to you. |
|
| Filename | Overview |
|---|---|
| apps/backend/internal/agent/runtime/lifecycle/executor_ssh_operations.go | Core platform detection logic: adds uname -s probe, SSHRemotePlatform struct, normalization functions, requireSupportedRemotePlatform, and SSHDefaultShellForPlatform. All three supported platforms are correctly handled and the unsupported path returns a populated struct (per the prior-thread fix). |
| apps/backend/internal/agent/runtime/lifecycle/agentctl_resolver.go | Refactors resolver to select platform-specific agentctl helper; adds legacy linux/amd64 env-var fallback, same-platform native binary fallback for dev mode, and clear error messages naming the env var and platform. Well-tested. |
| apps/backend/internal/agent/runtime/lifecycle/executor_ssh.go | Threads SSHRemotePlatform through prepareRemoteHost, preflightAgentBinary, maybeUploadCredentials, and startAndForwardAgentctl; switches shell selection to sshShellForRemote for platform-aware defaults. Changes are consistent and all callers updated. |
| apps/backend/internal/ssh/handlers.go | Adds probeRemotePlatform helper used by probeAgents and probeShells to determine platform-aware default shell; extends TestResult with os and platform fields; renames Verify arch to Verify platform step. Logic is correct with graceful fallback to bash when probe fails. |
| apps/backend/internal/launcher/bundle.go | Replaces conditional linux/amd64 helper check with an unconditional loop over all three required remote helpers. Removes the now-dead requiresAgentctlLinuxAMD64 function. |
| apps/desktop/src-tauri/src/backend.rs | Adds REMOTE_AGENTCTL_HELPERS constant array and iterates it in validate_runtime_dir and test helpers. Adds test for missing darwin helper. Consistent with Go and shell-script validation. |
| .github/workflows/release.yml | Adds darwin helper cross-compilation and bundle packaging. The inline macOS runtime binary check at line 599 lists darwin helpers but omits agentctl-linux-amd64, leaving a gap in the CI gate (though verify-desktop-runtime.sh still covers all three). |
| apps/backend/internal/system/metrics/collector_darwin_nocgo.go | New no-op metrics fallback for darwin && !cgo build tag, enabling CGO_ENABLED=0 cross-compilation of darwin agentctl helpers without Darwin SDK. |
| apps/web/components/settings/ssh-agent-readiness-card.tsx | Adds readinessProbeBody / readinessDisplayShell helpers; tracks defaultShell from server responses; omits explicit shell in probe body when field is empty so the backend can apply its platform default. Unit-tested. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant C as SSH Client (control plane)
participant R as Remote Host (Linux/macOS)
participant A as AgentctlResolver
participant B as Bundle (bin/)
C->>R: uname -a (info only)
C->>R: uname -m → Arch (x86_64 / arm64)
C->>R: uname -s → OS (Linux / Darwin)
note over C: normalizeSSHRemotePlatform()<br/>→ SSHRemotePlatform{GOOS,GOARCH}
C->>C: "requireSupportedRemotePlatform()<br/>linux/amd64 ✓ darwin/arm64 ✓ darwin/amd64 ✓"
C->>A: ResolveRemoteBinary(platform)
A->>B: "lookup agentctl-{goos}-{goarch}"
B-->>A: /bin/agentctl-darwin-arm64
A-->>C: binary path
C->>R: SCP agentctl helper (if sha256 differs)
C->>R: chmod +x agentctl
note over C: SSHDefaultShellForPlatform()<br/>darwin → zsh, linux → bash
C->>R: "{shell} -lc 'agentctl serve …'"
R-->>C: port forward ready
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant C as SSH Client (control plane)
participant R as Remote Host (Linux/macOS)
participant A as AgentctlResolver
participant B as Bundle (bin/)
C->>R: uname -a (info only)
C->>R: uname -m → Arch (x86_64 / arm64)
C->>R: uname -s → OS (Linux / Darwin)
note over C: normalizeSSHRemotePlatform()<br/>→ SSHRemotePlatform{GOOS,GOARCH}
C->>C: "requireSupportedRemotePlatform()<br/>linux/amd64 ✓ darwin/arm64 ✓ darwin/amd64 ✓"
C->>A: ResolveRemoteBinary(platform)
A->>B: "lookup agentctl-{goos}-{goarch}"
B-->>A: /bin/agentctl-darwin-arm64
A-->>C: binary path
C->>R: SCP agentctl helper (if sha256 differs)
C->>R: chmod +x agentctl
note over C: SSHDefaultShellForPlatform()<br/>darwin → zsh, linux → bash
C->>R: "{shell} -lc 'agentctl serve …'"
R-->>C: port forward ready
Reviews (6): Last reviewed commit: "fix: address ssh readiness review follow..." | Re-trigger Greptile
|
Claude finished @carlosflorencio's task in 1m 47s —— View job Code Review
I reviewed the full diff against FindingsBlocker (must fix before merge)
Suggestion (recommended, doesn't block)
Other notes (not filed as findings — informational)
Summary
Verdict: Blocked — fix the platform-normalization blocker before merge (small, well-scoped fix); the two suggestions are good follow-ups but don't need to gate this PR. |
|
OpenCode review complete OpenCode model: No suggestions found for commit |
There was a problem hiding this comment.
💡 Codex Review
kandev/scripts/release/prepare-desktop-runtime.sh
Lines 73 to 75 in 8029091
This now copies agentctl-darwin-* Mach-O executables into the desktop runtime resources, but the macOS release workflow’s “Sign macOS desktop runtime binaries” loop still signs only kandev and agentctl. In signed/notarized macOS desktop builds, these nested executable helpers can remain unsigned even though they are shipped inside the app bundle; include the Darwin helper names in that signing step before Tauri packages the resources.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude finished @carlosflorencio's task in 2m 12s —— View job Review
This is a follow-up pass after commits Verified fixes from the previous round
FindingsSuggestion (recommended, doesn't block)
Summary
Verdict: Ready with suggestions — the prior blocker and both suggestions from the earlier review round are fixed and well-tested; one new, low-risk suggestion (macOS signing coverage for the new Darwin helpers) is worth picking up before cutting a signed desktop release, but doesn't block this PR. |
|
I'll analyze this and get back to you. |
|
Claude finished @carlosflorencio's task in 1m 30s —— View job Review
This is a final pass after Verified fixes
FindingsNo new blockers or suggestions. I checked every file changed by the I wasn't able to execute Summary
Verdict: Ready to merge — all prior blocker and suggestion findings from this PR's review rounds are fixed, tested, and consistent across backend, desktop, release pipeline, and Docker packaging. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2946a1676b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude finished @carlosflorencio's task in 1m 32s —— View job Review
This pass focuses on Verified
FindingsNo blockers or new suggestions. The extra This closes out the last open item (Codex P2: readiness defaults diverging from launch defaults) from earlier review rounds. Combined with the fixes verified in prior passes (platform-normalization blocker, resolver test coverage, host-fallback comment, Darwin Docker helpers, Darwin codesigning), I found no remaining issues across the full diff. Summary
Verdict: Ready to merge. |
|
OpenCode findings that could not be posted inline OpenCode model: Some findings could not be posted inline, so they are preserved here. Inline placement unavailableapps/backend/internal/ssh/handlers.go:246 - Shell parameter from probe-agents body is concatenated into a remote command without validation
|
|
Claude finished @carlosflorencio's task in 1m 49s —— View job I'll analyze this and get back to you. |
FindingsReviewed the latest commit
No leftover references to the removed One pre-existing item, not introduced by this commit, for the record: OpenCode's earlier comment about No new blockers or suggestions from this commit. Summary
Verdict: Ready to merge. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
scripts/release/verify-desktop-runtime.sh (1)
71-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAvoid duplicating helper names in the Windows allowlist.
Line 71 hardcodes the same helper set that
REMOTE_AGENTCTL_HELPERSalready defines. That creates an easy drift path where a new helper is required at Lines 118-122 but still rejected on Windows here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/release/verify-desktop-runtime.sh` at line 71, The Windows allowlist in verify-desktop-runtime currently duplicates the helper names already defined by REMOTE_AGENTCTL_HELPERS, which can drift out of sync. Update the allowlist logic in verify-desktop-runtime.sh so it reuses the existing REMOTE_AGENTCTL_HELPERS source of truth instead of hardcoding agentctl helper names, and ensure the Windows case accepts any helper added there without needing a second manual update.apps/web/app/settings/executors/[profileId]/page.tsx (1)
512-530: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the SSH-only header action from
ProfileEditForm.
ProfileEditFormnow spans Lines 511-617, which crosses the repo’s 100-line function cap. This new SSH-only header-action block is a clean extraction point and would keep the form body focused on state/persistence wiring. As per coding guidelines,apps/web/**/*.{ts,tsx,js,jsx}enforcesFunctions ≤100 linesand says to extract a helper function, custom hook, or sub-component when hitting code-quality limits.♻️ Possible extraction
+function SSHConnectionSettingsAction({ executorId }: { executorId: string }) { + const router = useRouter(); + + return ( + <Button + variant="outline" + size="sm" + onClick={() => router.push(`/settings/executors/ssh/${executorId}`)} + className="w-full cursor-pointer sm:w-auto" + data-testid="ssh-connection-settings-link" + > + <IconShieldLock className="mr-1.5 h-4 w-4" /> + Connection Settings + </Button> + ); +} + function ProfileEditForm({ executor, profile }: { executor: Executor; profile: ExecutorProfile }) { - const router = useRouter(); const { items: secrets } = useSecrets(); const persistence = useProfilePersistence(executor, profile); const form = useProfileFormState(executor, profile); const relatedContainers = useDockerProfileContainers(profile.id, form.isDocker); const spritesTokenMissing = form.isSprites && !form.spritesSecretId; const headerActions = - executor.type === "ssh" ? ( - <Button - variant="outline" - size="sm" - onClick={() => router.push(`/settings/executors/ssh/${executor.id}`)} - className="w-full cursor-pointer sm:w-auto" - data-testid="ssh-connection-settings-link" - > - <IconShieldLock className="mr-1.5 h-4 w-4" /> - Connection Settings - </Button> - ) : undefined; + executor.type === "ssh" ? <SSHConnectionSettingsAction executorId={executor.id} /> : undefined;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/settings/executors/`[profileId]/page.tsx around lines 512 - 530, `ProfileEditForm` is over the function-length limit because the SSH-only `headerActions` block is embedded directly in the form body. Extract that `executor.type === "ssh"` button logic into a small helper function, custom hook, or sub-component near `ProfileEditForm` so the main form stays focused on state and persistence wiring. Keep the existing `useRouter`, `executor`, and `headerActions` behavior unchanged, but move the SSH-specific JSX behind a clearly named extraction referenced from `ProfileEditForm`.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/components/settings/ssh-agent-readiness-card.tsx`:
- Around line 28-34: Normalize the SSH shell value once and reuse it everywhere
in ssh-agent-readiness-card.tsx: readinessProbeBody already trims
whitespace-only input, but readinessDisplayShell and the branches around the
selector/default handling still use the raw shell string. Update the logic
around readinessProbeBody, readinessDisplayShell, and the related state/default
selection path so whitespace-only ssh_shell values are treated as empty for both
the probe request and the UI/display fallback, ensuring the detected default
shell is adopted consistently.
In `@scripts/release/prepare-desktop-runtime.sh`:
- Around line 116-119: The helper copy loop in prepare-desktop-runtime.sh
bypasses the script’s standard missing-file handling, so replace the direct
cp/chmod logic in the REMOTE_AGENTCTL_HELPERS loop with copy_one() and keep
using the existing copy_one() error path and symbols so missing helpers fail
with the same “Missing … in …” message as other assets.
---
Nitpick comments:
In `@apps/web/app/settings/executors/`[profileId]/page.tsx:
- Around line 512-530: `ProfileEditForm` is over the function-length limit
because the SSH-only `headerActions` block is embedded directly in the form
body. Extract that `executor.type === "ssh"` button logic into a small helper
function, custom hook, or sub-component near `ProfileEditForm` so the main form
stays focused on state and persistence wiring. Keep the existing `useRouter`,
`executor`, and `headerActions` behavior unchanged, but move the SSH-specific
JSX behind a clearly named extraction referenced from `ProfileEditForm`.
In `@scripts/release/verify-desktop-runtime.sh`:
- Line 71: The Windows allowlist in verify-desktop-runtime currently duplicates
the helper names already defined by REMOTE_AGENTCTL_HELPERS, which can drift out
of sync. Update the allowlist logic in verify-desktop-runtime.sh so it reuses
the existing REMOTE_AGENTCTL_HELPERS source of truth instead of hardcoding
agentctl helper names, and ensure the Windows case accepts any helper added
there without needing a second manual update.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c4e3f7ea-1e54-436d-a081-b3ea05668824
📒 Files selected for processing (43)
.github/workflows/release.ymlDockerfileMakefileapps/backend/Makefileapps/backend/internal/agent/runtime/lifecycle/agentctl_resolver.goapps/backend/internal/agent/runtime/lifecycle/agentctl_resolver_test.goapps/backend/internal/agent/runtime/lifecycle/executor_ssh.goapps/backend/internal/agent/runtime/lifecycle/executor_ssh_connection_test.goapps/backend/internal/agent/runtime/lifecycle/executor_ssh_credentials.goapps/backend/internal/agent/runtime/lifecycle/executor_ssh_credentials_test.goapps/backend/internal/agent/runtime/lifecycle/executor_ssh_operations.goapps/backend/internal/launcher/bundle.goapps/backend/internal/launcher/bundle_test.goapps/backend/internal/ssh/handlers.goapps/backend/internal/ssh/handlers_test.goapps/backend/internal/system/metrics/collector_darwin_nocgo.goapps/cli/README_internal.mdapps/cli/src/release-config.test.tsapps/desktop/AGENTS.mdapps/desktop/e2e/desktop-launch-smoke.mjsapps/desktop/src-tauri/src/backend.rsapps/web/app/settings/executors/[profileId]/page.tsxapps/web/app/settings/executors/new/[type]/ssh-create-page.tsxapps/web/components/settings/profile-edit/profile-edit-page-chrome.tsxapps/web/components/settings/ssh-agent-readiness-card.test.tsapps/web/components/settings/ssh-agent-readiness-card.tsxapps/web/components/settings/ssh-connection-card.tsxapps/web/e2e/tests/settings/mobile-ssh-profile-connection-link.spec.tsapps/web/e2e/tests/settings/ssh-profile-connection-link.spec.tsapps/web/e2e/tests/ssh/error-surfacing.spec.tsapps/web/e2e/tests/ssh/test-endpoint.spec.tsapps/web/e2e/tests/ssh/test-result.spec.tsapps/web/lib/types/http-ssh.tsdocs/plans/desktop-tauri-app/plan.mddocs/plans/desktop-tauri-app/task-03-desktop-runtime-resources.mddocs/specs/native-kandev-cli/plan.mddocs/specs/native-kandev-cli/spec.mddocs/specs/ssh-executor/e2e-plan.mddocs/specs/ssh-executor/spec.mdscripts/release-desktop.test.shscripts/release/package-bundle.shscripts/release/prepare-desktop-runtime.shscripts/release/verify-desktop-runtime.sh
|
Claude finished @carlosflorencio's task in 49s —— View job Code Review
This commit ("fix: address ssh readiness review followups") is a small, targeted diff closing out the two OpenCode findings from the previous round:
FindingsNo new issues. All four changes are correctly scoped fixes for previously-flagged review comments (the OpenCode unused-field finding, the OpenCode stale- Summary
Verdict: Ready to merge. |
Review: live validation on real Apple Silicon MacsValidated this change end-to-end against real Apple Silicon Macs over Tailscale (not loopback) — cross-built + ad-hoc-signed the darwin 🔴 Bug 1 — env-authenticated agents can't authenticate over SSH (blocks Claude tasks)Every Claude-ACP task on an SSH remote fails with
The adapter authenticates from the Suggested fix: run credential resolution for SSH (drop the container-only gating, or resolve env-type auth methods in the SSH executor) and forward the resolved credential env into 🔴 Bug 2 — terminals / workspace-restore broken on SSH sessionsOpening a terminal or restoring a finished SSH task throws Suggested fix: project the executor record's persistent SSH config into the workspace metadata as a fallback when no running record exists (non-overwriting, so a live record still wins). 🟡 Regression —
|
Follow-ups to kdlbs#1534 (remote macOS SSH hosts): - Restore linux/arm64 SSH-host support that existed before this change (Graviton/ARM Linux remotes). Re-adds the platform-gate case, the build-agentctl-linux-arm64 Make target, the release build/copy steps, the runtime-bundle required-helper entry, and the desktop runtime validators (Rust/shell/mjs). - Re-add ad-hoc signing of the darwin agentctl helpers in `make` (codesign on macOS, rcodesign on Linux/CI; warn if absent). Go already signs darwin/arm64 at link time; this also signs darwin/amd64 and re-signs uniformly, keeping the chain notarization-ready. - Enforce the darwin/arm64 helper carries a code signature at bundle-validation time (LC_CODE_SIGNATURE scan). An unsigned darwin/arm64 binary is SIGKILLed by Apple Silicon (AMFI) on launch, so catch it at packaging instead of on every remote launch. darwin/amd64 is intentionally exempt (Intel does not enforce; Go does not sign it). Verified: go build ./..., go test on lifecycle/launcher/ssh/metrics, release-desktop.test.sh, and a live SSH run to a real Apple Silicon Mac (agentctl uploaded, launched, "HTTP server bound successfully"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Opened #1559 with fixes for the two 🔴 bugs (env-auth creds not forwarded to remote SSH agents; terminal/workspace-restore missing host config) and the 🟡 |

Remote SSH hosts should not require Kandev to be installed locally on every Mac just to run agents. The SSH executor now packages and selects macOS agentctl helpers, so one control plane can drive trusted Linux amd64 and macOS SSH hosts.
Important Changes
uname -sanduname -m, with support forlinux/amd64,darwin/arm64, anddarwin/amd64.AgentctlResolver, bundle validation, desktop runtime validation, release scripts, and release packaging require platform-specific remote helpers.CGO_ENABLED=0.Validation
make fmtnode apps/web/scripts/generate-release-notes.mjsnode apps/web/scripts/generate-changelog.mjsmake typecheckmake testmake lintmake -C apps/backend build-agentctl-remotego test ./internal/agent/runtime/lifecycle ./internal/ssh ./internal/launcher ./internal/system/metricsbash scripts/release-desktop.test.shcargo testinapps/desktop/src-tauriKANDEV_E2E_CONTAINERS=1 pnpm e2e --project=containers tests/ssh/test-endpoint.spec.ts -g "returns step ordering"KANDEV_E2E_CONTAINERS=1 pnpm e2e --project=containers tests/ssh/test-result.spec.ts -g "uname output surfaces"pnpm e2e --project=chromium tests/settings/ssh-profile-connection-link.spec.tspnpm e2e --project=mobile-chrome tests/settings/mobile-ssh-profile-connection-link.spec.tsgit diff --checkPossible Improvements
Medium risk: macOS helper build and resolver paths are covered locally, but a live macOS SSH host should still be exercised manually or in future CI.
Checklist
apps/web/), I have added or updated Playwright e2e tests inapps/web/e2e/and verified them withmake test-e2e.