Public: anyone can read this; the crew can change it.

Hosted forges per person are limited by count and storage, each refused in plain words #9602

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A person's hosted forges are now limited by count and by total storage. Both limits are admin settings with defaults, and past either one a new forge is refused with one plain sentence. `tsc`, `scripts/gate.ts` and the tests that cover the change pass (the new file, `forge-sleep`, `mcp-size`, `limits`, `mcp-limits`, `tui-limits`, `spawn-api`); I did not run the full suite. I did not call `keep_pla…

shipped production is serving it deploy ef7edf1

opened this

A person's hosted forges are now limited by count and by total storage. Both limits are admin settings with defaults, and past either one a new forge is refused with one plain sentence. tsc, scripts/gate.ts and the tests that cover the change pass (the new file, forge-sleep, mcp-size, limits, mcp-limits, tui-limits, spawn-api); I did not run the full suite. I did not call keep_plan; I didn't have that tool.

What changed

  • Admin limits: /admin/limits now lists forges.per_person (default 3, from 1 to 1000) and forges.storage_mb (default 1000 MB). They show up on the web page, the API and the MCP limits list, and a change applies to the next request.
  • Count limit moved: the count used to come from the FORGE_FORGES_PER_PERSON environment variable. That variable is gone, so any host that set it falls back to the default of 3 until an admin sets the limit.
  • Refusals: the web form, POST /api/v1/forges and the MCP spawn tool all go through spawnForge, so one check in src/spawn/sleep.ts (personRefusal) covers all three. The API answers 409, and the web form answers 409 with the sentence on the page. Examples:

- "you already have 3 forges, the most one person may have (3); remove one first" - "your forges already hold 4 MB, the most one person's forges may hold (1000 MB); remove one first"

  • Storage use: the number is the files on disk under the person's forges' data directories.
  • Tests: the new test/spawn-person-limits.test.ts checks the defaults. It checks each refusal on web, API and MCP, that a site admin is not limited, and that raising the limit makes room again. test/forge-sleep.test.ts now uses the admin limit instead of the removed variable.
  • MCP: no new tool and no change to the MCP size ceilings.
  • Docs: docs/features/forge-sleep.md now names the two admin limits.

What I left out

  • Site admins stay exempt from both limits, as the old count limit already did.
  • Storage is checked only when a forge is made. A forge that grows past the limit afterwards is not stopped, and the next new forge is the one refused.
  • A real child forge is a few MB on its own, because of its database and repositories. Keep that in mind when setting forges.storage_mb (its minimum is 1 MB).

To try it Sign in as a non-admin, set forges.per_person to 1 at /admin/limits, make one forge, then try a second at /forges/new. bun test test/spawn-person-limits.test.ts runs the same flow on all three surfaces.

🤖 Generated with Claude Code

Closes #9581

commented

A person's hosted forges are now limited by count and by total storage. Both limits are admin settings with defaults, and past either one a new forge is refused with one plain sentence. tsc, scripts/gate.ts and the tests that cover the change pass (the new file, forge-sleep, mcp-size, limits, mcp-limits, tui-limits, spawn-api); I did not run the full suite. I did not call keep_plan; I didn't have that tool.

What changed

  • Admin limits: /admin/limits now lists forges.per_person (default 3, from 1 to 1000) and forges.storage_mb (default 1000 MB). They show up on the web page, the API and the MCP limits list, and a change applies to the next request.
  • Count limit moved: the count used to come from the FORGE_FORGES_PER_PERSON environment variable. That variable is gone, so any host that set it falls back to the default of 3 until an admin sets the limit.
  • Refusals: the web form, POST /api/v1/forges and the MCP spawn tool all go through spawnForge, so one check in src/spawn/sleep.ts (personRefusal) covers all three. The API answers 409, and the web form answers 409 with the sentence on the page. Examples:

- "you already have 3 forges, the most one person may have (3); remove one first" - "your forges already hold 4 MB, the most one person's forges may hold (1000 MB); remove one first"

  • Storage use: the number is the files on disk under the person's forges' data directories.
  • Tests: the new test/spawn-person-limits.test.ts checks the defaults. It checks each refusal on web, API and MCP, that a site admin is not limited, and that raising the limit makes room again. test/forge-sleep.test.ts now uses the admin limit instead of the removed variable.
  • MCP: no new tool and no change to the MCP size ceilings.
  • Docs: docs/features/forge-sleep.md now names the two admin limits.

What I left out

  • Site admins stay exempt from both limits, as the old count limit already did.
  • Storage is checked only when a forge is made. A forge that grows past the limit afterwards is not stopped, and the next new forge is the one refused.
  • A real child forge is a few MB on its own, because of its database and repositories. Keep that in mind when setting forges.storage_mb (its minimum is 1 MB).

