Skip to content

feat(server): 新增 zeabur server exec(DES-802)#263

Merged
leechenghsiu merged 2 commits into
mainfrom
matthewlee/des-802-cli-新增-zeabur-server-exec:在-server-上直接執行命令(免手動-ssh-拼裝)
Jul 10, 2026

Hidden character warning

The head ref may contain hidden characters: "matthewlee/des-802-cli-\u65b0\u589e-zeabur-server-exec\uff1a\u5728-server-\u4e0a\u76f4\u63a5\u57f7\u884c\u547d\u4ee4\uff08\u514d\u624b\u52d5-ssh-\u62fc\u88dd\uff09"
Merged

feat(server): 新增 zeabur server exec(DES-802)#263
leechenghsiu merged 2 commits into
mainfrom
matthewlee/des-802-cli-新增-zeabur-server-exec:在-server-上直接執行命令(免手動-ssh-拼裝)

Conversation

@leechenghsiu

@leechenghsiu leechenghsiu commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

本機實測:
CleanShot 2026-07-10 at 16 33 41

背景

DES-802。在 server 上跑命令原本要 server ssh-info 拿憑證 → 自己拼 ssh2/sshpass。這會把密碼塞進多層轉義的 shell 腳本,含特殊字元的密碼(]/@/…)會被弄壞導致認證失敗,也依賴外部 ssh/sshpass client。

實測證據:一組含特殊字元的 server 密碼透過 agent 的 node ssh2 腳本連線 → All configured authentication methods failed(轉義崩壞)。

改動

新增 zeabur server exec [server-id] -- <command>,用 golang.org/x/crypto/ssh 在 process 內拿憑證、開連線、跑命令:

  • 密碼不印出、不經任何 shell → 特殊字元安全。
  • 串流 stdout/stderr(長輸出不 buffer)。
  • 傳回 remote 命令的 exit code
  • StrictHostKeyChecking=no 等價(dedicated server host key 會輪替)。
  • 非互動:stdin 不接,remote 直接 EOF(不 hang)。
  • 命令語意同 ssh host <command>-- 後的 args 以空白 join);compound 命令用單一引號包起來即可。

ssh-info 原語保留,讓想自己拿憑證接手的用戶不受影響。

驗證

  • go build ./... / go vet / gofmt 全過(golangci-lint 本機未裝,交 CI)。
  • dry-run:server exec --help 正常;exec <id>(無 --)→ 「missing command」;exec <id> --(空)→ 「no command given」;exec 出現在 server --help
  • 實際 SSH 執行需真實 server 憑證,未在本地對 prod 測試。

後續(不在本 PR)

  • zeabur-server-ssh skill 改成優先推薦 server exec(agent-skills repo)。

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a server exec command to run remote commands on servers.
    • Accepts a server ID via arguments or the --id option (with interactive selection when appropriate).
    • Streams remote stdout/stderr to the terminal in real time.
    • Propagates the remote command’s exit code to the CLI.
  • Chores

    • Updated Go module dependencies, including adding golang.org/x/crypto and bumping related x/* versions.

Running a command on a dedicated server previously meant `server ssh-info` +
a hand-rolled ssh2/sshpass invocation. That embeds the password into a
multi-layer-escaped shell script, so passwords with special characters (]/@/…)
get corrupted and auth fails; it also needs an external ssh/sshpass client.

`server exec [server-id] -- <command>` fetches the credentials and opens the
connection entirely in-process via golang.org/x/crypto/ssh:
- Password is never printed nor passed through a shell → special chars are safe.
- Streams stdout/stderr live (no buffering for long output).
- Propagates the remote command's exit code.
- StrictHostKeyChecking=no equivalent (dedicated servers rotate host keys).
- Non-interactive: stdin left unset so the remote sees EOF (no hang).

`ssh-info` stays as the primitive for users who want the raw credentials to
drive their own client / take over manually.

DES-802

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@leechenghsiu leechenghsiu self-assigned this Jul 10, 2026
@zeabur-review-agent

Copy link
Copy Markdown

Note

Currently processing new changes in this PR, please wait...

📦 Commits (1)

Reviewed via multi-agent panel

📂 Files selected for processing (4)

Details available in the full review

@linear-code

linear-code Bot commented Jul 10, 2026

Copy link
Copy Markdown

DES-802

@opencodezebra

opencodezebra Bot commented Jul 10, 2026

Copy link
Copy Markdown

LGTM ✅ — New server exec subcommand is structurally correct; SSH lifecycle, API usage, and arg parsing align with existing patterns.
Reviewed at 7463ec9 (round 1)

What This PR Does

Adds zeabur server exec [server-id] -- <command> which uses golang.org/x/crypto/ssh to open an in-process SSH connection, avoiding shell-escaping issues with special characters in passwords. Stdout/stderr are streamed and the remote exit code is propagated.

How It Works

  • New internal/cmd/server/exec/exec.go implements a Cobra command that fetches server details and password via existing API calls (GetServer, RevealServerPassword), dials SSH with password auth, and runs the joined command string.
  • internal/cmd/server/server.go registers the new subcommand alongside existing ones.
  • go.mod/go.sum add golang.org/x/crypto/ssh dependency.

Findings

ID Severity Finding Location
F1 🟡 cobra.MinimumNArgs(1) makes the custom "no command given after '--'" error unreachable when user types exec --id X -- (empty after --) — Cobra fires first with a generic message internal/cmd/server/exec/exec.go:46
F2 🟡 No guard for SSHPort == 0; if API returns a server with no SSH port, CLI dials :0. Pre-existing gap (matches existing ssh command) internal/cmd/server/exec/exec.go:120
F3 🟢 Explicit session.Close() / client.Close() before os.Exit correctly handles deferred-func bypass internal/cmd/server/exec/exec.go:140-144
F4 🟢 Nil-safe dereference of SSHUsername with fallback to "root" matches existing ssh command pattern internal/cmd/server/exec/exec.go:95-98
F5 🟢 Subcommand registration follows existing pattern cleanly internal/cmd/server/server.go:37
Finding Details

🟡 F1: Unreachable custom error for empty command after --

Args: cobra.MinimumNArgs(1) validates before RunE, so the custom error at line 61 never fires when the user passes -- with nothing after it. Fix: switch to cobra.ArbitraryArgs and let RunE handle all validation for better UX messaging.

🟡 F2: No SSHPort zero-value guard

If the API ever returns SSHPort == 0, the dial target becomes :0. This is a pre-existing pattern gap shared with the existing ssh command — non-blocking, note only.

What's Good (🟢)
  • Password never printed or passed through shell — special character safety achieved.
  • Streaming stdout/stderr with proper exit-code propagation.
  • Clean resource cleanup before os.Exit.
  • Consistent patterns with existing ssh subcommand (username fallback, registration).
Baseline Check
  • Main already has: server ssh-info for credential retrieval, existing SSH-based commands.
  • Net-new value: in-process SSH execution avoids shell-escaping bugs with special-character passwords.
  • CI checks were IN_PROGRESS at review time (build-test, lint, CodeQL).
Review Metadata
  • Reviewers: rev1 (approve)
  • Consensus: approve
  • Absent reviewers: none

🔴×0 🟡×2 🟢×3 · 💬 Comment @opencodezebra <question> for a follow-up · 🔁 Push new commits or comment @opencodezebra review <fix notes> to re-run the council

Auth: auth,
// Equivalent to `-o StrictHostKeyChecking=no`: dedicated servers are
// reached by IP with rotating host keys, so pinning would only wedge.
HostKeyCallback: ssh.InsecureIgnoreHostKey(),

@opencodezebra opencodezebra Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Council: approved - see the review comment.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a8a0400-947a-4357-b0fe-ed51508ac7f5

📥 Commits

Reviewing files that changed from the base of the PR and between 7463ec9 and 921e998.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (2)
  • go.mod
  • internal/cmd/server/exec/exec.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/cmd/server/exec/exec.go

📝 Walkthrough

Walkthrough

Adds a zeabur server exec Cobra subcommand supporting server IDs, command arguments after --, SSH username selection, managed-server password retrieval, and in-process SSH execution. Remote output is streamed to the CLI, and remote exit statuses are propagated. The command is registered under the server command, with golang.org/x/crypto added as a direct dependency.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: adding the zeabur server exec command.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9487b6eb-9ffe-4392-83ec-07c140c2eda8

📥 Commits

Reviewing files that changed from the base of the PR and between bf02b75 and 7463ec9.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • go.mod
  • internal/cmd/server/exec/exec.go
  • internal/cmd/server/server.go

Comment thread go.mod Outdated
Comment thread internal/cmd/server/exec/exec.go Outdated
Comment thread internal/cmd/server/exec/exec.go
…assword errors

Review follow-up on #263:
- Bump golang.org/x/crypto to v0.52.0 (fixes GO-2026-5013 in ssh.Dial).
- Prompt for a server when no id is given in interactive mode (mirrors
  `server ssh`); the remote command stays non-interactive.
- Return RevealServerPassword errors instead of swallowing them as "no password
  available", so an API/authz failure surfaces its real cause.

HostKeyCallback stays ssh.InsecureIgnoreHostKey() intentionally: it's the
StrictHostKeyChecking=no equivalent already used by `server ssh`, since dedicated
servers are reached by IP with rotating host keys and can't be pinned.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@leechenghsiu

Copy link
Copy Markdown
Contributor Author

Addressed in 921e998:

  • crypto vuln (go.mod) — bumped golang.org/x/crypto to v0.52.0 (fixes GO-2026-5013 in ssh.Dial).
  • interactive server selection (exec.go:84) — when no id is given in interactive mode, exec now prompts to select a server (mirrors server ssh). --id / positional stay non-interactive; the remote command is always non-interactive.
  • password-reveal error (exec.go:104) — now returns the wrapped RevealServerPassword error instead of swallowing it as "no password available", so an API/authz failure surfaces its real cause.

On the CodeQL InsecureIgnoreHostKey (exec.go:117) — this is intentional and consistent with the existing server ssh command, which already uses -o StrictHostKeyChecking=no. Dedicated servers are reached by IP with rotating host keys, so there's no stable key to pin. Happy to dismiss the scanning alert with this justification (or revisit if there's a host-key distribution mechanism I'm missing).

@opencodezebra

opencodezebra Bot commented Jul 10, 2026

Copy link
Copy Markdown

LGTM ✅ — Round-2 fixes (crypto bump, interactive server selection, error propagation) are correct; prior non-blocking findings remain but are not blockers.
Reviewed at 921e998 (round 2)

What This PR Does

Adds zeabur server exec [server-id] -- <command> which fetches SSH credentials via the existing API and connects entirely in-process using golang.org/x/crypto/ssh. Passwords never touch a shell, so special characters are safe. Stdout/stderr are streamed and the remote exit code is propagated.

How It Works

  • internal/cmd/server/exec/exec.go: new Cobra command that resolves the server (positional arg, --id flag, or interactive prompt), fetches password via RevealServerPassword, dials SSH with password auth, and streams session.Run(cmd).
  • internal/cmd/server/server.go: registers the new subcommand.
  • go.mod/go.sum: adds golang.org/x/crypto v0.52.0 (fixes GO-2026-5013) and bumps transitive x/ deps.

Findings

Resolved

Resolved Severity Finding Fixed in
No round-1 findings were claimed as fixed by these IDs

Note: F1 and F2 from round 1 were not addressed by the author's fix-note and remain outstanding (non-blocking).

Outstanding

ID Severity Finding Location
F1 🟡 cobra.MinimumNArgs(1) makes the custom "no command given after '--'" error unreachable when user types exec --id X -- (empty after --) — Cobra fires first with a generic message internal/cmd/server/exec/exec.go:46
F2 🟡 No guard for SSHPort == 0; if API returns a server with no SSH port, CLI dials :0. Pre-existing gap (matches existing ssh command) internal/cmd/server/exec/exec.go:139

New

ID Severity Finding Location
F6 🟢 Interactive server selection mirrors server ssh pattern correctly — non-interactive mode bails early with clear error internal/cmd/server/exec/exec.go:84-114
F7 🟢 RevealServerPassword error now properly wrapped and surfaced, fixing silent auth failures internal/cmd/server/exec/exec.go:130
F8 🟢 golang.org/x/crypto bumped to v0.52.0, resolving GO-2026-5013 vulnerability in ssh.Dial go.mod:23
Finding Details

🟡 F1: Unreachable custom error for empty command after --

Args: cobra.MinimumNArgs(1) validates before RunE, so the custom error at line 60 never fires when the user passes -- with nothing after it. Fix: switch to cobra.ArbitraryArgs and let RunE handle all validation. Non-blocking — Cobra's default message is acceptable UX.

🟡 F2: No SSHPort zero-value guard

If the API ever returns SSHPort == 0, the dial target becomes <ip>:0. This is a pre-existing pattern gap shared with the existing ssh command. Non-blocking.

What's Good (🟢)
  • Password never printed or passed through shell — special character safety achieved.
  • Streaming stdout/stderr with proper exit-code propagation.
  • Clean resource cleanup (defer client.Close() / session.Close()) with explicit close before os.Exit.
  • golang.org/x/crypto bumped to v0.52.0 proactively fixing a known vulnerability.
  • Interactive server selection mirrors existing server ssh pattern for consistent UX.
  • Wrapped error on RevealServerPassword failure surfaces real API/authz cause.
Baseline Check
  • Main already has: server ssh-info for credential retrieval, server ssh for interactive SSH.
  • Net-new value: in-process non-interactive SSH execution avoids shell-escaping bugs with special-character passwords.
  • Round 1 verdict: LGTM ✅ at 7463ec9 with 2 non-blocking findings (F1, F2).
  • Round 2 delta: crypto bump, interactive server selection, improved error handling.
  • CI: build-test ✅ pass; lint, CodeQL still pending at review time.
Review Metadata
  • Reviewers: rev1 (approve, round 2)
  • Consensus: approve
  • Absent reviewers: none
  • Prior round: Round 1 approved at 7463ec9

🔴×0 🟡×2 🟢×3 · 💬 Comment @opencodezebra <question> for a follow-up · 🔁 Push new commits or comment @opencodezebra review <fix notes> to re-run the council

@opencodezebra opencodezebra Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Council: approved - see the review comment.

@canyugs canyugs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確認一下 CodeQL CI 的問題, 除此之外 LGTM

@leechenghsiu

Copy link
Copy Markdown
Contributor Author

@canyugs 關於 CodeQL 的 InsecureIgnoreHostKey(code-scanning #8)—— 確認過了,這是**刻意且無法「正確修」**的,說明如下:

  1. 與現有 server ssh 姿態一致internal/cmd/server/ssh/ssh.go 早就用 -o StrictHostKeyChecking=no(同樣不驗 host key)。差別只是它 shell out 到系統 ssh binary,所以 CodeQL 偵測不到;我改用 in-process crypto/ssh 才被這條 query 掃到。本 PR 沒有引入新的安全姿態
  2. 沒有 host key 可以 pinServerDetail / API(含 RevealServerPassword沒有回傳任何 host key / fingerprint 欄位,所以無法 pin key。
  3. dedicated server host key 會輪替:就算做 TOFU / known_hosts,換機/重裝後 host key 一變就會壞掉,反而卡住正常使用。
  4. 連線的信任其實錨定在**提供憑證的 Zeabur API(HTTPS)**上。

建議:把 code-scanning #8 以「won't fix / accepted risk」dismiss(帶上以上理由)。這是 maintainer 的決定,所以我沒有擅自 dismiss —— 你或有權限的人確認後點一下即可,或告訴我要不要我用 API 幫忙 dismiss。若之後 API 能提供 host key,我很樂意回來改成 pin。

@leechenghsiu leechenghsiu requested a review from canyugs July 10, 2026 13:08
@leechenghsiu leechenghsiu merged commit 8adcbc1 into main Jul 10, 2026
4 of 5 checks passed
@leechenghsiu leechenghsiu deleted the matthewlee/des-802-cli-新增-zeabur-server-exec:在-server-上直接執行命令(免手動-ssh-拼裝) branch July 10, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants