From 7762629634df827f61e5554659ad3fe9ebb7b4ca Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Thu, 10 Sep 2026 09:18:57 +0000 Subject: [PATCH] chore: replace em dashes with hyphens, add no-em-dash rule to CLAUDE.md --- .gitea/workflows/deploy.yml | 4 +- CLAUDE.md | 22 +++++---- Dockerfile | 2 +- deploy/docker-compose.yml | 6 +-- docs/getting-started/first-server.md | 2 +- docs/getting-started/self-hosted-install.md | 2 +- docs/hq/billing.md | 2 +- docs/hq/self-hosted-instances.md | 4 +- docs/operations/backup-and-restore.md | 52 ++++++++++----------- docs/operations/backups.md | 4 +- docs/reference/api-tokens.md | 38 +++++++-------- docs/reference/environment-variables.md | 2 +- docs/reference/ports-and-networking.md | 4 +- docs/reference/troubleshooting.md | 12 ++--- docs/vantage/mcp.md | 44 ++++++++--------- docs/vantage/notification-channels.md | 2 +- docs/vantage/servers.md | 4 +- docs/vantage/status-pages.md | 30 ++++++------ docs/vantage/workloads.md | 2 +- nginx.conf | 2 +- src/css/custom.css | 6 +-- static/img/favicon.svg | 2 +- 22 files changed, 126 insertions(+), 122 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 5aa8a9c..8c6a601 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -5,7 +5,7 @@ on: branches: - main # DOCS_URL, DOCS_BASE_URL and APP_URL are repo variables, and editing one - # pushes no commit — so nothing would rebuild on its own. This is the + # pushes no commit - so nothing would rebuild on its own. This is the # escape hatch for that, and DOCS_BASE_URL in particular is baked into # every asset path in the build. workflow_dispatch: @@ -27,7 +27,7 @@ jobs: docker login ${{ vars.DOCKER_HOST }} \ -u "${{ secrets.REGISTRY_USER }}" --password-stdin - # One image, one directory — the whole repository is the docs site, + # One image, one directory - the whole repository is the docs site, # so there is nothing to detect a change in. It rebuilds on every # push, which is what a docs site wants anyway. # diff --git a/CLAUDE.md b/CLAUDE.md index f1ffbfa..d872561 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,18 +6,18 @@ tracking `main`, search indexed at build time by `@easyops-cn/docusaurus-search-local` so nothing external is keyed or called. Extracted from the `vantage` monorepo with its history, where it was -`docsite/`. The whole repository is the site — there is no subdirectory. +`docsite/`. The whole repository is the site - there is no subdirectory. It documents the **product**, not the codebase: each repository's own CLAUDE.md remains the contributor's map, and the two are allowed to differ in altitude but -not in fact. Five sections — Getting started, Vantage, Vantage HQ, Reference, -Operations — with `sidebars.ts` authored by hand so ordering is a decision +not in fact. Five sections - Getting started, Vantage, Vantage HQ, Reference, +Operations - with `sidebars.ts` authored by hand so ordering is a decision rather than a filename accident. ## A path, not a subdomain `*.vantage.hostxtra.co.uk` is the per-tenant instance namespace, and the control -plane's `APP_ROOT_LABEL` resolves an instance from the label before `vantage` — +plane's `APP_ROOT_LABEL` resolves an instance from the label before `vantage` - so a `docs.` label would be read as a tenant slug. Hence `/docs` on the marketing host instead. @@ -25,7 +25,7 @@ marketing host instead. asset 404s:** 1. `DOCS_BASE_URL` (default `/docs/` in the Dockerfile) -2. the proxy location that routes here — which must also sort **above** the +2. the proxy location that routes here - which must also sort **above** the catch-all forwarding to `site:3003`, or Next answers the 404 first 3. the directory the runtime stage serves from, `/usr/share/nginx/html/docs` @@ -39,7 +39,7 @@ Unlike the three Next apps this builds to static files, so its runtime stage is ## Design tokens `src/css/custom.css` holds `vantage-site`'s `web/app/globals.css` token blocks -**copied verbatim** — same names, same values. This is the one place the tokens +**copied verbatim** - same names, same values. This is the one place the tokens are not consumed through Tailwind: everything below the token block maps Docusaurus's `--ifm-*` variables onto them. Docusaurus already stamps `data-theme` on ``, which is the selector the dark block keys on, so the @@ -47,7 +47,7 @@ built-in toggle needed no wiring. The rule holds all the same: **no rule in that file outside the token blocks may carry a hex.** The one concession is `static/img/favicon.svg`, which must, for -the same reason the email layout must — a browser tab cannot read a token. +the same reason the email layout must - a browser tab cannot read a token. **Nothing enforces the match across the four front ends, and they are now in four repositories.** A token change here follows one in `vantage-site`; there is @@ -71,13 +71,13 @@ Not part of a self-hosted install. ## CI/CD `.gitea/workflows/deploy.yml`, on every push to `main` plus `workflow_dispatch`. -One image, `vantage/vantage-docs:latest`, from the repository root — the whole +One image, `vantage/vantage-docs:latest`, from the repository root - the whole repository is the site, so there is nothing to detect a change in and it rebuilds on every push. **No build args are passed, deliberately.** `DOCS_URL`, `DOCS_BASE_URL`, `APP_URL` and `HQ_URL` all have correct defaults in the Dockerfile, and passing -an unset repo variable would blank one rather than leave it alone — which for +an unset repo variable would blank one rather than leave it alone - which for `DOCS_BASE_URL` means every asset path in the build. Change them in the Dockerfile. The monorepo's workflow passed none either; this preserves that. @@ -88,3 +88,7 @@ Dockerfile. The monorepo's workflow passed none either; this preserves that. | `DOCKER_HOST` | Variable | registry host used for image tags | Despite the name the workflow **does not deploy**; it only builds and pushes. + +## Writing style + +Never use em dashes (the long dash character) anywhere: code, comments, UI copy, docs, commit messages. Use a plain hyphen ` - `, a comma, a colon, or split the sentence instead. diff --git a/Dockerfile b/Dockerfile index f6f67a5..f1987ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ RUN npm run build # # Docusaurus emits a fully static site, so unlike web/ and site/ # there is no Node server at runtime. alpine-slim is roughly a quarter the size -# of caddy:alpine, and nothing here needs automatic TLS — the host proxy +# of caddy:alpine, and nothing here needs automatic TLS - the host proxy # terminates it. FROM nginx:alpine-slim AS runner diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 3a504b4..4790a12 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,6 +1,6 @@ # The user documentation, served at vantage.hostxtra.co.uk/docs. # -# A fragment, composed on top of the vantage repository's own file — see the +# A fragment, composed on top of the vantage repository's own file - see the # vantage-site fragment for the full command. # # A path on the marketing host, not a subdomain: *.vantage.hostxtra.co.uk is @@ -8,8 +8,8 @@ # would read a `docs.` label as a tenant slug. # # The proxy location that routes here must sort ABOVE the catch-all forwarding -# to site:3003, or Next answers the 404. It forwards the FULL path — it does -# not strip /docs — so the proxy location, DOCS_BASE_URL and the directory the +# to site:3003, or Next answers the 404. It forwards the FULL path - it does +# not strip /docs - so the proxy location, DOCS_BASE_URL and the directory the # image serves from (/usr/share/nginx/html/docs) must all agree. When they do # not, the HTML loads and every asset 404s. # diff --git a/docs/getting-started/first-server.md b/docs/getting-started/first-server.md index 4450697..00d1f16 100644 --- a/docs/getting-started/first-server.md +++ b/docs/getting-started/first-server.md @@ -58,7 +58,7 @@ as a Windows service and starts it. Windows agents register, heartbeat, report inventory, run workflow steps, serve the browser console, check and apply OS updates, and report workloads (services and containers). Managing `authorized_keys` is a Linux-only -feature, and so is package inventory and CVE scanning — the vulnerability +feature, and so is package inventory and CVE scanning - the vulnerability feeds this project uses carry no Windows data. ::: diff --git a/docs/getting-started/self-hosted-install.md b/docs/getting-started/self-hosted-install.md index 5ccf48a..a46297c 100644 --- a/docs/getting-started/self-hosted-install.md +++ b/docs/getting-started/self-hosted-install.md @@ -114,7 +114,7 @@ The `nginx` service routes **one hostname to two backends**: Both rules are required. The web app forwards nothing to the API, so a proxy that sends the whole hostname to `web:3000` serves the interface and answers -`404` to every request it makes — starting with the login form. If you replace +`404` to every request it makes - starting with the login form. If you replace the bundled nginx with your own proxy, it must route the same way. This is the shipped `nginx/vantage.conf`: diff --git a/docs/hq/billing.md b/docs/hq/billing.md index b577ca6..1ecfd3b 100644 --- a/docs/hq/billing.md +++ b/docs/hq/billing.md @@ -27,7 +27,7 @@ The instance is created and licensed as soon as payment confirms. ### Self-hosted -Install your control plane first — a licence binds to its instance ID. +Install your control plane first - a licence binds to its instance ID. 1. Set **Deployment** to **Self-hosted**. 2. Choose a **Plan** and configure its features and server allowance. diff --git a/docs/hq/self-hosted-instances.md b/docs/hq/self-hosted-instances.md index e920d1e..b6ee7f9 100644 --- a/docs/hq/self-hosted-instances.md +++ b/docs/hq/self-hosted-instances.md @@ -15,7 +15,7 @@ Install first, then link and claim. Step by step in ## Paid **Install first.** A licence is issued to one instance, so your control plane -has to exist and report an instance ID before you can buy for it — the same +has to exist and report an instance ID before you can buy for it - the same precondition Free has. ```mermaid @@ -36,7 +36,7 @@ flowchart LR ## Upgrading an instance you already have -Paste the same instance ID you already hold — an install on Free moves to the +Paste the same instance ID you already hold - an install on Free moves to the paid plan in place, keeping its ID and its history. An ID belonging to another account is refused. diff --git a/docs/operations/backup-and-restore.md b/docs/operations/backup-and-restore.md index cc604b9..31b7660 100644 --- a/docs/operations/backup-and-restore.md +++ b/docs/operations/backup-and-restore.md @@ -7,16 +7,16 @@ sidebar_label: Backup and restore `vantagectl` is a separate command-line tool that backs up and restores the MongoDB database behind a Vantage control plane. It talks to MongoDB directly, never to the Vantage API, so it works against a control plane that is down, -half-migrated, or gone — exactly the situation a backup tool has to survive. +half-migrated, or gone - exactly the situation a backup tool has to survive. -For the store-level overview — what holds what, and why the database alone is -not a backup — see [Backups](./backups.md). This page covers the tool. +For the store-level overview - what holds what, and why the database alone is +not a backup - see [Backups](./backups.md). This page covers the tool. :::danger The key comes first Vantage encrypts SSH private keys, key passphrases, vault secrets, SSO client secrets and console credentials with `KEY_ENCRYPTION_KEY`. **It is not in your backup, and it is not recoverable.** A database restored without it is -permanently unreadable — not degraded, not partially readable, unreadable. +permanently unreadable - not degraded, not partially readable, unreadable. Store it wherever you store the credentials you could not rebuild: a password manager, a secrets vault outside this control plane, a piece of paper in a @@ -26,7 +26,7 @@ safe. Anywhere but next to the archive. ## What a backup holds Every collection in the database, the index definitions each one needs to be -useful again, and a SHA-256 **fingerprint** of `KEY_ENCRYPTION_KEY` — never the +useful again, and a SHA-256 **fingerprint** of `KEY_ENCRYPTION_KEY` - never the key itself. The fingerprint is what lets a later `restore` or `verify` tell you that the key you are holding is the wrong one, before it writes a database nobody can read. @@ -37,8 +37,8 @@ nobody can read. true whenever Redis itself restarts. - **The vulnerability database.** It is re-pulled automatically on next boot. - **Agent state on managed servers.** Nothing needs re-enrolling: agents - reconnect on their own, because `servers.agent_token_hash` — the thing an - agent authenticates with — is itself in the backup. + reconnect on their own, because `servers.agent_token_hash` - the thing an + agent authenticates with - is itself in the backup. :::note Pin the version The image is published on each `vantagectl/v*` release and tagged with that @@ -80,7 +80,7 @@ backup: ``` `backup.enabled` defaults to `false`, and the chart refuses to render if it is -turned on without both `backup.image` and `backup.pvcName` — a backup needs a +turned on without both `backup.image` and `backup.pvcName` - a backup needs a known image and somewhere durable to land, and guessing at either is worse than refusing to start. `backup.exclude` names collections to leave out (recorded in the archive's manifest, so an archive never claims to be complete when it is @@ -88,13 +88,13 @@ not), and `backup.successfulJobsHistoryLimit` / `backup.failedJobsHistoryLimit` / `backup.resources` behave exactly as they do on any other `CronJob`. `backup` refuses to run without `KEY_ENCRYPTION_KEY` set in the environment, -unless you pass `--allow-no-key` — for a deployment that genuinely stores no +unless you pass `--allow-no-key` - for a deployment that genuinely stores no encrypted data. Everywhere else, treat the refusal as the tool doing its job. ## Where to put the archive `--out -` streams the tarball to stdout instead of writing a file, and every -line of progress output goes to stderr — so piping the archive into something +line of progress output goes to stderr - so piping the archive into something else is always safe, nothing progress-related lands in the stream. Into `restic`: @@ -109,7 +109,7 @@ Into S3: vantagectl backup --out - | aws s3 cp - s3://my-backups/vantage-$(date +%F).tar.gz ``` -An archive is as sensitive as a raw database dump — it carries every SSH key +An archive is as sensitive as a raw database dump - it carries every SSH key assignment, every secret group, every session-adjacent setting, in a form the right `KEY_ENCRYPTION_KEY` can decrypt. Whatever you pipe it into should encrypt it at rest; `vantagectl` itself does not. @@ -123,23 +123,23 @@ vantagectl verify /backups/vantage-backup-vantage-20260907T020000Z.tar.gz \ Each line of output answers a different question: -- **`Archive`** — every member's checksum still matches; the tarball has not +- **`Archive`** - every member's checksum still matches; the tarball has not been truncated or corrupted. -- **`Archive key`** / **`Your key`** — the fingerprint stored in the archive +- **`Archive key`** / **`Your key`** - the fingerprint stored in the archive next to the fingerprint of the `KEY_ENCRYPTION_KEY` in your environment. -- **`Key match`** — whether those two fingerprints agree. -- **`Live probe`** — given `--mongo-uri`, `verify` goes one step further and +- **`Key match`** - whether those two fingerprints agree. +- **`Live probe`** - given `--mongo-uri`, `verify` goes one step further and decrypts a real ciphertext value from that database with the key you hold. A fingerprint match proves two archives agree about a key; only the probe proves the key in your hand actually reads the data. `verify` exits non-zero the moment anything above is wrong, which is what makes -it worth putting on a schedule — a backup job that "succeeded" last night is +it worth putting on a schedule - a backup job that "succeeded" last night is not the same claim as a backup that will actually restore. ## Looking inside an archive -`inspect` prints an archive's manifest and touches no database at all — no +`inspect` prints an archive's manifest and touches no database at all - no `--mongo-uri`, no key. It is what to run against an archive of unknown origin, before deciding whether it is the one you want: @@ -173,8 +173,8 @@ collection named in the archive before loading it. `--force` always needs a second assurance, in one of two forms: - `--confirm-db NAME`, naming the target exactly. A mismatch is refused. This - works everywhere — on a terminal and in a Kubernetes Job, a CI step or a cron - entry alike — and is the form to script. + works everywhere - on a terminal and in a Kubernetes Job, a CI step or a cron + entry alike - and is the form to script. - Nothing, on a terminal: `--force` alone prompts you to type the target database's name back, a deliberate pause before something destructive. @@ -190,7 +190,7 @@ log lines are still there, joined to freshly restored runs. Dropping them instead would delete data you never asked to delete. `restore` also refuses when the archive's key fingerprint does not match the -`KEY_ENCRYPTION_KEY` in your environment — see "When the key is wrong" below. +`KEY_ENCRYPTION_KEY` in your environment - see "When the key is wrong" below. ## The restore drill @@ -210,7 +210,7 @@ monthly: ``` 3. Drop the scratch database. It served its purpose. -The failure this catches is not "the archive is corrupt" — `verify` alone +The failure this catches is not "the archive is corrupt" - `verify` alone catches that. It is "the archive is fine but nobody can actually stand a control plane back up from it," which only a real restore proves. @@ -221,13 +221,13 @@ If `restore` finds the archive's key fingerprint does not match the `--ignore-key-mismatch` proceeds anyway, but says plainly which collections will come back with ciphertext nobody can read: -- `keys` — SSH private keys and passphrases -- `secrets` — the vault -- `auth_providers` — OIDC/SSO client secrets -- `console_sessions` — RDP/VNC credentials +- `keys` - SSH private keys and passphrases +- `secrets` - the vault +- `auth_providers` - OIDC/SSO client secrets +- `console_sessions` - RDP/VNC credentials There is no way to recover that ciphertext afterwards. If you have reached -this point, the right key was lost along with the chance to read those rows — +this point, the right key was lost along with the chance to read those rows - the fix is to re-enter each of them by hand (re-upload SSH keys, re-save vault secrets, reconfigure SSO), not to keep searching for a way to decrypt what is already in the database. diff --git a/docs/operations/backups.md b/docs/operations/backups.md index 897bfa5..9d918de 100644 --- a/docs/operations/backups.md +++ b/docs/operations/backups.md @@ -27,7 +27,7 @@ Store the key somewhere other than the server it protects. :::info Use `vantagectl` [**Backup and restore**](./backup-and-restore.md) is the supported way to take and restore a backup. It writes an archive that carries a fingerprint of -`KEY_ENCRYPTION_KEY` — never the key — so a restore taken with the wrong key +`KEY_ENCRYPTION_KEY` - never the key - so a restore taken with the wrong key **refuses** rather than silently producing a database whose secrets nobody can read. It also checksums every archive member before writing anything, and refuses to restore into a database that already holds data. A plain @@ -52,7 +52,7 @@ docker compose exec -T mongo mongodump --archive --gzip --db vantage \ :::warning This archive records nothing about which `KEY_ENCRYPTION_KEY` it was taken under. Restoring it with the wrong key produces a database that looks intact -and is not — every secret in it is silently unreadable until something tries +and is not - every secret in it is silently unreadable until something tries to decrypt one. ::: diff --git a/docs/reference/api-tokens.md b/docs/reference/api-tokens.md index 23ea6d0..49ebde6 100644 --- a/docs/reference/api-tokens.md +++ b/docs/reference/api-tokens.md @@ -5,7 +5,7 @@ sidebar_label: API tokens --- A session cookie is fine for a browser. A script, a CI job or a cron task -needs something it can hold onto instead — an API token. +needs something it can hold onto instead - an API token. ## Creating one @@ -15,7 +15,7 @@ admin additionally see every key in the instance. **Create key** opens a dialog with four decisions, in the order they matter: -1. **Name and role.** The name says what will use the key — the CI pipeline, +1. **Name and role.** The name says what will use the key - the CI pipeline, the script, the cluster. The role list offers your own role and everything below it, never above. 2. **Scopes.** A grid of resources against read and write. Ticking write also @@ -26,7 +26,7 @@ admin additionally see every key in the instance. 4. **Expiry.** Each option names the date it resolves to, so "90 days" and "7 December 2026" are the same choice read two ways. -Beneath them sits a preview line that reads the key back as a sentence — +Beneath them sits a preview line that reads the key back as a sentence - *"gitea-ci-deploy acts as admin, may read and write servers and workflows, read secrets, and stops working on 7 December 2026."* Read it before you submit; an over-granted key is far easier to spot in a sentence than in a grid @@ -40,7 +40,7 @@ vt_8f2c1a9e4b6d0735a1c8e29f4b0d6e17... That is the only time you will see it. Vantage stores a hash of the token, never the value itself, so if you lose it there is no support ticket that gets -it back — create a new token and revoke the old one. +it back - create a new token and revoke the old one. The panel showing it also carries a ready-made `curl` line and a summary of what was granted, so the key can go straight into a secret store without a @@ -50,7 +50,7 @@ second trip to the list. A token can reach only what its scopes name. Each resource has a `:read` and a `:write` scope, and holding `:write` on a resource also -satisfies a `:read` requirement for it — you do not need to tick both. +satisfies a `:read` requirement for it - you do not need to tick both. | Resource | Covers | | ----------- | --------------------------------------------------- | @@ -64,7 +64,7 @@ satisfies a `:read` requirement for it — you do not need to tick both. | `settings` | Instance settings, members, single sign-on, licence, and token management itself | A token created with only `servers:read` can list and inspect servers but -cannot run a workflow against them, touch a key, or read a secret — each of +cannot run a workflow against them, touch a key, or read a secret - each of those needs its own scope. ## Tag restrictions @@ -72,12 +72,12 @@ those needs its own scope. A key can be pinned to part of the fleet as well as part of the API. **Restrict to servers tagged** in the create dialog offers the tag keys and values already in use across your servers, and the key then reaches only servers carrying -**every** pair listed — the restriction is an AND, not an OR. Leaving it empty +**every** pair listed - the restriction is an AND, not an OR. Leaving it empty is the opposite: no restriction at all, the whole fleet. The restriction is fixed at creation, like the role and the scopes. Changing what a credential already deployed in CI can reach, with no record of what it -could reach before, is worse than requiring a rotation — so to widen or narrow +could reach before, is worse than requiring a rotation - so to widen or narrow one, create a replacement and revoke the old key. Restricted keys show their tags as chips beside their scopes in the list. @@ -86,7 +86,7 @@ Unrestricted keys show nothing there, which is the common case. The restriction applies to every API token, not only ones handed to an [MCP agent](../vantage/mcp.md). A CI token, a monitoring script's token and an agent's token are all held to the same tag scope check wherever the -service layer resolves servers — the mechanism does not know or care what +service layer resolves servers - the mechanism does not know or care what kind of caller is holding the token. Nor can a token created under a restricted token reach further than its @@ -98,27 +98,27 @@ token that also sees production. Each key is one record rather than a row of bare strings: -- **Key** — the name, the `vt_` prefix hint, the holder (when viewing all +- **Key** - the name, the `vt_` prefix hint, the holder (when viewing all keys) and the role. -- **Scopes** — one chip per resource, its access half tinted: `rw` in accent, +- **Scopes** - one chip per resource, its access half tinted: `rw` in accent, `r` in grey. A key with nothing granted says so in words rather than showing a dash. -- **Lifetime** — a bar showing how much of the key's issued life is left, +- **Lifetime** - a bar showing how much of the key's issued life is left, with the date beside it. Four states, and the label always says which: | Bar | Means | | --- | ----- | | Green | More than seven days left | - | Amber | Seven days or fewer — rotate it | + | Amber | Seven days or fewer - rotate it | | Red | Already expired; the key no longer authenticates | | Grey, full width | No expiry at all | A key issued before the instance's maximum lifetime was tightened also - carries *"outside the current policy — rotate when convenient"*. That is a + carries *"outside the current policy - rotate when convenient"*. That is a prompt, not a failure: the cap is never applied retroactively and the key keeps working. -- **Last call** — when the key last authenticated, or *Never used*. +- **Last call** - when the key last authenticated, or *Never used*. Above the list, four counts summarise the same thing at fleet scale: keys listed, keys expiring within seven days, keys that never expire, and keys @@ -128,7 +128,7 @@ change with the **My keys** / **All keys** toggle. ## A token never outranks its owner A token's role can be at most the role of the person who created it, and its -effective role is **recomputed on every request** as the lower of the two — +effective role is **recomputed on every request** as the lower of the two - not fixed at creation. Demote the person from owner to member and every token they hold drops to member from that request onward. Remove the person and every token they hold stops working immediately: a token has no existence @@ -154,7 +154,7 @@ curl -H "Authorization: Bearer vt_…" https://acme.vantage.example.com/api/serv ``` Everything else about the [REST API](./rest-api.md) applies the same way it -does to a session — JSON errors, audit logging, licence gating on writes — +does to a session - JSON errors, audit logging, licence gating on writes - except that authority comes from the token's role and scopes rather than a signed-in person's role. @@ -178,12 +178,12 @@ already been deleted. This page covers the token model. Every route, request and response shape is in the generated OpenAPI reference, served by **your own instance** at -`/api/docs` — not this documentation site, since the routes and their shapes +`/api/docs` - not this documentation site, since the routes and their shapes are specific to your install. The raw document is at `/api/openapi.json`. :::danger Not the External Secrets token The bearer token read by `GET /api/secrets/:group/values` for the Kubernetes -External Secrets Operator is a **separate credential** — a single instance-wide +External Secrets Operator is a **separate credential** - a single instance-wide value, rotated from Settings, that reaches only that one endpoint. It is not an API token and an API token cannot be used in its place: the two are checked by different code, and neither substitutes for the other. See diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index 9270c51..6e2eac7 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -25,7 +25,7 @@ it is absent. | `VANTAGE_LICENSE` | no | | A licence supplied at startup, so an automated install does not have to paste one in | | `VANTAGE_TRIVY_DB_REF` | no | `ghcr.io/aquasecurity/trivy-db:2` | Where the vulnerability database is pulled from. Point it at a mirror for an air-gapped install | | `VANTAGE_VULNDB_DISABLED` | no | | `true` switches [vulnerability scanning](../vantage/vulnerabilities.md) off entirely. Findings already stored are still served, and still shown as stale | -| `TRUSTED_PROXIES` | no | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | Comma-separated CIDRs or addresses of proxies allowed to set `X-Forwarded-For`. The shipped Docker Compose and Helm chart default to the private RFC1918 ranges, which covers Nginx Proxy Manager on the Docker bridge network and Traefik on a Kubernetes pod CIDR. An operator whose proxy sits on a public address must set this themselves, or every visitor behind it shares one address for rate-limiting purposes. Unset entirely (outside those shipped defaults) trusts none, so the client address is the direct peer. **On a LAN-only install, narrow this to your proxy's address.** The RFC1918 default trusts every private range, so a client on 192.168.0.0/16 reaching the server directly is itself a "trusted proxy" and can put whatever it likes in `X-Forwarded-For` — and, on the public status route, in `X-Forwarded-Host`. Behind a proxy on a public address, or with no proxy at all, that is not reachable; on a flat LAN it is | +| `TRUSTED_PROXIES` | no | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | Comma-separated CIDRs or addresses of proxies allowed to set `X-Forwarded-For`. The shipped Docker Compose and Helm chart default to the private RFC1918 ranges, which covers Nginx Proxy Manager on the Docker bridge network and Traefik on a Kubernetes pod CIDR. An operator whose proxy sits on a public address must set this themselves, or every visitor behind it shares one address for rate-limiting purposes. Unset entirely (outside those shipped defaults) trusts none, so the client address is the direct peer. **On a LAN-only install, narrow this to your proxy's address.** The RFC1918 default trusts every private range, so a client on 192.168.0.0/16 reaching the server directly is itself a "trusted proxy" and can put whatever it likes in `X-Forwarded-For` - and, on the public status route, in `X-Forwarded-Host`. Behind a proxy on a public address, or with no proxy at all, that is not reachable; on a flat LAN it is | :::danger `KEY_ENCRYPTION_KEY` has no recovery path It encrypts SSH private keys, vault secrets, OIDC client secrets and console diff --git a/docs/reference/ports-and-networking.md b/docs/reference/ports-and-networking.md index 2aaa4e2..a6bf2ae 100644 --- a/docs/reference/ports-and-networking.md +++ b/docs/reference/ports-and-networking.md @@ -9,7 +9,7 @@ sidebar_label: Ports and networking | Port | Service | Who connects | Expose publicly | | ------- | ----------- | -------------------------------- | --------------- | | `3000` | web | Browsers, via your reverse proxy | Yes, behind TLS | -| `8080` | server API | Your reverse proxy | Not directly — proxied | +| `8080` | server API | Your reverse proxy | Not directly - proxied | | `9090` | server gRPC | Agents | **Yes** | | `4822` | guacd | The server | No, firewall it | | `27017` | MongoDB | The server | No | @@ -85,7 +85,7 @@ On a private network you can skip TLS instead, by setting `tls: false` in each | everything else | `web:3000` | The web app forwards nothing to the API. Sending the whole hostname to - `web:3000` loads the interface and every request it makes answers `404` — + `web:3000` loads the interface and every request it makes answers `404` - including the login form. - Both backends must be the **same** hostname and certificate. The browser diff --git a/docs/reference/troubleshooting.md b/docs/reference/troubleshooting.md index 070ad21..f07f8eb 100644 --- a/docs/reference/troubleshooting.md +++ b/docs/reference/troubleshooting.md @@ -22,7 +22,7 @@ or is not 64 hex characters. **Every request 404s and the interface loads fine.** Your reverse proxy sends the whole hostname to `web:3000`. `/api`, `/auth`, `/public`, `/install*` and -`/update*` belong to `server:8080` and the web app forwards nothing — see +`/update*` belong to `server:8080` and the web app forwards nothing - see [Ports and networking](./ports-and-networking.md#reverse-proxy-notes). The tell is `curl -si https:///auth/bootstrap-status` returning HTML with `x-powered-by: Next.js` instead of JSON. @@ -124,7 +124,7 @@ User-Agent: Vantage-Monitor/1.0 (+https://vantage.hostxtra.co.uk) That string is the hook to allow the check through. In whichever product is doing the blocking, add a rule that skips bot protection, managed rules and rate -limiting for requests carrying it — Cloudflare, AWS WAF, Azure Front Door, +limiting for requests carrying it - Cloudflare, AWS WAF, Azure Front Door, Akamai, Fastly, Imperva, Sucuri, ModSecurity, nginx and HAProxy all match on a request header. The shape of the rule is the same everywhere: @@ -137,7 +137,7 @@ Three details are worth getting right: exact match breaks silently on an upgrade, and the symptom is a monitor that goes down on deploy day. - **Keep the rule narrow.** Scope it to the specific host and path being - monitored. A User-Agent is not a secret — anyone can send it — so a rule that + monitored. A User-Agent is not a secret - anyone can send it - so a rule that skips protection site-wide on that string alone is a bypass you have published. - **Allow the source address too, where you can.** Combining the User-Agent with @@ -170,7 +170,7 @@ needs `host`, `port`, `from` and `to`, and Telegram needs both `token` and ## A status page 404s or shows no data **404, and it should be published.** Check the **Published** toggle on the -page's editor — an unpublished page answers *not found* for everyone, +page's editor - an unpublished page answers *not found* for everyone, including you, with no session exemption. Also check the host: the public URL is `.vantage./status/`, the same per-instance subdomain everything else in Vantage uses. A wrong or missing @@ -178,11 +178,11 @@ subdomain resolves to no instance at all, which is also a 404. Third possibility: `/public` is not routed to the server. Check with `curl -si https://.vantage./public/status/` -— JSON is correct, HTML carrying `x-powered-by: Next.js` means the proxy sent +- JSON is correct, HTML carrying `x-powered-by: Next.js` means the proxy sent that prefix to the web app. **Loads, but shows an explanation instead of components.** This is not a -fault — it is the page working as designed. It means either the licence has +fault - it is the page working as designed. It means either the licence has lapsed (a self-hosted instance past its grace period, or a cloud instance between billing events) or the current tier does not include the **Status pages** feature. Fix the licence or the plan and the same link starts serving diff --git a/docs/vantage/mcp.md b/docs/vantage/mcp.md index 1dce95e..4dddb9a 100644 --- a/docs/vantage/mcp.md +++ b/docs/vantage/mcp.md @@ -4,8 +4,8 @@ title: Agent access (MCP) sidebar_label: Agent access (MCP) --- -The Model Context Protocol is how an LLM agent — Claude, or any other MCP -client — talks to a tool server over a defined wire format instead of a +The Model Context Protocol is how an LLM agent - Claude, or any other MCP +client - talks to a tool server over a defined wire format instead of a one-off integration. Connecting Vantage to an agent this way lets it answer questions about your fleet and, if you choose to let it, act on it: run a workflow, apply updates, or draft a step for you to review. @@ -24,18 +24,18 @@ An agent authenticates the same way any script does: with an on the MCP endpoint specifically, on top of whatever resource scopes (`servers:read`, `workflows:write`, and so on) it also needs: -- **`mcp:read`** — permission to reach the MCP endpoint at all and use its +- **`mcp:read`** - permission to reach the MCP endpoint at all and use its read-only tools. -- **`mcp:write`** — permission to use its write tools: running a workflow, +- **`mcp:write`** - permission to use its write tools: running a workflow, applying updates, assigning a key, or creating a step, workflow or monitor. -Without `mcp:write`, write tools are not merely refused when called — they +Without `mcp:write`, write tools are not merely refused when called - they are left out of the tool listing entirely, so a read-only agent never learns they exist. A tool it cannot see is not one it can talk a user into asking for. Each tool additionally needs the ordinary resource scope for what it touches -— `run_workflow` needs `workflows:write`, `list_servers` needs +- `run_workflow` needs `workflows:write`, `list_servers` needs `servers:read`, and so on, exactly as the [scopes table](../reference/api-tokens.md#scopes) describes for any other token. `mcp:read` or `mcp:write` decides whether the MCP surface is open to this token at all; the resource scope decides what it @@ -56,7 +56,7 @@ and cannot run a workflow that would touch one. It can list and inspect staging servers and run a workflow already configured to target them. Point it at a workflow whose saved targets include even one production host, though, and `run_workflow` refuses the -call outright — see [what an agent cannot do](#what-an-agent-cannot-do) +call outright - see [what an agent cannot do](#what-an-agent-cannot-do) below for why that refusal is all-or-nothing rather than "run on the servers it can see." @@ -64,7 +64,7 @@ servers it can see." Agent access lives at `/api/mcp` on your instance, over the streamable HTTP transport, and takes a bearer token the same way the REST API does. It has -no separate authentication of its own — a browser tab with a signed-in +no separate authentication of its own - a browser tab with a signed-in session does not count, only a real API token does. Point an MCP client at it with a configuration block like this: @@ -83,15 +83,15 @@ Point an MCP client at it with a configuration block like this: The endpoint is stateless and answers POST requests only: it holds no per-connection session, so each call is self-contained and there is nothing -to resume. A GET request — a client probing for the endpoint, or a browser -navigating to it directly — gets a `405`, by design, rather than a page. That +to resume. A GET request - a client probing for the endpoint, or a browser +navigating to it directly - gets a `405`, by design, rather than a page. That is the protocol-correct response from a server that does not offer the GET/SSE half of MCP, not a misconfiguration. ## The tool list Every tool below calls the same service code the REST API and the Vantage -UI call. Nothing in this list has its own path to the database — the MCP +UI call. Nothing in this list has its own path to the database - the MCP surface is a presentation layer over existing authority, not a new one. | Tool | What it does | Scope required | @@ -111,7 +111,7 @@ surface is a presentation layer over existing authority, not a new one. | `get_server_packages` | List installed packages on one server, optionally filtered by name. | `mcp:read`, `vulns:read` | | `search_fleet` | Search every server's installed packages by name across the fleet. | `mcp:read`, `vulns:read` | | `list_audit_events` | List recent audit log events. | `mcp:read`, `settings:read` | -| `list_secret_names` | List secret group and key names — metadata only, never a value. | `mcp:read`, `secrets:read` | +| `list_secret_names` | List secret group and key names - metadata only, never a value. | `mcp:read`, `secrets:read` | | `run_workflow` | Run a workflow against the servers it is already saved to target. | `mcp:write`, `workflows:write` | | `cancel_run` | Cancel an in-progress workflow run. | `mcp:write`, `workflows:write` | | `apply_updates` | Apply pending OS package updates on real servers. | `mcp:write`, `servers:write` | @@ -132,13 +132,13 @@ or the agent to filter. ## Running a workflow: targets are fixed, not chosen `run_workflow` does not take a target list. A workflow runs the servers and -tags it was already saved with — an agent cannot redirect it at a different +tags it was already saved with - an agent cannot redirect it at a different part of the fleet on the fly. Before starting the run, Vantage checks that those saved targets sit entirely inside the calling token's tag restriction. If even one of them falls outside it, the whole call is refused; there is no partial run against just the servers the token happens to be able to see. A token scoped to `env=staging` cannot use a -production-and-staging workflow to touch only the staging half — it has to +production-and-staging workflow to touch only the staging half - it has to be pointed at a workflow whose saved targets are staging-only from the start. @@ -184,7 +184,7 @@ This is the section to read before turning the feature on. that runs a script, but it cannot open an interactive session on a machine. - **It cannot exceed its tag restriction.** Every tool that resolves - servers — by ID, by tag, or through a saved workflow — resolves them + servers - by ID, by tag, or through a saved workflow - resolves them through the token's own scope. A server outside that scope is invisible to `list_servers` and `get_server`, not-found to anything asking about it by ID, and excluded from what a write tool will act on. @@ -193,7 +193,7 @@ This is the section to read before turning the feature on. sees those tools in the listing. - **It cannot touch more than 25 servers in one call without saying so explicitly.** Any write tool that would affect more than 25 servers is - refused with a message asking for `confirm:true` before it will proceed — + refused with a message asking for `confirm:true` before it will proceed - cheap insurance against a mis-parsed tag selector reaching the whole fleet on one badly phrased instruction. - **It cannot edit or delete anything that already exists.** The creation @@ -201,10 +201,10 @@ This is the section to read before turning the feature on. workflow or monitor a person defined. A monitor whose runner server sits outside a token's scope is not hidden -from that token — it still appears in `list_monitors`. The REST API reaches +from that token - it still appears in `list_monitors`. The REST API reaches that outcome by redacting: it replaces the runner with a `RunnerRestricted` placeholder when the runner falls outside the caller's -visible servers. The MCP tools reach the same outcome a different way — +visible servers. The MCP tools reach the same outcome a different way - `list_monitors` and `get_monitor_status` project a monitor onto a summary that never carries a runner field for any caller, restricted or not, so there is nothing there to redact in the first place. Two mechanisms, chosen @@ -219,16 +219,16 @@ many. Every tool call reaches the audit log, whether it read something or changed something: -- A successful call — read or write — is recorded as `mcp.tool_call`. -- A call refused by policy — missing scope, an out-of-scope target, or a - fan-out over 25 servers without confirmation — is recorded as +- A successful call - read or write - is recorded as `mcp.tool_call`. +- A call refused by policy - missing scope, an out-of-scope target, or a + fan-out over 25 servers without confirmation - is recorded as `mcp.tool_denied`, naming the specific gate that refused it, so a human reading the log can tell "the agent was stopped" from "the agent tried and the underlying action failed." - A write tool that reached the underlying service and that service rejected is recorded as `mcp.tool_failed`. - Creating a step, workflow or monitor is recorded separately as - `mcp.created`, naming what was made and its ID — a question like "what + `mcp.created`, naming what was made and its ID - a question like "what has this agent added to my instance" should not require digging through hundreds of read rows to answer. diff --git a/docs/vantage/notification-channels.md b/docs/vantage/notification-channels.md index e1e0373..578c483 100644 --- a/docs/vantage/notification-channels.md +++ b/docs/vantage/notification-channels.md @@ -67,7 +67,7 @@ Port `465` uses implicit TLS; anything else uses STARTTLS. ### Credentials are never read back The SMTP `password`, the Telegram `token` and the webhook, Slack and Discord -`url`s come back from `GET /api/channels` as `••••••••` — a webhook URL is the +`url`s come back from `GET /api/channels` as `••••••••` - a webhook URL is the authorisation to post to that channel, so it is treated as a credential like the rest. Writing that value back unchanged keeps the stored one, which is what lets you rename a channel without retyping its password. Anything else you send diff --git a/docs/vantage/servers.md b/docs/vantage/servers.md index baeee07..0004843 100644 --- a/docs/vantage/servers.md +++ b/docs/vantage/servers.md @@ -89,7 +89,7 @@ metrics is normal rather than a fault. ### OS updates -Agents check for pending package updates hourly and report the count — the +Agents check for pending package updates hourly and report the count - the machine's own package manager on Linux, the Windows Update COM API on Windows. From the server page you can: @@ -113,7 +113,7 @@ Opens a browser SSH, RDP or VNC session. See [Browser console](./browser-console Windows agents register, heartbeat, run workflow steps, report inventory, check and apply OS updates, report workloads (services and containers), and serve the browser console. They do not manage `authorized_keys`, and they are -not covered by package inventory or CVE scanning — the vulnerability feeds +not covered by package inventory or CVE scanning - the vulnerability feeds this project uses carry no Windows data. ## Removing a server diff --git a/docs/vantage/status-pages.md b/docs/vantage/status-pages.md index 3e78553..abf2fdc 100644 --- a/docs/vantage/status-pages.md +++ b/docs/vantage/status-pages.md @@ -6,12 +6,12 @@ sidebar_label: Status pages A status page is a public page reporting a chosen set of monitors as up-front components, with a 90-day history and an uptime percentage per component. It -needs no session and no token to read — anyone with the link can open it, +needs no session and no token to read - anyone with the link can open it, which is the point: it is what you hand a customer instead of an incident email. Requires the **Status pages** licence feature. If the licence lapses, or the -tier does not include the feature, the page keeps serving — it renders an +tier does not include the feature, the page keeps serving - it renders an explanation rather than data or a broken page, so a customer who follows an old link never sees an error. @@ -28,7 +28,7 @@ https:///status/ On **Vantage Cloud** that address is your instance's own subdomain, so the page is at `https://.vantage.hostxtra.co.uk/status/`. -On a **self-hosted** install it is whatever address you reach Vantage on — +On a **self-hosted** install it is whatever address you reach Vantage on - `https://vantage.acme.com/status/`, or an IP and port on a LAN install. A self-hosted install serves exactly one Vantage instance, so no subdomain is needed to say which one you mean. The **Copy** control next to the @@ -37,26 +37,26 @@ the one to hand out. **The page id cannot be changed after creation.** Once you have shared the link, changing the id would break it, so pick something you would still be -happy with in a year — `platform`, `api`, a customer's own name for a +happy with in a year - `platform`, `api`, a customer's own name for a dedicated page. ## Draft versus published A new page starts unpublished. Unpublished pages answer *not found* to -anyone who requests them, including you, from a browser without a session — +anyone who requests them, including you, from a browser without a session - so you can build out the components and copy before announcing it. Toggle **Published** when it is ready. Un-publishing later takes it back to *not found* rather than deleting anything. **Delete page**, in the editor header, is the only way to correct a page id you -regret — the id is fixed once created. It takes the page, its sections and its +regret - the id is fixed once created. It takes the page, its sections and its authored incidents with it; monitors and their history are untouched. If you only want the page off the internet, un-publish it instead. ## Sections and components -A page is organised into **sections** — arbitrary groupings such as "API" or -"Region: EU" — each holding one or more **components**. A component is a +A page is organised into **sections** - arbitrary groupings such as "API" or +"Region: EU" - each holding one or more **components**. A component is a monitor plus a **display name** you choose for this page. The display name is never the monitor's own name unless you type it in. An @@ -64,7 +64,7 @@ internal monitor name ("prod-db-primary-eu1") is rarely what you want a customer reading; give it whatever name makes sense to them, and change it for a different page without touching the monitor. -If a monitor listed on a page is later deleted, its component still appears — +If a monitor listed on a page is later deleted, its component still appears - reading `Unknown` rather than up or down, because nothing is checking it any more and claiming otherwise would be a false claim of health. @@ -77,7 +77,7 @@ more and claiming otherwise would be a false claim of health. - A 90-day history bar per component. - Any active incidents, upcoming maintenance, and a rolling history of both. - An optional banner across the top of the page, for anything you want said - regardless of component state. It is one notice with one appearance — there + regardless of component state. It is one notice with one appearance - there are no severity levels to choose between. A visitor never sees a target URL, host or port, the check's expected status @@ -90,17 +90,17 @@ reachable is on this page. Two kinds of entries appear on a page's timeline: -- **Automatic** — a monitor going down opens an incident on any page that +- **Automatic** - a monitor going down opens an incident on any page that lists it, with no action from you. These appear the moment the monitor's state changes and close the moment it recovers. -- **Authored** — an incident or maintenance window you create by hand, with +- **Authored** - an incident or maintenance window you create by hand, with its own title, impact and a set of affected components you choose. You post updates to it (Investigating → Identified → Monitoring → Resolved) as the situation develops, and each update is timestamped and kept on the page's history. An authored incident is attached to one or more pages explicitly when you -create it — it does not follow a monitor onto every page that monitor happens +create it - it does not follow a monitor onto every page that monitor happens to be listed on. ### Scheduling maintenance @@ -111,7 +111,7 @@ is in progress and its affected components are within the scheduled time, those components are drawn as "under maintenance" instead of up or down. **Maintenance changes how a day is drawn, never the uptime number itself.** -The 90-day percentage is computed from what actually happened — a component +The 90-day percentage is computed from what actually happened - a component that stayed up throughout a maintenance window still shows as up in its history, it is only the live status pill that reads "under maintenance" for the duration. @@ -120,6 +120,6 @@ the duration. A visitor's read of a page is cached for up to 30 seconds, so posting an update or flipping Published does not necessarily change what a visitor sees -instantly — though most authoring actions invalidate that cache immediately, +instantly - though most authoring actions invalidate that cache immediately, so in practice it usually shows within a second or two. If a change genuinely does not appear, reloading after 30 seconds always will. diff --git a/docs/vantage/workloads.md b/docs/vantage/workloads.md index 34d7464..b203b85 100644 --- a/docs/vantage/workloads.md +++ b/docs/vantage/workloads.md @@ -4,7 +4,7 @@ title: Workloads sidebar_label: Workloads --- -A **workload** is one Docker container or one service — a systemd unit on +A **workload** is one Docker container or one service - a systemd unit on Linux, a Windows service on Windows. Every server reports what it is running, and you can start, stop and restart those workloads, and read their recent logs, without opening a console. diff --git a/nginx.conf b/nginx.conf index f77bc7e..25cf238 100644 --- a/nginx.conf +++ b/nginx.conf @@ -11,7 +11,7 @@ server { return 302 /docs/; } - # Hashed assets are immutable — the filename changes when the content does. + # Hashed assets are immutable - the filename changes when the content does. location /docs/assets/ { expires 1y; add_header Cache-Control "public, immutable"; diff --git a/src/css/custom.css b/src/css/custom.css index 451b355..96fb9d1 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -1,7 +1,7 @@ /* ========================================================================== Vantage documentation design tokens - The token block below is COPIED VERBATIM from site/app/globals.css — same + The token block below is COPIED VERBATIM from site/app/globals.css - same names, same values. adminsite/ holds a copy too, web/ holds the dark half, and shared/mail/templates/layout.html.tmpl holds it a fifth time as literal hex because email clients support neither var() nor prefers-color-scheme. @@ -13,7 +13,7 @@ value outside the two token blocks. Docusaurus always stamps data-theme on , so unlike site/ there is no - prefers-color-scheme branch to keep in step — the theme toggle is the only + prefers-color-scheme branch to keep in step - the theme toggle is the only writer. ========================================================================== */ @@ -200,7 +200,7 @@ color: var(--ink); } -/* Machine output — install one-liners, key blobs, run logs — sits on a floor +/* Machine output - install one-liners, key blobs, run logs - sits on a floor beneath the panel, the same distinction web/ draws with --well. */ .theme-code-block { border: 1px solid var(--rule-soft); diff --git a/static/img/favicon.svg b/static/img/favicon.svg index c413a36..a827acc 100644 --- a/static/img/favicon.svg +++ b/static/img/favicon.svg @@ -1,7 +1,7 @@