multipart uploads, tool-server routes, and the General Legal submission - #103
Merged
Merged
Conversation
added 2 commits
August 29, 2026 17:29
Three things, in dependency order: two platform capabilities the General Legal app needs, then the app itself. ## Multipart uploads A partner endpoint that takes a file could not be expressed at all. The obvious encoding does not fit: ipc.MaxFrameSize caps one envelope at 1 MiB, so base64 tops out near 740 KiB of real file against a 20 MiB partner limit. That is not a constant to tune — a 20 MiB document needs a 27 MiB envelope. So the file is staged in frame-sized chunks, reassembled under $APP/blobs, and sent as one multipart body. Declaring `multipart:` on a route generates the three staging methods, the blob store and the form builder. Three broker changes were needed: - the forward hardcoded application/json, which strips the boundary and makes the body undecodable. Content-Type is now forwarded for allow-listed types only — the media type selects the partner's parser, and letting a caller choose that freely is the lever tenancy already refuses for duplicate keys. - tenancy could only read JSON bodies, so it refused multipart outright. It now parses the form to ownership-check refs, with the same fail-closed stance. - oversize bodies were silently TRUNCATED rather than refused, surfacing as a bogus 401 or an opaque 404. Now 413, with a per-app cap. An app that forwards multipart while declaring no tenancy.body_refs now fails the boot: an upload names its resource in a form field, so that config would ownership-check nothing on exactly the route that needs it most. ## Tool-server routes `mcp:` maps a method onto a JSON-RPC tool server over Streamable HTTP, as a second backend alongside base_url. The generated client takes no credentials and has no field to hold any, so a REST key cannot reach the tool server's host — asserted from the outside against a running adapter, not just by construction. ## The General Legal submission Contract review (byo key) plus Delaware company formation (open), 19 methods. Includes the product demo, a next-steps graph shaped as two strongly connected components, and all four signed platform bundles. Methods now carry `billable` — distinct from `gated`, which means "your plan does not include this". These work and charge you; conflating the two misleads in both directions. ## Verification - 69 multipart tests, all green under -race - generated adapter built and run for real over a socket: 3 MiB staged in chunks, sha256 intact at the partner - full topology adapter -> broker -> partner, including an upload into an unowned matter being refused - a drift guard keeps the emitted blob store byte-identical to the tested reference (verified it fails when the copy diverges) - all 4 bundles pass `pilot-app verify`, no AppleDouble junk - demo scores 84.6 against a 60 gate
staticcheck ST1013. The mock partner servers wrote numeric literals for 422 and 400; 422 in particular reads as noise next to http.StatusNotFound a few lines away. No behaviour change. Verified with the same command CI runs (staticcheck ./...), which I should have done before pushing rather than after.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three things in dependency order: two platform capabilities the General Legal
app needs, then the app itself.
1. Multipart uploads
A partner endpoint that takes a file could not be expressed at all. The obvious
encoding does not fit, and this is the constraint the whole design turns on:
ipc.MaxFrameSizecaps one envelope at 1 MiB. base64 inflates by 4/3, soinline encoding tops out near 740 KiB of real file — against General Legal's
20 MiB limit. A 20 MiB document needs a 27 MiB envelope, 27× the frame. That is
not a constant to tune.
So the file never travels in one envelope. The agent stages it in frame-sized
chunks, the adapter reassembles it under
$APP/blobs, and only then builds onemultipart body.
TestBase64InOneEnvelopeExceedsIPCFrameasserts the premiserather than leaving it as a comment.
Declaring
multipart:on a route generates<ns>.upload_begin,upload_chunkandupload_abort, the blob store, and the form builder.Three broker changes
application/json, stripping the boundaryThe allow-list is deliberate rather than a passthrough: the request media type
selects which parser the partner runs, and letting a caller choose that freely
is the same lever tenancy already refuses for duplicate keys.
New boot check: an app that forwards multipart while declaring no
tenancy.body_refsnow fails to load. An upload names its resource in a formfield, never the path, so that config would ownership-check nothing on exactly
the route that needs it most — and
param_typesalone satisfied the old check,which is how it would have slipped through.
Bugs this surfaced
The silent truncation was pre-existing, not something multipart introduced.
Also fixed: an off-by-one that rejected a body sitting exactly on the part
budget, and a duplicate-field ban that was too broad — repeated names are legal
multipart (multi-file uploads), and no security decision rests on fields nobody
checks, so it now applies to ref fields only.
2. Tool-server routes (
mcp:)Maps a method onto a JSON-RPC tool server over Streamable HTTP, as a second
backend alongside
base_url. Handles the session handshake, SSE or plainJSON replies, and
structuredContentunwrapping.The separation is the point: the generated client takes no credentials and has
no field to hold any, so a REST API key cannot reach the tool server's host.
TestGeneratedToolServerNeverSeesTheRestKeyE2Easserts that from the outsideagainst a running adapter — including checking the REST side does carry the
key first, so the negative isn't vacuous.
Only the no-credential mode is implemented;
backend.mcp.authis rejectedrather than silently ignored.
3.
io.pilot.generallegalContract review (byo key) plus Delaware company formation (open), 19 methods,
all four signed bundles, product demo and next-steps graph.
The graph is two strongly connected components — contract review and formation
— joined by two bridges, with the formation entry reachable from all 11
contract-review nodes.
A new
billablefieldMethods carry
billable(what the call costs), separate fromgated. Gatedmeans your plan does not include this; these methods work and charge you.
Conflating them tells an agent something false in both directions.
<ns>.helprenders billable methods under their own heading.
Verification
-race; full suite green,go vetcleanchunks (every envelope asserted under the frame limit), sha256 intact at the
partner, plus blob single-use, checksum mismatch, aborted blob and path-param
cases
unowned matter refused with the opaque 404 and never reaching the partner
reference in
internal/multipartkit— verified it actually fails when thecopy diverges
pilot-app verify; no AppleDouble junk in the macOS tarsNote for the reviewer
meter_test.gohas one deliberate edit outside this feature.TestRunMeter_OneTickseeded enough credit to survive ~1.19s of metering against a 2s deadline — under
a second of headroom, so any unrelated work added to the package could flake it,
and this branch did (1 in 10 vs 0 in 20 on main). It now seeds 1 micro-dollar so
the first tick exhausts it deterministically. 0 failures in 20 runs after.
Companion website PR: pilot-protocol/website#226