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

Merged opened this in answer to

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

Merged

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

opened this

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, so getApiV1Credit and putApiV1ForgesNameCredit sit in their sorted places in src/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.ts and bun x tsc --noEmit are clean.
  • test/child-credit.test.ts passes (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.ts could 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.

Closes #10484

commented

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, so getApiV1Credit and putApiV1ForgesNameCredit sit in their sorted places in src/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.ts and bun x tsc --noEmit are clean.
  • test/child-credit.test.ts passes (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.ts could 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 requested — before the latest push

Changes needed (one round, all of them):

  1. 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_BODY refuses it, and the new guard on it reads Content-Length, which the rules forbid as a cap. lendModel also checks Content-Length only, before the bearer-authenticated body is read in full by req.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.
  2. 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.
  3. MEDIUM, lost bearer: in lendCredit, on the first call setCap writes the credit file and the bearer's hash, then keepFunding can fail (child dir not written). The call returns 502 but the credit exists, so every retry takes the before !== undefined branch, 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.
  4. 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.
  5. MEDIUM: forge.credit_set is 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.
  6. Clean: const row = listed; is a no-op rename; creditWords was inserted between spendWords' 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.
  7. 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.
  8. Design (principles 1, 7): the crew note and the /forges credit span are fine in form, but the 'funded' sentence is added to crewReadiness and so repeats the readiness paragraph's muted line; fold it into one sentence rather than a second <p>.
commented

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

  1. 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.

  1. 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.

  1. Lost bearer. setCap now takes a hand-off callback and saves only after the bearer is in the child's parent.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.
  2. 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.

  1. Reader audience. forge.credit_set is out of READER_KINDS and is now an admins' event. Removing it alone broke the audience coverage test, so it is classified explicitly.
  2. 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.

  1. 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.

  1. 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 a data-funded span inside the one paragraph.

Where I disagree

  • Bun holds one maxRequestBodySize per 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+k in the terminal and choose Lend a child credit.
  • Then open /forges and the child's /settings/crew.
commented

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.ts didn'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.

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs