vssh 4.2.1__tar.gz → 4.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vssh-4.3.1/CHANGELOG.md +858 -0
- vssh-4.3.1/CONTRIBUTING.md +44 -0
- vssh-4.3.1/HELP.md +127 -0
- {vssh-4.2.1 → vssh-4.3.1}/Makefile +2 -2
- vssh-4.3.1/PKG-INFO +322 -0
- vssh-4.3.1/README.ko.md +220 -0
- vssh-4.3.1/README.md +301 -0
- vssh-4.3.1/SECURITY.md +67 -0
- {vssh-4.2.1 → vssh-4.3.1}/cmd/vssh/doctor.go +17 -0
- {vssh-4.2.1 → vssh-4.3.1}/cmd/vssh/main.go +552 -22
- {vssh-4.2.1 → vssh-4.3.1}/cmd/vssh/mcp.go +82 -26
- vssh-4.3.1/cmd/vssh/setup.go +108 -0
- vssh-4.3.1/docs/AGENT_READINESS_ROADMAP.md +139 -0
- vssh-4.3.1/docs/DESIGN_RATIONALE_AND_DIRECTION.md +166 -0
- vssh-4.3.1/docs/GAP_ANALYSIS.md +55 -0
- vssh-4.3.1/docs/PUBLISH_HANDOFF.md +76 -0
- vssh-4.3.1/docs/PYTHON_SDK.md +93 -0
- vssh-4.3.1/docs/ROADMAP.md +66 -0
- vssh-4.3.1/docs/SECURITY_AUDIT_PACKAGE.md +97 -0
- vssh-4.3.1/docs/SECURITY_TRANSPORT_MIGRATION.md +229 -0
- vssh-4.3.1/docs/SSH_REPLACEMENT_ROADMAP.md +73 -0
- {vssh-4.2.1 → vssh-4.3.1}/go.mod +1 -0
- {vssh-4.2.1 → vssh-4.3.1}/go.sum +2 -0
- vssh-4.3.1/install.sh +85 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/server/auth.go +0 -35
- {vssh-4.2.1 → vssh-4.3.1}/internal/server/client.go +5 -21
- vssh-4.3.1/internal/server/fmux.go +308 -0
- vssh-4.3.1/internal/server/forward.go +580 -0
- vssh-4.3.1/internal/server/identity.go +262 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/server/jobs.go +2 -1
- vssh-4.3.1/internal/server/policy.go +456 -0
- vssh-4.3.1/internal/server/policy_e2e_test.go +223 -0
- vssh-4.3.1/internal/server/policy_templates_test.go +44 -0
- vssh-4.3.1/internal/server/policy_test.go +124 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/server/relay.go +3 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/server/rpc.go +15 -3
- vssh-4.3.1/internal/server/server.go +468 -0
- vssh-4.3.1/internal/server/server_auth_test.go +37 -0
- vssh-4.3.1/internal/server/sync.go +113 -0
- vssh-4.3.1/internal/server/tlsident.go +180 -0
- vssh-4.3.1/internal/server/transfer.go +1050 -0
- vssh-4.3.1/internal/server/transfer_advanced.go +460 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/ssh/ssh.go +56 -6
- vssh-4.3.1/policies/README.md +25 -0
- vssh-4.3.1/policies/backup.json +12 -0
- vssh-4.3.1/policies/ci.json +13 -0
- vssh-4.3.1/policies/deploy.json +13 -0
- vssh-4.3.1/policies/readonly.json +9 -0
- {vssh-4.2.1 → vssh-4.3.1}/pyproject.toml +10 -4
- vssh-4.3.1/scripts/audit_transport_scan.sh +62 -0
- vssh-4.3.1/scripts/authorize_fleet.sh +43 -0
- vssh-4.3.1/scripts/build_node_registry.sh +35 -0
- vssh-4.3.1/scripts/cross_authorize_fleet.sh +84 -0
- vssh-4.3.1/scripts/deploy_fleet.sh +92 -0
- vssh-4.3.1/scripts/enable_require_tls.sh +50 -0
- vssh-4.3.1/scripts/enroll.sh +149 -0
- vssh-4.3.1/scripts/pin_fleet_keys.sh +55 -0
- vssh-4.3.1/src/vssh/__init__.py +21 -0
- vssh-4.3.1/src/vssh/__main__.py +6 -0
- vssh-4.3.1/src/vssh/_bootstrap.py +137 -0
- vssh-4.3.1/src/vssh/_cli.py +19 -0
- vssh-4.3.1/src/vssh/_version.py +12 -0
- vssh-4.3.1/test/agent_suite.sh +113 -0
- vssh-4.2.1/CHANGELOG.md +0 -74
- vssh-4.2.1/CONTRIBUTING.md +0 -6
- vssh-4.2.1/HELP.md +0 -115
- vssh-4.2.1/PKG-INFO +0 -353
- vssh-4.2.1/README.ko.md +0 -326
- vssh-4.2.1/README.md +0 -332
- vssh-4.2.1/SECURITY.md +0 -16
- vssh-4.2.1/install.sh +0 -51
- vssh-4.2.1/internal/server/auth_test.go +0 -57
- vssh-4.2.1/internal/server/server.go +0 -239
- vssh-4.2.1/internal/server/sync.go +0 -225
- vssh-4.2.1/internal/server/transfer.go +0 -581
- vssh-4.2.1/internal/server/transfer_advanced.go +0 -949
- vssh-4.2.1/src/vssh/__init__.py +0 -11
- {vssh-4.2.1 → vssh-4.3.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/.github/workflows/ci.yml +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/.github/workflows/release.yml +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/.gitignore +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/cmd/vssh/fanout_test.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/cmd/vssh/mcp_test.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/docs/AI_NATIVE_CAPABILITIES.ko.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/docs/CODEX_ORCHESTRATION.ko.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/docs/CODEX_ORCHESTRATION.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/docs/DIRECTION.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/docs/DISTRIBUTION.ko.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/docs/NETWORK_TRAVERSAL_AUDIT.ko.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/docs/PERFORMANCE.ko.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/docs/PUBLISHING_AUDIT.ko.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/docs/PUBLISHING_AUDIT.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/docs/PYTHON_SDK.ko.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/docs/WHY_VSSH.ko.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/docs/WHY_VSSH.md +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/adapter/vssh.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/agent/agent.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/agent/api.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/config/config.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/event/event.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/server/artifact_test.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/server/exec_test.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/server/jobs_test.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/server/pty_darwin.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/server/pty_linux.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/server/transfer_test.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/internal/ssh/ssh_test.go +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/src/vssh/client.py +0 -0
- {vssh-4.2.1 → vssh-4.3.1}/tests/test_python_sdk.py +0 -0
vssh-4.3.1/CHANGELOG.md
ADDED
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
- Docs (README/HELP/SECURITY + ko) updated to the key-only model (no shared
|
|
6
|
+
secret in quickstart, auth via `~/.vssh/authorized_keys`). pip `vssh` 4.3.1
|
|
7
|
+
fetches the 0.7.38 binary.
|
|
8
|
+
|
|
9
|
+
### Security/cleanup (2026-06-14) — P4 finish: secret-free daemon, dead HMAC code removed (v0.7.38)
|
|
10
|
+
|
|
11
|
+
- `vssh server` no longer requires (or reads) a shared secret to start — auth is
|
|
12
|
+
purely per-node Ed25519 keys. Removed the secret-required startup gate and the
|
|
13
|
+
`VSSH_INSECURE_ALLOW_EMPTY_SECRET` escape hatch from `cmd/vssh`.
|
|
14
|
+
- Deleted the now-dead HMAC code: `server.ValidateAuthToken`,
|
|
15
|
+
`server.GenerateAuthToken`, the legacy HMAC-authenticated client transfer
|
|
16
|
+
functions (`SendFileCompressed/SendFileResume/ParallelDownload/PipeUp/PipeDown/
|
|
17
|
+
MultiPut/SyncFile` + helpers), and the HMAC token unit tests. The modern
|
|
18
|
+
`dialAuth`/`Handle*` (VAUTH1) paths are unaffected.
|
|
19
|
+
- `scripts/enroll.sh` is now key-only: it installs the daemon unit WITHOUT
|
|
20
|
+
`VSSH_SECRET` and verifies via VAUTH1 (no shared secret anywhere in onboarding).
|
|
21
|
+
- `go vet`/`go test` green (incl. `TestServerRejectsLegacyHMACAuth`).
|
|
22
|
+
|
|
23
|
+
### Security (2026-06-14) — P4: key-only auth (legacy shared-secret HMAC removed)
|
|
24
|
+
|
|
25
|
+
- The daemon now accepts **only per-node Ed25519 VAUTH1**. The legacy shared-secret
|
|
26
|
+
(HMAC) authentication path was removed from `internal/server/server.go`: any
|
|
27
|
+
non-VAUTH1 auth line — including a *valid* HMAC token for the configured secret —
|
|
28
|
+
is rejected (`AUTH_FAILED`). This removes the entire class of risk where one
|
|
29
|
+
fleet-wide shared secret, if leaked, grants access, and eliminates the secret
|
|
30
|
+
that could be hardcoded or re-leaked.
|
|
31
|
+
- `dialAuth` (the canonical client path) has been VAUTH1-only since 0.7.25; the
|
|
32
|
+
remaining `GenerateAuthToken` callers (`transfer_advanced.go` / `sync.go`) are
|
|
33
|
+
legacy client functions not wired to any CLI command. The HMAC helpers remain
|
|
34
|
+
for now but are unreachable by the daemon.
|
|
35
|
+
- New regression test `TestServerRejectsLegacyHMACAuth`; `go vet`/`go test` green.
|
|
36
|
+
- Operational: the live fleet already enforces VAUTH at runtime
|
|
37
|
+
(`VSSH_REQUIRE_VAUTH=1`, incl. the m1 controller as of 2026-06-14); this makes
|
|
38
|
+
key-only the built-in default. Rollout (build → d1 canary → deploy_fleet → drop
|
|
39
|
+
`VSSH_SECRET` from units + retire legacy HMAC helpers) is deferred until all
|
|
40
|
+
nodes are reachable. **Not yet deployed.**
|
|
41
|
+
|
|
42
|
+
### Distribution (2026-06-14) — P1 one-line install pipeline (v0.7.36)
|
|
43
|
+
|
|
44
|
+
- **Makefile version single-sourced** from `cmd/vssh/main.go` (was hardcoded
|
|
45
|
+
`0.7.5`, 6 minors behind the binary). `make release` now always labels the
|
|
46
|
+
4-arch binaries with the real version; `checksums` globs binaries only (was
|
|
47
|
+
sweeping stale Python wheels into `checksums.txt`).
|
|
48
|
+
- **`install.sh` hardened**: SHA-256 verification against the release
|
|
49
|
+
`checksums.txt` (fail-closed), `VSSH_VERSION=` pinning (default latest),
|
|
50
|
+
`INSTALL_DIR=` override, atomic temp-dir install, clearer PATH/next-step output.
|
|
51
|
+
- **`pip install vssh` unified (CLI + SDK)**: the `vssh` wheel ships a
|
|
52
|
+
console-script that downloads the matching Go release binary to `~/.vssh/bin`
|
|
53
|
+
on first use (stdlib-only, checksum-verified, `VSSH_BIN`/`VSSH_VERSION`/`VSSH_HOME`
|
|
54
|
+
overrides, atomic cache write) AND keeps `from vssh import VSSH` importable.
|
|
55
|
+
Package version (`src/vssh/_version.py:__version__`) stays on PyPI's existing
|
|
56
|
+
`vssh` 4.x line (4.3.0) so `pip install vssh` resolves to it (highest wins); the
|
|
57
|
+
launcher downloads the decoupled `BINARY_VERSION` (0.7.36) GitHub release asset.
|
|
58
|
+
`python -m vssh` works.
|
|
59
|
+
- Verified on m1: `go vet`/`go test` green, Python SDK 8/8, `make release`
|
|
60
|
+
4-arch + checksums, wheel/sdist build, fresh-venv install (SDK import +
|
|
61
|
+
console-script exec), and an adversarial download test (happy path +
|
|
62
|
+
tampered-checksum rejection fail-closed + `VSSH_BIN` override).
|
|
63
|
+
- PUBLISHED 2026-06-14: pushed `main` + tag `v0.7.36`; GitHub release (CI-built
|
|
64
|
+
4 binaries + `checksums.txt`, marked Latest); PyPI `vssh` **4.3.0**. Verified
|
|
65
|
+
live on m1: one-line `install.sh` (checksum-verified -> `vssh 0.7.36`) and
|
|
66
|
+
`pip install vssh` -> import SDK + `vssh --version` downloads 0.7.36 binary.
|
|
67
|
+
|
|
68
|
+
### Tooling (2026-06-13) — P1a one-command node onboarding
|
|
69
|
+
|
|
70
|
+
- New `scripts/enroll.sh <node>`: idempotent end-to-end enrollment from the
|
|
71
|
+
controller — detect OS/arch, install/upgrade the binary (atomic), install +
|
|
72
|
+
enable the daemon service with strict VAUTH if missing (systemd `vsshd` /
|
|
73
|
+
launchd `ai.meshclaw.vsshd`, `VSSH_REQUIRE_VAUTH=1`), cross-authorize
|
|
74
|
+
node<->controller keys, TOFU-pin the node key into `~/.vssh/known_hosts`, then
|
|
75
|
+
verify (TLS+VAUTH1 `AUTH_OK` + `agent_suite` ALL GREEN). `DRY_RUN=1` prints the
|
|
76
|
+
plan without changing anything. Collapses install + cross_authorize + pin +
|
|
77
|
+
strict-mode + verify into one command (DESIGN_RATIONALE §4 item 4).
|
|
78
|
+
- Validated idempotently against d1 (converges, ENROLLED OK). Fresh-node
|
|
79
|
+
service-creation paths await the return of an un-enrolled node (n1/s1/s2) for
|
|
80
|
+
live validation; a native `vssh enroll` subcommand wrapper is a follow-up.
|
|
81
|
+
|
|
82
|
+
### Tooling (2026-06-13) — §5.3 transport stabilization gate scanner
|
|
83
|
+
|
|
84
|
+
- New `scripts/audit_transport_scan.sh`: scans every fleet node's vssh audit log
|
|
85
|
+
and counts authenticated connections by `transport` (tls vs plain), reading the
|
|
86
|
+
right path per OS (linux root daemon → `sudo /var/log/vssh/audit.log`; darwin
|
|
87
|
+
user daemon → `~/.vssh/audit.log`). Reports plaintext-auth sources by
|
|
88
|
+
`key_name`, supports a `SINCE=<RFC3339>` window, and exits non-zero if any
|
|
89
|
+
plaintext-auth record is present — the measurable go/no-go for flipping
|
|
90
|
+
`VSSH_REQUIRE_TLS=1` fleet-wide in 0.7.27. `NODES=` overrides the host list.
|
|
91
|
+
- Clarification: exec **is** audited with the transport marker (EXEJ →
|
|
92
|
+
`auditLog`, transfer.go:290/525). An earlier note claiming exec was unaudited
|
|
93
|
+
was a permissions misread — the linux audit.log is root-owned 0700, so the
|
|
94
|
+
command's drop-priv user (e.g. uid 1000) gets EACCES; it must be read as root.
|
|
95
|
+
- First scan: the only plaintext-auth source fleet-wide is `m1-controller`
|
|
96
|
+
(the still-0.7.25 in-memory `vssh mcp` process); a Claude Desktop restart moves
|
|
97
|
+
it to 0.7.26 TLS-first.
|
|
98
|
+
|
|
99
|
+
### Docs (2026-06-13) — P0 transport security migration design
|
|
100
|
+
|
|
101
|
+
- New `docs/SECURITY_TRANSPORT_MIGRATION.md` (design only, no code):
|
|
102
|
+
threat model of the plaintext wire (no post-auth encryption/MAC, no server
|
|
103
|
+
authentication/channel binding, client-side legacy-HMAC downgrade in
|
|
104
|
+
dialAuth, no signature domain separation) and the decided fix — TLS 1.3
|
|
105
|
+
(Go stdlib) with Ed25519 self-signed certs from the existing node identity,
|
|
106
|
+
raw-pubkey pinning against authorized_keys / new known_hosts, same-port
|
|
107
|
+
first-byte sniff, `VSSH_REQUIRE_TLS` kill-switch, staged rollout
|
|
108
|
+
0.7.25→0.7.27 with external review after the client flips to TLS.
|
|
109
|
+
- Same doc §6: P1(b) unattended-automation whitelist design —
|
|
110
|
+
`policy=<name>` key tag + JSON policy files (anchored exec allow/deny,
|
|
111
|
+
path scopes, forward targets, rate bounds, `danger_preapproved`),
|
|
112
|
+
fail-closed, typed `policy_denied`, MCP auto-approve profiles generated
|
|
113
|
+
from the same policy file. Templates: readonly/backup/ci/deploy.
|
|
114
|
+
- Recorded decisions: no interactive TTY over MCP; `vssh_tunnel` MCP tool is
|
|
115
|
+
P2 after the TLS transport lands.
|
|
116
|
+
|
|
117
|
+
### Ops (2026-06-13) — legacy tunnel path retirement plan + macmini stray resolved
|
|
118
|
+
|
|
119
|
+
- **macmini stray binary resolved.** /usr/local/bin/vssh (ancient 0.1.0) could
|
|
120
|
+
not be moved (no passwordless sudo, directory not writable by odt) but the
|
|
121
|
+
file itself is odt-owned — overwritten in place with the current 0.7.24
|
|
122
|
+
binary and codesigned. No more version confusion; no sudo required.
|
|
123
|
+
- **Decision: legacy FWD/RFWD/RDATA removal in two releases, gated on a
|
|
124
|
+
stabilization window.** Trigger criteria (all must hold):
|
|
125
|
+
(a) 7 consecutive daily conformance runs green on 0.7.24,
|
|
126
|
+
(b) zero non-fmux FWD/RFWD audit records fleet-wide in that window
|
|
127
|
+
(legacy records lack the "(fmux)" tag — `grep 'FWD' audit.log | grep -v
|
|
128
|
+
fmux` per node),
|
|
129
|
+
(c) n1/s1/s2 redeployed to 0.7.24 + strict + caps on return (or formally
|
|
130
|
+
retired from the roster).
|
|
131
|
+
Then: **step 1 (0.7.25)** remove the client-side legacy fallbacks
|
|
132
|
+
(fwdOpenTunnel legacy branch, forwardRemoteLegacy); **step 2 (0.7.26)**
|
|
133
|
+
remove the daemon's top-level FWD/RFWD/RDATA verbs (typed
|
|
134
|
+
unsupported_method). FMUX stream headers (FWD/RFWD/UDP/RCONN inside a
|
|
135
|
+
session) are a separate namespace and stay. Rollback = revert the step's
|
|
136
|
+
commit.
|
|
137
|
+
|
|
138
|
+
## [v0.7.35] - 2026-06-13
|
|
139
|
+
|
|
140
|
+
### Resolver: exclude local/self IPs from remote peer candidates
|
|
141
|
+
|
|
142
|
+
Root fix for the misroute class: `CandidateHosts`/`ResolveHost` now drop any
|
|
143
|
+
candidate equal to one of THIS machine's own non-loopback interface IPs, so a
|
|
144
|
+
remote peer can never resolve to the controller's own daemon when its real
|
|
145
|
+
address is down. If every candidate is local, the target IS this machine ->
|
|
146
|
+
loopback (127.0.0.1) fallback (self-targeting preserved). Verified: d1 runs,
|
|
147
|
+
m1/self via loopback, d2 (down) now fails cleanly (i/o timeout to its real IP)
|
|
148
|
+
instead of silently reaching the controller. Host-identity verification (0.7.33)
|
|
149
|
+
remains the post-connect backstop. go vet + go test green.
|
|
150
|
+
|
|
151
|
+
## [v0.7.34] - 2026-06-13
|
|
152
|
+
|
|
153
|
+
### MCP self-bootstrap: `vssh_setup` tool
|
|
154
|
+
|
|
155
|
+
Any model on any client (Claude/Cursor/Codex/AI Studio) configures vssh by calling
|
|
156
|
+
ONE tool. `vssh_setup` auto-detects peers, builds the trusted node-key registry
|
|
157
|
+
(`~/.vssh/node_keys`) via loopback handshakes (enabling host-identity
|
|
158
|
+
verification), runs the doctor, and reports the one remaining manual gate
|
|
159
|
+
(REQUIRE_TLS). Idempotent and merge-preserving (keeps a node's prior key if a
|
|
160
|
+
refresh can't reach it). `vssh_doctor` now warns on an empty registry and points
|
|
161
|
+
to `vssh_setup`. Verified end-to-end over the MCP protocol. (The binary itself
|
|
162
|
+
still installs via pip/github/install.sh — only that prerequisite is not
|
|
163
|
+
self-bootstrapping.)
|
|
164
|
+
|
|
165
|
+
## [v0.7.33] - 2026-06-13
|
|
166
|
+
|
|
167
|
+
### Security: host-identity verification DEFAULT-ON (anti-misroute, resolved)
|
|
168
|
+
|
|
169
|
+
Root-causes and resolves the v0.7.31 misroute risk.
|
|
170
|
+
- Why daemon `server_key` != on-disk `vssh_id`: `vsshConfigDir()` falls back to
|
|
171
|
+
`/etc/vssh` when `$HOME` is unset (systemd daemon), so the daemon's identity is
|
|
172
|
+
`/etc/vssh/vssh_id` (the TLS key) while `sudo vssh pubkey` reads `/root/.vssh`
|
|
173
|
+
(HOME=/root). authorized_keys (collected from the latter) was the wrong source.
|
|
174
|
+
- `scripts/build_node_registry.sh` captures each node's authoritative daemon TLS
|
|
175
|
+
key via a LOOPBACK handshake on the node (`handshake-test --tls 127.0.0.1` —
|
|
176
|
+
its own daemon, unmisroutable) into `~/.vssh/node_keys`.
|
|
177
|
+
- `server.NodeKey(name)` reads that registry; the client host-identity check
|
|
178
|
+
sources expected keys from it. Flipped to **default-ON** (opt-out:
|
|
179
|
+
`VSSH_NO_HOSTKEY_VERIFY=1`).
|
|
180
|
+
- Validated live: legit nodes run; a wrong registry key hard-fails ("refusing to
|
|
181
|
+
run on the wrong host"); unknown nodes (no entry) skip safely. go test green.
|
|
182
|
+
|
|
183
|
+
Operational: keep `~/.vssh/node_keys` fresh (run build_node_registry.sh after
|
|
184
|
+
enroll/key changes); the `d2` Tailscale device collision that triggers the
|
|
185
|
+
intermittent misroute should be cleaned up at the tailnet level.
|
|
186
|
+
|
|
187
|
+
## [v0.7.32] - 2026-06-13
|
|
188
|
+
|
|
189
|
+
### P1b complete: MCP danger_preapproved auto-approve (step 2)
|
|
190
|
+
|
|
191
|
+
- Daemon: new conn-aware `policy_check` RPC (read-only, no execution) returns the
|
|
192
|
+
policy decision (`none|allow|preapproved|deny`) for the authenticated key and a
|
|
193
|
+
command. Handled in `HandleRPCCommand` (where the connection identity is known),
|
|
194
|
+
not the connectionless `HandleRPC`.
|
|
195
|
+
- MCP: `toolExec`, before blocking a heuristically-"dangerous" command, asks the
|
|
196
|
+
target daemon via `policy_check`; if the key's policy allows/pre-approves it the
|
|
197
|
+
exec proceeds with no `allow_dangerous` flag — unattended automation works while
|
|
198
|
+
the daemon stays authoritative. Single source of truth: the same policy file.
|
|
199
|
+
- Test: `TestPolicyCheckRPC` (deny / preapproved / allow). go vet + go test green.
|
|
200
|
+
|
|
201
|
+
This completes the P1b whitelist (DESIGN_RATIONALE §1 roadmap + SECURITY_TRANSPORT
|
|
202
|
+
_MIGRATION §6): daemon-side exec_allow/deny + file_paths + fwd_targets + rate, and
|
|
203
|
+
MCP-side danger_preapproved — all behind per-key opt-in (`policy=` tag).
|
|
204
|
+
|
|
205
|
+
## [v0.7.31] - 2026-06-13
|
|
206
|
+
|
|
207
|
+
### Security: opt-in host-identity verification (anti-misroute)
|
|
208
|
+
|
|
209
|
+
Defends against the name->wrong-host misroute class. Observed live: a node name
|
|
210
|
+
(d2) transiently resolved to the controller (m1) via a stale/colliding Tailscale
|
|
211
|
+
address; because the TLS pin is keyed by IP, the wrong host's pin still matched
|
|
212
|
+
and the command ran on m1 instead of d2.
|
|
213
|
+
|
|
214
|
+
- The client records, per dial address, the daemon key it EXPECTS for the logical
|
|
215
|
+
target — the pin of the target's canonical config IP (`ConfigNodeIP` +
|
|
216
|
+
`KnownHostPub`), set in `resolveNativeHost`/`resolveReachableHost` and the MCP
|
|
217
|
+
exec path. `dialAuth` hard-fails after the TLS handshake when the reached
|
|
218
|
+
daemon key differs. Gated by `VSSH_VERIFY_HOST_IDENTITY=1` (default OFF — no
|
|
219
|
+
behavior change until enabled).
|
|
220
|
+
- Source is `known_hosts` (correct IP->TLS-key), deliberately NOT
|
|
221
|
+
`authorized_keys`: verified the latter stores `vssh_id` pubkeys that differ
|
|
222
|
+
from the TLS daemon cert key, so verifying against it would falsely reject
|
|
223
|
+
every node.
|
|
224
|
+
- Validated: opt-in ON does not break legit nodes (d1/c1/d2 live); a deterministic
|
|
225
|
+
unit/e2e test (`TestHostIdentityVerification`) confirms mismatch -> hard fail,
|
|
226
|
+
cleared -> success. go vet + go test ./... green; rolled out fleet-wide.
|
|
227
|
+
|
|
228
|
+
To enable fleet-wide: pin every node's canonical config IP (so each has an
|
|
229
|
+
expected key), then set `VSSH_VERIFY_HOST_IDENTITY=1`. vssh must run well over
|
|
230
|
+
both Wire and Tailscale — this hardens the Tailscale-resolution path.
|
|
231
|
+
|
|
232
|
+
## [v0.7.30] - 2026-06-13
|
|
233
|
+
|
|
234
|
+
### MCP cross-client robustness
|
|
235
|
+
|
|
236
|
+
- `cmdMcp` raises the stdin scanner buffer (64KB default -> 1MB initial / 16MB
|
|
237
|
+
max) so a strict MCP client (Cursor, Codex, or a custom JSON-RPC client) that
|
|
238
|
+
sends a >64KB `tools/call` request line (large file/script arguments) is not
|
|
239
|
+
silently dropped (which would end the stdio session). Mirrors `run-batch`.
|
|
240
|
+
- A cross-client compliance audit (Claude Desktop, Cursor, Codex CLI, custom
|
|
241
|
+
"AI Studio") found the MCP server otherwise spec-compliant: newline-delimited
|
|
242
|
+
JSON-RPC 2.0 over stdio, correct `initialize`/`tools/list`/`tools/call` shapes,
|
|
243
|
+
graceful notifications + unknown-method handling, clean stdout. No protocol
|
|
244
|
+
changes required. Ready-to-paste client configs: see `VSSH_MCP_CLIENTS.md`.
|
|
245
|
+
- Resynced the stale `/opt/homebrew/bin/vssh` CLI (was 0.7.26) to current.
|
|
246
|
+
|
|
247
|
+
## [v0.7.29] - 2026-06-13
|
|
248
|
+
|
|
249
|
+
### Security (P1b step 3): fwd_targets allow-list + per-key exec rate limit
|
|
250
|
+
|
|
251
|
+
Completes the daemon-side P1b enforcement surface (exec + file_paths +
|
|
252
|
+
fwd_targets + rate).
|
|
253
|
+
|
|
254
|
+
- FWD checks the requested target `host:port` against the policy `fwd_targets`
|
|
255
|
+
allow-list (`fwdTargetMatch`: host exact or CIDR membership for IPs; port exact
|
|
256
|
+
or `*`). A policied key with empty `fwd_targets` denies all forwards
|
|
257
|
+
(fail-closed). RFWD remains fail-closed for policied keys (no reverse-bind
|
|
258
|
+
axis in the schema). Typed `policy_denied` + audit (`fwd_ok`|`fwd_denied`).
|
|
259
|
+
- Per-key exec rate limit: `rate.exec_per_min` enforced via a 60s sliding window
|
|
260
|
+
keyed by the connection pubkey; over-limit execs get `policy_denied`
|
|
261
|
+
(`policy_rule: rate_exceeded`). 0 = unlimited.
|
|
262
|
+
|
|
263
|
+
Unit tests for `fwdTargetMatch` + `rateExceeded`; go vet + go test ./... green;
|
|
264
|
+
d1 canary agent_suite 10/10 (opt-in, no regression); rolled out fleet-wide.
|
|
265
|
+
Remaining P1b: MCP-side `danger_preapproved` auto-approve (step 2).
|
|
266
|
+
|
|
267
|
+
## [v0.7.28] - 2026-06-13
|
|
268
|
+
|
|
269
|
+
### Security (P1b): file_paths scope on PUT/GET + fail-closed unscoped verbs
|
|
270
|
+
|
|
271
|
+
Completes the daemon-side file scoping of the policy engine and closes the
|
|
272
|
+
verb-bypass so a policied key cannot dodge its scope through an unguarded verb.
|
|
273
|
+
|
|
274
|
+
- PUT/GET enforce the policy `file_paths` scope via `Policy.PathAllowed`
|
|
275
|
+
(symlinks/.. resolved on the longest existing ancestor before the glob check).
|
|
276
|
+
Out-of-scope or fail-closed paths return typed `policy_denied` + audit
|
|
277
|
+
(`policy_rule: file_path_denied|file_path_ok`).
|
|
278
|
+
- Every file/forward verb the engine does not yet scope (PUTZ, RESUME, MPUT,
|
|
279
|
+
GETM, GETC, PIPE_UP/DOWN, SYNC, FWD, RFWD, RDAT, FMUX, RELAY) now **fails
|
|
280
|
+
closed** for a policied key (`policyBlockUnscoped`) instead of running
|
|
281
|
+
unscoped. Net effect: a policied key may only exec (command-scoped) and
|
|
282
|
+
PUT/GET (path-scoped). Keys with no `policy=` tag are unaffected (opt-in).
|
|
283
|
+
|
|
284
|
+
Remaining P1b (step 3): `fwd_targets` allow-list + per-key `rate` enforcement
|
|
285
|
+
(currently parsed only; forwards are denied for policied keys until then), and
|
|
286
|
+
the MCP-side `danger_preapproved` auto-approve reading the same policy file.
|
|
287
|
+
|
|
288
|
+
Note: `VSSH_REQUIRE_TLS=1` fleet drop-in (transport §5 step 4) is an env/config
|
|
289
|
+
change, not a code release — gated on the §5.3 stabilization window.
|
|
290
|
+
|
|
291
|
+
go vet + go test ./... green; d1 canary agent_suite 10/10 (non-policied file/exec
|
|
292
|
+
unaffected); rolled out fleet-wide.
|
|
293
|
+
|
|
294
|
+
## [v0.7.27] - 2026-06-13
|
|
295
|
+
|
|
296
|
+
### Security (P1b step 1): per-key command/path policy engine (opt-in)
|
|
297
|
+
|
|
298
|
+
Implements DESIGN_RATIONALE §1 roadmap item 1 (per-key command allow/deny) and
|
|
299
|
+
SECURITY_TRANSPORT_MIGRATION §6. Lands behind a per-key opt-in: a key with no
|
|
300
|
+
`policy=` tag keeps current behavior, so the fleet is untouched until policies
|
|
301
|
+
are assigned.
|
|
302
|
+
|
|
303
|
+
- `authorized_keys` gains an optional `policy=<name>` tag (`KeyPolicy`); caps
|
|
304
|
+
verbs stay the hard floor, the policy narrows what an exec-capable key may run.
|
|
305
|
+
- `internal/server/policy.go`: loads `<cfg>/policies/<name>.json` or
|
|
306
|
+
`/etc/vssh/policies/<name>.json` (mtime-cached, hot-reloaded). Evaluation is
|
|
307
|
+
deny-first, then `danger_preapproved`, then `exec_allow`; no match = refuse,
|
|
308
|
+
typed `error_code: policy_denied` with the matched rule id and (for
|
|
309
|
+
preapproved) a `preapproved` flag in the audit record.
|
|
310
|
+
- Anchored (`^...$`) full-command matching defeats metachar/newline smuggling
|
|
311
|
+
inside an allowed prefix; load-time lint warns on unanchored rules. `file_paths`
|
|
312
|
+
scoping resolves symlinks/.. (longest existing ancestor) before the glob check.
|
|
313
|
+
- **Fail-closed**: a key tagged `policy=<name>` whose file is missing/invalid is
|
|
314
|
+
unusable (never unrestricted).
|
|
315
|
+
- Enforced on every exec path (single EXEJ, mux EXEJ, legacy EXE).
|
|
316
|
+
- Templates `policies/{readonly,backup,ci,deploy}.json` + README; `deploy`
|
|
317
|
+
carries `danger_preapproved` (systemctl restart) — the unattended list.
|
|
318
|
+
|
|
319
|
+
Note: REQUIRE_TLS (migration §5 step 4) moves to 0.7.28 — it is gated on the
|
|
320
|
+
§5.3 stabilization window (7 days zero plaintext-auth), which P1b does not block.
|
|
321
|
+
|
|
322
|
+
Tests: unit (EvalExec deny-first/anchoring/no-match, PathAllowed escape,
|
|
323
|
+
fail-closed, template compile+anchor) + in-process e2e (real daemon on loopback:
|
|
324
|
+
allow / deny / no-match / smuggle-blocked / danger / mux / fail-closed + audit
|
|
325
|
+
rule ids). go vet + go test ./... green; d1 canary agent_suite 10/10 (opt-in, no
|
|
326
|
+
regression); rolled out to 14 online nodes.
|
|
327
|
+
|
|
328
|
+
## [v0.7.26] - 2026-06-13
|
|
329
|
+
|
|
330
|
+
### Security (P0 step 2): client TLS-first by default
|
|
331
|
+
|
|
332
|
+
- `dialAuth` now attempts **TLS 1.3 (VTLS1) first on every connection** instead
|
|
333
|
+
of only under `VSSH_PREFER_TLS=1`. The connection is wrapped in TLS, the
|
|
334
|
+
daemon's Ed25519 raw pubkey is pinned via `~/.vssh/known_hosts` (TOFU on
|
|
335
|
+
first contact), and VAUTH1 runs inside the channel.
|
|
336
|
+
- **Downgrade-resistant:** a pinned-key (known_hosts) mismatch is a *hard
|
|
337
|
+
failure*, never a plaintext fallback — an attacker presenting a wrong cert
|
|
338
|
+
cannot force the client onto the plaintext wire.
|
|
339
|
+
- `VSSH_REQUIRE_TLS=1` (client) = no plaintext fallback at all; wins over the
|
|
340
|
+
`VSSH_NO_TLS=1` debug-only escape hatch.
|
|
341
|
+
- Pre-0.7.25 / non-TLS daemons: a loud stderr WARNING is printed, then a
|
|
342
|
+
plaintext-VAUTH1 fallback on a fresh connection. These warnings are the
|
|
343
|
+
fleet-upgrade TODO list and disappear once `REQUIRE_TLS` flips in 0.7.27.
|
|
344
|
+
- Refactored the Ed25519 challenge–response into a shared `vauth1()` used by
|
|
345
|
+
both the TLS and plaintext paths.
|
|
346
|
+
- **Audit transport marker:** the daemon tags each authenticated connection
|
|
347
|
+
`transport=tls|plain` and records it in the audit log, so the §5.3
|
|
348
|
+
stabilization gate (zero plaintext-auth records fleet-wide) is measurable
|
|
349
|
+
with `grep '"transport":"plain"' audit.log`. (Note: exec is not yet audited —
|
|
350
|
+
only forward ops are; wiring exec through auditLog is the gate prerequisite.)
|
|
351
|
+
- `scripts/pin_fleet_keys.sh`: pre-seed `~/.vssh/known_hosts` pins fleet-wide.
|
|
352
|
+
|
|
353
|
+
Validation: d1 canary `agent_suite` 10/10 ×3 (old-client / new-plaintext /
|
|
354
|
+
new-TLS); fleet `agent_suite` 38/38 GREEN over the new TLS client across
|
|
355
|
+
`d1 g1 c1 v1 macmini` (linux + darwin); `handshake-test --tls` AUTH_OK against
|
|
356
|
+
both a 0.7.26 (d1) and a 0.7.25 (c1) daemon; rolled out to 14 online nodes
|
|
357
|
+
(`deploy_fleet.sh`), `n1 s1 s2` offline/skipped. m1 coordinator binary was on 0.7.25 during rollout, then upgraded to 0.7.26 the
|
|
358
|
+
same day via the safe bootout/bootstrap procedure (internal release note).
|
|
359
|
+
|
|
360
|
+
## [v0.7.25] - 2026-06-13
|
|
361
|
+
|
|
362
|
+
### Security (P0 step 1): VTLS1 — the native protocol now runs inside TLS 1.3
|
|
363
|
+
|
|
364
|
+
Implements step 1 of `docs/SECURITY_TRANSPORT_MIGRATION.md` §5.
|
|
365
|
+
|
|
366
|
+
- **Daemon accepts TLS 1.3 on the same port.** A first-byte sniff (TLS record
|
|
367
|
+
`0x16` vs the ASCII auth line) routes the connection through Go stdlib
|
|
368
|
+
`tls.Server`; the unchanged line protocol (VAUTH1 → verbs → FMUX) runs
|
|
369
|
+
inside the channel. Identity = the existing Ed25519 `~/.vssh/vssh_id`
|
|
370
|
+
wrapped in an in-memory self-signed cert; trust = **raw-pubkey pinning**,
|
|
371
|
+
not PKI (new `internal/server/tlsident.go`). TLS 1.3 only, session tickets
|
|
372
|
+
off, ALPN `vssh/1`. If a TLS client presents a client cert, its key must
|
|
373
|
+
match the in-band VAUTH1 key (no cross-layer identity confusion).
|
|
374
|
+
- **`VSSH_REQUIRE_TLS=1` daemon kill-switch** (mirror of REQUIRE_VAUTH):
|
|
375
|
+
plaintext connections are refused outright. Default OFF until the fleet
|
|
376
|
+
stabilization window passes (flip planned for 0.7.27).
|
|
377
|
+
- **Client legacy shared-HMAC fallback removed (design finding F3).**
|
|
378
|
+
`dialAuth` no longer retries with the replayable HMAC token when VAUTH1 is
|
|
379
|
+
refused — with the fleet 100% strict, the only remaining "caller" of that
|
|
380
|
+
path was an attacker harvesting tokens by rejecting VAUTH1. VAUTH1 is now
|
|
381
|
+
the only client auth method; the `secret` parameter is dead.
|
|
382
|
+
- **Opt-in client TLS: `VSSH_PREFER_TLS=1`** wraps the dial in TLS 1.3 with
|
|
383
|
+
the daemon key pinned via new `~/.vssh/known_hosts` (`<host> <pubB64>`,
|
|
384
|
+
TOFU on first contact, hard fail on mismatch). Foreign-format lines in the
|
|
385
|
+
file (e.g. OpenSSH-style entries from other tools) are ignored, not
|
|
386
|
+
misread. Becomes the default in 0.7.26 per the migration plan.
|
|
387
|
+
- **`vssh handshake-test --tls <host>`** proves the full VTLS1 path and
|
|
388
|
+
reports ALPN, server key, and pin status in the JSON result.
|
|
389
|
+
- Verified: isolated daemon matrix 11/11 (plaintext, TLS TOFU, pinned
|
|
390
|
+
reconnect, PREFER_TLS exec, plaintext exec, 0.7.24-CLI compat, REQUIRE_TLS
|
|
391
|
+
refusing plaintext + accepting TLS, poisoned-pin rejection); d1 canary
|
|
392
|
+
handshake-test --tls AUTH_OK + `(fmux)`-era agent_suite 10/10 × {0.7.24
|
|
393
|
+
client, 0.7.25 plaintext, 0.7.25 PREFER_TLS}; new-client → 0.7.24-daemon
|
|
394
|
+
(v1) compat OK; fleet rollout via deploy_fleet (d1 canary first).
|
|
395
|
+
- Known limitation: known_hosts pins are keyed by the resolved address the
|
|
396
|
+
dialer used (IP), not the friendly node name — acceptable for 0.7.25,
|
|
397
|
+
consolidating pins by node name lands with `vssh enroll` (P1a).
|
|
398
|
+
|
|
399
|
+
## [v0.7.24] - 2026-06-13
|
|
400
|
+
|
|
401
|
+
### Phase B: reverse forwarding (`-R`) over FMUX — full -L/-R/-D parity on one session
|
|
402
|
+
|
|
403
|
+
- **`-R` now rides the shared FMUX session.** The client opens a control stream
|
|
404
|
+
(`RFWD <bind> <port>`); the daemon listens and pushes every accepted
|
|
405
|
+
connection back as a daemon-initiated `RCONN` stream. Gone: the
|
|
406
|
+
per-connection authenticated RDATA dial, the id pairing table, and the 15s
|
|
407
|
+
unclaimed-accept reaper. The listener lives exactly as long as the control
|
|
408
|
+
stream; if the session breaks the client re-establishes it automatically
|
|
409
|
+
(2s backoff). Bind refusals are fatal (no retry loop on a taken port).
|
|
410
|
+
- **Compat fallbacks verified:** a 0.7.23 daemon (FMUX without RFWD streams)
|
|
411
|
+
answers bad_request → client falls back to the legacy RFWD/RDATA path
|
|
412
|
+
(real payload verified against pre-upgrade d1); pre-FMUX daemons use the
|
|
413
|
+
same legacy path via the existing FMUX-unsupported detection.
|
|
414
|
+
- Audited as `RFWD <bind>:<port> (fmux)` with key attribution; chain intact.
|
|
415
|
+
- Verified: isolated strict daemon (-R x2 reverse fetches over one session),
|
|
416
|
+
0.7.23-d1 legacy fallback, 0.7.24-d1 canary -R x2 with `(fmux)` audit +
|
|
417
|
+
chain verified:true, agent_suite 10/10, fleet 14/14 = 0.7.24, m1 CLI +
|
|
418
|
+
daemon binaries updated (codesigned).
|
|
419
|
+
- All three tunnel modes (-L/-R/-D incl. SOCKS UDP) now share one
|
|
420
|
+
authenticated FMUX session per daemon.
|
|
421
|
+
|
|
422
|
+
## [v0.7.23] - 2026-06-13
|
|
423
|
+
|
|
424
|
+
### Phase B: tunnels over one authenticated session (FMUX) + SOCKS5 UDP
|
|
425
|
+
|
|
426
|
+
- **`FMUX` verb — many tunnels, one VAUTH1 handshake.** After auth the client
|
|
427
|
+
sends `FMUX`; the connection becomes a hashicorp/yamux session and every
|
|
428
|
+
tunneled connection is a stream opened with a one-line header. `-L` and `-D`
|
|
429
|
+
now share a single authenticated session (lazily created, transparently
|
|
430
|
+
re-dialed if it breaks) instead of paying one VAUTH1 round trip per tunneled
|
|
431
|
+
connection. Old daemons reply `unsupported_method`, and the client falls back
|
|
432
|
+
to the per-connection FWD path automatically (verified against a 0.7.22 d1).
|
|
433
|
+
Gated by the `forward` capability; every stream audited (`FWD h:p (fmux)`)
|
|
434
|
+
against the control connection's key identity.
|
|
435
|
+
- **SOCKS5 UDP ASSOCIATE (`-D` is now TCP+UDP).** A `UDP` FMUX stream relays
|
|
436
|
+
length-prefixed datagram frames (SOCKS5-style ATYP/ADDR/PORT + payload) to a
|
|
437
|
+
daemon-side UDP socket, so UDP egresses from the node like CONNECT does.
|
|
438
|
+
Datagram fragmentation is not supported (FRAG must be 0); the association
|
|
439
|
+
lives exactly as long as the SOCKS TCP control connection. Audited as
|
|
440
|
+
`UDPA (fmux)`.
|
|
441
|
+
- `halfPipe` now half-closes via a `CloseWrite() error` interface (TCP conns
|
|
442
|
+
and yamux streams alike). New dependency: github.com/hashicorp/yamux v0.1.2.
|
|
443
|
+
- Verified: isolated strict daemon — two -L fetches over ONE control connection
|
|
444
|
+
(same remote port in both audit records), SOCKS CONNECT, and a full SOCKS5
|
|
445
|
+
UDP ASSOCIATE echo round trip; m1→d1 canary -L x2 over one FMUX session with
|
|
446
|
+
chained `(fmux)` audit records attributed to m1-controller; agent_suite
|
|
447
|
+
10/10; fleet 14/14 = 0.7.23; m1 CLI + daemon binary updated (codesigned).
|
|
448
|
+
- Not yet over FMUX: `-R` (reverse) still uses the RFWD/RDATA per-accept path.
|
|
449
|
+
|
|
450
|
+
## [v0.7.22] - 2026-06-13
|
|
451
|
+
|
|
452
|
+
### Security (daemon — audit attribution + legacy auth client removed)
|
|
453
|
+
|
|
454
|
+
- **Audit records now carry the authenticated identity.** After a successful
|
|
455
|
+
handshake the daemon registers the connection's identity (VAUTH1 pubkey +
|
|
456
|
+
its authorized_keys comment via new `KeyName()`, or `legacy-hmac`) and every
|
|
457
|
+
audit record gains `key` / `key_name` fields — the audit trail attributes
|
|
458
|
+
each command to a key, not just a source IP. This closes the gap found while
|
|
459
|
+
designing the caps policy (records previously had only `remote`).
|
|
460
|
+
Chain format is unchanged; `audit-verify` passes over mixed old+new logs.
|
|
461
|
+
- **Dead legacy client removed.** `ExecCommand` (plain shared-HMAC EXEC, the
|
|
462
|
+
last pre-dialAuth client helper) had zero callers — deleted. Every remaining
|
|
463
|
+
client path authenticates through `dialAuth()` (VAUTH1 preferred).
|
|
464
|
+
- Verified: local isolated strict daemon (record carries key+key_name,
|
|
465
|
+
audit-verify true), d1 canary (key_name attribution live in
|
|
466
|
+
/var/log/vssh/audit.log, chain verified:true, agent_suite 10/10), fleet
|
|
467
|
+
deploy 14/14 = 0.7.22.
|
|
468
|
+
- Note: macmini has a stray ancient `/usr/local/bin/vssh` (0.1.0) — the daemon
|
|
469
|
+
uses `~/.local/bin/vssh`; clean up the stray someday.
|
|
470
|
+
|
|
471
|
+
### Ops (2026-06-13) — per-key caps policy live fleet-wide (least privilege)
|
|
472
|
+
|
|
473
|
+
- **Audit-driven policy.** Aggregated every node's hash-chained audit log by
|
|
474
|
+
client IP x verb: the only real users are m1 (EXEC everywhere + FWD) and each
|
|
475
|
+
node's own key over loopback (EXEC only, daily conformance). Zero node-to-node
|
|
476
|
+
usage, so node keys lost everything except exec.
|
|
477
|
+
- **Policy applied to authorized_keys on all 15 hosts** (14 nodes /etc/vssh or
|
|
478
|
+
~odt/.vssh + m1's own ~/.vssh): `m1-controller` stays unrestricted, all 14
|
|
479
|
+
node keys now carry `caps=exec`. Backups `authorized_keys.bak.caps.*`. No
|
|
480
|
+
daemon restart needed (file is read per handshake).
|
|
481
|
+
- Verified: d1 canary then fleet — node self-key loopback exec OK, put/rpc
|
|
482
|
+
refused with typed `capability_denied` (required_capability file/rpc, nothing
|
|
483
|
+
written); m1 key full agent_suite 10/10 on d1 and real put/get round trip to
|
|
484
|
+
v1; 14/14 vssh_exec green; d1 audit chain still verified:true.
|
|
485
|
+
- Gotcha fixed: m1's own authorized_keys names its key `m1` (not
|
|
486
|
+
`m1-controller`), so the first pass restricted the controller key on m1
|
|
487
|
+
itself - reverted that one line to unrestricted.
|
|
488
|
+
- **Audit gap found:** audit records carry no key identity (only remote IP), so
|
|
489
|
+
per-key attribution leaned on IPs. Follow-up: include the authenticated
|
|
490
|
+
pubkey/comment in each audit record.
|
|
491
|
+
|
|
492
|
+
### Ops (2026-06-13) — legacy-HMAC kill switch fleet-wide
|
|
493
|
+
|
|
494
|
+
- **`VSSH_REQUIRE_VAUTH=1` enabled on all 14 online nodes** (was d1-only canary):
|
|
495
|
+
v1-v5, c1, c2, g1-g4, d2 via systemd drop-in
|
|
496
|
+
`/etc/systemd/system/vsshd.service.d/require_vauth.conf` (mkdir+tee+daemon-reload,
|
|
497
|
+
restart detached via `systemd-run --on-active=2` so the controlling connection
|
|
498
|
+
survives); macmini via `EnvironmentVariables.VSSH_REQUIRE_VAUTH` in the
|
|
499
|
+
`ai.meshclaw.vsshd` launchd plist (backup `*.plist.bak.require_vauth`).
|
|
500
|
+
- Verified per node before moving on: running-process env shows
|
|
501
|
+
VSSH_REQUIRE_VAUTH=1 (`/proc/<pid>/environ` / `launchctl print`), vssh_exec
|
|
502
|
+
(VAUTH1) green, agent_suite 10/10 on v1-v5, c1, c2, g3, g4, d2. g1/g2/macmini
|
|
503
|
+
asserted via vssh_exec + loopback `handshake-test` AUTH_OK instead (their m1
|
|
504
|
+
CLI path still resolves through the stale /etc/hosts WIRE block - pre-existing,
|
|
505
|
+
unrelated to auth).
|
|
506
|
+
- Gotchas hit: exec runs as unprivileged user on most nodes (sudo -n required);
|
|
507
|
+
macmini `launchctl bootout` killed the detached restart script before
|
|
508
|
+
bootstrap, daemon restored over ssh from m1 (`launchctl bootstrap`); d2 path
|
|
509
|
+
showed transient MCP timeouts - apply was split into conf/reload then restart.
|
|
510
|
+
- Offline n1/s1/s2: apply the same drop-in when they return (they are also still
|
|
511
|
+
on an older binary - deploy first).
|
|
512
|
+
- Rollback per node: delete the drop-in (or plist key) and restart the daemon.
|
|
513
|
+
|
|
514
|
+
## [v0.7.21] - 2026-06-12
|
|
515
|
+
|
|
516
|
+
### Phase B: tunneling completed — reverse (`-R`) and dynamic/SOCKS (`-D`)
|
|
517
|
+
|
|
518
|
+
- **`vssh fwd <host> -D [bind:]<port>` — SOCKS5 dynamic forwarding (ssh -D).**
|
|
519
|
+
A local SOCKS5 proxy whose every CONNECT is tunneled through the node's daemon
|
|
520
|
+
via the existing `FWD` verb (client-only; CONNECT with IPv4/IPv6/domain
|
|
521
|
+
address types). Point a browser or `curl --socks5-hostname` at it to reach
|
|
522
|
+
anything the node can reach.
|
|
523
|
+
- **`vssh fwd <host> -R [bind:]<rport>:<lhost>:<lport>` — reverse forwarding
|
|
524
|
+
(ssh -R).** New daemon verbs `RFWD` (control: bind a port on the node, push an
|
|
525
|
+
`ACCEPT <id>` per incoming connection back over the control connection) and
|
|
526
|
+
`RDATA` (per-connection data channel the client pulls back and splices to its
|
|
527
|
+
local target). Listener lifetime is tied to the control connection; unclaimed
|
|
528
|
+
accepts are reaped after 15s. Both gated by the `forward` capability and
|
|
529
|
+
audited (`RFWD <bind>:<port>`).
|
|
530
|
+
- Shared proxy helpers (`halfPipe`/`bidiPipe`) with TCP half-close propagation.
|
|
531
|
+
- Verified locally for all three modes (-L/-D/-R) with real HTTP payloads and
|
|
532
|
+
SOCKS by-IP + by-hostname; on the d1 canary, an `-L` tunnel to d1's own daemon
|
|
533
|
+
port completed a VAUTH1 handshake end-to-end, FWD/RFWD records are present, and
|
|
534
|
+
the hash-chained audit log still verifies (177 records). agent_suite 10/10.
|
|
535
|
+
- Remaining Phase B nice-to-haves: carry tunnels over a single MUX connection
|
|
536
|
+
(today each tunneled connection pays one auth round trip); UDP associate.
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
## [v0.7.20] - 2026-06-12
|
|
540
|
+
|
|
541
|
+
### Phase B begins: tunneling (daemon + CLI)
|
|
542
|
+
|
|
543
|
+
- **`FWD` verb + `vssh fwd` — the `ssh -L` replacement.** New
|
|
544
|
+
`vssh fwd <host> -L [bind:]<lport>:<rhost>:<rport>` listens locally and pipes
|
|
545
|
+
every accepted connection through the node's daemon to a target the *node*
|
|
546
|
+
can reach. Each tunneled connection is individually authenticated (VAUTH1
|
|
547
|
+
preferred, one daemon connection per tunnel — a dead tunnel never wedges the
|
|
548
|
+
listener), gated by the new `forward` capability, and server-side audited as
|
|
549
|
+
`FWD <host>:<port>` in the hash chain. Daemon side: `HandleForward`
|
|
550
|
+
(internal/server/forward.go) dials the target, replies `FWD_OK` or a typed
|
|
551
|
+
error (`bad_request`/`unreachable`/`capability_denied`), then proxies with
|
|
552
|
+
half-close propagation (TCP CloseWrite) in both directions.
|
|
553
|
+
- Verified: HTTP round trip through a local tunnel chain; real-world m1:15610 →
|
|
554
|
+
d1 → d1-localhost:18124 fetch; `caps=exec` key refused with typed
|
|
555
|
+
`capability_denied`; FWD audit record present and chained on d1.
|
|
556
|
+
- Remaining Phase B: `-R` (reverse) and `-D` (SOCKS) modes, tunnels over MUX
|
|
557
|
+
(today each tunneled connection pays one auth round trip).
|
|
558
|
+
|
|
559
|
+
### Ops
|
|
560
|
+
|
|
561
|
+
- **Kill-switch canary live on d1:** `VSSH_REQUIRE_VAUTH=1` set in d1's unit
|
|
562
|
+
(`/etc/systemd/system/vsshd.service.d/require_vauth.conf`). CLI paths all
|
|
563
|
+
green via VAUTH1 (run/put/get/run-batch, agent_suite 10/10); legacy-only
|
|
564
|
+
clients are refused with typed `auth_failed` — including currently-running
|
|
565
|
+
pre-0.7.17 vssh MCP server processes, whose `vssh_exec → d1` is auth-blocked
|
|
566
|
+
until they restart on the 0.7.17+ binary. ssh and CLI control paths are
|
|
567
|
+
unaffected, so rollback (delete the override, restart) stays available.
|
|
568
|
+
- `deploy_fleet.sh` verify now runs as root on Linux nodes (`verify_newline`
|
|
569
|
+
prefix arg) so it keeps working on REQUIRE_VAUTH nodes where the plain ssh
|
|
570
|
+
user's key is not authorized.
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
## [v0.7.19] - 2026-06-12
|
|
574
|
+
|
|
575
|
+
### Security (daemon — Phase A extension: capability scoping + tamper-evident audit)
|
|
576
|
+
|
|
577
|
+
- **Capability scoping on authorized_keys.** A key line may now carry a
|
|
578
|
+
`caps=` tag — `<pubB64> caps=exec,file,rpc,shell [comment]` — and the daemon
|
|
579
|
+
enforces it per verb: `EXEC/EXEJ`→exec, `PUT/GET/PUTZ/SYNC/RESUME/MPUT/GETM/
|
|
580
|
+
GETC/PIPE`→file, `RPC`→rpc, `MUX`→exec, interactive PTY→shell. `INFO` stays
|
|
581
|
+
open to any authenticated key. No tag (or `caps=all`) = unrestricted, so every
|
|
582
|
+
existing key keeps working. Violations get a typed
|
|
583
|
+
`{"error_code":"capability_denied","required_capability":...}` reply.
|
|
584
|
+
Legacy-HMAC connections remain unrestricted (the shared secret already
|
|
585
|
+
implies full trust; it is on its way out via VSSH_REQUIRE_VAUTH).
|
|
586
|
+
- **Hash-chained audit log.** Every audit record now carries
|
|
587
|
+
`prev` = SHA-256 of the previous line, so any in-place edit, deletion, or
|
|
588
|
+
reordering breaks the chain. New CLI `vssh audit-verify [path]` walks the log
|
|
589
|
+
and reports `{verified, records, chained_records, violations[]}` (exit 1 on
|
|
590
|
+
violation). Head-trimming (rotation) is tolerated; chain seeds from the last
|
|
591
|
+
pre-chain line, so mixed old+new logs verify cleanly. Assumes the single-
|
|
592
|
+
daemon-writer model (true on every node).
|
|
593
|
+
- Verified locally (isolated-HOME server): caps=exec key can run but is denied
|
|
594
|
+
put/rpc with typed errors while facts/INFO still work; tampering with a
|
|
595
|
+
middle audit line flips audit-verify to verified:false with a chain-break
|
|
596
|
+
violation. d1 canary: agent_suite 10/10, live audit chain confirmed.
|
|
597
|
+
|
|
598
|
+
## [v0.7.18] - 2026-06-12
|
|
599
|
+
|
|
600
|
+
### Security (daemon — legacy-HMAC kill switch)
|
|
601
|
+
|
|
602
|
+
- **`VSSH_REQUIRE_VAUTH=1`** (daemon env): rejects the legacy shared-HMAC token
|
|
603
|
+
so only VAUTH1 per-node keys authenticate. Default off — enable per node once
|
|
604
|
+
its clients all hold authorized keys. Tested both ways on an isolated pair of
|
|
605
|
+
local daemons: strict daemon accepts VAUTH1 / rejects legacy with typed
|
|
606
|
+
`auth_failed`; permissive daemon still accepts the legacy fallback.
|
|
607
|
+
|
|
608
|
+
### Ops (same day, no daemon change)
|
|
609
|
+
|
|
610
|
+
- **`scripts/cross_authorize_fleet.sh`** — full-mesh VAUTH1 trust: collects
|
|
611
|
+
every online node's Ed25519 pubkey (root daemon identity on Linux, user
|
|
612
|
+
identity on macmini, plus m1) and merges all 15 keys into every node's
|
|
613
|
+
authorized_keys (idempotent, additive). Verified 14/14: every node completes
|
|
614
|
+
a loopback VAUTH1 handshake with its own key against its own daemon
|
|
615
|
+
(node-to-node TCP paths are not universally open in the tailnet, so loopback
|
|
616
|
+
+ controller-to-node is the meaningful proof). This satisfies the
|
|
617
|
+
precondition for VSSH_REQUIRE_VAUTH=1.
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
## [v0.7.17] - 2026-06-12
|
|
621
|
+
|
|
622
|
+
### Security (client — Phase A: VAUTH1 becomes the default for every verb)
|
|
623
|
+
|
|
624
|
+
- **All seven client auth sites now use VAUTH1 first.** `ExecCommandStructuredTimeout`,
|
|
625
|
+
`SendFile`, `RecvFile`, `CallRPC`, `GetInfo`, `RunMux` (transfer.go) and `Connect`
|
|
626
|
+
(client.go) authenticate through a new shared `dialAuth()` helper: VAUTH1 Ed25519
|
|
627
|
+
challenge–response preferred, legacy shared-HMAC token as a fallback on a *fresh*
|
|
628
|
+
connection when VAUTH1 is rejected/unsupported. Until now only `handshake-test`
|
|
629
|
+
spoke VAUTH1; every real command now uses per-node keys (no shared secret, no
|
|
630
|
+
replay) wherever the client key is authorized.
|
|
631
|
+
- Canary proof on d1: run/put/get/rpc/facts/run-batch all succeed with a deliberately
|
|
632
|
+
**wrong shared secret** (VAUTH1-only path), succeed with an **unauthorized fresh
|
|
633
|
+
identity + correct secret** (HMAC fallback path), and fail with a typed
|
|
634
|
+
`auth_failed` when both are wrong. agent_suite 10/10 green.
|
|
635
|
+
- `dialAuth` returns the handshake's buffered reader and all callers now read daemon
|
|
636
|
+
output from it (closes a buffered-byte-loss window in `GetInfo`/`Connect`).
|
|
637
|
+
- `test/agent_suite.sh`: the auth-failure asserts now force a fresh unauthorized
|
|
638
|
+
identity (`HOME=$(mktemp -d)`), since a bad secret alone no longer fails by design.
|
|
639
|
+
- Remaining for full Phase A: cross-distribute per-node pubkeys (today only m1's key
|
|
640
|
+
is in fleet authorized_keys), `VSSH_REQUIRE_VAUTH=1` to disable legacy HMAC,
|
|
641
|
+
capability scoping. Legacy plain-`EXEC` helper (`ExecCommand`) intentionally left
|
|
642
|
+
on HMAC until its callers are audited.
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
## [v0.7.16] - 2026-06-12
|
|
646
|
+
|
|
647
|
+
### Security (daemon — requires redeploy) — Phase A core
|
|
648
|
+
|
|
649
|
+
- **Per-node Ed25519 identity + challenge–response auth (`VAUTH1`).** The daemon now
|
|
650
|
+
accepts asymmetric per-node keys: client sends its public key, daemon checks it
|
|
651
|
+
against `authorized_keys` (`~/.vssh` or `/etc/vssh`), issues a random nonce, and the
|
|
652
|
+
client must sign it — **no shared secret, no replay** (the legacy `ts:HMAC(secret)`
|
|
653
|
+
token has a 30s replay window and one key for the whole fleet). Identity auto-creates
|
|
654
|
+
at `~/.vssh/vssh_id`. New CLI `vssh pubkey` and `vssh handshake-test <host>`.
|
|
655
|
+
- **Dual-auth migration:** the legacy HMAC token is still accepted, so nothing breaks
|
|
656
|
+
until keys are distributed and the legacy path is turned off. New `identity.go`
|
|
657
|
+
(`LoadOrCreateIdentity`, `SignChallenge`, `VerifyChallenge`, `IsAuthorizedKey`).
|
|
658
|
+
|
|
659
|
+
## [v0.7.15] - 2026-06-12
|
|
660
|
+
|
|
661
|
+
### Runtime (daemon — requires redeploy)
|
|
662
|
+
|
|
663
|
+
- **Connection multiplexing.** New `MUX` session: authenticate once, then run many
|
|
664
|
+
commands over a single connection (newline-delimited JSON per result, idle timeout).
|
|
665
|
+
CLI `vssh run-batch <host>` reads commands from stdin (one per line) and runs them
|
|
666
|
+
over one connection. Measured: 10 commands in 249ms (~25ms each) vs 671ms for 10
|
|
667
|
+
separate runs — 2.7x, approaching ssh ControlMaster territory. Foundation for an
|
|
668
|
+
MCP connection pool (next) and port forwarding. Server-side `HandleMux`, client
|
|
669
|
+
`RunMux`.
|
|
670
|
+
|
|
671
|
+
## [v0.7.14] - 2026-06-12
|
|
672
|
+
|
|
673
|
+
### Performance (daemon — requires redeploy)
|
|
674
|
+
|
|
675
|
+
- **Command exec ~3.8x faster.** The root daemon ran every command through a login
|
|
676
|
+
shell (`sudo -u user bash -lc`) which re-sourced the user's whole profile (nvm,
|
|
677
|
+
cargo, …) — ~350ms per command, measured. Now it runs a non-login `bash -c` and
|
|
678
|
+
injects the user's login `$PATH`, computed once and cached. `vssh run <node> true`
|
|
679
|
+
dropped from ~441ms to ~117ms while preserving embedded newlines and full PATH.
|
|
680
|
+
- **GET single-pass.** Download stopped double-reading the file (a separate checksum
|
|
681
|
+
pass + send pass introduced in 0.7.7); it now streams once while hashing and sends
|
|
682
|
+
the MD5 as a trailing line. Integrity preserved, GET disk I/O halved.
|
|
683
|
+
|
|
684
|
+
## [v0.7.13] - 2026-06-12
|
|
685
|
+
|
|
686
|
+
### Runtime (daemon — requires redeploy)
|
|
687
|
+
|
|
688
|
+
- Server-side audit log: every command the daemon executes (the `EXEJ` path) is
|
|
689
|
+
appended as a JSONL record `{ts, remote, command, success, exit_code, duration_ms}`
|
|
690
|
+
to `/var/log/vssh/audit.log` (root) or `~/.vssh/audit.log` (user). Unconditional and
|
|
691
|
+
server-side, so a client cannot suppress it — the compliance/audit backbone a plain
|
|
692
|
+
ssh wrapper can't provide. Best-effort: write failures never affect execution.
|
|
693
|
+
|
|
694
|
+
## [v0.7.12] - 2026-06-12
|
|
695
|
+
|
|
696
|
+
### Runtime (daemon — requires redeploy)
|
|
697
|
+
|
|
698
|
+
- No more silent PTY fallthrough on an unknown verb. When a client sends a
|
|
699
|
+
protocol-shaped token (two or more leading uppercase ASCII letters) that matches
|
|
700
|
+
no handler, the daemon now replies with a typed
|
|
701
|
+
`{"success":false,"error_code":"unsupported_method","proto_version":"1"}` and
|
|
702
|
+
closes — instead of dropping into an interactive login shell that hangs an agent.
|
|
703
|
+
Interactive `vssh shell` clients send an ESC window-size sequence first (never an
|
|
704
|
+
uppercase token), so real shells are unaffected (verified).
|
|
705
|
+
- Introduced `ProtoVersion` ("1"), surfaced in the unsupported-verb reply as the
|
|
706
|
+
first step toward explicit version negotiation.
|
|
707
|
+
|
|
708
|
+
## [v0.7.11] - 2026-06-12
|
|
709
|
+
|
|
710
|
+
### Runtime
|
|
711
|
+
|
|
712
|
+
- New `vssh run-async <host> <cmd> [--wait <s>]`: runs the command as a daemon job and
|
|
713
|
+
waits up to `<s>` seconds. If it finishes in time the full result is returned inline
|
|
714
|
+
(short commands still feel synchronous); otherwise it returns `{promoted:true, job_id,
|
|
715
|
+
status:"running", poll:...}` for the caller to poll. The command runs **exactly once**
|
|
716
|
+
(as a job) — this dodges fixed call-timeout ceilings (e.g. an MCP client's ~60s cap)
|
|
717
|
+
without abandoning or double-running work.
|
|
718
|
+
- `rpc`/`facts`/job/`run-async` now use the reachability-aware resolver too, so they no
|
|
719
|
+
longer stall on a node whose preferred IP has moved.
|
|
720
|
+
|
|
721
|
+
## [v0.7.10] - 2026-06-12
|
|
722
|
+
|
|
723
|
+
### Runtime
|
|
724
|
+
|
|
725
|
+
- Reachability-aware host resolution for `run` and `deploy-binary`: candidate
|
|
726
|
+
endpoints (Tailscale → VPN → LAN → Public) are now probed concurrently with a short
|
|
727
|
+
(1.5s) deadline and the highest-preference *reachable* one is chosen, instead of
|
|
728
|
+
blindly dialing the preferred IP and stalling for the full timeout when it has moved.
|
|
729
|
+
Fixes the d2 stall (`vssh run d2` went from a 30s+ hang to ~1s) caused by a stale
|
|
730
|
+
Tailscale IP whose port was closed. New `Connector.CandidateHosts`.
|
|
731
|
+
|
|
732
|
+
## [v0.7.9] - 2026-06-12
|
|
733
|
+
|
|
734
|
+
### Runtime
|
|
735
|
+
|
|
736
|
+
- New first-class `vssh deploy-binary <local> <host> <remote-path>` verb: atomic +
|
|
737
|
+
checksum-verified upload (P1.1), privileged atomic install into the target path
|
|
738
|
+
(ETXTBSY-safe — replaces a running binary), optional `--service` restart
|
|
739
|
+
(systemd or launchd), and a `--verify` step — the deploy_fleet.sh dance collapsed
|
|
740
|
+
into one auditable call that returns a structured `{success, phase, verify_output,
|
|
741
|
+
error_code}` result. `phase` pinpoints where a failure happened (upload/install/verify).
|
|
742
|
+
|
|
743
|
+
## [v0.7.8] - 2026-06-12
|
|
744
|
+
|
|
745
|
+
### Runtime
|
|
746
|
+
|
|
747
|
+
- Structured error codes on the exec result (`error_code` + `retryable`): client-side
|
|
748
|
+
failures are now machine-branchable instead of free text — `auth_failed` (secret
|
|
749
|
+
rejected), `unreachable` (refused/no-route), `timeout` (dial/read deadline),
|
|
750
|
+
`bad_response` (malformed reply), `remote_exit_nonzero` (command ran, exited non-zero).
|
|
751
|
+
- CLI `run` prints the code (`Error [auth_failed]: …`) and the MCP `vssh_exec` error
|
|
752
|
+
object now propagates the specific code/retryable instead of a generic
|
|
753
|
+
`native_execution_failed`. Fields are additive (`omitempty`) — fully backward compatible.
|
|
754
|
+
|
|
755
|
+
## [v0.7.7] - 2026-06-12
|
|
756
|
+
|
|
757
|
+
### Runtime
|
|
758
|
+
|
|
759
|
+
- Default `put`/`get` now use the robust transfer path: server stages uploads to a
|
|
760
|
+
temp file in the destination directory and `rename`s into place — **atomic and
|
|
761
|
+
ETXTBSY-safe** (a running binary can be replaced without "text file busy").
|
|
762
|
+
- End-to-end **MD5 verification**: server reports the checksum (`OK <bytes> <md5>` on
|
|
763
|
+
PUT, `SIZE <bytes> <md5>` on GET) and the client verifies it; a mismatch errors
|
|
764
|
+
instead of silently delivering a corrupt file. Backward compatible — older clients
|
|
765
|
+
ignore the extra field, newer clients skip verification against older daemons.
|
|
766
|
+
- `get` writes to a temp file and atomically renames on success, so an interrupted
|
|
767
|
+
download never leaves a half-written file at the target path.
|
|
768
|
+
|
|
769
|
+
## [v0.7.6] - 2026-06-12
|
|
770
|
+
|
|
771
|
+
### Runtime
|
|
772
|
+
|
|
773
|
+
- Honor `timeout_seconds` in native exec (was a hardcoded 30s read deadline);
|
|
774
|
+
plumb through `ExecCommandStructuredTimeout`.
|
|
775
|
+
- Fix newline stripping: root daemon ran commands via `sudo -u <user> -i <shell> -c`
|
|
776
|
+
whose login shell deleted embedded `\n`; switched to `sudo -u <user> <shell> -lc`.
|
|
777
|
+
- Honest CLI exit code: `vssh rpc` now exits non-zero on a structured `success:false`
|
|
778
|
+
(e.g. `unknown method`) instead of exiting 0.
|
|
779
|
+
|
|
780
|
+
### Ops & docs
|
|
781
|
+
|
|
782
|
+
- `scripts/deploy_fleet.sh`: idempotent, arch-aware fleet deploy (dir-stage guard,
|
|
783
|
+
atomic `mv`, ETXTBSY-safe, per-node newline verification).
|
|
784
|
+
- `test/agent_suite.sh`: live agent-contract conformance suite (exits non-zero on
|
|
785
|
+
regression; suitable for scheduled runs).
|
|
786
|
+
- `docs/AGENT_READINESS_ROADMAP.md`, `docs/GAP_ANALYSIS.md`, `docs/DEV_STATUS_2026-06-12.md`:
|
|
787
|
+
agent-readiness plan, code audit, and session handoff.
|
|
788
|
+
|
|
789
|
+
## [v0.7.5] - 2026-05-22
|
|
790
|
+
|
|
791
|
+
### Runtime
|
|
792
|
+
|
|
793
|
+
- Add `vssh doctor` / `vssh setup-check` for AI-operator setup diagnostics:
|
|
794
|
+
effective binary, stale binary conflicts, secret source, Wire config, and peer
|
|
795
|
+
counts.
|
|
796
|
+
- Add MCP `vssh_doctor` so Codex, Claude, Cursor, and other MCP clients can
|
|
797
|
+
diagnose VSSH before attempting execution or facts calls.
|
|
798
|
+
- Prefer MCP-safe underscore tool names and deduplicate exposed tools.
|
|
799
|
+
- Read native secrets from `/etc/vssh/secret` and `~/.vssh/secret` before
|
|
800
|
+
falling back to Wire-derived secrets.
|
|
801
|
+
|
|
802
|
+
### Python SDK
|
|
803
|
+
|
|
804
|
+
- Add `VSSH.doctor()` to call `vssh doctor --json`.
|
|
805
|
+
|
|
806
|
+
## [v0.7.4] - 2026-05-16
|
|
807
|
+
|
|
808
|
+
### Runtime
|
|
809
|
+
|
|
810
|
+
- Add `vssh.route.select` / `vssh_route_select` for capability, tag, and health-aware host routing.
|
|
811
|
+
- Add `vssh.exec.routed` / `vssh_exec_routed` to route first, then execute with policy and evidence.
|
|
812
|
+
- Return route decisions with selected host, score, reasons, missing capabilities, health, tags, and candidate host records.
|
|
813
|
+
- Keep monitoring separate: `vssh.hosts.list`, `vssh.route.select`, and `vssh.exec.routed` can optionally merge live health from an external MeshClaw/mpop-style monitor endpoint using `monitor_url` or `monitor_port`.
|
|
814
|
+
|
|
815
|
+
## [v0.7.3] - 2026-05-16
|
|
816
|
+
|
|
817
|
+
### Runtime
|
|
818
|
+
|
|
819
|
+
- Enrich `vssh.hosts.list` output with `addresses`, `tags`, `capabilities`, `health`, `stats`, `os`, `arch`, and metadata fields for agent routing.
|
|
820
|
+
- Extend `~/.vssh/servers.json` support with optional `tags`, `capabilities`, `roles`, `os`, `arch`, `public_ip`, `lan_ip`, `port`, and `metadata`.
|
|
821
|
+
- Infer basic capabilities from tags/roles/OS names, including `gpu`, `cuda`, `ollama`, `browser`, `controller`, `mail`, `docker`, `linux`, and `macos`.
|
|
822
|
+
- Add health summaries based on provider online state, `last_seen`, and resource pressure.
|
|
823
|
+
|
|
824
|
+
## [v0.7.2] - 2026-05-16
|
|
825
|
+
|
|
826
|
+
### Runtime
|
|
827
|
+
|
|
828
|
+
- Add agent-facing MCP tool aliases: `vssh.hosts.list`, `vssh.exec`, `vssh.exec.safe`, and `vssh.policy.check`.
|
|
829
|
+
- Add a built-in safety policy that blocks destructive/service-impacting command patterns unless `allow_dangerous` is explicitly set.
|
|
830
|
+
- Wrap MCP execution responses in evidence envelopes with timestamps, policy decision, target, command, timeout, and structured execution result.
|
|
831
|
+
|
|
832
|
+
### Documentation
|
|
833
|
+
|
|
834
|
+
- README / README.ko: link to canonical stack snapshot [`meshpop/wire` **docs/CURRENT_STATE.md**](https://github.com/meshpop/wire/blob/main/docs/CURRENT_STATE.md).
|
|
835
|
+
- Document Codex/Runtime MCP usage in English and Korean.
|
|
836
|
+
|
|
837
|
+
## [v0.7.1] - 2026-05-16
|
|
838
|
+
|
|
839
|
+
### Runtime
|
|
840
|
+
|
|
841
|
+
- Preserve MCP `vssh_exec` shell commands as one script instead of splitting with `strings.Fields`.
|
|
842
|
+
- Return structured execution evidence with stdout, stderr, exit code, duration, attempts, transport, fallback, and typed retryable errors.
|
|
843
|
+
- Fix root-run `vsshd` transfer ownership so PUT/PUTZ/RESUME/MPUT/PIPE_UP outputs are usable by the default non-root runtime user.
|
|
844
|
+
|
|
845
|
+
## [v0.7.0] - 2026-05-14
|
|
846
|
+
|
|
847
|
+
### Changed
|
|
848
|
+
|
|
849
|
+
- Remove `mesh-event` dependency; standalone `go build`.
|
|
850
|
+
- Document `internal/adapter` as **discovery-only**; `VSSHAdapter.Exec` remains unimplemented by design until explicitly specified.
|
|
851
|
+
|
|
852
|
+
### Fixed
|
|
853
|
+
|
|
854
|
+
- `go vet` cleanups (IPv6 literals) where applicable.
|
|
855
|
+
|
|
856
|
+
## Earlier releases
|
|
857
|
+
|
|
858
|
+
See Git tags and GitHub Releases.
|