fix: harden 0.6.1 runtime checks

This commit is contained in:
Peter Steinberger 2026-05-07 01:14:26 +01:00
parent 98af5a3e8f
commit f4695953bc
No known key found for this signature in database
9 changed files with 69 additions and 16 deletions

View File

@ -1,10 +1,11 @@
# Changelog
## Unreleased
## 0.6.1 - Unreleased
### Fixed
- Fixed Tailscale exit-node bootstrap paths to prefer tailnet metadata and fail clearly when remote exit-node egress is not active.
- Fixed `run --no-sync` timing summaries so they report `sync_skipped=true`.
## 0.6.0 - 2026-05-07

View File

@ -401,13 +401,38 @@ func validateTailscaleExitNodeEgress(ctx context.Context, server Server, target
if strings.TrimSpace(meta.ExitNode) == "" {
return nil
}
command := `set -eu
prefs="$(tailscale debug prefs 2>/dev/null || true)"
command := tailscaleExitNodeEgressCheckScript()
if out, err := runSSHCombinedOutput(ctx, target, command); err != nil {
detail := strings.TrimSpace(out)
if detail == "" {
detail = err.Error()
}
return exit(5, "tailscale exit node %s joined but remote internet egress failed; verify the exit node is approved and forwarding internet traffic: %s", meta.ExitNode, detail)
}
return nil
}
func tailscaleExitNodeEgressCheckScript() string {
return `set -eu
if ! command -v tailscale >/dev/null 2>&1; then
printf '%s\n' "tailscale is not installed for exit-node egress check" >&2
exit 87
fi
prefs="$(tailscale debug prefs 2>/dev/null)" || {
printf '%s\n' "tailscale prefs unavailable for exit-node egress check" >&2
exit 88
}
case "$prefs" in
*'"ExitNodeID": ""'*|*'"ExitNodeID":""'*)
printf '%s\n' "exit node is not selected in tailscale prefs" >&2
exit 86
;;
*'"ExitNodeID":'*)
;;
*)
printf '%s\n' "tailscale prefs did not include ExitNodeID" >&2
exit 89
;;
esac
if command -v curl >/dev/null 2>&1; then
timeout 12 sh -c 'curl -4fsS --connect-timeout 5 https://ifconfig.me/ip || curl -4fsS --connect-timeout 5 https://icanhazip.com' >/tmp/crabbox-exit-node-ip
@ -417,12 +442,4 @@ else
fi
test -s /tmp/crabbox-exit-node-ip
`
if out, err := runSSHCombinedOutput(ctx, target, command); err != nil {
detail := strings.TrimSpace(out)
if detail == "" {
detail = err.Error()
}
return exit(5, "tailscale exit node %s joined but remote internet egress failed; verify the exit node is approved and forwarding internet traffic: %s", meta.ExitNode, detail)
}
return nil
}

View File

@ -2,6 +2,7 @@ package cli
import (
"context"
"strings"
"testing"
)
@ -65,6 +66,24 @@ func TestBootstrapNetworkHonorsExplicitPublic(t *testing.T) {
}
}
func TestTailscaleExitNodeEgressCheckFailsClosed(t *testing.T) {
script := tailscaleExitNodeEgressCheckScript()
for _, want := range []string{
"command -v tailscale",
"tailscale debug prefs",
"tailscale prefs unavailable",
"tailscale prefs did not include ExitNodeID",
"exit node is not selected",
} {
if !strings.Contains(script, want) {
t.Fatalf("egress check script missing %q:\n%s", want, script)
}
}
if strings.Contains(script, "debug prefs 2>/dev/null || true") {
t.Fatalf("egress check script must not ignore tailscale prefs failures:\n%s", script)
}
}
func TestRenderTailscaleHostname(t *testing.T) {
got := renderTailscaleHostname("CBX-{slug}-{provider}-{id}", "cbx_abcdef123456", "Blue Lobster", "aws")
if got != "cbx-blue-lobster-aws-cbx-abcdef123456" {

View File

@ -464,6 +464,7 @@ func (a App) runCommand(ctx context.Context, args []string) (err error) {
fmt.Fprintf(a.Stderr, "sync complete in %s\n", timings.sync.Round(time.Millisecond))
recorder.Event("sync.finished", "synced", fmt.Sprintf("duration=%s skipped=false", timings.sync.Round(time.Millisecond)))
} else {
timings.syncSkipped = true
recorder.Event("sync.finished", "synced", "skipped by --no-sync")
}
afterSync:

View File

@ -48,6 +48,21 @@ func TestFormatRunSummaryIncludesGitHydrateSkipReason(t *testing.T) {
}
}
func TestFormatRunSummaryNoSync(t *testing.T) {
got := formatRunSummary(runTimings{
syncSkipped: true,
}, 500*time.Millisecond, 0)
for _, want := range []string{
"sync=0s",
"sync_skipped=true",
"exit=0",
} {
if !strings.Contains(got, want) {
t.Fatalf("summary missing %q in %q", want, got)
}
}
}
func TestTimingJSONShape(t *testing.T) {
var buf bytes.Buffer
err := writeTimingJSON(&buf, timingReportFromRun("aws", "cbx_123", "blue-crab", runTimings{

View File

@ -1,3 +1,3 @@
package cli
var version = "0.6.0"
var version = "0.6.1"

View File

@ -1,6 +1,6 @@
{
"name": "@openclaw/crabbox-plugin",
"version": "0.6.0",
"version": "0.6.1",
"description": "OpenClaw plugin for running Crabbox remote testbox workflows",
"license": "MIT",
"type": "module",

View File

@ -1,12 +1,12 @@
{
"name": "@openclaw/crabbox-worker",
"version": "0.6.0",
"version": "0.6.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/crabbox-worker",
"version": "0.6.0",
"version": "0.6.1",
"dependencies": {
"@novnc/novnc": "1.6.0",
"aws4fetch": "^1.0.20",

View File

@ -1,6 +1,6 @@
{
"name": "@openclaw/crabbox-worker",
"version": "0.6.0",
"version": "0.6.1",
"private": true,
"type": "module",
"scripts": {