sg-compute.sgit.ai / agents

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.

GroupRoutes
ConsoleGET / — a capability-driven HTML console
Health/health/info · /health/status · /health/capabilities
Browser primitivesPOST /browser/navigate|click|fill|get-content|get-url
ScreenshotPOST /browser/screenshot — raw PNG bytes, timings in X-*-Ms headers
SequencesPOST /sequence/execute — the 25-verb step language
CapturePOST /screenshot · /screenshot/batch · /inspect
SessionsPOST /session/open, /session/{id}/act|probe|close
DesktopPOST /desktop/browser — headed, on an X display. VNC image only; returns 400 on a headless image
MetricsGET /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.

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: