Skip to content

docs+ui(resources): document sandbox quotas, and finish the UI they point at - #709

Merged
HYChou0515 merged 5 commits into
masterfrom
worktree-app-resources-docs
Aug 12, 2026
Merged

docs+ui(resources): document sandbox quotas, and finish the UI they point at#709
HYChou0515 merged 5 commits into
masterfrom
worktree-app-resources-docs

Conversation

@HYChou0515

@HYChou0515 HYChou0515 commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Documents the sandbox resource quotas, which have shipped since #688 and had no
usage documentation — and finishes the two frontend gaps that writing the guide
exposed.

Why

docs/adding-an-app.md never mentioned resources. docs/configuration.md had
two rows in a lookup table and no section. Predictably, none of the four
built-in Apps declares any
— the capability is complete and unused.

What

adding-an-app.md — the app.json half: the block, the per-dimension
fall-through, and three things worth knowing before touching it.

  • exceeding resources.per_app.max fails at boot, not by silent trimming
  • cpu/memory are enforced by the sandbox backend, so an http deployment
    must redeploy sandbox-host or the declaration does nothing
  • disk: "0" means unlimited; cpu: 0 means unset — deliberately asymmetric

configuration.md §6.5 — numbered like the file's existing 11.5 so §7–§14
keep their numbers. Three-layer resolution; the stock-vs-flow split that
explains why disk and cpu behave differently; the four per_user dimensions;
and the per-person override, which is superuser API only, has no UI, and
answers 404 rather than 403 so "who has an exception" doesn't leak.

Also points the three quick-reference rows at the new section and adds one for
the override.

Verified against a running server, not against the plan

Brought the app up with per_user.count: 1, took the real 507, and drove the
recovery in a browser. The error payload, the page's own wording, and the whole
loop are quoted from what happened:

507 {"error":"sandbox_quota_exceeded","dimension":"sandboxes","used":2,"limit":1.0}
  → /my-resources shows "Live environments 1 / 1"
  → click Close → "0 / 1 · No live environments"
  → same exec returns 200

Then: the two things documenting it exposed

Writing the guide from a running server surfaced two places where the frontend
had stopped one step short of the backend, so this branch finishes them rather
than documenting them as caveats.

