diff --git a/skills/proxy-manager/SKILL.md b/skills/proxy-manager/SKILL.md new file mode 100644 index 0000000..8610272 --- /dev/null +++ b/skills/proxy-manager/SKILL.md @@ -0,0 +1,94 @@ +--- +name: Proxy Manager +description: Use when asked which domains this machine serves, where a hostname forwards to, whether a site is up, or when a TLS certificate expires — and before taking a proxy host in or out of service. Carries the routes, the one 404 that means something else, and what this surface deliberately cannot do. +version: 1 +--- + +Officer proxies through **Nginx Proxy Manager**, and this plugin is how you reach it. Everything below is +`/api/proxy-manager/...`, authenticated as the signed-in account like any other platform route. + +**Go through the platform, never at NPM directly.** The admin credential lives encrypted on the server and +is never handed out — not to you, not to the owner. A route here already holds it, already translates NPM's +odder answers, and is the only path that works for an account that is not the owner. + +## What is running, and where it goes + +``` +GET /api/proxy-manager/hosts proxy hosts, sorted by first domain +GET /api/proxy-manager/hosts/:id one host, with its certificate and access list expanded +GET /api/proxy-manager/redirections redirection hosts +GET /api/proxy-manager/streams TCP/UDP streams +GET /api/proxy-manager/dead-hosts domains served deliberately as nothing (NPM calls them 404 hosts) +GET /api/proxy-manager/access-lists with their items and clients +GET /api/proxy-manager/status is a connection configured, and does it answer +``` + +A host carries `domain_names`, `forward_scheme`, `forward_host`, `forward_port`, `enabled`, `ssl_forced`, +`certificate_id` (0 means none), `access_list_id`, and `meta.nginx_online` — which is `false` when nginx +itself reports the site down, and is the field to check when somebody says a site is broken. + +**This is the honest answer to "what domains does this machine serve".** Better than any document: a +hand-maintained list in a repository is right on the day it is written, and this is what nginx is actually +configured with right now. + +## Certificates + +``` +GET /api/proxy-manager/certificates +``` + +Sorted by expiry rather than name, with `daysUntilExpiry` computed server-side. That ordering is the point: +a list by name is a reference, a list by expiry answers the only question anyone has. + +**Read the numbers the way Let's Encrypt behaves.** Renewal runs at 30 days, so under 30 is normal and not +worth mentioning. Under 14 means renewal has actually failed and the domain goes dark at zero — that is +worth raising unprompted. `expires_on` is null for some uploaded certificates, and `daysUntilExpiry` is +then null too; that is unknown, not urgent. + +**Nothing warns anybody.** No notification exists yet. If you notice a certificate inside 14 days, say so +in the conversation — there is no other mechanism that will. + +## The one write + +``` +POST /api/proxy-manager/hosts/:id/enable +POST /api/proxy-manager/hosts/:id/disable +``` + +Taking a host in or out of service. **It is the only write this surface has**, deliberately: it is +reversible in one click, and creating, editing and deleting hosts are not. Do not look for a create route — +there isn't one, and its absence is a decision rather than a gap. + +**Disabling a host takes a domain off the internet.** Confirm before you do it, name the domains, and do not +infer it from something adjacent. "Is `foo.example.com` up?" is a question about `meta.nginx_online`, not an +instruction to change anything. + +For anything this cannot do — a new proxy host, a certificate to issue, an access list to edit — say so and +point at NPM's own UI. Do not attempt it another way. + +## The 404 that is not a 404 + +**NPM answers an unauthenticated read with 404, not 401.** Its router only registers a resource once the +request carries a token, so "not signed in" and "no such endpoint" are the same response. + +The plugin already knows this: it re-mints its token and retries once, and if a 404 survives that it reports +the credential rather than the route. So a failure reaching you through these routes has already been +interpreted — **read the message, do not re-diagnose it as a missing endpoint.** + +It is not a rule about the whole API, only about the routes that need it. `GET /nginx/certificates/dns-providers`, +for instance, answers a proper 403. + +## Two states that are not failures + +- **409 with `unconfigured`** — no NPM connection is stored. Nothing is broken; the owner has not connected + one. Only the owner can, at `/proxy-manager?config=1`. +- **403 on anything under `/_config`** — the connection settings are owner-only, including the test. It is + an NPM admin login and NPM has no read-only role, so anyone holding it can redirect any hostname this + machine serves. Reading the hosts and certificates is grantable; the credential is not. + +## What is coming, so you do not promise it + +Handing DNS and certificates over properly — a domain registry, provider API keys in the secret store, and +one request that turns *"expose sub.example.com on the private network over HTTPS"* into a DNS record, a +certificate and a proxy host. **None of it is built.** `plugins/proxy-manager/README.md` has the design. +Until it exists, do not offer to create a host or issue a certificate. diff --git a/web/layout.ts b/web/layout.ts index dded01f..a7c057b 100644 --- a/web/layout.ts +++ b/web/layout.ts @@ -1,13 +1,39 @@ import type { LayoutNode } from 'officerdev'; -// Hosts wide, certificates beside it. Certificates is a glance rather than a workspace — you look at it -// when something is red — so it gets the narrower half. +// Hosts wide, certificates beside it, a chat across the bottom. +// +// ── Why the chat panel is the platform's and not one of ours ── +// +// `officerdev/chat` is a panel app in the shared registry — `seedAppRegistry` merges the platform's panels +// and every plugin's into ONE registry, so naming it here is all it takes. Shipping a second, simpler chat +// from this plugin would mean maintaining a second chat, and the first one to fall behind would be ours. +// +// It gets a third of the height rather than a column, because a conversation reads badly in a narrow +// column and the two lists do not: they are scannable at any width, and a chat is not. +// +// ── What makes the chat useful HERE ── +// +// Nothing in this layout. The agent gets the `Proxy Manager` skill the same way it gets every other one — +// from this plugin's `skills/` directory, offered in every conversation on the machine, costing two lines +// until something actually needs it. A chat in this panel is an ordinary chat that happens to be sitting +// underneath the thing it is being asked about. export const defaultLayout: LayoutNode = { type: 'group', id: 'proxy-manager-root', - direction: 'horizontal', + direction: 'vertical', children: [ - { node: { type: 'panel', id: 'proxy-manager-hosts', appType: 'proxy-manager-hosts' }, size: 62 }, - { node: { type: 'panel', id: 'proxy-manager-certificates', appType: 'proxy-manager-certificates' }, size: 38 }, + { + node: { + type: 'group', + id: 'proxy-manager-lists', + direction: 'horizontal', + children: [ + { node: { type: 'panel', id: 'proxy-manager-hosts', appType: 'proxy-manager-hosts' }, size: 62 }, + { node: { type: 'panel', id: 'proxy-manager-certificates', appType: 'proxy-manager-certificates' }, size: 38 }, + ], + }, + size: 66, + }, + { node: { type: 'panel', id: 'proxy-manager-chat', appType: 'officerdev/chat' }, size: 34 }, ], };