Skip to content

feat: edge-verified identity, prefix-aware page, and job retention - #5

Merged
cyrilcaoyang merged 2 commits into
mainfrom
feat/edge-identity
Sep 7, 2026
Merged

feat: edge-verified identity, prefix-aware page, and job retention#5
cyrilcaoyang merged 2 commits into
mainfrom
feat/edge-identity

Conversation

@cyrilcaoyang

Copy link
Copy Markdown
Collaborator

Makes a submission attributable to a signed-in person instead of a name somebody typed, by letting this service sit behind the lab's single Caddy edge. Companion PR: AccelerationConsortium/ac-organic-lab#33 (the /bambu/* route and the framed panel).

Why the edge rather than a login here: a session cookie cannot be shared with this gateway on its own address — raw 100.x addresses cannot carry a Domain cookie and *.ts.net is on the Public Suffix List, so browsers drop tailnet-wide cookies (AUTH_DESIGN, Why sessions can't be shared per-host). One origin behind the edge is the only arrangement that yields one login, so a page served on this port is architecturally excluded from SSO no matter what we build on it.

The security-relevant part — please review identity.py closely

It trusts the edge's injected X-Auth-User only when the request also carries X-Edge-Auth matching BAMBU_EDGE_SHARED_SECRET — something a caller coming straight off the tailnet cannot produce, which matters because this port stays directly reachable. Same mechanism as the xArm's arrangement.

Three deliberate properties:

  • Fails closed. No configured secret means no trusted identity, ever — never "believe the header because we have nothing to check it against". A deployment with no secret behaves exactly as it did before this PR, which is what makes merging ahead of deployment safe.
  • Constant-time comparison (hmac.compare_digest). == on a secret leaks it a byte at a time.
  • A verified identity overrides any client-supplied name, or a signed-in person could file work under someone else's. An edge that proves itself but names nobody is anonymous, not authenticated.

11 tests cover it directly, including forged headers, wrong/missing secrets, an unconfigured deployment, and control-character/length handling on the injected name.

What it records

requested_by_verified / approved_by_verified on every job, plus a "(verified identity)" marker in the history note. Stored per job rather than inferred later from how the service happened to be deployed when the job arrived.

GET /whoami reports what the current request carries so the page can word itself honestly — identity_available separates "not signed in" from "this deployment cannot tell who you are". It echoes nothing the caller did not already present, and never the secret (asserted by a test).

Prefix-aware page

Derives its API base by stripping the trailing /ui from its own URL, so one file serves the direct deployment (base "") and any edge prefix (/bambu) with no server-side rewrite and no build-time config — the same arrangement as the OT-2 SPA. Hardcoded /printers would have reached the dashboard instead of this gateway.

It also answers at both /ui and /ui/. Serving only one would make Starlette redirect between them with a Location that drops the edge prefix, landing the visitor on the dashboard — the trap the /xarm5 edge block documents.

With a verified identity the page shows the signed-in account and stops offering a name field; the "no sign-in" wording appears only when it is true.

Not deployed — and the bind is deliberately left wide

Three root steps, in order (recorded in docs/TODO.md):

  1. Install the updated Caddyfile.single-edge and reload Caddy.
  2. Set the same BAMBU_EDGE_SHARED_SECRET in Caddy's EnvironmentFile and in bambu-server's unit env; restart both.
  3. Then revert the bind to 127.0.0.1. It is 0.0.0.0 today so the page is reachable at all; reverting before the route exists would break what currently works.

Until step 2, the gateway trusts nothing and the embed shows a blank frame — both fail closed.

uv run ruff check . and uv run pytest -q (152 tests, 22 new) pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UQvsfEeDitEyNzbCwrcEdD

YoyoLiuuu and others added 2 commits September 7, 2026 00:53
Makes a submission attributable to a signed-in person instead of a name
somebody typed, by letting this service sit behind the lab's single Caddy edge.
The companion route and the framed panel are in ac-organic-lab.

Why the edge and not a login here: a session cookie cannot be shared with this
gateway on its own address -- raw 100.x addresses cannot carry a `Domain`
cookie and *.ts.net is on the Public Suffix List, so browsers drop
tailnet-wide cookies (AUTH_DESIGN, "Why sessions can't be shared per-host").
One origin behind the edge is the only arrangement that yields one login, so a
page served on this port is architecturally excluded from SSO.

## identity.py

Trusts the edge's injected `X-Auth-User` only when the request also carries
`X-Edge-Auth` matching `BAMBU_EDGE_SHARED_SECRET` -- something a caller coming
straight off the tailnet cannot produce, which matters because this port stays
directly reachable. Same mechanism as the xArm's arrangement.

Three properties worth reviewing closely, since this is the security-relevant
part:

- **Fails closed.** No configured secret means no trusted identity, ever --
  never "believe the header because we have nothing to check it against". A
  deployment with no secret behaves exactly as before this commit, which is
  what makes shipping ahead of deployment safe.
- **Constant-time comparison** (`hmac.compare_digest`): `==` on a secret leaks
  it a byte at a time.
- **A verified identity overrides any client-supplied name.** Otherwise a
  signed-in person could file work under someone else's name. An edge that
  proves itself but names nobody is anonymous, not authenticated.

## What it records

`requested_by_verified` / `approved_by_verified` on every job, and a
"(verified identity)" marker in the history note. The distinction is stored per
job rather than inferred from how the service happened to be deployed when the
job arrived.

`GET /whoami` reports what the current request carries, so the page can word
itself honestly -- `identity_available` separates "not signed in" from "this
deployment cannot tell who you are". It echoes nothing the caller did not
already present, and never the secret.

## Prefix-aware page

The page derives its API base by stripping the trailing `/ui` from its own URL,
so one file serves both the direct deployment (base "") and any edge prefix
(`/bambu`), with no server-side rewrite and no build-time config -- the same
arrangement as the OT-2 operator SPA. Hardcoded `/printers` would have reached
the dashboard instead of this gateway.

It also answers at both `/ui` and `/ui/`: serving only one would make Starlette
redirect between them with a `Location` that drops the edge prefix, landing the
visitor on the dashboard. That is the trap the /xarm5 edge block documents.

When an identity is verified the page shows the signed-in account and stops
offering a name field; the "no sign-in" wording is only shown when it is true.

## Not deployed

Needs root: install the Caddy route, set the same secret on both sides, restart
both. The bind stays 0.0.0.0 until then -- reverting it to loopback before the
edge route exists would break the page that currently works. Sequence recorded
in docs/TODO.md.

`uv run ruff check .` and `uv run pytest -q` (152 tests, 22 new) pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQvsfEeDitEyNzbCwrcEdD
Two follow-ups from bringing the pipeline up, both flagged during it.

## Retention

Terminal records used to accumulate forever, and the only way to clear one was
`rm` on the store directory -- which left the running process serving a record
whose file was gone. Both halves are fixed:

- Terminal records are swept at startup once older than
  `submissions.retain_terminal_days` (30 by default, null disables).
- `DELETE /submissions/{id}` removes one finished job's record and artifact
  immediately.

Three decisions worth recording:

- **A job still in play is never swept, however old.** One stuck in
  `validating` is a signal, not litter, and quietly deleting it would destroy
  the evidence of whatever wedged it.
- **Only a terminal job can be deleted.** Withdrawing a waiting job is
  `cancel`, which leaves a record of the decision; allowing delete there would
  erase the decision along with the job.
- **Sweeping at startup, not on a timer.** The store loads from disk once, so a
  record removed underneath a running process lingers in memory until restart.
  Startup is the one moment the two views are guaranteed to agree -- and that
  divergence is precisely what made hand-cleanup necessary during bring-up.

`TERMINAL_STATES` is derived from the transition table (states with no onward
edge) rather than listed a second time, so a state added there cannot be
forgotten here. A test pins that correspondence in both directions.

## docs/EDGE_DEPLOY.md

An ordered runbook for putting the page behind the dashboard's login, since the
remaining work is all root steps and easy to get in the wrong order. It leads
with what the two access paths mean for attribution, validates the Caddyfile
before installing it (a bad config takes the whole dashboard down), reloads
rather than restarts Caddy, and ends with the step that must come last --
narrowing the gateway's bind back to loopback, which is only safe once the edge
route works.

The verification step is the one that matters: a forged `X-Auth-User` presented
directly must come back `verified: false` while `identity_available: true`
confirms the gateway actually picked the secret up. Both were checked against
the real code.

The secret itself is generated into the gitignored `.env` (which the unit
already loads, so the gateway half needs no root) and the runbook reads it back
with `sed` rather than having anyone retype it. It is not in git and not in any
transcript.

`uv run ruff check .` and `uv run pytest -q` (161 tests, 9 new) pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQvsfEeDitEyNzbCwrcEdD
@cyrilcaoyang cyrilcaoyang changed the title feat: accept an edge-verified identity, and make the page prefix-aware feat: edge-verified identity, prefix-aware page, and job retention Sep 7, 2026
@cyrilcaoyang

Copy link
Copy Markdown
Collaborator Author

Two further commits landed on this branch after the description above — flagging them so the review scope is clear.

51003e0 — retention for finished jobs

Terminal records used to accumulate forever, and the only way to clear one was rm on the store directory, which left the running process serving a record whose file was gone. Both halves are fixed: terminal records are swept at startup past submissions.retain_terminal_days (30 default, null disables), and DELETE /submissions/{id} removes one finished job immediately.

Three decisions worth a look:

  • A job still in play is never swept, however old. One stuck in validating is a signal, not litter — quietly deleting it would destroy the evidence of whatever wedged it.
  • Only a terminal job can be deleted. Withdrawing a waiting job is cancel, which leaves a record of the decision; allowing delete there would erase the decision along with the job.
  • Sweeping at startup, not on a timer. The store loads from disk once, so a record removed underneath a running process lingers in memory until restart. Startup is the one moment both views are guaranteed to agree.

TERMINAL_STATES is derived from the transition table (states with no onward edge) rather than listed twice, so a state added there cannot be forgotten here; a test pins that correspondence both ways.

docs/EDGE_DEPLOY.md

An ordered runbook for the remaining root steps, which are easy to get in the wrong order. It validates the Caddyfile before installing (a bad config takes the whole dashboard down), reloads rather than restarts Caddy, and ends with the step that must come last — narrowing the gateway's bind back to loopback, only safe once the edge route works.

Its verification step is the one that matters, and I checked both outputs against the real code: a forged X-Auth-User presented directly must return verified: false, while identity_available: true confirms the gateway actually picked the secret up.

The secret is generated into the gitignored .env (which the unit already loads, so the gateway half needs no root) and the runbook reads it back with sed rather than having anyone retype it. Not in git, not in any transcript.

CI green. 161 tests.

@cyrilcaoyang
cyrilcaoyang merged commit 59cb591 into main Sep 7, 2026
1 check passed
@cyrilcaoyang
cyrilcaoyang deleted the feat/edge-identity branch September 7, 2026 05:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants