The machine surface
This page is for the thing that will actually call it. Two FastAPI applications with different auth postures, a declarative step language with 25 verbs, an endpoint that describes its own capabilities at runtime — and one correction to the received wisdom about how authentication works, because the received wisdom gives the right advice for the wrong reason.
Two applications, two auth postures
App 1 — the Playwright data plane
Image diniscruz/sg-playwright. All routes require X-API-Key, enforced by middleware. There is no anonymous surface.
| Group | Routes |
|---|---|
| Console | GET / — a capability-driven HTML console |
| Health | /health/info · /health/status · /health/capabilities |
| Browser primitives | POST /browser/navigate|click|fill|get-content|get-url |
| Screenshot | POST /browser/screenshot — raw PNG bytes, timings in X-*-Ms headers |
| Sequences | POST /sequence/execute — the 25-verb step language |
| Capture | POST /screenshot · /screenshot/batch · /inspect |
| Sessions | POST /session/open, /session/{id}/act|probe|close |
| Desktop | POST /desktop/browser — headed, on an X display. VNC image only; returns 400 on a headless image |
| Metrics | GET /metrics — Prometheus |
| Agentic admin | /admin/health|info|env|boot-log|error|manifest|capabilities|skills/{name} |
App 2 — the SG/Compute control plane
Three routes are auth-free by design: GET /api/health, /api/health/ready and /catalog/caller-ip. Everything else is authenticated.
/api/specs— the catalogue, live from the manifests/api/nodes— CRUD, plus/{node}/pods|stats|logs|stopthrough the sidecar/api/stacks— returns a list; there is no multi-node orchestration behind it/api/amis- per-spec routes, auto-discovered by convention —
spec_id 'docker'resolves tosg_compute_specs.docker.api.routes.Routes__Docker__Stack, with no registration list /legacy/*, carryingX-Deprecatedon every response- host-plane routes (
Auth,Containers,Docs,Images,Logs,Pods,Shell,Status), TLS routes, vault-spec routes, and two Lambdas
A design note worth knowing before you debug something: “specs without a route class are silently skipped.” Convenient during development, a silent-failure mode in production. A spec that loses its route class disappears from the API with no error.
⚠ The auth model — the guidance is right, the mechanism is not what it implies
The received wisdom is: “x-sgraph-access-token for /pw/…, X-API-Key for direct stack access.” That guidance is correct and you should keep following it. The mechanism behind it is not what it sounds like.
x-sgraph-access-token appears zero times in this platform's Python except as a header to discard. The service only ever validates X-API-Key. The translation happens in the vault's reverse proxy:
ENV_VAR__TOKEN = 'SGRAPH_SEND__ACCESS_TOKEN' # injected upstream as X-API-Key UPSTREAM_API_KEY = 'X-API-Key' STRIP_REQUEST = {'host', 'content-length', 'x-sgraph-access-token', 'cookie'} ... headers[UPSTREAM_API_KEY] = token # vault auth -> upstream auth
So x-sgraph-access-token is the vault's auth header, not this platform's. The proxy strips it and injects X-API-Key from its own environment.
One sub-claim in the existing documentation is wrong, and it is corrected here. The claim is: “a caller sending X-API-Key to the proxy gets 401 at the proxy.”
It does not. X-API-Key is not in STRIP_REQUEST, and the proxy overwrites it unconditionally. The 401 comes from the vault application's own middleware, sitting in front of the proxy. Same outcome, different component — which matters the moment you are debugging one of them.
And a fact worth stating plainly: the /pw proxy is in neither repository. The reverse-proxy class is written to be vendored into a vault image — “kept single-file and dependency-light… so it can be vendored into a container we do not build.” Grepping the vault repository for /pw, the route table or the image name returns zero hits.
So anyone documenting the production auth path — including this page — is documenting a component they cannot see. That is G6, and it is a real gap rather than an editorial hedge.
The sequence language — 25 verbs, one round trip
POST /sequence/execute takes a declared list of steps and runs them against a single fresh browser, returning a per-step result and a full timings block. It is the endpoint that makes this useful to an agent rather than to a script: one authenticated call, one browser lifecycle, one deadline.
# the shape — a declared step list, ids unique, run against a deadline { "steps": [ {"id": "go", "action": "navigate", "url": "https://example.com"}, {"id": "wait", "action": "wait_for", "selector": "#results"}, {"id": "shot", "action": "screenshot", "full_page": true} ] } # the response distinguishes three outcomes, per run: COMPLETED every step ran FAILED a step failed PARTIAL the deadline hit; remaining steps returned marked SKIPPED
That third outcome is the one that matters for an agent deciding whether to retry: a halted run tells you exactly how far it got, rather than discarding the work.
Self-description — ask the service, not the file
GET /health/capabilities returns the live capability set, built at runtime by Capability__Detector, which works out its own deployment target from the environment — laptop | ci | claude_web | container | lambda — and reports what is actually available there.
Do not read capabilities.json. It is COPY'd into the image and served verbatim at /admin/capabilities, and it is frozen at v0.1.29 while the code is at v0.2.71 — 42 minor versions behind. The platform's own documentation says so: “The UI reads GET /health/capabilities… never the stale capabilities.json.”
The three axioms that file names are good and are implemented; they are on the front page. The file itself is fix F5: delete it or regenerate it.
The JavaScript allowlist is deny-by-default
Worth knowing before you plan around it: JS__Expression__Allowlist is deny by default, exact match. An agent cannot evaluate anything until an operator populates the allowlist with the specific expressions it may run. Nothing is permitted by omission.
There is one deliberate exception, and it is the only place deny-by-default is switched off — it is named and examined on the security page rather than left in the source.
Stable URLs on this site
Two promises, stated rather than left to be inferred:
- Every source document is fetchable at
/briefs/<filename>. The index → - The spec catalogue is machine-readable at
/data/specs.json, and every spec page on this site is generated from it. If you want the data rather than the prose, take the JSON. - /llms.txt and /llms-full.txt.