cli-web-ops features¶
Inventory of what cli-web-ops does today. Scope changes should land in the same commit that touches code, so this file stays a faithful mirror of the public API + threat model.
Threat model¶
cli-web-ops takes an HTTP request and forwards it to an MCP server that runs a command on the host. The bridge property is the centerpiece of every design choice below.
- Tailscale binding only. Refuses to start unless the listen address resolves to a Tailscale-managed interface (
tailscale0on Linux,utun*with a 100.64/10 address on macOS). - No public listener. Never
0.0.0.0, never port-forwarded, never reverse-proxied to the public internet. Use Tailscale Funnel or a deliberate ACL change if cross-network is needed. - MCP server is the trust boundary. cli-web-ops has no direct process-spawn capability for user-defined commands. The MCP server it talks to defines what's callable; cli-web-ops just renders + relays.
- Pluggable auth. The
Authinterface gates every request. The stubDangerouslyAllowAllAuthis for dev only. - Stateless modulo auth. No persistent tool config, no operator-supplied YAML. Annotation-driven favorites recompiled into the wrapped CLI.
Dangerously*opt-outs. Any flag that turns a safety property off carries theDangerouslyprefix so call sites cannot pretend they picked a sensible default.- Composes with cli-guard. When the wrapped command tree routes through
verb.Wrap, each web-triggered call gets an audit row at the cli-guard layer.
MCP client¶
webops.NewServer(MCPTarget, Options) (*Server, error)- Connects to an MCP server via the official Go SDK. Stdio subprocess (MCPTarget.Command). HTTP transport reserved.Server.Run(ctx)- Performs the Tailscale gate, opens the MCP session, callsListTools, mounts the HTTP server.- One subprocess per server - cli-web-ops manages the lifecycle of one upstream MCP server. Reuse the server across many tool calls; concurrency is single-goroutine per call (mutex around
root.Run).
HTTP surface¶
GET /- Mobile-first home with annotated favorites grouped bywebops.group.GET /tool/<name>- One page: description (Markdown via goldmark) + form (fromtool.InputSchema) + log pane.POST /run/<name>- Callssession.CallTool. StreamsTextContentline-by-line as SSEevent: stdout. Closes withevent: doneorevent: error.GET /docs- Full reference index. Every tool, every description, every input schema. Shares the layout shell with cli-web-docs.
Annotations contract¶
Read from MCP tool.Meta, stamped there by cli-mcp:
| Meta key | Effect |
|---|---|
webops.favorite |
Tool appears as a button on the home screen |
webops.label |
Button label (defaults to tool name) |
webops.group |
Section heading on the home screen |
tool.Annotations.DestructiveHint → confirm button (red, requires explicit tap).
Dangerously* opt-outs¶
Options.DangerouslySkipTailscale- Bypass the Tailscale interface gate. Forces loopback bind.Options.DangerouslyBindAnywhere- Additionally bypass the loopback check. For namespace-isolated integration tests only.DangerouslyAllowAllAuth{}- Auth driver that allows every request. Dev / tests only.
Deployment¶
deploy/Caddyfile.example- Recommended posture: Caddy-in-front with TLS via Tailscale Serve, caddy-security for WebAuthn at the edge. Bearer-token placeholder for early dev.
Repo development¶
.agent-guard/agent-guard.yamldeclares local dev verbs.Makefileis the source of truth.coily lintvalidates the yaml/Makefile contract on every CI run..golangci.yaml,staticcheck.confmirror urfave/cli.- GitHub Actions CI runs vet/build/test/lint.