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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 2.5.0

Assistants get the whole recovery layer over MCP, with a guide for connecting your own.

**Update the helper on the hub.** The phone app and the computers' helpers don't need 2.5.

- **New MCP tools on the hub.** `list_machines` and `get_events` show what the hub sees, including frozen computers and the evidence. `propose_reset` and `propose_force_off` ask you to reset a computer, or force it off, and `get_recovery` follows the request to the end. `withdraw_recovery` takes back the agent's own request. There's still no way for an agent to confirm: only your phone does. The hub's MCP instructions tell the model the rules. A helper with no KVM linked lists only the 5 device tools.
- **Which key asked stays private.** Other keys no longer see which key proposed a recovery (`proposerKey`): not in `GET /v2/machines`, not on the event stream, not over MCP. Webhooks never carry it.
- **`powerdeck-helper webhook add|list|test|rm`** on the hub. Until now a webhook could only be added through the API.
- **Guides:**
- [Integrating an assistant](docs/integrating-an-assistant.md): keys, MCP, REST with a pinned certificate, the event stream and webhooks, a recovery step by step, what to do with each result, the rules, and running your own copy. Its examples run against a test hub in CI, so they stay true.
- [Hardware test](docs/hardware-test.md): a checklist for a real NanoKVM and computer, from the power light to a real freeze and a reset that doesn't work.
- The app's **Full guide on GitHub** link (Allow AI agents) opens the assistant guide.

## 2.4.0

A frozen computer can now be reset with one tap from a notification, and an assistant can ask for a reset that you confirm.
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,17 @@ Let an AI agent or a script check and control your devices through [MCP](https:/

Port 47323 is plain HTTP for agents that can't pin a self-signed certificate. It's only reachable over Tailscale, which encrypts it end to end. If your agent can pin a certificate, use `https://<server>:47321/mcp` instead.

Tools: `list_devices`, `get_status`, `power_on`, `power_off`, `cancel`. Whatever the agent asks for, the helper enforces these limits:
Tools: `list_devices`, `get_status`, `power_on`, `power_off`, `cancel`. On the hub that drives your NanoKVMs, also `list_machines` and `get_events` (what the hub sees, including frozen machines), `propose_reset` and `propose_force_off` (ask you to reset a frozen machine), `get_recovery` and `withdraw_recovery`. Whatever the agent asks for, the helper enforces these limits:
- only the devices and permissions on its key;
- never while someone is using the computer, and always with the 60-second countdown;
- at most one shutdown per device per 10 minutes;
- never the computer the agent itself runs on.
- never the computer the agent itself runs on;
- an agent never resets or forces off a computer itself. It asks, and nothing is pressed until you tap **Reset** on your phone.

The key works only from the first device that uses it. Every action appears in History and in a notification on your phone. Each device's **Allow AI agents** switch turns agents away from it at any time: they can still see whether it's on, but can't turn it on or off.

Connecting your own assistant, with REST, live events and webhooks too: [docs/integrating-an-assistant.md](docs/integrating-an-assistant.md). Testing it all on real hardware: [docs/hardware-test.md](docs/hardware-test.md).

## Troubleshooting

<details>
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ PowerDeck can shut computers down, wake them, and (through a NanoKVM) press thei

- **Other devices on the tailnet** may be hostile, including other devices signed in to your own Tailscale account. A helper fully trusts only its **owner devices**: nodes recorded by scanning its code (which carries a one-time pairing code: 8 characters, 40 bits, 10 minutes, single use, 5 wrong guesses and it's gone) or by `powerdeck-helper owner add` on the machine. `tailscale whois` identifies them on every request. A phone on another account that enters the code gets a key for that device only, without admin: it can power it on and off but can't manage keys, settings or owners, relay to other devices, use MCP, or schedule waking others. Ten failures from one address block it for 10 minutes. The phone pins each helper's certificate.
- **Helpers trust each other only as far as their peer keys** (status, wake and power, bound to one node), which the owner's app sets up. A compromised helper can power the others on and off through them, but can't change their settings, keys or owners, or relay further.
- **AI agents and scripts** may misbehave. Their keys carry only the scopes and devices they were given, can't override someone using the device, always leave a 60-second countdown, are rate-limited (one shutdown per device per 10 minutes, six wakes per hour), can't shut down the device the agent runs on, and are bound to the tailnet node that first uses them. Every action is logged with the key's name.
- **AI agents and scripts** may misbehave. Their keys carry only the scopes and devices they were given, can't override someone using the device, always leave a 60-second countdown, are rate-limited (one shutdown per device per 10 minutes, six wakes per hour), can't shut down the device the agent runs on, and are bound to the tailnet node that first uses them. They can't reset a computer or force it off: they can only propose it, and nothing is pressed until an owner device confirms it (not an agent, not a hub relaying for one), at most once per computer every 30 minutes. A proposal's reason is shown to the owner quoted, as the agent's words, and which key proposed it is shown only to the owner and that key. Every action is logged with the key's name.
- **The local network** is not trusted. Helpers listen only on loopback and their Tailscale addresses (never on all interfaces), check that every caller has a tailnet or loopback address, and on Windows the firewall rule allows the Tailscale range only. The MCP port 47323 is plain HTTP for agent tools that can't pin a self-signed certificate: it serves only `/mcp`, is bound to the Tailscale addresses, and Tailscale's WireGuard encrypts it end to end. Prefer `https://<hub>:47321/mcp` where the agent supports it.
- **The phone sends no plain HTTP** except to Tailscale (`*.ts.net`) names, which is how it reaches NanoKVMs (they only speak HTTP). Everything else, including every helper, is TLS.
- **A KVM wired wrongly** could press the wrong button. PowerDeck presses to turn on only when the computer's helper doesn't answer and the power light reads off, and only after it has seen that light on (an unwired LED reads off forever). Linking checks that the light agrees with the computer's helper. Whether the power and reset switches are wired is learned from what a press does, never assumed.
Expand All @@ -28,7 +28,7 @@ PowerDeck can shut computers down, wake them, and (through a NanoKVM) press thei
|---|---|
| Helper's TLS private key, issued keys (SHA-256 hashes only), local CLI token | `C:\ProgramData\PowerDeck` (SYSTEM and Administrators only) or `/var/lib/powerdeck` (root only) |
| Phone's helper keys (other accounts only), NanoKVM passwords | Encrypted with an AES-256-GCM key in the Android Keystore (non-exportable). App backups are disabled |
| NanoKVM passwords on the hub | In the helper's state file, sealed: on Linux with AES-256-GCM under a random key in `/etc/powerdeck/secret.key` (root only, kept apart from `/var/lib/powerdeck` so a copy of the data folder or a backup of it doesn't give them away); on Windows with DPAPI for the service account. Root or an administrator on the hub can still open them. Uninstalling from the app (or with `--purge`) deletes the key |
| NanoKVM passwords and webhook secrets on the hub | In the helper's state file, sealed: on Linux with AES-256-GCM under a random key in `/etc/powerdeck/secret.key` (root only, kept apart from `/var/lib/powerdeck` so a copy of the data folder or a backup of it doesn't give them away); on Windows with DPAPI for the service account. Root or an administrator on the hub can still open them. Uninstalling from the app (or with `--purge`) deletes the key |
| Update-signing key, APK signing key | GitHub Actions secrets of this repository (and the maintainer's offline backup) |

## Known limits
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import androidx.compose.ui.unit.dp
import net.odysseylabs.powerdeck.data.Device
import net.odysseylabs.powerdeck.data.KeyInfo

const val AGENTS_GUIDE = "https://github.com/ProjectZero02/powerdeck#ai-agents-mcp"
const val AGENTS_GUIDE = "https://github.com/ProjectZero02/powerdeck/blob/main/docs/integrating-an-assistant.md"

data class AgentsActions(
val back: () -> Unit = {},
Expand Down
125 changes: 125 additions & 0 deletions docs/hardware-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Hardware test

The automated tests use a fake NanoKVM. This checklist is for the real thing. Run it once after
wiring a NanoKVM to a computer, and again after a NanoKVM firmware update or any rewiring. It takes
about an hour.

**Save your work on the computer first.** Steps 7 to 10 cut its power.

Each step says what to do and what you should see. When something doesn't match, note the step,
what the app said, and the hub's **History** (the hub's page in the app), and report it. Don't work
around it.

## Before you start

- [ ] The phone app, the hub and the computer run PowerDeck 2.5 or later.
- [ ] The NanoKVM's ATX board is wired to the computer's front-panel header: power switch, reset
switch and power LED. The LED pins are polarised; your motherboard manual shows the header.
- [ ] Write down the motherboard model, and whether the case has its own reset button. Both
matter if a step fails.
- [ ] The NanoKVM has its own account for PowerDeck, not `admin`
([NanoKVM hardening](nanokvm-hardening.md)).
- [ ] In the app, the computer's page → **Link a NanoKVM** is done, and the computer's helper
answers.

Most steps are read from the computer's page in the app. **Power button** shows the KVM and
what the hub has learned about each lead (power light, power button, reset button). While the
computer's helper is silent, **What *hub* sees** shows whether PowerDeck on it answers, the
network, and the power light.

The page shows changes within seconds. Notifications can take up to about 15 minutes, because
Android lets the app check in the background only that often.

## A. The power light

| # | Do | Expect | ✓ |
|---|---|---|---|
| 1 | With the computer on, wait a minute and open its page | **Power light: Wired** | |
| 2 | Sleep the computer (Start → Power → Sleep), wait 2 minutes, open its page | Asleep, or **May be asleep** if your board keeps the light on while asleep. Never frozen | |
| 3 | Wake it from the app | It answers again within about a minute | |

If step 1 says the light reads off while the computer runs, the LED lead is unplugged or the wrong
way round. Turn the plug round. Nothing is pressed until the light has read on once.

## B. Off and on

| # | Do | Expect | ✓ |
|---|---|---|---|
| 4 | **Shut down** from the app | A 60-second warning on the computer, then off. The light reads off within a minute | |
| 5 | **Turn on** from the app | The hub presses power. The light comes on and the helper answers within about 90 seconds. **Power button: Works** | |
| 6 | **Restart**, then **Cancel** within the 60 seconds | The warning goes away and nothing happens | |
| 6b | Unplug the KVM's power lead, shut down from the app, and **Turn on** | The press does nothing, so the hub sends Wake-on-LAN instead and the computer starts. **Power button: A press did nothing** | |
| 6c | Plug the power lead back in and tap **Check again** under **Power button** | Every lead is back to being checked; nothing is pressed | |

Step 6b needs Wake-on-LAN on in the BIOS. On many boards that's *ErP Ready: Disabled* and *Power
On By PCI-E: Enabled*. If it doesn't wake, PowerDeck still works through the power button, but
note it.

## C. The memory test, in a VM first

This test shows what a freeze from running out of memory looks like before you freeze the real
computer. Use a VM with the PowerDeck helper installed and no swap, or one with little swap.

The memory hog keeps taking 256 MB more and writes to all of it, until the system gives
out. Stop it with Ctrl+C while you still can:

```sh
python3 -c "a=[]; [a.append(b'x'*(256<<20)) for _ in iter(int, 1)]"
```

| # | Do | Expect | ✓ |
|---|---|---|---|
| C1 | In the VM, use up all memory: run the memory hog below, or open things until it stalls | In the app, the VM's helper stops answering within a few minutes. Note how long it took, and whether the app's status for it ever answers slowly instead | |
| C2 | Stop the memory hog (or reset the VM from its host) | The helper answers again. Note how long it took | |

The VM has no NanoKVM, so the hub can't call it frozen. This step is only about whether a
memory-starved machine stops answering at all.

## D. A real freeze

A common freeze is running out of memory, for example in WSL. This reproduces one on purpose. If
your computer froze some other way, use that instead.

| # | Do | Expect | ✓ |
|---|---|---|---|
| 7 | Save your work. In WSL, run the memory hog until Windows stops responding. WSL is capped at half the PC's memory unless `.wslconfig` raises its `memory=` limit, so you may need to raise it first | The computer's page says **Not answering**, then, after 3 minutes, **Frozen since** *time*, with "*hub* suggests a reset" and **Reset** and **Not now**. The same comes as a notification | |
| 8 | Before answering, write down **What *hub* sees** | PowerDeck on it: not answering. Network: no reply to ping. Power light: on | |
| 9 | Tap **Reset** (on the page, or in the notification on an unlocked phone) | "Resetting…", then within 5 minutes "*computer* is back". **Reset button: Works**. The hub's History: `confirm reset`, then `answers again after the reset` | |

**If step 7 never says frozen**, step 8 says why. If the network answers ping while the computer is
frozen, the page says **Running, but PowerDeck on it isn't answering** and the hub won't suggest a
reset; you can still press **Reset** on its page. Report what step 8 showed: the hub's rules are
built on it.

## E. A reset that doesn't work

| # | Do | Expect | ✓ |
|---|---|---|---|
| 10 | Unplug the KVM's reset lead. Freeze the computer again (step 7) until its page says frozen, then press **Reset** on its page | After 5 minutes: "*computer* didn't come back", with **Force off**. A notification follows on the alerting **Frozen machines** channel | |
| 11 | Tap **Force off** | The hub holds power for 12 seconds and the light goes off. Turn it on from the app | |
| 12 | Plug the reset lead back in and tap **Check again** under **Power button** | Every lead is back to being checked; nothing is pressed | |

The hub doesn't suggest a reset within 30 minutes of the last one, so step 10 uses the page's own
**Reset** button. Your own buttons are never held back.

## F. An assistant

Start at least 30 minutes after section E. You need an agent key for the computer that **Can shut
down and restart**, as in [integrating-an-assistant.md](integrating-an-assistant.md).

| # | Do | Expect | ✓ |
|---|---|---|---|
| 13 | Have the assistant call `list_machines` while the computer runs | The computer is `on` | |
| 14 | Have it call `propose_reset` while the computer runs | Refused: `helper_answers` | |
| 15 | Freeze the computer (step 7). Once its page says frozen, have the assistant call `propose_reset` | `created: false`: it gets the hub's own request back. Nothing is pressed | |
| 16 | Tap **Not now** | The assistant sees `declined` with `get_recovery`. Nothing is pressed | |
| 17 | Have it call `propose_reset` again, confirm in the app, and follow it with `get_recovery` | `running`, then `back` | |
| 18 | Turn off the computer's **Allow AI agents** switch and have it propose again | Refused: `agents_off` | |

Turn the switch back on afterwards if you want the assistant to keep its access.

## When you're done

- [ ] Every row is ticked, or reported with what the app and History said.
- [ ] Automatic resets (the hub's page → **Frozen machines**) are left as you want them. They're off
unless you turn them on.
Loading
Loading