Platform · Topology
Where everything lives.
The fleet resolves into four namespaces over two hosts, one tailnet, and one DNS authority. This page is the public half of that map; the internal half stays on the operator surface.
Namespaces
Four surfaces, one fleet.
public · division
The division umbrella — the surface this site occupies. Docs hub, OSS index, R&D notes, and division marketing.
public · entity
The registered-entity corporate surface: investor, careers, policy, and legal pages.
public · products
Each product on its own brand and domain — Index of News, Attorney Atlas, Vagary Voice — over the shared substrate.
internal
The operator and Vagaris daily surface: console, dashboards, and logs — access-gated, never linked from public pages.
Substrate
Two hosts, one mesh.
Hosts
Two dedicated hosts carry the fleet, connected by a private mesh with fixed addresses. The substrate is declarative — rebuilding either host is a single playbook run plus an encrypted restore.
Source: 00-overview/foundation-overview.md (dual-VPS topology + tailnet).
Scale
187 services under operation · 8 zones · 271+ DNS records · one DNS authority · encrypted backups on both hosts.
Service count probed from registry/services.yaml 2026-08-12.
Platform stack
The services behind the capabilities.
The capability graph names 29 platform services with their declared operations. A curated slice of the public-relevant stack:
| Service | Role |
|---|---|
| coolify | Operations: {'id': 'deploy', 'verb': 'deploy/redeploy an application from its git repo', 'api': 'mcp__coolify__deploy · POST /applications/{uuid}/deploy', 'blast_radius': 'write', 'constraints': 'needs a server attached + a github app source'}, {'id': 'rollback', 'verb': 'redeploy a prior commit (roll back)', 'api': 'mcp__coolify__deployment + deploy', 'blast_radius': 'write'}, {'id': 'env_update', 'verb': 'set/update application env vars', 'api': 'mcp__coolify__bulk_env_update', 'blast_radius': 'keystone', 'constraints': 'secret VALUES come from Infisical, never inline (§4)'}, {'id': 'logs', 'verb': "read an application's runtime logs", 'api': 'mcp__coolify__application_logs', 'blast_radius': 'read_only'}, {'id': 'restart', 'verb': 'restart/stop an application or its stack', 'api': 'mcp__coolify__control · restart_project_apps', 'blast_radius': 'write'}…. |
| caddy | Operations: {'id': 'route', 'verb': 'add/change a hostname→upstream vhost (reverse_proxy or file_server)', 'api': 'edit caddy_vhosts[] in vps-ansible/roles/caddy/defaults/main.yml → Caddyfile.j2 converge (Mechanism A canonical); SoT mirror vps_host/configs/caddy/Caddyfile; apply via `systemctl restart caddy`', 'blast_radius': 'keystone', 'constraints': 'SOLE reverse proxy on core-1 — a bad Caddyfile downs ALL core-1 vhosts (gallery/portainer/apps→Coolify/status/…); ALWAYS `caddy validate` first; no zero-downtime reload (admin off) → restart is a brief edge blip'}, {'id': 'validate', 'verb': 'validate Caddyfile syntax before applying', 'api': "ssh vagary-core-1 'caddy validate --config /etc/caddy/Caddyfile'", 'blast_radius': 'read_only', 'constraints': 'mandatory pre-check before any restart'}, {'id': 'apply', 'verb': 'apply config change (restart the service — reload is unavailable)', 'api': "ssh vagary-core-1 'sudo systemctl restart caddy'", 'blast_radius': 'keystone', 'constraints': '`admin off` disables `caddy reload` → must restart (§10.2 gotcha 1); brief public 80/443 blip for all vhosts; grace_period 3s drains clean'}, {'id': 'issue_cert', 'verb': 'obtain/renew a vhost TLS cert (auto-LE via DNS-01-Cloudflare, ZeroSSL fallback)', 'api': 'automatic on vhost add + at renewal (acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}); CF token Infisical-rendered into /etc/caddy/.env', 'blast_radius': 'write', 'constraints': 'DNS-01 (OW-216, not HTTP-01) — bypasses CF Access, no /.well-known hop; needs the CF_API_TOKEN present in /etc/caddy/.env'}, {'id': 'status', 'verb': 'check service run-state + live cert lifecycle', 'api': "ssh vagary-core-1 'systemctl status caddy'; journalctl -u caddy -f", 'blast_radius': 'read_only'}…. |
| traefik-coolify | Operations: {'id': 'list_routers', 'verb': 'list live HTTP routers/services/middlewares + their status (drift/health introspection)', 'api': 'GET http://127.0.0.1:8080/api/http/routers (also /api/http/{services,middlewares}, /api/overview, /api/rawdata) — anonymous, from compute-1', 'blast_radius': 'read_only', 'constraints': 'API is loopback/tailnet-only, public-BLOCKED at DOCKER-USER; no CLI list subcommand (HTTP API is canonical)'}, {'id': 'ping', 'verb': 'liveness probe of the proxy (200 OK / 503 during graceful shutdown)', 'api': 'GET http://127.0.0.1:80/ping · or docker exec coolify-proxy traefik healthcheck', 'blast_radius': 'read_only'}, {'id': 'metrics', 'verb': 'read Prometheus metrics the edge emits (per-router/entrypoint request+latency+TLS-cert-expiry series)', 'api': 'GET http://127.0.0.1:8082/metrics — scraped by compute-1 Prom job traefik', 'blast_radius': 'read_only', 'constraints': 'metrics EP is 127.0.0.1-only; feeds Grafana traefik-edge-health + AM traefik-rules.yml'}, {'id': 'route_app', 'verb': 'add/change a public route for a Coolify-managed app (Host rule, middlewares, TLS)', 'api': "mcp__coolify__* — set per-app 'Custom Traefik Labels' (docker provider); Coolify re-renders the app compose", 'blast_radius': 'write', 'constraints': 'prefer Coolify labels; hand-edits to /data/coolify/proxy/docker-compose.yml are re-rendered from Coolify DB on proxy start'}, {'id': 'route_override', 'verb': 'route a NON-Coolify service via a Pattern-Y file-override (file provider, hot-reload)', 'api': 'write /data/coolify/proxy/dynamic/<svc>.yaml on compute-1 (ADR-022 A4 carve-out: update rebuild-sources first + .bak)', 'blast_radius': 'write', 'constraints': 'file provider hot-reloads (no proxy restart); MUST also update the vps_host + vps-ansible rebuild-sources (md5-parity) or it drifts'}…. |
| infisical | Operations: {'id': 'get_secret', 'verb': 'fetch a single secret value by name/path (the core inject-secret op every consumer composes)', 'api': 'mcp__infisical__get-secret · GET /api/v4/secrets/{secretName}', 'blast_radius': 'read_only', 'constraints': 'scope by workspaceId+environment(prod)+path; default MCP identity mcp-operator-readonly has viewer on all 11 projects'}, {'id': 'list_secrets', 'verb': 'list all secrets in a project/environment/path', 'api': 'mcp__infisical__list-secrets · GET /api/v4/secrets?workspaceId=..&environment=..', 'blast_radius': 'read_only'}, {'id': 'list_projects', 'verb': 'list Infisical projects/workspaces', 'api': 'mcp__infisical__list-projects · GET /api/v3/workspace', 'blast_radius': 'read_only'}, {'id': 'create_secret', 'verb': 'write a new secret into the sole fleet secrets SoT', 'api': 'mcp__infisical__create-secret · POST /api/v4/secrets', 'blast_radius': 'keystone', 'constraints': 'default MCP identity is READ-ONLY (403 on write); real write path needs vps-operator admin creds (§10.3 #11, /etc/infisical/*.bak-W10B-* on compute-1); value propagates fleet-wide via render agents (ADR-025)'}, {'id': 'update_secret', 'verb': 'rotate/update a secret value (new version; rollback-able)', 'api': 'mcp__infisical__update-secret · PATCH /api/v4/secrets/{secretName}', 'blast_radius': 'keystone', 'constraints': 'READ-ONLY MCP 403s — use vps-operator admin identity (§10.3 #11); rotation cascades to every consumer via ADR-025 3-tier render'}…. |
| cloudflare | Operations: {'id': 'dns_record', 'verb': 'create/update/delete a DNS record in a product zone (CF is sole authoritative DNS for all 7 zones)', 'api': 'CF API v4 POST/PUT/DELETE https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records (Bearer token; NOT MCP — Zone DNS is not in the CF Developer Platform MCP). IaC: vps-ansible/roles/cloudflare-dns/ + docs/runbooks/dns-as-code.md', 'blast_radius': 'write', 'constraints': 'DNS-XOR: CF is sole authority per zone (Hostinger holds 0 authoritative records); prefer the DNS-as-code path over ad-hoc edits; a wrong apex/MX record can break resolution/mail fleet-wide'}, {'id': 'purge_cache', 'verb': 'purge Cloudflare edge cache for a zone (or by URL/tag) after a deploy', 'api': 'CF API v4 POST https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache (host_page-cache-purge token, all-7-zone scope)', 'blast_radius': 'write', 'constraints': 'transient — forces cache re-fill; a full purge briefly elevates origin load'}, {'id': 'access_gate', 'verb': 'create/update a Zero-Trust Access app+policy fronting an admin UI (IdP-gate or bypass carve-out)', 'api': 'CF API v4 POST/PUT https://api.cloudflare.com/client/v4/accounts/dc5d0bbfdb9c8e7be5373520908cf315/access/apps (+ /policies) (Global-Key+Email; NOT MCP)', 'blast_radius': 'keystone', 'constraints': 'Access fronts EVERY admin UI — a wrong policy can lock the operator out of all admin surfaces at once; ADR-007 A1 invariant: bypass apps MUST carry allowed_idps:[]; drift-guarded by cf-zone-drift-check.md §3'}, {'id': 'tunnel_ingress', 'verb': 'read/manage a Cloudflare Tunnel + ingress route to a Tailnet-only service (Infisical, rclone)', 'api': 'CF API v4 GET https://api.cloudflare.com/client/v4/accounts/{acct}/cfd_tunnel?is_deleted=false (reads); ingress is dashboard/remote-config managed — token-mode tunnels, no git-tracked config.yml', 'blast_radius': 'keystone', 'constraints': 'the 2 tunnels are the SOLE public path to Tailnet-only services (secrets. → Infisical, rclone. → rcd); breaking a tunnel severs access to secrets/backup control planes'}, {'id': 'tls_setting', 'verb': 'read/set edge SSL/TLS + zone-hardening settings (ssl mode, min-TLS, HSTS, always_online)', 'api': 'CF API v4 GET/PATCH https://api.cloudflare.com/client/v4/zones/{zone_id}/settings/{setting} (Global-Key+Email; NOT MCP)', 'blast_radius': 'keystone', 'constraints': 'ssl=strict on all 7 zones — flipping SSL mode off or lowering min-TLS breaks origin-cert validation / weakens TLS fleet-wide; edge-cert expiry is Prometheus-monitored (cf-edge-health dashboard)'}…. |
| prometheus-vagary | Operations: {'id': 'query', 'verb': 'run an instant PromQL query against the compute-1 Prom TSDB', 'api': "GET /api/v1/query?query=<promql> (ssh vagary-compute-1 'curl -sG http://127.0.0.1:19090/api/v1/query --data-urlencode query=...'); OR mcp__grafana__query_prometheus", 'blast_radius': 'read_only', 'constraints': "compute-1 = :19090 host-mapped / 100.64.0.2:9090 tailnet (NOT :9091 — that is core-1 'prometheus'). Grafana MCP default DS is now VictoriaMetrics (W5); to hit THIS Prom pass the compute-1 Prom datasource UID (retained isDefault=false), else the query goes to the VM fleet store"}, {'id': 'check_targets', 'verb': 'list active scrape targets + find any DOWN', 'api': 'GET /api/v1/targets (curl http://127.0.0.1:19090/api/v1/targets | jq \'.data.activeTargets[] | select(.health!="up")\')', 'blast_radius': 'read_only', 'constraints': '80 targets / 54 jobs live 2026-06-23; pyroscope/tempo/vagary-voice flap context-deadline-exceeded under CPU-throttle (raise scrape_timeout, not a real down)'}, {'id': 'check_alerts', 'verb': 'list currently firing/pending alerts + the AM it forwards to', 'api': 'GET /api/v1/alerts + GET /api/v1/alertmanagers (curl http://127.0.0.1:19090/api/v1/alerts | jq)', 'blast_radius': 'read_only', 'constraints': 'compute-1 forwards to alertmanager:9093 (internal docker DNS, same host)'}, {'id': 'list_rules', 'verb': 'list loaded alerting + recording rule groups (389 rules live)', 'api': "GET /api/v1/rules (curl http://127.0.0.1:19090/api/v1/rules | jq '.data.groups[].rules[].name')", 'blast_radius': 'read_only'}, {'id': 'edit_alert_rule', 'verb': 'add/change an alerting or recording rule (atomic-write → promtool check → reload)', 'api': 'promtool check rules /tmp/<rules>.new (docker exec prometheus) → mv into /opt/prometheus/config/rules/ → POST /-/reload', 'blast_radius': 'write', 'constraints': "MUST pass 'promtool check rules' before mv (atomic-config-update-rule); a bad/absent rule can silence fleet alerting (TC-04) — the promtool gate + PrometheusConfigCompletenessProducerStale diff-guard keep it recoverable. Rules are IaC-tracked (vps-ansible roles/observability) — prefer templated converge over host-edit for durability"}…. |
| grafana | Operations: {'id': 'query_metrics', 'verb': 'run a PromQL/MetricsQL query across a fleet datasource (VictoriaMetrics default, Prometheus, Prometheus-core1)', 'api': 'mcp__grafana__query_prometheus (+ query_prometheus_histogram) · POST /api/datasources/proxy/uid/{ds_uid}/api/v1/query', 'blast_radius': 'read_only', 'constraints': '8 datasources live-probed; VictoriaMetrics is the sole isDefault'}, {'id': 'query_logs', 'verb': 'run a LogQL query against Loki via the Grafana proxy (incident log investigation)', 'api': 'mcp__grafana__query_loki_logs (+ query_loki_patterns/query_loki_stats) · POST /api/datasources/proxy/uid/{loki}/loki/api/v1/query_range', 'blast_radius': 'read_only'}, {'id': 'search_dashboards', 'verb': 'find dashboards/folders by query in the search index', 'api': 'mcp__grafana__search_dashboards · GET /api/search?query=', 'blast_radius': 'read_only'}, {'id': 'get_dashboard', 'verb': "fetch a dashboard's JSON + panel queries by uid", 'api': 'mcp__grafana__get_dashboard_by_uid (+ get_dashboard_panel_queries) · GET /api/dashboards/uid/{uid}', 'blast_radius': 'read_only'}, {'id': 'upsert_dashboard', 'verb': 'create or update a dashboard (as-code; git-tracked JSON is canonical after first-cut UI authoring)', 'api': 'mcp__grafana__update_dashboard · POST /api/dashboards/db {dashboard,overwrite}', 'blast_radius': 'write', 'constraints': 'file-provisioning is on-start (docker restart grafana) not hot-reload; RW SA token required'}…. |
| loki | Operations: {'id': 'query_logs', 'verb': 'run a LogQL range/instant query over fleet container+journal logs (the agent STATE route for log/grep questions)', 'api': 'mcp__loki__query-loki · mcp__grafana__query_loki_logs · GET /loki/api/v1/query_range?query=<logql>&start=&end=', 'blast_radius': 'read_only', 'constraints': 'auth_enabled:false at Loki layer; read surface fronted by loki-auth basicAuth + CF Access. Loki listens :3101 in-container; :3100 is nginx loki-auth'}, {'id': 'list_labels', 'verb': 'introspect the queryable label/stream set (what containers/jobs are emitting now)', 'api': 'mcp__grafana__list_loki_label_names · list_loki_label_values · GET /loki/api/v1/labels + /label/<name>/values', 'blast_radius': 'read_only', 'constraints': 'default probe window is 1h and SILENTLY under-reports jobs — use a 24h+ window (loki-label-probe-window-discipline.md)'}, {'id': 'find_error_pattern', 'verb': 'auto-cluster a noisy log burst into root-cause candidates (Sift pattern detection)', 'api': 'mcp__grafana__find_error_pattern_logs · mcp__grafana__query_loki_patterns', 'blast_radius': 'read_only'}, {'id': 'tail_logs', 'verb': 'live-tail a log stream (websocket)', 'api': 'GET /loki/api/v1/tail?query=<logql> (websocket)', 'blast_radius': 'read_only', 'constraints': "curl disconnects don't auto-reconnect; use logcli --tail for durable tailing"}, {'id': 'list_rules', 'verb': 'read the live LogQL ruler rule inventory (~24 detection rules across 14 groups → AM → slack)', 'api': 'GET /loki/api/v1/rules', 'blast_radius': 'read_only', 'constraints': 'rules are FILE-based on disk (/opt/loki/data/rules/); this is the read/inventory path, not a REST create surface'}…. |
| glitchtip | Operations: {'id': 'list_issues', 'verb': 'list deduplicated error issues for a project', 'api': 'mcp__glitchtip__glitchtip_issues · GET /api/0/projects/{org}/{project}/issues/', 'blast_radius': 'read_only', 'constraints': 'read-path MCP tool; mgmt API needs Bearer $GLITCHTIP_API_TOKEN (Infisical vagary-core-1/glitchtip-orchestrator)'}, {'id': 'latest_event', 'verb': 'fetch the latest event for an issue (stack trace, request body, environment)', 'api': 'mcp__glitchtip__glitchtip_latest_event · GET /api/0/issues/{issue_id}/events/latest/', 'blast_radius': 'read_only'}, {'id': 'ingest_event', 'verb': 'accept a Sentry-SDK error/envelope event into the store (core write path)', 'api': 'POST /api/{project_id}/envelope/ (also /store/) via glitchtip-relay:9095', 'blast_radius': 'write', 'constraints': 'SDK-driven, rarely manual; relay sanitizes+rate-limits+auth-gates; CF Access bypass on /api/* ingest paths'}, {'id': 'health_probe', 'verb': 'probe backend liveness', 'api': "GET /api/0/ (LB) · GET /_health/ returns 'ok' (internal)", 'blast_radius': 'read_only'}, {'id': 'list_projects', 'verb': 'list org projects (each has a DSN)', 'api': 'GET /api/0/projects/', 'blast_radius': 'read_only', 'constraints': 'Bearer-auth required'}…. |
| uptime-kuma | Operations: {'id': 'health_probe', 'verb': 'check Kuma-self liveness (unauthenticated health route)', 'api': 'GET /api/health -> {"status":"ok"} (curl https://status.chinmayramraika.in/api/health)', 'blast_radius': 'read_only', 'constraints': 'no /api/healthcheck route exists (gotcha #2); container also has built-in Go HEALTHCHECK binary'}, {'id': 'read_status_page', 'verb': 'read a public per-brand SLA status page (main/host/vagary-platform/vagary-voice)', 'api': 'GET /status/<slug> (e.g. https://status.chinmayramraika.in/status/main)', 'blast_radius': 'read_only', 'constraints': 'public, CF-Access-bypassed for /status/*; subpath deploy unsupported so it is a dedicated FQDN'}, {'id': 'push_heartbeat', 'verb': 'emit a cron/job heartbeat (up/down) into a push monitor from an external command', 'api': 'CLI /usr/local/bin/uptime-kuma-push <monitor-name> <cmd> (canonical) | fallback GET /api/push/<token>?status=up&msg=OK&ping=<ms>', 'blast_radius': 'write', 'constraints': 'token file /etc/uptime-kuma/push-tokens 0600 root:root; tokens canonical in Infisical vagary-core-1/UPTIME_KUMA_PUSH_*; use the wrapper not raw curl'}, {'id': 'scrape_metrics', 'verb': 'pull per-monitor status/latency/cert-days time-series for SLA math', 'api': 'GET /metrics (Bearer-form Basic auth: empty user, password uk2_<plaintext>) on http://172.17.0.1:3001/metrics', 'blast_radius': 'read_only', 'constraints': 'key in Infisical vagary-core-1/UPTIME_KUMA_PROM_API_KEY -> /opt/prometheus/secrets/; NEVER inline the key (§4); already scraped by Prom job uptime_kuma -> VictoriaMetrics'}, {'id': 'query_metrics_via_prom', 'verb': 'query uptime_kuma monitor_status/response_time/cert_days series through Grafana/Prometheus', 'api': 'mcp__grafana__query_prometheus (query=\'monitor_status\' / \'up{job="uptime_kuma"}\') OR Grafana dashboard uid=uptime-kuma-health', 'blast_radius': 'read_only', 'constraints': 'indirect path — no native Kuma MCP; 59 monitor_status series live on Prom+VM (VAG-378)'}…. |
| mailcow | Operations: {'id': 'send_mail', 'verb': 'send transactional/notification email as a mailbox (the consumed capability — grafana/glitchtip/infisical/n8n/vagary-platform auth here)', 'api': 'SMTP submission mail.chinmayramraika.in:587 STARTTLS, SASL AUTH as <mailbox>; creds in Infisical /mailcow/mailboxes/<addr>', 'blast_radius': 'write', 'constraints': 'must auth as a real provisioned mailbox; port 25 is inter-MTA, 587 is the app-send path'}, {'id': 'provision_mailboxes', 'verb': "declaratively create the mailboxes+aliases a domain/product needs (fleet-canonical 'give X email')", 'api': 'vps_host/scripts/provision-mailboxes.sh (reads vps_host/configs/mail-manifest.json)', 'blast_radius': 'write', 'constraints': "idempotent — creates only what's missing, never modifies/deletes existing; a project gets mail by adding a manifest block + re-running"}, {'id': 'add_domain', 'verb': 'add a mail domain (MX/SPF/DKIM/DMARC DNS is a separate operator+CF step)', 'api': 'POST /api/v1/add/domain (X-API-Key RW)', 'blast_radius': 'write', 'constraints': 'DNS records (MX/SPF/DKIM publish) are the load-bearing sibling — see docs/runbooks/mail-add-domain.md'}, {'id': 'add_mailbox', 'verb': 'create a single mailbox (lower-level than provision_mailboxes; the raw API fallback)', 'api': 'POST /api/v1/add/mailbox body {local_part,domain,quota,password}', 'blast_radius': 'write', 'constraints': 'prefer provision_mailboxes.sh for fleet consistency; store password in Infisical /mailcow/mailboxes'}, {'id': 'add_alias', 'verb': 'add an alias/forwarding address to a mailbox', 'api': 'POST /api/v1/add/alias body {address,goto,active}', 'blast_radius': 'write'}…. |
| n8n | Operations: {'id': 'list_workflows', 'verb': 'list all workflows (active + inactive) — the discovery op that makes fire/activate composable', 'api': 'GET /api/v1/workflows (X-N8N-API-KEY) · fallback: docker exec n8n n8n list:workflow --active', 'blast_radius': 'read_only', 'constraints': 'API-key first bootstrapped via UI (Rule #53 carve-out); returns metadata, not credential plaintext'}, {'id': 'fire_webhook', 'verb': 'trigger a workflow by POSTing its public webhook — the primary cross-service automation entrypoint', 'api': 'POST /webhook/<id> (per-workflow public URL; test path POST /webhook-test/<id>)', 'blast_radius': 'write', 'constraints': 'webhook paths are CF-Access-bypassed by design; downstream fan-out (Slack/GitHub/CF/Hostinger side-effects) varies by the target workflow; per-workflow auth = HMAC OR URL-token'}, {'id': 'activate', 'verb': 'activate/deactivate a workflow (makes its trigger live or dormant)', 'api': 'POST /api/v1/workflows/{id}/activate · POST /api/v1/workflows/{id}/deactivate', 'blast_radius': 'write', 'constraints': 'activating a cron/poll-trigger workflow starts recurring side-effects; deactivate is the reversible inverse'}, {'id': 'create_workflow', 'verb': 'create or replace a workflow definition from JSON', 'api': 'POST /api/v1/workflows (create) · PUT /api/v1/workflows/{id} (full replace)', 'blast_radius': 'write', 'constraints': 'canonical authoring is the visual UI editor (Rule #53 carve-out) exported to git Cramraika/n8n-workflows; a PUT replaces the FULL def'}, {'id': 'list_executions', 'verb': 'read workflow execution history (observability of past runs)', 'api': 'GET /api/v1/executions (filterable by workflowId + status) · GET /api/v1/executions/{id} for full payload', 'blast_radius': 'read_only', 'constraints': 'full execution payloads may carry PII (§10.4 gotcha 10 — workflows can emit secrets into exec logs)'}…. |
| restic | Operations: {'id': 'backup', 'verb': 'create an encrypted deduplicated snapshot of filesystem data / DB dumps for a scope', 'api': "systemd restic-backup-<scope>.timer -> /usr/local/bin/restic-backup-<scope>.sh (calls `restic backup --tag host:<vps> --tag scope:<scope> --tag schedule:<daily|4h>`); ad-hoc: ssh <host> 'restic backup <paths> --tag daily'", 'blast_radius': 'write', 'constraints': 'one .env per scope (mixing paths/repos backs up wrong data to wrong repo); DB scopes need pg_dump/mongodump pre-hook into staging first'}, {'id': 'restore', 'verb': 'restore snapshot data to a target dir (DR / restore-test)', 'api': "ssh <host> 'restic restore latest --target <dir> --include <path>'; DR drill: /usr/local/bin/restore-test-monthly.sh", 'blast_radius': 'write', 'constraints': 'in-place restore OVERWRITES live data — restore to a scratch target dir; laptop-DR posture (ADR-017) needs the password from /root/.cosign/.tier2-mirror.txt if host is lost'}, {'id': 'snapshots', 'verb': 'list snapshots in a repo (read-only inventory)', 'api': "ssh <host> 'set -a; source /root/.infisical-rendered/restic.env; set +a; restic snapshots --no-lock'", 'blast_radius': 'read_only', 'constraints': '--no-lock is READ-ONLY only (G-NEW-B); NEVER --no-lock on a write op — two writers -> silent corruption'}, {'id': 'check', 'verb': 'verify repo integrity (structural + optional read-data-subset)', 'api': "ssh <host> 'restic check --read-data-subset=5%' (weekly rolling; daily metadata-only `restic check`; quarterly full `restic check --read-data`)", 'blast_radius': 'read_only', 'constraints': 'full --read-data = full-repo egress cost; 5% weekly subset covers whole repo over ~5 months'}, {'id': 'prune', 'verb': 'apply retention + repack (forget unreferenced snapshots then delete/repack packs)', 'api': 'systemd restic-prune.service/.timer (Sun 06:30 UTC) -> `restic forget --keep-hourly=24 --keep-daily=14 --keep-weekly=8 --keep-monthly=12 --keep-yearly=5 --prune`; ExecStartPost `restic check --read-data-subset=10%`', 'blast_radius': 'keystone', 'constraints': 'IRREVERSIBLE — forgotten+pruned snapshots are unrecoverable; ALWAYS --dry-run a retention-policy change first; decouple from backup (per-backup prune cascades into rate-limit lockouts on cloud backends, G-NEW-A)'}…. |
| slack | Operations: {'id': 'send_message', 'verb': 'post a message to a channel/DM (optionally as a custom identity)', 'api': 'chat.postMessage (+ chat:write.customize for username/icon)', 'blast_radius': 'write'}, {'id': 'open_dm', 'verb': 'resolve/open a DM channel with a user', 'api': 'conversations.open', 'blast_radius': 'read_only'}, {'id': 'create_canvas', 'verb': 'create/update a channel canvas (runbook surface)', 'api': 'canvases.create/edit', 'blast_radius': 'write', 'constraints': 'canvases:write scope'}, {'id': 'list_item', 'verb': 'read/write a Slack List row (fleet-backlog mirror)', 'api': 'slackLists.items.*', 'blast_radius': 'write', 'constraints': 'lists:write scope; the OW ledger is SoT (downward mirror only)'}, {'id': 'add_reaction', 'verb': 'react to a message (ack signal)', 'api': 'reactions.add', 'blast_radius': 'write'}. |
Full graph: registry/capability-graph.yaml.
Contracts
Bindings are explicit.
Cross-repo contracts — the addresses, names, and channel identifiers other systems depend on — are recorded in one registry with their producers, consumers, and drift risk, so a rename or value change is a conscious, reviewable act rather than a silent break.
Source: 02-governance/cross-repo-contract-registry.md.