# WolfPanel Agent — v1 Development Foundation

A lightweight agent that runs on customer servers and talks to the central
WolfPanel control plane. This is the **v1 development foundation**: a clean,
extensible base — not a production release. Discovery is strictly read-only and
no destructive server actions are implemented.

> The WolfPanel application always runs on WolfPanel infrastructure. Customer
> servers run only this agent.

---

## Project structure

```txt
wolfpanel-agent/
├── README.md
├── VERSION                     # version file containing the raw version string (e.g. 0.1.0)
├── .gitignore                  # blocks *.pem/*.key/private_key.pem/release_private_key* etc.
├── install.sh                  # canonical installer (terminal + panel-assisted)
├── uninstall.sh                # safe, scoped uninstaller
├── docs/
│   ├── RELEASE_GUIDE.md        # key generation/storage, build/sign/publish/verify
│   ├── DEPLOYMENT_GUIDE.md     # downloads-server layout, post-deploy commands
│   └── ROLLBACK_GUIDE.md       # rollback mechanics + systemd cgroup caveat
├── scripts/
│   ├── build-release.sh        # release builder script
│   ├── sign_release.py         # production-key signing (no mock fallback)
│   ├── publish-release.sh      # release publisher script
│   ├── verify-release.sh       # published-artifact verifier
│   └── release.sh              # build + publish + verify, one command
├── systemd/
│   └── wolfpanel-agent.service # service unit template
├── config/
│   └── agent.conf.example      # documented config template
├── src/
│   ├── main.py                 # entrypoint
│   ├── config.py               # config + path resolution + lifecycle states
│   ├── logger.py               # structured logging + secret redaction
│   ├── api_client.py           # Central API client (real + mock, swappable)
│   ├── identity.py             # fingerprint, secret storage, credentials
│   ├── heartbeat.py            # periodic liveness + light metrics
│   ├── update.py               # update-check + production pipeline stubs
│   ├── cli.py                  # argument parsing + command handlers
│   └── discovery/
│       ├── system.py           # core facts + safe read-only subprocess helper
│       ├── services.py         # runtimes, systemd, databases, cron
│       ├── git.py              # git metadata (credential-sanitized)
│       ├── projects.py         # WordPress/Laravel/Node/Static classification
│       ├── quick.py            # fast baseline snapshot
│       └── full.py             # full inventory + snapshot persistence
└── tests/
    └── test_basic.py           # smoke tests (no network, temp dirs)
```

## Production directory layout (target)

The installer prepares this layout; `current` is an atomic-swap symlink so
future updates/rollbacks are a symlink flip.

```txt
/opt/wolfpanel/
├── current -> versions/dev
└── versions/dev/{wolfpanel-agent, src/}
/etc/wolfpanel/
├── agent.conf                              # static config only (systemd EnvironmentFile)
└── secrets/agent.token                     # 0700 dir, 0600 plain file
/var/lib/wolfpanel/
├── state.json                              # runtime status/last_error/registration_pending/revoked
└── {spool,jobs,discovery,cache}
/var/log/wolfpanel/{agent.log,jobs.log,update.log}
```

---

## CLI commands

```bash
wolfpanel-agent run                  # service mode: initial discovery + heartbeat loop
wolfpanel-agent register --token X   # exchange install token for agent credentials
wolfpanel-agent heartbeat            # send a single heartbeat
wolfpanel-agent discover --quick     # fast baseline snapshot (stdout)
wolfpanel-agent discover --full      # full read-only inventory (default)
wolfpanel-agent update-check         # report update availability (no download in v1)
wolfpanel-agent status               # local agent status
wolfpanel-agent version              # agent version
```

---

## How to run locally (no Linux, no Central API required)

The agent rebases all of its paths under a single folder when
`WOLFPANEL_DEV_HOME` is set, and uses a built-in mock API when
`WOLFPANEL_API_MOCK=1`. This makes the whole thing runnable on any machine.

```bash
cd wolfpanel-agent
export WOLFPANEL_DEV_HOME=/tmp/wolf-dev      # any writable dir; on Windows use a path
export WOLFPANEL_API_MOCK=1

python src/main.py version
python src/main.py register --token dev-token
python src/main.py status
python src/main.py heartbeat
python src/main.py discover --quick
python src/main.py update-check
```

Run the tests:

```bash
python tests/test_basic.py          # or: python -m pytest tests/
```

---

## How to install on a test Linux server

```bash
# From the repo checkout on the server:
sudo ./install.sh --token dev-token

# Or simulate the production one-liner (downloads + verifies a signed package):
curl -fsSL https://downloads.wolfpanel.net/agent/install.sh | sudo bash -s -- \
  --token TOKEN \
  --api-url https://api.wolfpanel.net \
  --channel stable
```

The installer: checks root → detects OS/arch → creates the directory tree →
installs agent files + the `wolfpanel-agent` wrapper → writes
`/etc/wolfpanel/agent.conf` → registers (if a token is given) → installs and
starts the systemd service → prints a summary.

Without a token it prints the **pairing-flow placeholder** (browser pairing is
intentionally not implemented in v1).

For a development server with no Central API, set the mock first:

```bash
echo "WOLFPANEL_API_MOCK=1" | sudo tee -a /etc/wolfpanel/agent.conf
```

Uninstall:

```bash
sudo ./uninstall.sh                 # interactive scope prompt
sudo ./uninstall.sh --scope 3       # remove everything incl. config + logs
```

---

## How to test each piece

| What | Command | Expected |
|---|---|---|
| **Register** | `python src/main.py register --token dev-token` (mock) | `agent.token` written; `WOLFPANEL_SERVER_ID` saved to `agent.conf`; install token never persisted |
| **Heartbeat** | `python src/main.py heartbeat` (mock) | `[mock] heartbeat ...` logged; `heartbeat ok` |
| **Discovery** | `python src/main.py discover --full` | JSON inventory printed; snapshot at `…/discovery/last.json`; nothing on the server changes |
| **Service loop** | `wolfpanel-agent run` then `journalctl -u wolfpanel-agent` | initial discovery, then heartbeats every 60s; survives API outages |

---

## Authentication & lifecycle (v1)

- **Install token** — temporary, used in-memory during `register` only, **never
  written to disk**.
- **Agent token** — returned by registration, stored at
  `/etc/wolfpanel/secrets/agent.token` (0700 dir, 0600 plain file). The agent
  has **no third-party Python dependencies** (stdlib only), so it installs and
  runs on a clean Linux server with no `pip`. At-rest sealing/encryption is a
  marked production TODO that must not reintroduce a runtime dependency.
- **Agent ID** — stable identifier, written to `agent.conf`.
- **Fingerprint** — opaque, stable hash of machine-id + hostname + MAC; supports
  future de-dup / re-registration.

Lifecycle states are defined in `config.LIFECYCLE_STATES`:
`pending_install, pairing_waiting, registered, online, offline, updating,
rollback, degraded, revoked, uninstalled`.

---

## Security rules enforced

- Install token never persisted; agent/refresh tokens never logged (see
  `logger.redact`).
- Discovery is **read-only**: no service start/stop/reload, no DB connections,
  no `.env` contents, no private keys. Git remote URLs are credential-sanitized.
- Secrets directory is root-only (`0700`); secret files are `0600`.
- No `git clone`/`git pull` for installation or updates — package-based model.
- The API client has a hard network timeout and never blocks the agent forever.

---

## Known limitations (v1)

- Browser **pairing flow** is a placeholder (`DEV-XXXX`); only `--token`
  registration works.
- **Update pipeline** (download → verify size/checksum/signature → stage →
  package-VERSION guard → atomic swap → rollback watcher) is implemented in
  `update.py`; the systemd rollback watcher fix has been reasoned through
  statically but not yet exercised on a real systemd VM — see
  [docs/ROLLBACK_GUIDE.md](docs/ROLLBACK_GUIDE.md) for the exact VM test to
  run before relying on it in production.
- **Panel-assisted install** is documented (SSH → same `install.sh`) but the
  panel side is out of scope for this repo.
- Secrets are **plain files** (no sealing/encryption yet).
- Remote command execution / task queue **not implemented** (foundation only).
- Discovery scans are **bounded-depth** under common roots and intentionally
  shallow; production will need configurable roots and incremental scanning.
- Some probes are Linux-specific and degrade to empty values on other OSes.

---

## Release Pipeline, Channels, and Operations