/my-resources had no stylesheet. It shipped a full vocabulary of semantic
classes and not one was ever defined — Meter was two zero-height divs, so a
person could not see how close to the limit they were, and each row read as one
run of text. This is the page every quota refusal routes to (#692).

Per-person allowances had no UI. P7 delivered the model, the two-layer read
and three admin routes and called that the "admin entry point", but only someone
who writes curl could use it. A superuser now gets a section on the same page;
everyone else sees nothing, matching the backend's 404-not-403.

The form starts empty rather than pre-filling from current numbers: PUT
replaces all four dimensions and the read returns EFFECTIVE limits, so
pre-filling would submit inherited values back as explicit overrides and pin
them. There is a test for that specifically.

Verified in both themes in a real browser — dark masks light-mode palette
defects, not the other way round — and the whole loop driven from the UI:
refused 507 → raise the allowance in the form → same call returns 200. A
wrapping bug was found that way and fixed.

Still true, and stated in the guide

Every quota here is currently bypassable. owner is the debtor, it is still
editable by anyone, and it is not the permission holder (#687 is open). The plan
calls this a known trade; a config guide that omitted it would be selling
enforcement that isn't there.

348 files / 2872 tests green; tsc clean; mkdocs build --strict clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LJagZZoJ8jKF7QsQAM56tC

The capability has been shipped since #688 and nothing documents how to use it:
`adding-an-app.md` never mentioned `resources` at all, and `configuration.md`
had two rows in a lookup table and no section. Predictably, none of the four
built-in Apps declares any.

`adding-an-app.md` gains the app.json half — the block, the per-dimension
fall-through, and the three things worth knowing before touching it: exceeding
`per_app.max` fails at BOOT rather than trimming silently, cpu/memory are
enforced by the sandbox backend so http deployments must redeploy sandbox-host,
and `disk: "0"` means unlimited while `cpu: 0` means unset.

`configuration.md` gains §6.5 (the file's own `11.5` precedent, so §7–§14 keep
their numbers): the three-layer resolution, the stock/flow split that explains
why disk and cpu behave differently, the per-user dimensions, and the
per-person override — which is superuser API only, has no UI, and answers 404
rather than 403 so that "who has an exception" doesn't leak.

Documented from a running server rather than from the plan: brought one up with
`per_user.count: 1`, took the 507, and drove the recovery in a browser. The
error payload, the page's own wording and the whole blocked → close → retry loop
are quoted from what actually happened.

Two things stated plainly because a reader would otherwise be misled:

- `/my-resources` is functionally complete and visually unstyled — a bare
  bulleted list with the item name, its specs and the Close button run together.
  Worth knowing before pointing a blocked user at it.
- `owner` is still editable by anyone (#687 is open), and it is not the
  permission holder, so every quota here is currently bypassable. The plan calls
  this a known trade; a config guide that omitted it would be selling
  enforcement that isn't there.

mkdocs --strict clean. Docs only — no code touched.
…a UI

Two halves of the same gap: the backend for quotas landed complete, the
frontend stopped one step short in two places.

**The page had no stylesheet.** It shipped a full vocabulary of semantic
classes — `.page`, `.summary`, `.detail`, `.empty`, `.meter`, `.meter-fill` —
and not one of them was ever defined; `.page` appears nowhere else in the repo.
That is not "a bit plain": `Meter` rendered two zero-height divs, so a person
could not see how close to the limit they were, and each row read as one run of
text ("quota demo A1 cores · 512 MB Close"). This is the page every quota
refusal routes to (#692), so it is the page someone lands on at their worst
moment. Its own file, like sheet.css / topic-hub.css, tokens only.

**Per-person allowances had no UI.** P7 delivered the model, the two-layer read
and three admin routes, and called that the "admin entry point" — but only
someone who writes curl could use it. Now a superuser gets a section at the foot
of the same page; everyone else sees nothing, matching the backend, which
answers 404 rather than 403 so that "who has an exception" cannot be probed.

The form deliberately starts EMPTY rather than pre-filling from the person's
current numbers. `PUT` replaces all four dimensions and the read endpoint
returns EFFECTIVE limits (override merged over the deploy default), so
pre-filling would submit inherited values back as explicit overrides and pin
them — a later change to the site default would then silently skip everyone who
had ever been edited here. Blank means "keep the default", which is what the
backend's own 0/"" sentinel means. There is a test for exactly that.

Also: `1 cores` → `1 core` (a separate key, since Chinese has no plural form);
Close gets the house `.btn`; the admin labels say "limit" so they no longer
collide with the section headings they sit under.

Verified in a real browser in BOTH themes — dark masks light-mode palette
defects, not the other way round — and the whole loop driven from the UI:
second item refused 507 → raise the allowance in the admin form → same call
returns 200 → clear the override. A wrapping bug found that way and fixed: the
row broke between a label and its input, so "Memory limit" ended one line and
its box started the next beside a different field.

Doc claims updated in the same commit rather than left to rot: configuration.md
no longer says the override is API-only, and the warning that this page had no
styling is gone because it no longer does.

348 files / 2872 tests green; tsc clean; mkdocs --strict clean.
@HYChou0515 HYChou0515 changed the title docs(resources): how to set sandbox quotas, per App and per person docs+ui(resources): document sandbox quotas, and finish the UI they point at Aug 12, 2026
Two questions the admin section could not answer, both asked the moment it was
first used:

**"What is the default?"** Every number on the page was the EFFECTIVE limit —
override merged over the deploy default — so looking someone up told you they
get 5, but not whether 5 is their exception or simply what everybody gets. The
backend already had `UserLimits.default`; it was just never exposed. (The CSS
shipped `.overridden` / `.inherited` classes for a distinction the API could not
express — written in anticipation and left dead. Now the baseline is on screen,
so the comparison is one a reader can make.)

**"Who has an exception?"** There was no way to find out. The only read was by
id, so an operator could confirm an exception they already suspected and nothing
else — "who is above the baseline?", the question someone inheriting the system
actually has, had no answer at all.

`UserLimits.list_overrides()` + `GET /admin/user-resources` return the
exceptions and the baseline in one payload, because a number means nothing
without the other beside it. Rows carry the RAW override, not resolved limits: a
dimension left at 0/"" is one that person has no exception for, and merging
would make every row look overridden in every dimension — the page would say
everyone is special, which is the opposite of its job.

⚠️ The list filters `is_deleted == False`. `clear_for` calls `rm.delete`, which
soft-deletes, and `list_resources` returns soft-deleted rows — so without the
filter a REVOKED exception stays listed for ever and the page reports privileges
nobody holds. This is the same trap the activity ledger documents; it bites
identically here. Probe-verified: dropping the filter turns
`test_a_revoked_exception_leaves_the_list` red.

Each row revokes in place, so the common admin action no longer requires typing
an id you had to already know.

101 quota tests / 348 files / 2875 web tests green; ruff, ty and tsc clean;
checked in a real browser in both themes against a live server holding two real
exceptions (one count+memory, one disk-only) — each row shows only the
dimensions actually granted.
Two problems in one line of markup.

**Usage was hidden whenever a dimension had no cap.** cpu and memory were only
rendered `limits.cpu ? … : ""`, so on a deploy that caps only the environment
count — the shape a first rollout is most likely to have — you could not find
out how much cpu or memory you were holding at all. The numbers were in the
payload the whole time. An unlimited dimension now shows its usage with no
denominator and no bar, since there is nothing to be a fraction of.

**One bar stood under three numbers, and tracked only one of them.** At
"1 / 2 · CPU 1 / 4 · Memory 512 MB / 2.0 GB" the bar sat at 50% while cpu and
memory were both at 25%. A reader takes a bar under a heading as "how full am
I"; this one answered whichever dimension it happened to be wired to. Any of the
three can refuse a turn on its own, so each gets its own row and its own bar —
and so does storage, which now goes through the same component instead of
repeating the markup.

Checked in a browser both ways round: with cpu/memory capped (three bars at
50% / 25% / 25%, each honest) and with them uncapped (`1 (no limit)` /
`512 MB (no limit)`, no bar).

348 files / 2877 tests green; tsc clean.
A 101-character assertion in the override-list test. My mistake was in how I
checked: I ran `ruff check` against the two source files I had edited and never
against the test file, and never ran `ruff format --check` at all — CLAUDE.md
asks for both, and CI runs both.
@HYChou0515
HYChou0515 merged commit 7bedc38 into master Aug 12, 2026
9 checks passed
HYChou0515 added a commit that referenced this pull request Aug 12, 2026
`ruff format` at the repo root does not reach `sandbox-host/` — it is a separate
package with its own config, and CI lints it as its own job. Same shape as the
miss on #709: I verified with a subset of what CI actually runs.
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.

1 participant