Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "sshx",
"version": "0.19.0",
"version": "0.20.0",
"description": "Agent-oriented remote host execution over SSH — JSON results, dry-run plans, safety rails, audit trail, MCP server, and an installable Agent skill.",
"author": {
"name": "talkincode",
Expand Down
42 changes: 35 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
go: ["1.26.8"]
go: ["1.26.10"]

steps:
- name: Checkout code
Expand Down Expand Up @@ -63,7 +63,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.8"
go-version: "1.26.10"

- name: Cache Go modules
uses: actions/cache@v4
Expand Down Expand Up @@ -112,7 +112,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.8"
go-version: "1.26.10"

- name: Download dependencies
run: go mod download
Expand All @@ -138,11 +138,39 @@ jobs:
security list-keychains -d user -s "$keychain_path"
security default-keychain -d user -s "$keychain_path"

- name: Prepare disposable native sudo policies
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
for role in password nopasswd cached; do
sudo useradd -m -s /bin/sh "sshx-sudo-$role"
printf 'sshx-sudo-%s:operator-pass\n' "$role" | sudo chpasswd
done
sudo install -m 0440 /dev/stdin /etc/sudoers.d/sshx-e2e <<'SUDOERS'
Defaults:sshx-sudo-password timestamp_timeout=0
Defaults:sshx-sudo-cached timestamp_type=global,timestamp_timeout=5
sshx-sudo-password ALL=(ALL) PASSWD: ALL
sshx-sudo-cached ALL=(ALL) PASSWD: ALL
sshx-sudo-nopasswd ALL=(ALL) NOPASSWD: ALL
SUDOERS
sudo visudo -cf /etc/sudoers.d/sshx-e2e

- name: Run compiled-binary E2E tests
env:
SSHX_E2E_REAL_KEYRING: ${{ runner.os == 'macOS' && '1' || '0' }}
SSHX_E2E_NATIVE_SUDO: ${{ runner.os == 'Linux' && '1' || '0' }}
run: go test -v -count=1 ./tests/e2e

- name: Remove disposable native sudo policies
if: always() && runner.os == 'Linux'
shell: bash
run: |
sudo rm -f /etc/sudoers.d/sshx-e2e
for role in password nopasswd cached; do
sudo userdel -r "sshx-sudo-$role" || true
done

- name: Remove isolated macOS keychain
if: always() && runner.os == 'macOS'
shell: bash
Expand Down Expand Up @@ -197,7 +225,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.8"
go-version: "1.26.10"

- name: Verify native SQL prerequisites
# ubuntu-latest includes native clients. Missing prerequisites must fail
Expand Down Expand Up @@ -246,7 +274,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.8"
go-version: "1.26.10"

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v7
Expand All @@ -267,12 +295,12 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.8"
go-version: "1.26.10"

- name: Run Go vulnerability scanner
uses: golang/govulncheck-action@v1
with:
go-version-input: "1.26.8"
go-version-input: "1.26.10"
go-package: ./...
repo-checkout: false

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.8"
go-version: "1.26.10"

- name: Get version
id: get_version
Expand Down
38 changes: 24 additions & 14 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Operating guide for humans and AI coding agents working on **sshx**. It defines
what this project is, what it deliberately is *not*, how it is built, and how to
make changes that fit. Read this before making non-trivial changes.

Module: `github.com/talkincode/sshx` · Language: Go 1.26.8 · License: MIT
Module: `github.com/talkincode/sshx` · Language: Go 1.26.10 · License: MIT

---

Expand Down Expand Up @@ -140,6 +140,8 @@ internal/app/ → CLI surface (argument parsing, routing, sub-comman
sql.go → sshx sql: guarded SQL pipeline (classify → gate → explain → backup → execute)
apply.go → sshx apply: guarded single-file mutation (hash → backup → atomic write)
text.go → sshx text: bounded remote text/log dissection
ros.go → RouterOS commands/workflows; shared transport and file evidence
ros_lifecycle.go → ROS operation metadata and shared execution/file adapters
login.go → sshx login: human TTY session, optional sudo privileged shell
mcp.go → sshx mcp: stdio MCP server; tools self-exec sshx as one-shot children
internal/execution/ → versioned request/result model, selectors, executor
Expand All @@ -149,6 +151,7 @@ internal/runtimepath/ → ~/.sshx / SSHX_HOME runtime-root resolution
internal/skillinstall/ → conflict-safe, atomic Agent skill installation
internal/sqlsafe/ → fail-closed SQL classification, policy gates, transactional backup decisions, psql/sqlite3 assembly
internal/textsafe/ → lexical presets, exception blocks, redaction, bounded text scan
internal/ros/ → RouterOS parsing/encoding and diagnostics (raw bytes remain raw)
internal/sshclient/ → SSH/SFTP core
client.go → SSHClient: dial, auth, exec, SFTP, sudo-over-stdin
remote_state.go → restrictive atomic remote observation I/O
Expand Down Expand Up @@ -210,7 +213,7 @@ skills/ → canonical Agent skill plus its embedded asset packa

## 5. Tech Stack

- **Language:** Go (module directive pinned to **`go 1.26.8`** — see constraint below).
- **Language:** Go (module directive pinned to **`go 1.26.10`** — see constraint below).
- **SSH/crypto:** `golang.org/x/crypto/ssh`
- **SFTP:** `github.com/pkg/sftp`
- **Keyring:** `github.com/zalando/go-keyring`
Expand All @@ -219,10 +222,13 @@ skills/ → canonical Agent skill plus its embedded asset packa

> Security baseline reviewed for v0.14.0: x/crypto v0.56.0 fixes reachable
> SSH channel deadlocks GO-2026-6354 and GO-2026-6355 and requires Go 1.26.
> Go 1.26.8 is the patched build baseline; CI and releases use the same version.
> The v0.20 security review advances the patched baseline to Go 1.26.10:
> govulncheck found reachable standard-library issues in 1.26.8, including
> GO-2026-6809 (fixed in 1.26.10) and GO-2026-66xx (fixed in 1.26.9).
> CI and release artifacts use the same reviewed patch baseline.
>
> ⚠️ **Toolchain constraint:** CI's test/lint/security jobs run on **Go 1.26.8**.
> The `go` directive in `go.mod` must stay at `1.26.8` unless a deliberate
> ⚠️ **Toolchain constraint:** CI's test/lint/security jobs run on **Go 1.26.10**.
> The `go` directive in `go.mod` must stay at `1.26.10` unless a deliberate
> security or compatibility review changes the baseline. New dependencies must
> support that toolchain; do not let `go get` silently bump the directive.

Expand Down Expand Up @@ -260,7 +266,7 @@ Notes:

### CI (`.github/workflows/`)

- `ci.yml`: **Test** (ubuntu + macOS, Go 1.26.8, `-race -cover`),
- `ci.yml`: **Test** (ubuntu + macOS, Go 1.26.10, `-race -cover`),
**Test (windows-latest)** (`-short`, build + vet), **Lint** (golangci-lint),
**Security Scan** (`gosec` plus `govulncheck`), **Analyze** (CodeQL, Go),
**E2E** (ubuntu + macOS), and **SQL (native PostgreSQL and MySQL)** with
Expand All @@ -278,7 +284,7 @@ Notes:
Windows Credential Manager needs an isolated OS profile and is not proved
by the test-only secret backend. Native CI results are required; a Windows
cross-build on macOS/Linux is not execution evidence.
- `release.yml`: builds release artifacts with Go 1.26.8 and bundles the matching
- `release.yml`: builds release artifacts with Go 1.26.10 and bundles the matching
Agent skill in every archive.

All `ci.yml` checks must be green before merge.
Expand Down Expand Up @@ -306,12 +312,16 @@ tests.
or in an explicit encrypted local vault. Inline `--password-set=key:value`
and `SSH_PASSWORD` are supported but warned about. Never silently degrade
the backend, and never emit vault secrets on `--password-get`.
3. **Sudo password over stdin.** Never interpolate the password into the command
string. `sudoStdinCommand` rewrites a leading `sudo` to `sudo -S -p ''` and the
password is fed via `session.Stdin`. This avoids quote breakage and injection.
3. **Sudo conversation is separate from application stdin.** Never interpolate
credentials into argv/env or unconditionally prefix them to a script/SQL
stream. `sudoExchange` sends a credential only on a nonce-bound prompt and
payload only after child readiness; NOPASSWD/cache/root must consume no
credential. Test both branches with actual shells and disposable native sudo.
Human `login --sudo` leaves authentication to the TTY; it never prefills a secret.
4. **Sudo auto-fill only supports leading `sudo`.** `CommandUsesSudo` returns
true only when the remote command starts with `sudo`, matching the exact
form `sudoStdinCommand` can safely rewrite. Non-leading sudo inside shell
true only when the remote command starts with `sudo`. The readiness wrapper
preserves argv and outer shell operators; ambiguous sudo login/shell modes
fail closed. A restricted command-only sudoers grant may reject the wrapper. Non-leading sudo inside shell
wrappers or pipelines is left untouched and is never silently ignored: sshx
warns on stderr before connecting and again when the remote reports
`sudo: a password is required`, suggesting `sudo sh -c "<command>"`.
Expand Down Expand Up @@ -513,9 +523,9 @@ When working in this repo:
1. **Stay within the mission.** Re-read §3 before adding features. Default to a
smaller change. Never introduce a daemon, a connection pool, an HTTP/SSE
protocol server, tunneling, or a GUI.
2. **Hold the toolchain line.** Keep `go.mod` at `go 1.26.8`. If a dependency
2. **Hold the toolchain line.** Keep `go.mod` at `go 1.26.10`. If a dependency
forces a newer directive, pin an older compatible version instead of bumping
the directive (CI runs Go 1.26.8).
the directive (CI runs Go 1.26.10).
3. **Verify before declaring done.** Run `make check` (and `golangci-lint run`)
locally; reproduce the original symptom and confirm it is gone. For PR work,
watch CI to green (`gh pr checks <n> --watch`).
Expand Down
52 changes: 51 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.20.0] - 2026-10-27

### Security

- Update the reviewed Go build baseline to 1.26.10 across the module, CI and
releases, addressing newly reported reachable standard-library issues in
1.26.8 (including GO-2026-6809).
- Separate noninteractive sudo authentication from script, SQL and other
application stdin using a nonce-bound prompt/readiness handshake. No
credential is sent when sudo does not request one. Authentication framing
is bounded and withheld from application output.
- Human `login --sudo` now uses native TTY authentication without reading or
prefilling the sudo keyring credential.
- Redact named token suffixes, authorization headers and escaped continuations
in audit records; label the policy explicitly as best-effort.
- RouterOS file workflows reuse guarded SFTP publication and downloads instead
of direct truncate/copy, preserving symlink protection and close errors.

### Fixed

- RouterOS raw commands preserve original quoting and option order. Recognized
device diagnostics fail even with SSH exit status zero; execution/audit
evidence, SSH credential references, jump hosts and deadlines stay aligned.
- RouterOS backup/export default filenames are unique per invocation. Script
updates do not retry a mutation after an uncertain transport result.
- Host diagnostics retain trust configuration and cancellation/deadline limits.
- Prefer already-known host-key algorithms and distinguish an unregistered
algorithm from a changed key of the same type, without relaxing verification.
- Restore bounded SFTP read/write pipelining while keeping hashes, publication
acknowledgement, cleanup and partial-effect evidence.

### Added

- `apply --sudo --stage-dir=PATH` (MCP `stage_dir`) selects a private SFTP
staging filesystem separately from backups and final atomic publication.
- Compiled-CLI regressions and disposable Linux native-sudo CI policies for
password, NOPASSWD, cached, root and rejected-authentication paths.

### Changed

- Plan schema remains v1; execution semantics advance to `sshx.execution.v2`.
Re-preview existing plans after upgrading.
- Sudo auto-fill uses a shell readiness wrapper. Restricted command-only
sudoers policies may reject it; ambiguous sudo login/shell options fail
closed. Use the human login path instead of bypassing policy.
- Raw RouterOS commands no longer add `without-paging` implicitly; include it
explicitly when needed. See `docs/execution-safety.md` for compatibility and
verification limits.

## [0.19.1] - 2026-09-28

### Fixed
Expand Down Expand Up @@ -845,7 +894,8 @@ diagnostics).
- CI/CD workflow and automated release process
- Tag creation script

[Unreleased]: https://github.com/talkincode/sshx/compare/v0.19.1...HEAD
[Unreleased]: https://github.com/talkincode/sshx/compare/v0.20.0...HEAD
[0.20.0]: https://github.com/talkincode/sshx/compare/v0.19.1...v0.20.0
[0.19.1]: https://github.com/talkincode/sshx/compare/v0.19.0...v0.19.1
[0.19.0]: https://github.com/talkincode/sshx/compare/v0.18.0...v0.19.0
[0.18.0]: https://github.com/talkincode/sshx/compare/v0.17.0...v0.18.0
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ It remains a single binary with one-shot invocations and no resident component o
stderr scan progress so a wide window is never mistaken for a hang.
16. Agent skill (`skills/sshx/SKILL.md`): `npx skills add talkincode/sshx` or `sshx skill install`.

## Execution safety upgrade (v0.20)

Sudo authentication and script/SQL input now use separate prompt/readiness
phases. `login --sudo` authenticates at the human TTY, without credential
prefill. RouterOS raw commands preserve their text, device errors fail clearly,
and file actions share guarded SFTP. Host tests honor deadlines; transfers use
bounded pipelining. `apply --sudo --stage-dir=/data/sshx-stage` supports a private
staging filesystem separate from backups. Re-preview plans after upgrading
(execution semantics v2). See [safety and compatibility notes](docs/execution-safety.md).

## Installation

### Agent skill
Expand Down
9 changes: 9 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ Agent 需要的不是另一个交互式 SSH shell,而是一份稳定、可组
15. `sshx text`:有界远端日志/文本解剖(异常块、preset、行窗),经 SFTP 或 sshx 持有的 journalctl;读取走并发流水线,扫描进度写 stderr,宽窗口不再像卡死。
16. Agent skill(`skills/sshx/SKILL.md`):`npx skills add talkincode/sshx` 或 `sshx skill install`。

## v0.20 执行安全升级

sudo 认证与脚本/SQL 输入改为独立的 prompt/ready 阶段,不再无条件拼接口令。
`login --sudo` 改由人类 TTY 完成认证,不预填 keyring 密码。RouterOS raw 保留原文、
设备错误明确失败,文件动作复用受控 SFTP;主机测试继承超时,传输使用有界流水线。
`apply --sudo --stage-dir=/data/sshx-stage` 可指定独立私有暂存目录,备份目录与最终
原子发布语义不变。升级后必须重新预览计划(execution semantics v2)。
详情与兼容边界见 [执行安全说明](docs/execution-safety.md)。

## 安装

### 安装 Agent skill
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ We take security seriously. The following versions of SSHX are currently support

| Version | Supported |
| -------- | ------------------ |
| 0.20.x | :white_check_mark: |
| 0.19.x | :white_check_mark: |
| 0.18.x | :white_check_mark: |
| < 0.18.0 | :x: |
| < 0.19.0 | :x: |

Security updates are provided for the latest minor release and the previous
minor release (N-1). Older lines do not receive patches; please upgrade.
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [Audit Query and Export](audit.md)
- [Contract Freeze Policy](contract.md)
- [Plans, Outcomes, and Safe Retries](execution-contract.md)
- [Execution Safety Upgrade](execution-safety.md)
- [Usage Scenarios](usage-scenarios.md)
- [Security Guidelines](security-guidelines.md)
- [Troubleshooting](troubleshooting.md)
Expand Down
8 changes: 8 additions & 0 deletions docs/apply.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ sshx apply -h=prod-web --path=/etc/nginx/nginx.conf --from=./nginx.conf \
--expect-sha256=<current> --sudo --json
```

## Private staging on another filesystem

With `--sudo`, `--stage-dir=/absolute/private/path` overrides the SFTP staging
area under the remote home. It is included in the plan hash and retains the
private-directory/symlink checks. `--backup-dir` remains independent; the final
publication still uses the target filesystem's atomic primitive. MCP exposes
`stage_dir` with the same sudo requirement. See [execution safety](execution-safety.md).

## What Apply Does

1. Refuse anything that is not a clean absolute regular-file path.
Expand Down
Loading
Loading