Skip to content

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.

  1. Tailscale binding only. Refuses to start unless the listen address resolves to a Tailscale-managed interface (tailscale0 on Linux, utun* with a 100.64/10 address on macOS).
  2. 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.
  3. 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.
  4. Pluggable auth. The Auth interface gates every request. The stub DangerouslyAllowAllAuth is for dev only.
  5. Stateless modulo auth. No persistent tool config, no operator-supplied YAML. Annotation-driven favorites recompiled into the wrapped CLI.
  6. Dangerously* opt-outs. Any flag that turns a safety property off carries the Dangerously prefix so call sites cannot pretend they picked a sensible default.
  7. 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, calls ListTools, 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 by webops.group.
  • GET /tool/<name> - One page: description (Markdown via goldmark) + form (from tool.InputSchema) + log pane.
  • POST /run/<name> - Calls session.CallTool. Streams TextContent line-by-line as SSE event: stdout. Closes with event: done or event: 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.yaml declares local dev verbs.
  • Makefile is the source of truth.
  • coily lint validates the yaml/Makefile contract on every CI run.
  • .golangci.yaml, staticcheck.conf mirror urfave/cli.
  • GitHub Actions CI runs vet/build/test/lint.