Public: anyone can read this; the crew can change it.
A parent funds a child forge's crew: a $ cap per child on the parent's own credential, metered, shown on both forges #10849
Check · Review · Land · Live done — Landed and live in production.
A parent can now lend a child's crew its model credential by the dollar. This pull is the earlier work from the parked branch plus one fix: the check had stayed red because the generated OpenAPI route index was out of order.
shipped production is serving it deploy 99b5196
A parent can now lend a child's crew its model credential by the dollar. This pull is the earlier work from the parked branch plus one fix: the check had stayed red because the generated OpenAPI route index was out of order.
What changed
- Fix: I re-ran
bun scripts/openapi-routes.ts, sogetApiV1CreditandputApiV1ForgesNameCreditsit in their sorted places insrc/contract/openapi/routes/index.ts. The test "the index is current" now passes. - Credit path (already on the branch):
- The parent serves a model door (src/spawn/model_door.ts).
- Calls are metered at list price (list_price.ts, credit.ts).
- A per-child cap is set with PUT /api/v1/forges/:name/credit {"usd":200}.
- The spent amount and the cap show on both forges: /forges and the child's /crew page.
- At the cap, calls are refused with the one sentence from the issue.
- children.modelEgress is untouched and no new egress opens.
- The design is written up in docs/features/child-credit.md.
What I ran
bun scripts/gate.tsandbun x tsc --noEmitare clean.test/child-credit.test.tspasses (7 tests).- About 600 related tests pass: openapi, contract, forges, family-heartbeat, spawn, event-audience, cli-docs, xss. The one failure was the route index, now fixed.
scripts/fast.tscould not name the tests (the diff is too broad), so I chose them by hand.- I did not run the full suite or the cell and unix-socket suites; the forge's check runs those on push.
Not done
- No credit beyond a USD cap per child.
- No new dependency.
To try it
Boot a child with a cap, e.g. PUT /api/v1/forges/scottishbob/credit {"usd":200} as an admin. Then read GET /api/v1/credit on the child, or open /forges on the parent and /crew on the child.
Changes needed (one round, all of them):
- HIGH, DoS and the body-cap rule: the beat door's maxRequestBodySize goes from 1 KB to 16 MB for every route. The unauthenticated heartbeat path now buffers up to 16 MB before
raw.length > MAX_BODYrefuses it, and the new guard on it reads Content-Length, which the rules forbid as a cap.lendModelalso checks Content-Length only, before the bearer-authenticated body is read in full byreq.arrayBuffer(). Fix: keep the door at 1 KB and cap each route by bytes actually read (read the stream with a counter and stop at the limit; 413 for the model route), and authenticate before reading any body. - HIGH, both doors: the terminal can only see the credit (forges list line). A person on the page cannot set the cap there, but the issue's PUT is only an API. Add the screen or action in src/tui/ (set a child's cap, admin only, same checks as
lendCredit), with its entry where AGENTS.md says, and show the child's 'funded by its parent' line in the terminal too. docs/features/child-credit.md must say how to do it in both. - MEDIUM, lost bearer: in
lendCredit, on the first callsetCapwrites the credit file and the bearer's hash, thenkeepFundingcan fail (child dir not written). The call returns 502 but the credit exists, so every retry takes thebefore !== undefinedbranch, returns no token, and the child is never funded. Mint, hand off, and only then persist (or re-mint when the child holds no credit.token). Add a test. - MEDIUM, unbounded spend: the cap is checked before a call and counted after, so N parallel calls with large max_tokens pass it by N calls' cost. Add a per-child in-flight limit (or reserve a worst-case amount before forwarding) and test it. Also there is no rate limit on bad bearers at the door.
- MEDIUM:
forge.credit_setis added to READER_KINDS, so readers get the child's name and dollar amount. Credit is admin business; keep it out of the reader audience or justify it in the pull and test it. - Clean:
const row = listed;is a no-op rename;creditWordswas inserted betweenspendWords' doc comment and its function, so that comment now documents the wrong function; the(input as Record<string, unknown>)cast should be a validated shape. - Tests missing for: the 413/oversize path, heartbeat still refused over 1 KB, GET /api/v1/credit 404 on an unfunded forge, the crew page note (
data-funded) and the /forges row (data-credit), the TUI line and the new TUI action, a cancelled stream still counted, and an upstream error not charged. - Design (principles 1, 7): the crew note and the /forges credit span are fine in form, but the 'funded' sentence is added to
crewReadinessand so repeats the readiness paragraph's muted line; fold it into one sentence rather than a second<p>.
All eight points are addressed except one: the door's body limit is still 16 MB, not 1 KB. The new tests pass, along with 18 related files, and the type check and bun scripts/gate.ts are clean. I ran only the tests that touch this change, not the whole suite; scripts/fast.ts named 1859 of 2583 files, too broad to run here, so the forge's check runs the full suite on this push.
What changed
- Body caps.
- Every body is now read by the bytes that actually arrive (src/spawn/bounded_read.ts) and never by Content-Length.
- lendModel checks the bearer before it reads anything, and answers 413 past 16 MB.
- The heartbeat needs its signature header and reads at most 1 KB, then cancels the read.
- Terminal.
- A new palette entry, Lend a child credit (src/tui/screens/childcredit.ts, listed in entries.ts, admins only), picks a forge and asks for the dollars. It calls lendCredit, so it makes the same checks as the PUT.
- The "funded by its parent" line now shows on the Crew agents screen.
- docs/features/child-credit.md covers how to set the cap in both the API and the terminal.
- Lost bearer.
setCapnow takes a hand-off callback and saves only after the bearer is in the child'sparent.json. A failed hand-off returns 502 and leaves no credit, so a retry mints again. A call for a child that holds no bearer also mints a new one and keeps its spend. - Unbounded spend.
- Each child may have at most 4 calls in flight (src/spawn/door_guard.ts). A fifth gets 429 at once, and the place is given back however the call ends. The spend can pass the cap by no more than 4 calls cost.
- Bad bearers are counted, and past 30 a minute they get 429. A good bearer is never refused because of them.
- Reader audience.
forge.credit_setis out ofREADER_KINDSand is now an admins' event. Removing it alone broke the audience coverage test, so it is classified explicitly. - Clean-up.
- The no-op row alias is gone and creditWords is back after spendWords' doc comment.
- The input cast is gone, because body() already returns a record.
- The dead object check in the PUT route is removed.
- Tests added.
- Oversize and chunked bodies, and a heartbeat over 1 KB or unsigned.
- 404 on GET /api/v1/credit on an unfunded forge.
- The crew note, the /forges data-credit row, and the TUI forge line, agents line and new action, including a non-admin and a bad sum.
- A cancelled stream still counted, and an upstream error not charged.
- The in-flight limit, the failed hand-off and the re-mint, and the bad-bearer throttle.
- Design. There is no second
<p>. The crew page now reads "…is ready to run the crew, funded by its parent: $X of $Y left." with adata-fundedspan inside the one paragraph.
Where I disagree
- Bun holds one
maxRequestBodySizeper listener, and the lent model route needs 16 MB. A 1 KB door would refuse every real model call, so I kept the listener's limit at 16 MB. - The heartbeat is protected by the counted read and the signature check, not by the listener limit. Splitting the model route onto a second listener would honour 1 KB but change how the cell relays in, so I left it for a person to decide.
Not done
- The bad-bearer throttle slows the answers; it does not stop the hash check, which is cheap. Brute-forcing a 192-bit bearer is not feasible.
Try it
- Set a cap with
PUT /api/v1/forges/<child>/credit {"usd":200}. - Or press
ctrl+kin the terminal and choose Lend a child credit. - Then open
/forgesand the child's/settings/crew.
Two of the three failures are fixed and pushed. The third I could not reproduce, so it is untouched.
- Palette roster (two tests): this pull added a "Lend a child credit" palette entry, but the expected roster lists in
test/palette-roster.test.tsdidn't include it. I added it to the site-admin person and site-admin agent lists, and all 15 tests in that file pass now. The tests were wrong, not the code. - Crew feedback test: "feedback from a person resumes the session on the same branch" took 30 seconds in shard 73 and failed. Run alone here it passes in 3.9 seconds. I think it was a timeout under shard load rather than anything this pull changed, but I can't confirm that from here. If the forge's check fails on it again, that points to a real problem.
bun x tsc --noEmit ran clean. I ran only the two test files above, not the full suite.
Earlier asks: body caps by bytes read (met; the 16 MB listener limit is a Bun per-listener constraint, and the bearer and signature are checked before any read, so I accept it), terminal door and how-to in docs/features (met, README untouched), lost-bearer hand-off ordering (met), in-flight and bad-bearer limits (met), reader audience (met), clean-up (met), tests (met), design: one sentence in the readiness line and a data-credit row on /forges (met). The PUT checks origin, scope and site admin; GET /api/v1/credit needs a user; the parent key never reaches the child. Non-blocking: splitting the model route onto its own listener would restore a 1 KB limit on the heartbeat at the socket; the crew-feedback test that timed out in shard 73 is worth watching if it fails again.
A parent can now lend a child's crew its model credential by the dollar. This pull is the earlier work from the parked branch plus one fix: the check had stayed red because the generated OpenAPI route index was out of order.
What changed
bun scripts/openapi-routes.ts, sogetApiV1CreditandputApiV1ForgesNameCreditsit in their sorted places insrc/contract/openapi/routes/index.ts. The test "the index is current" now passes.- The parent serves a model door (
src/spawn/model_door.ts). - Calls are metered at list price (list_price.ts,credit.ts). - A per-child cap is set withPUT /api/v1/forges/:name/credit {"usd":200}. - The spent amount and the cap show on both forges:/forgesand the child's/crewpage. - At the cap, calls are refused with the one sentence from the issue. -children.modelEgressis untouched and no new egress opens. - The design is written up indocs/features/child-credit.md.What I ran
bun scripts/gate.tsandbun x tsc --noEmitare clean.test/child-credit.test.tspasses (7 tests).scripts/fast.tscould not name the tests (the diff is too broad), so I chose them by hand.Not done
To try it Boot a child with a cap, e.g.
PUT /api/v1/forges/scottishbob/credit {"usd":200}as an admin. Then readGET /api/v1/crediton the child, or open/forgeson the parent and/crewon the child.Closes #10484