The WolfPanel Agent supports a dynamic, channel-based release pipeline (`stable`, `beta`, `dev`). All metadata (version, manifest_version, channel, tarball filename, SHA-256, size, signature) is parsed dynamically from the channel's `latest.json` file — there is no `url` field; installers and the agent always derive the download location themselves as `{RELEASE_BASE}/{CHANNEL}/{FILE}`.

Full details (key generation/storage, manifest schema, test-vs-production
key separation) are in **[docs/RELEASE_GUIDE.md](docs/RELEASE_GUIDE.md)**;
downloads-server layout and post-deploy commands are in
**[docs/DEPLOYMENT_GUIDE.md](docs/DEPLOYMENT_GUIDE.md)**; rollback mechanics
and the systemd cgroup caveat are in
**[docs/ROLLBACK_GUIDE.md](docs/ROLLBACK_GUIDE.md)**.

### Public Directory Structure Layout
The downloads server (`downloads.wolfpanel.net`) has the following directory structure layout:
```txt
PUBLIC_ROOT/
├── install.sh
├── uninstall.sh
├── stable/
│   ├── latest.json
│   ├── latest.tar.gz (alias to wolfpanel-agent-{version}.tar.gz)
│   ├── wolfpanel-agent-{version}.tar.gz
│   ├── wolfpanel-agent-{version}.tar.gz.sha256
│   └── checksums.txt
├── beta/
│   └── ... (same structure as stable)
├── dev/
│   └── ... (same structure as stable)
└── archive/
    ├── stable/
    ├── beta/
    └── dev/            # reserved layout for relocating old versioned tarballs; not automated
```

### 1. Release Command
To build a release for a specific channel (defaulting to `stable`):
```bash
# General syntax
bash scripts/release.sh [stable|beta|dev]

# Example: Release to stable
bash scripts/release.sh stable
```
This runs the full orchestration pipeline:
1. **`build-release.sh --channel <channel>`**: Generates a reproducible versioned archive (`wolfpanel-agent-${version}.tar.gz`), cryptographically signs it with `scripts/sign_release.py`, and writes `latest.json` (including `channel`, `size_bytes`, `published_at`).
2. **`publish-release.sh <channel>`**: Reads `deploy/release.conf`, publishes all versioned artifacts to the target channel subdirectory (`${PUBLIC_ROOT}/${CHANNEL}/`), updates the `latest.tar.gz` alias, copies the `install.sh`/`uninstall.sh` to the parent folder, and regenerates `checksums.txt`.
3. **`verify-release.sh <channel>`**: Validates that all files are present, that `latest.json`'s fields are well-formed and match the requested channel, that the tarball's size and SHA-256 match the manifest, and that the signature verifies against the embedded production public key.

### 2. Signing Configuration
Exactly two ways to supply the **production** signing key are supported — there is **no mock/fallback signing path**; a release build with neither set fails immediately with `Release aborted: no production signing key configured.`:
* `RELEASE_PRIVATE_KEY_FILE=/secure/path/release_private_key.pem`
* `RELEASE_PRIVATE_KEY="<PEM_CONTENT>"`

See [docs/RELEASE_GUIDE.md](docs/RELEASE_GUIDE.md) for key generation and why the test-only key used by `WOLFPANEL_API_MOCK=1` local runs is a completely separate keypair that can never verify a production-signed package (or vice versa).

### 3. Verification Command
To verify a deployment independently:
```bash
bash scripts/verify-release.sh [stable|beta|dev]
```

### 4. Rollback Operations
See **[docs/ROLLBACK_GUIDE.md](docs/ROLLBACK_GUIDE.md)** for the full mechanics, including a systemd cgroup issue found and fixed during hardening (the rollback watcher is now launched via `systemd-run` as an independent transient unit so `systemctl restart wolfpanel-agent` cannot kill it before it can act).

#### Automatic Rollback
If the agent fails to check in after an update, the rollback watcher automatically triggers and points the `/opt/wolfpanel/current` symlink back to `/opt/wolfpanel/versions/{VERSION}.backup`, then restarts the service.

#### Manual Rollback
To manually roll back to a previously installed version:
1. Change the symlink to point to the desired version directory:
   ```bash
   sudo ln -sfn /opt/wolfpanel/versions/<desired-version> /opt/wolfpanel/current
   ```
2. Restart the systemd service:
   ```bash
   sudo systemctl restart wolfpanel-agent
   ```
