hand over NPM's API instead of a curated subset of it
this plugin refused to create, edit or delete anything, and argued the refusal was care: NPM's own UI does those well and each is a form with real consequences. the owner's call on 2026-08-25 is that the argument ends up somewhere else — "even though we are assuming users that don't know how to operate nginx, the last thing I want to do is treat users as stupid" and a curated surface cannot keep up anyway. every NPM feature we did not wrap is one the owner cannot reach through Officer at all, so the plugin becomes a worse NPM rather than a way into the real one. so /api/proxy-manager/npm/* forwards method, path, query and body, attaches a token, and returns NPM's own status and body unchanged — errors included. a proxy that improves the answers is not a proxy. authentication is the only thing it does not pass through, which is the entire job. the permission gate needed nothing: isRequestAllowedAtLevel already returns true for safe methods and false otherwise, so read level IS a read-only view of NPM through here, with no code in this file deciding it twice. the curated routes stay, for the panels. a screen wants sorted hosts and a computed daysUntilExpiry; an agent wants NPM. what replaces the refusal is confirmation, and it lives in the skill because that is what it is — an instruction, not a boundary. saying so is better than a boundary that only looks like one. the skill now carries NPM's API, names what must be confirmed before it happens (deleting, editing a live upstream, touching a certificate in use), says creating needs no ceremony, and forbids reaching NPM any way but this route. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -1,94 +1,110 @@
|
||||
---
|
||||
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.
|
||||
description: Use for anything about this machine's domains, proxying or TLS — which hostnames it serves, where one forwards, whether a site is down, when a certificate expires, and creating, editing, enabling or deleting any of it. Carries the pass-through route into Nginx Proxy Manager's own API, the one 404 that means something else, and what to confirm before doing.
|
||||
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.
|
||||
Officer proxies through **Nginx Proxy Manager**. This plugin holds NPM's admin credential and hands you
|
||||
NPM's own API through it.
|
||||
|
||||
**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
|
||||
## The route
|
||||
|
||||
```
|
||||
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
|
||||
/api/proxy-manager/npm/<anything> -> <npm>/api/<anything>
|
||||
```
|
||||
|
||||
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.
|
||||
Method, path, query string and body are forwarded unchanged, and **NPM's own status and body come back
|
||||
unchanged** — including its errors. A token is attached for you.
|
||||
|
||||
**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
|
||||
```bash
|
||||
curl -s -H "Authorization: Bearer $OFFICER_AGENT_KEY" -H "Host: $OFFICER_API_HOST" \
|
||||
"$OFFICER_API_URL/api/proxy-manager/npm/nginx/proxy-hosts?expand=certificate,access_list"
|
||||
```
|
||||
|
||||
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.
|
||||
**The contract is NPM's API, not ours.** If NPM can do it, this can do it — `docs/npm-api.md` in the
|
||||
plugin has the endpoints, and NPM's own docs cover the rest. Do not go looking for a curated Officer route
|
||||
for each operation; there isn't one, and that is the design.
|
||||
|
||||
**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.
|
||||
**Never reach NPM directly.** Not by its port, not by `docker exec`, not by finding the credential. The
|
||||
credential is deliberately not yours, this route is the sanctioned path, and going around it is
|
||||
unauthorised, unlogged and unbounded. If this route refuses you, that refusal is the answer.
|
||||
|
||||
**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 panels use different routes
|
||||
|
||||
## The one write
|
||||
`/api/proxy-manager/hosts`, `/certificates` and `/status` also exist. They sort and compute
|
||||
`daysUntilExpiry` for the screen. Read them if you want the same shaped answer the panel shows; use
|
||||
`/npm/*` for anything else.
|
||||
|
||||
```
|
||||
POST /api/proxy-manager/hosts/:id/enable
|
||||
POST /api/proxy-manager/hosts/:id/disable
|
||||
```
|
||||
## What you can do
|
||||
|
||||
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.
|
||||
Everything NPM can, which is roughly:
|
||||
|
||||
**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.
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `GET/POST/PUT/DELETE /npm/nginx/proxy-hosts[/:id]` | the proxy hosts |
|
||||
| `POST /npm/nginx/proxy-hosts/:id/enable` · `/disable` | in and out of service |
|
||||
| `…/redirection-hosts`, `…/dead-hosts`, `…/streams` | the other three kinds of host |
|
||||
| `…/certificates[/:id]`, `…/certificates/:id/renew` | Let's Encrypt and uploaded |
|
||||
| `GET /npm/nginx/certificates/dns-providers` | which DNS providers this NPM can drive, with their credential field names |
|
||||
| `…/access-lists[/:id]` | who may reach a host |
|
||||
|
||||
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.
|
||||
A proxy 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 the
|
||||
field to read when somebody says a site is down**, not whether the row exists.
|
||||
|
||||
## Confirm before you change something
|
||||
|
||||
**Do what is asked.** The owner runs this machine; a request to create, edit, enable, disable or delete a
|
||||
host is a legitimate request and refusing it is not caution, it is being unhelpful about somebody's own
|
||||
server.
|
||||
|
||||
**But say what will happen first, and wait, when the change is not trivially reversible:**
|
||||
|
||||
- **Deleting** a proxy host, a redirection, a stream, a certificate or an access list. Name exactly what
|
||||
goes and what it currently serves. A deleted host is not recoverable from here.
|
||||
- **Editing** an existing host's domains or upstream. That is a live site changing where it points.
|
||||
- **Disabling.** One click back, so a short confirmation is enough — but a domain goes dark meanwhile.
|
||||
- **Anything touching a certificate** that a live host is using.
|
||||
|
||||
Creating something new needs no ceremony: report what you made.
|
||||
|
||||
**Never infer a change from a question.** "Is `foo.example.com` up?" asks about `meta.nginx_online`. It is
|
||||
not an instruction to restart, disable or recreate anything.
|
||||
|
||||
## Certificates, read 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 — raise that unprompted. `expires_on` is null on some
|
||||
uploaded certificates; that is unknown, not urgent.
|
||||
|
||||
**Nothing warns anybody.** There is no notification for this yet. If you notice one inside 14 days, say so
|
||||
in the conversation, because nothing else will.
|
||||
|
||||
## 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.
|
||||
request carries a token, so "not signed in" and "no such endpoint" are identical responses.
|
||||
|
||||
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.**
|
||||
The pass-through already re-mints its token and retries once, so a 404 that reaches you is about your
|
||||
request rather than about the token. Read it as "no such thing" — but if every route 404s, the stored
|
||||
credential is wrong and the owner needs to fix it at `/proxy-manager?config=1`.
|
||||
|
||||
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.
|
||||
It is not a rule about the whole API: `GET /npm/nginx/certificates/dns-providers` answers a proper 403
|
||||
unauthenticated.
|
||||
|
||||
## Two states that are not failures
|
||||
## Two answers 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.
|
||||
- **409 `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** — either the account lacks `proxy-manager`, or it has it at **read** level, which permits GET and
|
||||
refuses every write. Do not look for another way round; report it.
|
||||
|
||||
## What is coming, so you do not promise it
|
||||
## A private hostname needs a DNS credential, and that is not built
|
||||
|
||||
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.
|
||||
TLS on a name that only resolves inside a VPN cannot use the usual challenge — Let's Encrypt cannot reach
|
||||
it — so it needs a DNS-01 challenge, which needs that domain's provider API key.
|
||||
|
||||
**Officer does not store DNS provider credentials yet.** `GET /npm/nginx/certificates/dns-providers` lists
|
||||
what this NPM can drive and the field names each one wants, and a certificate can be created through the
|
||||
pass-through if the owner supplies the credential in the request. Do not promise the smoother version:
|
||||
`plugins/proxy-manager/README.md` has the design and none of it exists.
|
||||
|
||||
Reference in New Issue
Block a user