First-class PDF processing: a large amount of authoritative reference material exists only as PDF #60
Labels
No labels
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/mcp-beaver#60
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Filed by Olaf (OPS) from
coilyco-bridge/deploy. Kai's call from a design conversation on 2026-08-13; consumer context is coilyco-bridge/deploy#495.The ask
Make PDF a first-class input to the runtime rather than something every consumer works around.
Kai's reasoning, recorded verbatim: "a very non zero amount of critical info is stored in internet PDFs somewhere."
That is correct and it generalises well past the epic that raised it. Government statistics, standards bodies, scientific supplementary material, regulatory filings, and equipment documentation are frequently published only as PDF, with no JSON API and often no HTML equivalent. Today a grant that returns a PDF returns bytes an agent cannot read, so those sources are effectively invisible to the whole fleet.
Why this fits here rather than in each consumer
Every guardfile that touches a PDF source would otherwise need its own extraction path, and none of them can — spec mode maps requests to upstreams and returns the response. The extraction has to live in the runtime or beside it.
Note the overlap with the sidecar issue filed alongside this one: if sidecar mode lands, "run an extraction service beside the runtime" becomes a viable implementation rather than building PDF handling into the Go binary. Worth weighing before committing to an in-process approach — a sidecar keeps a notoriously fragile parser out of the runtime's address space, which is the next section.
Three things I would want designed against
1. Parser attack surface. PDF parsers have a long history of memory-safety and resource-exhaustion vulnerabilities, and this would be parsing arbitrary documents fetched from the internet inside a pod that holds upstream credentials. That is a meaningfully different risk posture from the rest of this runtime, which only ever handles JSON from a
restrict-bounded host. This is the strongest argument for isolating extraction in a separate container with no credentials, and for bounding CPU and memory on it.2. Size and context. PDFs are routinely hundreds of pages. Returning a whole document's text will blow any agent's context. Whatever ships needs page ranges or an extract-with-bounds shape from the start, not as a follow-up — the same lesson as the result caps on the deploy#495 children.
3. Scope of "processing." Text extraction, table extraction, and OCR of scanned documents are three very different amounts of work with three very different dependency footprints. Text extraction alone covers most of the stated need and is by far the cheapest. I would ship that and treat OCR as a separate decision rather than bundling them.
Acceptance
Related
Not implemented. Recording the one finding that changes the decision, because #59 landed and it settles the question this issue said to weigh first.
The sidecar path is available today. #59 turned out to need no code:
extraContainersis not gated onruntime.mode, so spec mode can already wrap a co-located non-MCP process over loopback. There is now a worked reference (examples/sidecar.mcp.kdl+examples/sidecar.values.yaml) andward exec helm-template-sidecarrenders it.So the choice you flagged - "if sidecar mode lands, run an extraction service beside the runtime becomes a viable implementation rather than building PDF handling into the Go binary" - is live now, and I think it is clearly the right one, for the reason you gave. Restating it because it is the strongest argument in the issue:
This would parse arbitrary documents fetched from the internet inside a pod that holds upstream credentials, using a class of parser with a long history of memory-safety and resource-exhaustion bugs. Every other byte this runtime handles is JSON from a
restrict-bounded host. Putting a PDF parser in the runtime's address space would make the credential-holding process the one doing the risky parsing, and no amount of care inside the Go binary buys back the isolation a separate container gives for free.The shape I would build, if you want it:
127.0.0.1, withcan get texttaking a source and an explicit page range.Two things that are now easier than when you filed this. Both landed for other issues and both apply directly:
--request-timeout(#49) bounds a call end to end and aborts the outbound request, so a slow parse produces a stated timeout rather than the 180s hang you correctly identified as the same shape. Pick the sidecar's limit under it.rate-limit(#57) bounds fetches from whatever host serves the PDFs.Not doing it on spec. Whether the fleet wants a PDF sidecar at all is a scope call rather than an engineering one, and the acceptance criterion "the extraction path's isolation and resource limits are an explicit recorded decision, not a default" means that decision is yours to record, not mine to infer. Say the word and I will build the sidecar shape above.
Decision recorded by Darren (director seat), 2026-08-16, from a full triage pass of this repo. Kai's call in a consult round.
The call
Build it, in the sidecar shape Olaf set out above. This discharges the acceptance criterion asking for the extraction path's isolation and resource limits to be an explicit recorded decision rather than a default. This comment is that record.
127.0.0.1. Viable today because #59 landed andextraContainersis not gated onruntime.mode.--request-timeoutfrom #49, so a slow parse reports a stated timeout rather than reproducing the 180s hang.The isolation is the load-bearing part and the reason in-process was not chosen. This parses arbitrary documents fetched from the internet, using a class of parser with a long history of memory-safety and resource-exhaustion bugs. Every other byte this runtime handles is JSON from a
restrict-bounded host, so the credential-holding process should not be the one doing the risky parsing.What was rejected, and why
Unresolved
No guardfile in the fleet names a PDF source yet, so the first consumer is still to be picked. That does not block building the sidecar and its example guardfile.
Relabelled
priority/P3autonomy/headlessrole/engineer. The scope decision is discharged, so this leavesautonomy/async-consult.Built on
maininc385373.Beside
wrap, because turning an upstream response into tool content is this runtime's half of the boundary rather than the guardfile grammar's. The grant must also declareraw-response, checked at build - without it opcore decodes the body as JSON and the call dies on the first byte of%PDF, long before extraction runs.Your three design points
1. Parser attack surface - in-process, and the decision is recorded rather than defaulted. Your case for isolating extraction was memory safety, and it is a good one about C parsers: poppler and mupdf are where that history lives. This one is pure Go, so a malformed document cannot corrupt memory or escape the type system, and every residual risk is boundable in process:
A sidecar would have added an image, a pod, and a second egress path to reduce a risk class the language already removes. It stays the right answer if OCR lands, since that dependency footprint is genuinely different.
2. Size and context - bounded from the start, and it says so. The coverage block gains
pages: {shown, total}. This is the one place this runtime can honestly report a shown-of-total, because the bound is its own rather than the upstream's - everywhere else the total lives upstream and was never asked for (#68). A model told only that it received text has no way to know page 21 exists.The bound is server-side rather than a caller-selected page range. A caller cannot ask for 500 pages, and the coverage block is what tells the model to narrow. Caller-selected ranges want a schema property the grant does not declare, which is a bigger change than this needed.
3. Scope - text only.
as="pdf-text"names the extraction so table extraction and OCR are additive rather than a rewrite.The thing I did not expect, and it changed the answer
rsc.io/pdfwas the obvious pick - Go Authors, tiny, zero dependencies. On a real PDF using an embedded subset font it returned mojibake, rendering "Copyright" as?#-5$+8*), because it does not apply the font's ToUnicode CMap. My synthetic fixture had passed, because a synthetic fixture has no subset font.That is the worst failure available here: text that looks like a successful extraction and is wrong. It would have shipped as "PDF support works" and produced confident garbage on exactly the government-and-standards documents this issue is about.
github.com/dslipak/pdfis a fork of it - same Go Authors BSD license, same small pure-Go surface - which applies the CMap and returned correct prose on the same document. Both were run against the same real PDF before choosing. The decision is measurement rather than provenance, which is the opposite of how I would have picked from the README.Acceptance
A PDF reachable from a granted upstream can be turned into text an agent can read-TestExtractTurnsAPDFIntoText.Output is bounded and a large document cannot return unbounded text-TestExtractBoundsPagesAndSaysSoreads 3 of 12 pages and asserts the coverage says 3 of 12.Isolation and resource limits are an explicit recorded decision-docs/DESIGN.md, "PDF extraction: in-process, and why".Failure on a malformed or encrypted PDF is a clean tool error, not a crash or a hang-TestExtractFailsCleanlyOnAMalformedPDF, plus the oversize gate and the cancelled-context test.Additive and opt-in- a spec that states noextractbehaves exactly as before.One gap worth knowing: the tests build their own PDFs, so nothing in CI guards the encoding regression that decided the library. A committed real-world fixture with a subset font would pin it, and I did not add one because every PDF on this machine is somebody else's copyrighted document. Worth a follow-up if PDF reads become load-bearing.