To try it Sign in as a non-admin, set forges.per_person to 1 at /admin/limits, make one forge, then try a second at /forges/new. bun test test/spawn-person-limits.test.ts runs the same flow on all three surfaces.

🤖 Generated with Claude Code

commented

The check failed only because no commit message had a "Try:" line. I added an empty commit with one and pushed it. bun scripts/gate.ts now passes locally, and I haven't seen the forge's check re-run yet.

The code is unchanged from the first push:

  • Limits: forges.per_person (default 3) and forges.storage_mb (default 1000 MB) are admin settings at /admin/limits.
  • Refusals: web, API and MCP all refuse a new forge past either limit with one sentence naming the limit and the person's use. Site admins are not limited.
  • Tests: test/spawn-person-limits.test.ts covers each refusal on each surface.

To try it on the preview: sign in as a non-admin and set forges.per_person to 1 at /admin/limits. Make one forge, then open /forges/new and make a second to see the refusal.

🤖 Generated with Claude Code

commented

The failing test was test/settings-visible.test.ts, and I fixed it and pushed. I haven't seen the forge's check re-run on this push.

The test looks for each limit's label in the rendered /admin/limits page. My storage label, "Storage one person's hosted forges may hold together", has an apostrophe, and the page escapes it to ', so the label wasn't found. I renamed it to "Storage a person can hold in hosted forges". The limit itself is unchanged.

test/settings-visible.test.ts, test/spawn-person-limits.test.ts, test/limits.test.ts and test/mcp-limits.test.ts pass locally, and tsc is clean.

The change adds two per-person limits, set in admin limits. A new hosted forge is refused through the web, API and MCP with one plain sentence once a person is past either limit.

  • Count: forges.per_person, default 3.
  • Storage: forges.storage_mb, default 1000 MB.
  • Admins: site admins are not limited.
  • Storage check: it runs only when a forge is made, so a forge that grows later isn't stopped.

To try it on the preview: open /admin/limits and set the hosted-forges count to 1. Then, signed in as a non-admin, make one forge at /forges/new and try a second to see the refusal.

🤖 Generated with Claude Code

Changes requested — before the latest push

The limits work and the refusals are plain on web, API and MCP, but two things need fixing in one round.

  1. test/forge-sleep.test.ts: "a person's forge limit holds through spawnForge" no longer sets any limit. It only passes because the earlier test set forges.per_person=1 and never reset it, since the reset sits in the later test's finally. Set the limit to 1 inside that test (try/finally), and reset the earlier one too, so each test stands alone and survives reordering or --only.
  2. The refusal grammar reads "you already have 1 forges". Say "1 forge" when the count is 1. Update the exact-string assertion in test/spawn-person-limits.test.ts to match.

Smaller points:

  • heldByPerson walks and stats every file of every child forge synchronously on the spawn request path. Fine for now, but say so in the pull, or compute it only after the count check passes (it already is).
  • docs/features/forge-sleep.md: "the admin limits" should start with a capital after the full stop. Also name where they are set (/admin/limits, and the same list in the terminal limits screen).
  • The sleep.ts header comment got a long run-on line; reflow it.

Ticks and lefts are not given because this is not an approval.

commented

I addressed both requested changes and the smaller points, and pushed. tsc, scripts/gate.ts and the two test files (test/forge-sleep.test.ts, test/spawn-person-limits.test.ts) pass locally. I haven't seen the forge's check re-run on this push.

  • Tests stand alone: "a person's forge limit holds through spawnForge" now sets forges.per_person to 1 itself. The other limit test in test/forge-sleep.test.ts also resets it in a finally. Neither depends on the other's leftover state any more.
  • Grammar: the refusal now says "you already have 1 forge" and "3 forges" for larger counts. The exact-string assertion in test/spawn-person-limits.test.ts matches.
  • Storage walk: heldByPerson walks and stats every file of the person's forges synchronously on the spawn request. It only runs after the count check passes, and spawning is rare. That is now stated in the src/spawn/sleep.ts header comment, and it's the note for the pull.
  • Docs: docs/features/forge-sleep.md starts the sentence with a capital. It names /admin/limits and says the same list is on the terminal's limits screen. I assumed the terminal screen lists every limit, as LIMIT_KEYS does elsewhere, and did not open it.
  • Header comment: reflowed. It also no longer claims the per-person count comes from an environment variable.

🤖 Generated with Claude Code

Approved

Clean read: both limits are admin settings with defaults, one check (personRefusal) sits behind web, API and MCP via spawnForge, admins are exempt, refusal is one plain sentence naming limit and use, no new dependency, how-to in docs/features/forge-sleep.md, README untouched. Tests cover each refusal on each surface and would fail without the change. Not blocking: the storage walk is synchronous and runs on every refused attempt once over the limit, so a repeated spawn call costs a full stat walk; cache or cap it later. The storage test computes MB before the request and compares to the message, which can flake if a child writes in between.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs