AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
Whiskers is a self-hosted control plane for Docker hosts and Kubernetes workloads. Instead of handing operators or AI agents unrestricted SSH access, it exposes explicit tools governed by permissions,
개발 도구C# · HTMLGitHub @LupusMalusDeviant온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
Self-hosted control plane that gives AI agents production access to Docker hosts and Kubernetes without handing them credentials: MCP server with scoped keys, code-enforced guardrails, human approvals and a full audit trail. SSH-key-free (mesh + mTLS).
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Controlled infrastructure operations for humans and AI agents.
Whiskers is a self-hosted control plane for Docker hosts and Kubernetes workloads. Instead of handing operators or AI agents unrestricted SSH access, it exposes explicit tools governed by permissions, code-enforced guardrails, approvals and a complete action history.
Bootstrap a host once, then operate over mesh + mTLS without a standing private SSH key.
⚠️ Beta. Whiskers is under active development and not yet API-stable. Run it on a trusted network, review the security policy, and expect breaking changes before
1.0.
AI agents can operate infrastructure — but the usual way to let them is to hand over an SSH key or a shell, which grants far more power than any single task needs. Whiskers puts a governed layer in between:
The same control plane reaches Docker, Kubernetes, logs, deployments, services and CVE context — but that reach is evidence of coverage, not the headline. The headline is governed, accountable operations. See docs/product/POSITIONING.md for the full positioning.
flowchart LR
H[Human operator]
A[AI agent]
CP[Whiskers control plane]
AUTH[Authentication & permissions]
GR[Code-enforced guardrails]
AP[Human approval]
TOOLS[Explicit infrastructure tools]
INFRA[Docker / Kubernetes / hosts]
AUDIT[Agent history & audit log]
H --> CP
A --> CP
CP --> AUTH
AUTH --> GR
GR -->|allow| TOOLS
GR -->|confirm| AP
AP -->|approved| TOOLS
GR -->|block| AUDIT
TOOLS --> INFRA
TOOLS --> AUDIT
AP --> AUDIT
Prebuilt multi-arch images (amd64 / arm64) are published to
ghcr.io/lupusmalusdeviant/whiskersfrom v0.12.0. Before that release, use the From source path below.
# Installer: pulls the image, writes a small compose file in ./whiskers/, waits until healthy, prints the URL.
curl -fsSL https://raw.githubusercontent.com/LupusMalusDeviant/Whiskers/main/deploy/install.sh | bash
# ...or a bare docker run:
docker run -d --name whiskers -p 127.0.0.1:5100:8080 \
-v whiskers-data:/app/data -v /var/run/docker.sock:/var/run/docker.sock \
ghcr.io/lupusmalusdeviant/whiskers:latest
Open http://127.0.0.1:5100. The installer accepts --port, --bind, --data, --yes (install.sh --help); re-running it updates in place (pull + up).
Download docker-compose.yml from the latest release (already pinned to the image), then docker compose up -d. Optional settings go in a .env beside it — everything is optional, Whiskers runs with none (see .env.example).
The app listens on 127.0.0.1:5100 by default (HOST_BIND / HOST_PORT) and never binds publicly unless you change the bind.
On first launch a guided setup wizard in the browser creates the admin account — no .env and no config file required. Because whoever completes the wizard becomes Admin, it first asks for a one-time setup token that only someone with access to the server can read: docker exec whiskers cat /app/data/setup-token.txt (the installer prints it; Helm users kubectl exec the same path, or preset it with WHISKERS_SETUP_TOKEN). From then on you sign in with that local login (email + password; 5 wrong passwords lock the account for 15 minutes, and login attempts are rate-limited per client). Alternatives: seed the admin unattended with WHISKERS_ADMIN_EMAIL + WHISKERS_ADMIN_PASSWORD_FILE; use Google or generic OIDC (see Configuration); or, for a trial on a trusted network, AUTH_DISABLED=true — every request is then an Admin, so Whiskers only answers clients from private networks, with an IP or localhost as host name and without a foreign Origin (see Configuration).
Whiskers is self-hosted: there is no hosted or cloud endpoint. Your MCP client talks to your own instance at
/mcp (Streamable HTTP) with an API key created under Settings → MCP:
{
"mcpServers": {
"whiskers": {
"type": "http",
"url": "http://localhost:5100/mcp",
"headers": { "Authorization": "Bearer <WHISKERS_MCP_API_KEY>" }
}
}
}
Details, scopes and the guardrail behaviour are in MCP server.
location /whiskers/ {
proxy_pass http://127.0.0.1:5100/whiskers/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /whiskers;
proxy_read_timeout 300s;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
}
When serving under a subpath, set PATH_BASE=/whiskers in .env.
docker-compose.yml, default): privileged + host PID namespace so Whiskers can manage
its own host (firewall/Nginx/systemd/execute_command via nsenter) and run an in-container VPN.docker-compose.hardened.yml): for monitoring remote hosts only — non-root, no
privileged, dropped capabilities, read-only rootfs, and Docker access through a verb-restricted
socket-proxy. Trades away local host-shell management for a much smaller attack surface.See docs/container-hardening.md for the full matrix and trade-offs.
Run Whiskers on a cluster as the control plane for your remote Docker fleet (and Kubernetes clusters — pods appear next to containers on the dashboard):
helm install whiskers oci://ghcr.io/lupusmalusdeviant/charts/whiskers \
--set vault.key="$(openssl rand -hex 32)"
kubectl port-forward svc/whiskers 8080:8080 # → http://localhost:8080 (setup wizard)
Single-replica by design (Blazor Server + stateful loops), non-root, read-only rootfs, PVC-backed data. All values, ingress/WebSocket notes and the Postgres variant: deploy/helm/whiskers/README.md. To manage a cluster from Whiskers instead, see deploy/k8s/README.md.
git clone https://github.com/LupusMalusDeviant/Whiskers.git
cd Whiskers
docker compose up -d # builds the image locally
Copy .env.example to .env to override any defaults. .env is gitignored — secrets never land in the repository.
The flow that defines the product — Observe → Propose → Check policy → Request approval → Execute → Verify → Audit:
This is the reference workflow for AI-operations onboarding and the acceptance test for governance.
Docker, Kubernetes and the rest are targets the control plane governs — grouped by what they do for you:
Confirm tool pauses execution until an authorized user approves the exact action + parameters.X-Hub-Signature-256-compatible) and fail-closed authorization — every endpoint requires authentication unless it explicitly opts out.Whiskers manages each onboarded host across three separate planes. None of them uses SSH in steady state, and none of them needs a stored SSH key:
| Plane | What | Transport |
|---|---|---|
| Telemetry | host CPU/RAM/disk, container stats | node_exporter → VictoriaMetrics (pull over the mesh) |
| Docker API | list/restart/deploy/inspect | Docker Engine API over mTLS (client certificate, CN-pinned) + verb-whitelisting socket proxy |
| Host control | execute_command, systemd, journald, edits | one-shot privileged nsenter container, driven over the same mTLS Docker channel |
SSH is a bootstrap channel only. Onboarding opens a single SSH connection — with host-key verification (trust-on-first-use, pinned afterwards) — to join the host to a private WireGuard mesh (Tailscale/NetBird), roll out the mTLS certificates and the Docker proxy, and install telemetry. The bootstrap credential (uploaded key or a password held in memory only) is then deleted. From that point on the host is operated key-free over the three mTLS/mesh planes; Whiskers holds no standing SSH key for it. (A server that was added as a plain SSH connection and never onboarded keeps using SSH until it is migrated.)
What these planes are, honestly: the Docker-API and host-control planes are root-equivalent on
the managed host — creating a container or running nsenter is root by construction. mTLS and the mesh
decide who can reach that power; scopes, guardrails and approvals decide what a given caller may do
with it. The full design, PKI (step-ca), onboarding flow and trade-offs are in
docs/ARCHITECTURE.md; the threat model is in SECURITY.md.
Confirm. See How the policy layer applies.api-keys.json no longer exists (see the MCP server section)..env, the encrypted vault, the DB) live under /app/data / .env, both gitignored and volume-mounted — never baked into the image or committed./metrics.Every published release image is, from v0.12.1 on:
Verify the signature before you run it:
cosign verify ghcr.io/lupusmalusdeviant/whiskers:0.15.0 \
--certificate-identity-regexp '^https://github.com/LupusMalusDeviant/Whiskers/.github/workflows/release.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
A valid result means the image was built by this repository's release workflow from a tag — not rebuilt or tampered with. (Images published before v0.12.1 are provenance-attested but not cosign-signed.)
If you discover a security issue, please report it privately — see SECURITY.md.
Whiskers ships an integrated MCP server (Streamable HTTP, endpoint /mcp) so AI agents (e.g. Claude Code)
can operate your infrastructure. Add it to your MCP client:
{
"mcpServers": {
"whiskers": {
"type": "http",
"url": "https://whiskers.example.com/mcp",
"headers": { "Authorization": "Bearer <WHISKERS_MCP_API_KEY>" }
}
}
}
Behind a reverse proxy on a subpath (PATH_BASE=/whiskers) the URL becomes
https://example.com/whiskers/mcp; for a local instance it is http://localhost:5100/mcp.
Note for older index entries (e.g. MCP directories that crawled the ServerWatch-era README): Whiskers was called ServerWatch before the rebrand in v0.12. The old flat, unscoped key file
/app/data/api-keys.jsonis gone: its last compatibility fallback was removed in v0.15.0, no code path reads it, and a leftover copy is deleted on start. MCP access works only with a scoped key created in the web UI, and those keys are stored as SHA-256 hashes, never in plaintext.
Tools span:
execute_command (Admin)deploy_app, deploy_composeinstruct_agent (delegate a natural-language task to the in-process agent)The complete, current list with permission levels is in docs/mcp-tool-catalog.md and in the web UI under Settings > MCP.
Prompts (user-invokable workflows; they only return instructions, every tool call they lead to is still scope- and guardrail-checked):
| Prompt | What it does |
|---|---|
triage_unhealthy_containers | Read-only diagnosis of unhealthy/stopped/restarting containers, ranked fixes |
security_review_server | Read-only review of one server: CVEs, firewall, TLS expiry, unexpected services |
plan_safe_update | Risk, blast radius and CVE check for an image update, plan with rollback — stops before changing anything |
investigate_host_load | Explains host load, including load no container accounts for |
Resources (read-only context about the MCP surface itself, no infrastructure data):
| URI | Content |
|---|---|
whiskers://tools/catalog | Every served tool with its required level and whether it is enabled |
whiskers://me/permissions | The caller's own scope and the tools it may call |
Whiskers' AI guardrails are a deterministic policy-enforcement layer (Read / Write / Admin scopes, action classification and human-in-the-loop approvals), evaluated in code at the tool boundary — never in a prompt, so no model output can talk its way past it.
| Caller | Scope check (Read / Write / Admin, tool allow-list) | Guardrail engine (Allow / Confirm / Block) | Recorded in |
|---|---|---|---|
| External MCP client, direct tool call | ✅ by API key | ✅ every call; Confirm raises an approval, and the identical call runs once after a human approved it (approve-then-retry, valid 10 min) | Agent History |
Signed-in web user calling /mcp | ✅ by role (Viewer → Read, Operator → Write, Admin → Admin) | ✅ same as above | Agent History |
In-process agent (triggered from the UI or via MCP instruct_agent) | ✅ capped at the triggering user's / key's scope | ✅ every call; Confirm pauses the run until a human approves the exact action + parameters | Agent History |
Approvals are resolved on the Approvals page; approving requires at least the level the action needs. The guardrail engine is part of the agent module (on by default). With that module switched off there is no Approvals page either, and direct calls are bounded by their scope alone.
Beyond the MCP server (which serves external agents), Whiskers has an in-process acting agent: you describe an operations task in natural language and it plans and executes using Whiskers's own tools. It supports multiple LLM providers (OpenAI, OpenRouter, Ollama, Gemini, Anthropic, and Claude Code) selectable in the UI. An optional read-only advisor chat is also available — it explains and suggests but runs nothing.
Its safety model is enforced in code, not in the prompt:
guardrails.json) are a deterministic policy-enforcement layer: Allow/Confirm/Deny rules evaluated at the tool boundary, "most-restrictive wins", identical whether the agent was started from the web UI or via MCP.See src/Whiskers/Services/Agent/ for the implementation.
Whiskers is configured entirely through environment variables (.env); everything is optional. The most important groups:
| Group | Keys | Notes |
|---|---|---|
| Authentication | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_ADMIN_EMAIL, AUTH_DISABLED, WHISKERS_SETUP_TOKEN | AUTH_DISABLED=true makes every request an Admin; it is meant for trusted LAN-only trials. Requests are then only served from loopback/private networks (Auth__DisabledAllowedNetworks__0=… to narrow), with an IP or localhost as host name (Auth__DisabledAllowedHosts__0=whiskers.lan to add one) and no cross-site Origin. WHISKERS_SETUP_TOKEN presets the first-run setup token. |
| Admin bootstrap | WHISKERS_ADMIN_EMAIL | Seeded as an Admin on first run (provider-neutral; GOOGLE_ADMIN_EMAIL does the same for Google) so a fresh instance is never admin-less. Also flips the email whitelist fail-closed once any role exists. Existing installs (with a roles.json) are untouched. |
| Local login | WHISKERS_ADMIN_PASSWORD_FILE, Auth__LocalLogin__Enabled | Username/password login without an IdP (ASP.NET Identity, its own tables in the same DB). On by default; set Auth__LocalLogin__Enabled=false for federated-only. Password policy: min 12 chars. |
| Secrets vault | VAULT_KEY | Passphrase that encrypts stored secrets at rest (AES-256-GCM). Empty = vault disabled. Keep it stable — losing it makes stored secrets undecryptable |
| OIDC (optional) | OIDC_ENABLED, OIDC_AUTHORITY, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, ... | Generic OpenID Connect (Authentik, Keycloak, Authelia, Zitadel, ...) for real 2FA/passkeys from your IdP |
| Notifications | MATTERMOST_WEBHOOK_URL, MATTERMOST_ENABLED | Matrix is configured in the UI |
| Routing | PATH_BASE | Path prefix when reverse-proxied under a subpath |
| AI chat | AICHAT_ENABLED, AICHAT_API_KEY, AICHAT_API_URL, AICHAT_MODEL, AICHAT_PROVIDER | Read-only advisor chat |
See .env.example for the full, commented list.
WHISKERS_DATA_DIR, default /app/data); never in the image./healthz (liveness) and /readyz (readiness) endpoints; the image ships a Docker HEALTHCHECK and Kubernetes can use them as probes. (/health without the z is the in-app status page.)| Layer | Technology |
|---|---|
| Backend | C# / .NET 10 / ASP.NET Core |
| Frontend | Blazor Server + MudBlazor |
| Docker API | Docker.DotNet |
| Database | SQLite (zero-config default) or PostgreSQL (Entity Framework Core) + JSON file stores |
| Auth | Local accounts (ASP.NET Identity), Google OAuth 2.0 or generic OIDC + roles & email whitelist |
| Real-time | SignalR |
| MCP | ModelContextProtocol.AspNetCore |
| Metrics | VictoriaMetrics (Prometheus-compatible) |
The screens below trace the governance model end to end — controlled access, enforced governance, complete accountability. A live walkthrough of the hero workflow (unhealthy workload → agent analysis → guardrail decision → approval → execution → audit trail) lives on the project website: whiskers.app.lupusmalus.dev, and the in-app handbook (Help) carries screenshots for every area.
Guardrails — enforced governance. A code-enforced, non-bypassable policy checked at the tool boundary (not in the prompt): a read-only kill switch, the highest level allowed without confirmation, protected resources, and forbidden argument patterns.
Agent History — controlled access. Every agent/MCP tool call, with actor, privilege level (read / write / admin), the guardrail verdict (Allow / Confirm / Deny), duration and outcome.
Audit Log — complete accountability. The approval and execution trail: who approved what, which action a guardrail blocked, and the resulting change.
Secure AI Operations — a governed starting point. Connect a client, mint a least-privilege read-only key, and activate a starter guardrail preset (Observe only / Safe operations / Approval required).
Dashboard — the reach. Real-time status across Docker hosts and Kubernetes workloads, with live host metrics — the surface the governance model sits in front of.
Screenshots are captured against an anonymized demo dataset. See docs/product/demo-script.md for the scripted walkthrough and docs/product/screenshots.md for how each image is reproduced.
Beta is feature-rich but not finished. Planned / not-yet-implemented:
Accounts & roles
Monitoring & triggers
Fleet & deployment
Agent governance (building on the shipped Agent-History, approvals/human-in-the-loop and rich chat widgets)
tools/call requests are recorded but not held for approval.Hardening & resilience
Have a request? Open an issue.
Each source folder carries its own README.md describing the files within. High-level map:
Whiskers/
├── src/
│ ├── Whiskers/ # the application
│ │ ├── Components/ # Blazor UI (Pages, Layout, Shared)
│ │ ├── Configuration/ # strongly-typed settings classes
│ │ ├── Hubs/ # SignalR hubs (container + terminal streams)
│ │ ├── Mcp/ # MCP server tools + permission layer
│ │ ├── Models/ # data models (Agent, Cloud, Cve, Hetzner, Hostinger)
│ │ ├── Services/ # all business logic (see Services/README.md)
│ │ ├── Utils/ # small helpers (secret redaction, shell quoting)
│ │ ├── wwwroot/ # static assets
│ │ └── Program.cs # composition root (DI, middleware, MCP, auth)
│ └── Whiskers.Tests/ # xUnit test suite
├── deploy/telemetry/ # mesh/mTLS deploy templates (node_exporter, VictoriaMetrics, Tailscale ACL)
├── docs/ARCHITECTURE.md # architecture, security-critical areas & change control
├── docs/product/ # product positioning (see docs/product/POSITIONING.md)
├── Dockerfile
├── docker-compose.yml
└── README.md
The Services/ tree is the heart of the app — see src/Whiskers/Services/README.md for a guided tour.
IFoo interface and registered in DI; consumers depend on the interface.Requirements: the .NET 10 SDK.
# build
dotnet build src/Whiskers/Whiskers.csproj
# run tests
dotnet test src/Whiskers.Tests/Whiskers.Tests.csproj
# run locally (listens on :8080)
dotnet run --project src/Whiskers/Whiskers.csproj
Issues and pull requests are welcome. Please:
src/Whiskers.Tests/;dotnet build (0 warnings) and dotnet test before opening a PR;Apache License 2.0, see LICENSE.
Copyright © 2026 Whiskers Contributors
| Agent |
AGENT_ENABLED, AGENT_PROVIDER, AGENT_MODEL, AGENT_API_KEY, ... |
| Acting agent (see above) |
| Mesh VPN | VPN_PROVIDER, TAILSCALE_AUTHKEY, NETBIRD_SETUP_KEY, ... | Pluggable VPN bring-up (none/tailscale/netbird); empty = legacy in-container Tailscale |
| Image search | HARBOR_URL, HARBOR_USERNAME, HARBOR_PASSWORD | Add a self-hosted Harbor marketplace (Docker Hub + GHCR are on by default) |
| Host binding | HOST_BIND, HOST_PORT | The container always listens on 8080 internally |
| Data directory | WHISKERS_DATA_DIR | Root for SQLite, JSON stores, keys & certificates (default /app/data); repoint at any volume / host path |
| Database | WHISKERS_DB_PROVIDER, WHISKERS_DB_CONNECTION, WHISKERS_DB_CONNECTION_FILE | sqlite (default, zero-config) or postgres. For Postgres set a connection string, or mount it as a file and point _FILE at it. Ready-to-run overlay: deploy/docker-compose.postgres.yml. |