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
28 changes: 14 additions & 14 deletions .claude/skills/serverbee-vps-e2e-test/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
---
name: serverbee-vps-e2e-test
description: Run the ServerBee install.sh deploy / agent-recover end-to-end regression on a real Linux VPS — cross-compile the current branch locally, push the binary or docker image to the VPS, then execute install.sh against it and verify HTTPS + agent connection. Use this whenever the user wants to validate deploy changes on a real VPS, smoke-test install.sh after editing it, verify the agent.toml refresh / recover flow, check that Caddy + Let's Encrypt automation still works, or test the binary vs docker agent install paths. Trigger on phrases like "用 VPS 跑下测试", "跑 deploy 回归", "测 recover 闭环", "test the installer on VPS", "VPS regression", "在 VPS 上验一下", "跑下 runbook" — even when the user doesn't name a specific runbook.
description: Run the ServerBee install.sh deploy / Agent re-enrollment end-to-end regression on a real Linux VPS — cross-compile the current branch locally, push the binary or docker image to the VPS, then execute install.sh against it and verify HTTPS + agent connection. Use this whenever the user wants to validate deploy changes on a real VPS, smoke-test install.sh after editing it, verify the Agent Authority re-enrollment flow, check that Caddy + Let's Encrypt automation still works, or test the binary vs docker agent install paths. Trigger on phrases like "用 VPS 跑下测试", "跑 deploy 回归", "测重新接入闭环", "test the installer on VPS", "VPS regression", "在 VPS 上验一下", "跑下 runbook" — even when the user doesn't name a specific runbook.
---

# ServerBee VPS e2e regression

End-to-end regression for ServerBee's `deploy/install.sh` against a real Linux VPS, using the **current branch's** code (not the released ghcr image). Two canonical runbooks already live in the repo:

- `tests/manual/full-deploy-e2e.md` — full lifecycle (server + agent docker + agent binary + uninstall)
- `tests/manual/agent-recover-e2e.md` — narrow recover-only flow (agent.toml refresh, ~3 min)
- `tests/manual/agent-reenrollment-e2e.md` — narrow Agent Authority re-enrollment flow

This skill orchestrates them autonomously. The runbooks are reference; SKILL.md routes + handles credentials + reports.

Expand All @@ -17,26 +17,26 @@ This skill orchestrates them autonomously. The runbooks are reference; SKILL.md
Run this skill whenever the user wants to exercise the deploy flow on a real VPS — explicitly named or not. Concrete triggers:

- After editing `deploy/install.sh` (any path)
- After editing `crates/server/src/router/api/server.rs` recover endpoint or related routes
- After editing front-end `recover-agent-dialog.tsx` / `regenerate-code-dialog.tsx` / `add-server-dialog.tsx` mutation paths
- After editing the Agent Authority re-enrollment, offer, or revocation routes
- After editing front-end `agent-reenrollment-dialog.tsx` / `enrollment-offer-dialog.tsx` / `add-server-dialog.tsx` mutation paths
- After editing `Dockerfile*` or the `docker-compose.*.yml` shapes install.sh generates
- After a sea-orm migration / OnboardingResponse change
- Whenever the user says some variation of "在 VPS 上跑一遍", "测下 deploy", "跑下 recover", "VPS 回归", "run the install.sh test"
- Whenever the user says some variation of "在 VPS 上跑一遍", "测下 deploy", "跑下重新接入", "VPS 回归", "run the install.sh test"

Do **not** trigger for: `cargo test` / `bun run test` unit tests, local-only smoke tests on the user's laptop, or anything that doesn't touch deploy/install.

## Step 1 — Pick the runbook

Default to **full-deploy**. Narrow to recover-only if and only if the change is clearly localized to the recover slice.
Default to **full-deploy**. Narrow to re-enrollment-only if and only if the change is clearly localized to the Agent Authority lifecycle slice.

| Recent change touches… | Use |
| --- | --- |
| `deploy/install.sh` main paths / `cmd_domain` / Caddyfile gen / `install_*_{server,agent}` | full-deploy |
| Migration / `auth.rs` middleware / OnboardingResponse / Dockerfiles | full-deploy |
| `Cargo.toml` / dep bumps where you want a smoke confirmation | full-deploy |
| Only the recover endpoint, recover-agent-dialog, or the `agent.toml` `else` branch (`toml_set` of `server_url` / `enrollment_code` / `token`) | agent-recover |
| Only Agent Authority routes, re-enrollment UI, or Agent-side staged-token claim recovery | agent-reenrollment |
| User says "全部跑一遍" / "complete deploy" / "from scratch" | full-deploy |
| User says "只测 recover" / "fast" / "just the refresh" | agent-recover |
| User says "只测重新接入" / "fast" / "just re-enrollment" | agent-reenrollment |
| Genuinely unsure | full-deploy (superset) |

State your choice in one sentence before going further: e.g. *"Running tests/manual/full-deploy-e2e.md because the change touches install.sh §install_docker_server."*
Expand Down Expand Up @@ -65,7 +65,7 @@ Read the chosen runbook top to bottom. Follow it section by section, but keep th

- **Binary mode test**: install.sh's `install_binary_agent` has an adopt-mode short-circuit at [`deploy/install.sh:1502`](../../../deploy/install.sh#L1502): `if [ -f "${INSTALL_DIR}/serverbee-agent" ] then ... "skipping download (adopting existing)"`. Before running install.sh in binary mode, `scp` your locally built `target/x86_64-unknown-linux-musl/release/serverbee-agent` to `/opt/serverbee/bin/serverbee-agent` and `chmod +x`. install.sh will then use *your* binary and still write agent.toml + systemd unit. Without this, install.sh downloads the released v1.0.0-alpha.4 binary from GitHub — useful for testing install.sh itself, but not testing your branch's binary.

- **Recover test** (the agent.toml `else` branch): the exact sequence is `uninstall agent --yes` (no `--purge` — that preserves agent.toml, which is the prerequisite for hitting the refresh branch) → recover endpoint with `revoke_immediately: true` → `install agent --enrollment-code <new>` → cat `/opt/serverbee/etc/agent.toml` and check the three fields (see §7.4 of the recover runbook). Directly re-running `install agent` without uninstall is rejected by install.sh's `meta_has` guard.
- **Re-enrollment test**: follow `tests/manual/agent-reenrollment-e2e.md` exactly. Exercise both `graceful` and `emergency`, use the exact visible offer ID for replacement or revocation, confirm the Agent persists its proposed run token before claim, and verify the old WebSocket is fenced. Do not expect the Server to return a run token.

- **Onboarding**: a freshly installed server has `must_change_password=true` on the admin user. Only `POST /api/auth/onboarding` is whitelisted (see `is_onboarding_whitelisted` in `crates/server/src/middleware/auth.rs`); calling `PUT /api/auth/password` first returns `MUST_CHANGE_PASSWORD` and stops you. Login → `/api/auth/onboarding` with `new_password` → then everything else opens up.

Expand All @@ -87,7 +87,7 @@ End with a summary in this exact shape so multiple runs are easy to compare:
```
## VPS e2e regression result

Runbook: <full-deploy | agent-recover>
Runbook: <full-deploy | agent-reenrollment>
VPS: <ipv4>, <distro/version>, <cpu/cores>
Domain: <domain>
Total: <m:ss>
Expand All @@ -100,7 +100,7 @@ Total: <m:ss>
- [✓ | ✗ | -] onboarding + create server
- [✓ | ✗ | -] install.sh install agent --method docker
- [✓ | ✗ | -] install.sh install agent --method binary
- [✓ | ✗ | -] recover flow (agent.toml three-field check)
- [✓ | ✗ | -] Agent Authority re-enrollment + fencing
- [✓ | ✗ | -] uninstall --purge → clean

### Evidence
Expand All @@ -113,7 +113,7 @@ Total: <m:ss>
<one sentence: PASS / PARTIAL / FAIL, plus the deciding stage>
```

Use `-` for stages the chosen runbook doesn't cover (e.g. recover-only skips most full-deploy stages). On a hard fail, name the runbook section, paste the exact error, and stop the remaining stages rather than charging through teardown.
Use `-` for stages the chosen runbook doesn't cover (e.g. re-enrollment-only skips most full-deploy stages). On a hard fail, name the runbook section, paste the exact error, and stop the remaining stages rather than charging through teardown.

## Failure-mode quick map

Expand All @@ -122,8 +122,8 @@ The canonical table is in [`tests/manual/full-deploy-e2e.md` §9](../../../tests
- `[ERROR] Failed to get latest version from GitHub` → VPS can't reach api.github.com (firewall / DNS), or you tried to use `RESOLVED_VERSION` env to override (doesn't work, line 745 nukes it).
- `MUST_CHANGE_PASSWORD` on `/api/servers` → forgot `POST /api/auth/onboarding` first.
- compose `Pulling` from ghcr instead of using local image → local tag isn't `${PROD_TAG}` exactly. Re-`docker tag` and re-run.
- `serverbee-agent.service: status=78/CONFIG` → enrollment code expired/used; recover for a fresh one, clear `token` line, restart.
- `serverbee-agent is already installed (...). Use 'upgrade'` → meta-file guard; for recover test you want `uninstall agent --yes` (no `--purge`) first, then re-install with new code.
- `serverbee-agent.service: status=78/CONFIG` → enrollment code expired/used; issue or exactly replace the current offer, then retry with the same staged run token.
- `serverbee-agent is already installed (...). Use 'upgrade'` → meta-file guard; use the dedicated temporary-Agent procedure in the re-enrollment runbook instead of overwriting a live installation.

## Related skills / docs

Expand Down
14 changes: 14 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,17 @@
- **Full server status** — the complete ~35-field snapshot (`ServerStatus`) carried by `full_sync` and REST. The only sources allowed to seed the catalog or overwrite static facts (totals, os, tags, geo, enrollment).
- **Rollup policy (降采样策略)** — how raw metric records compress over time: per-column aggregation (AVG vs window MAX), the raw/hourly table switch point, and which columns alerts may read. Owned by `service::rollup`; declared per column in the **metric column descriptor** (see ADR-0003). _Avoid_: aggregation logic, downsampling code.
- **Metric column descriptor** — the one-row-per-column table (`METRIC_COLUMNS`) declaring a scalar metric's SQL name, rollup aggregation, alert rule type, and typed accessor. Adding a scalar metric means adding one descriptor row (plus entity/migration, which the compiler enforces). The web analogue is `METRIC_CHART_SPECS` for chart display.
- **Enrollment code (Agent 注册码)** — a short-lived, single-use credential bound to exactly one pre-created Server identity. Possession authorizes one Agent enrollment; the plaintext is shown only when the code is issued.
- **Agent enrollment (Agent 注册)** — the act of an Agent claiming the Server identity bound to an Enrollment code. The Agent durably stages its proposed run token before submitting the claim, so authority does not depend on receiving a once-only secret in the HTTP response. This claim protocol is a hard cut: requests without an Agent-generated run token are rejected rather than routed through a legacy server-generated-token adapter. Enrollment does not collect a machine fingerprint or discover, create, merge, or deduplicate Server identities from host-derived identifiers. _Avoid_: Agent fingerprint, fingerprint-based enrollment, legacy enrollment claim.
- **Agent run token (Agent 运行令牌)** — the persistent, per-Server credential authorizing an Agent to connect as that Server identity. The Agent generates and durably stages this high-entropy secret before enrollment; the Server accepts its hash atomically with the claim and never returns the plaintext. Re-enrollment replaces it; Agent authority revocation removes it.
- **Agent authority (Agent 身份权限)** — whether a Server identity is currently claimed by an Agent with a valid run token. It is either **Claimed** or **Unclaimed**, independently of whether that Agent is currently online. The **Agent Authority** module owns Enrollment offer transitions, run-token transitions, and the resulting connection-fencing request; it does not own Server onboarding, profile data, or online/offline connection state (see ADR-0004).
- **Enrollment offer (Agent 注册许可)** — the current opportunity to claim a Server identity with an Enrollment code. An offer begins **Outstanding** and ends in exactly one immutable outcome: **Consumed**, **Revoked**, **Replaced**, or **Expired**. Replacement identifies the successor offer; expiry takes effect from `expires_at` without requiring a background task. Terminal offers remain non-blocking enrollment history. Offer lifecycle is independent of Agent authority, so a Claimed or Unclaimed Server may each have an Enrollment offer.
- **Enrollment offer replacement (注册码替换)** — the explicit exchange of one Outstanding Enrollment offer for a new one. Replacement identifies the exact offer being superseded; a missing or stale identity is rejected instead of silently applying last-writer-wins.
- **Pending Server (待接入服务器)** — a UI projection for an **Unclaimed** Server identity, not a separate enrollment lifecycle state. Online and offline are connection facts outside the enrollment model.
- **Server onboarding (Server 接入建档)** — the atomic, request-idempotent creation of a new Server identity, its operator-supplied profile, its default monitoring configuration, and its first Enrollment offer. Retrying the same onboarding request identity with the same normalized input returns the same Server; reusing it with different input conflicts. Server onboarding composes enrollment but is not part of Agent enrollment itself.
- **Agent re-enrollment (Agent 重新注册)** — a new enrollment of an existing Server identity, used when an Agent must be reinstalled or replaced without creating another Server or losing its history. Re-enrollment is either graceful or emergency. _Avoid_: Agent recovery, recover.
- **Graceful re-enrollment (平滑重新注册)** — re-enrollment in which the current Agent remains authorized while the Enrollment code is outstanding. Consuming the code transfers authority to the newly enrolled Agent and immediately invalidates the previous run token and live connection.
- **Emergency re-enrollment (紧急重新注册)** — re-enrollment in which the current Agent's run token and live connection are invalidated when the Enrollment code is issued, leaving the Server unclaimed until that code is consumed.
- **Agent authority revocation (Agent 身份权限撤销)** — an operator action that removes the current Agent run token and fences its live connection, leaving the Server **Unclaimed** without creating an Enrollment offer. Restoring authority requires enrollment. _Avoid_: run-token rotation, token rotation.
- **Agent authority event (Agent 身份权限事件)** — an immutable, structured, secret-free record of an Enrollment offer or Agent authority transition, including its actor, Server snapshot, related offer, mode, request source, and time. The **Agent Authority** module commits the event atomically with the state transition; generic best-effort audit logs are not lifecycle history. Authority events survive Server deletion until an explicit audit-retention purge, while credential hashes and offer rows are deleted with the Server.
- **Connection fencing (Agent 连接隔离)** — the hard guarantee that an Agent whose authority was transferred or revoked can neither continue on its existing WebSocket nor enter through a handshake that raced with the transition. Agent Authority transitions and final WebSocket admission serialize through the same per-Server lock, and the token is revalidated immediately before admission. A transition reports success only after its durable state, authority event, and runtime fence are complete.
34 changes: 2 additions & 32 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions ENV.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ Agent top-level keys use single underscore. Nested keys use `__` (double undersc
| Environment Variable | TOML Key | Type | Default | Description |
|---------------------|----------|------|---------|-------------|
| `SERVERBEE_SERVER_URL` | `server_url` | string | - (required) | Server HTTP base URL (e.g. `http://your-server:9527`). Agent appends API paths automatically |
| `SERVERBEE_ENROLLMENT_CODE` | `enrollment_code` | string | `""` | One-time enrollment code for first-time agent registration. Generated by an admin in the server UI Settings (or `POST /api/agent/enrollments`). Single-use and short-lived (default 10 min); only needed until the agent has a persisted token |
| `SERVERBEE_ENROLLMENT_CODE` | `enrollment_code` | string | `""` | One-time offer shown by Add Server or an Agent Authority re-enrollment action. Single-use and short-lived (default 10 min); used only while the Agent has no persisted run token |

### Common

Expand All @@ -185,7 +185,7 @@ Agent top-level keys use single underscore. Nested keys use `__` (double undersc

| Environment Variable | TOML Key | Type | Default | Description |
|---------------------|----------|------|---------|-------------|
| `SERVERBEE_TOKEN` | `token` | string | `""` | Agent authentication token. Auto-populated after registration, do not set manually |
| `SERVERBEE_TOKEN` | `token` | string | `""` | Agent run token. When empty during enrollment, the Agent generates and persists one before claim; an explicit environment override is used as provided and is not written to disk |
| `SERVERBEE_FILE__MAX_FILE_SIZE` | `file.max_file_size` | u64 | `1073741824` | Maximum file size in bytes for read/download (default 1GB) |
| `SERVERBEE_FILE__DENY_PATTERNS` | `file.deny_patterns` | string[] | `*.key,*.pem,id_rsa*,.env*,shadow,passwd` | Glob patterns for files the agent will refuse to access |
| `SERVERBEE_IP_CHANGE__EXTERNAL_IP_URLS` | `ip_change.external_ip_urls` | string[] | `["https://api.ipify.org","https://ifconfig.me/ip","https://icanhazip.com","https://checkip.amazonaws.com"]` | Ordered list of external IP services tried at startup and on every IP-change check. First success wins. Required for agents behind NAT, in containers, or anywhere interface enumeration can't see the routable public IP. Air-gapped deployments can set this to an empty list to skip external lookups entirely |
Expand Down
Loading
Loading