Skip to content

Hosted ChatGPT plugin: pdfmux serve --remote (extract_tables, sandboxed, nothing-skipped) - #42

Merged
NameetP merged 8 commits into
mainfrom
feat/chatgpt-remote
Oct 6, 2026
Merged

NameetP merged 8 commits into
mainfrom
feat/chatgpt-remote

Conversation

@NameetP

@NameetP NameetP commented Oct 2, 2026

Copy link
Copy Markdown
Owner

What

pdfmux serve --remote serves a hosted ChatGPT plugin (OpenAI Apps SDK) with one tool, extract_tables.

  • Input: a PDF, either uploaded through openai/fileParams or given as pdf_url.
  • Output: CSV inline, an XLSX download via a signed 15-minute link, and "rows verified N of M" whenever the table carries a running balance.
  • Positioning: nothing skipped. The generic PDF→Excel slot is already taken by Adobe, iLovePDF and others.

Decision: business-agent products/pdfmux/decisions/2026-10-02-chatgpt-plugin-posture-exception.md, a founder-approved, bounded exception to the posture lock.

Live at mcp.pdfmux.com/mcp/<token>, deployed from this branch.

  • Conformance passes 12/12 with a real PDF.
  • The existing /mcp endpoint is untouched; nginx routes only /mcp/<48-hex>, /dl/ and the challenge path to the plugin.

Design: the front end never parses, the worker never fetches

Module Job
remote/tables.py Deterministic extraction (no models, no paid fallback): PyMuPDF find_tables() plus the whitespace fallback, stitching across pages, typed numbers and dates, balance reconciliation, scan detection
remote/fetch.py SSRF-safe download (details below)
remote/sandbox.py + worker.py + deploy/sandbox.Dockerfile Parses in docker run --network none --read-only --cap-drop ALL --user 65534 with memory, CPU and pid limits. The timeout is enforced from outside. Verified on the box: network blocked, filesystem read-only.
remote/outputs.py CSV and XLSX built from typed values, never from markdown
remote/kit.py Python port of chatgpt-plugin-kit (details below)
remote/server.py Starlette app, signed downloads, sweeper

fetch.py accepts only:

  • https on port 443
  • hosts where every resolved address is global; this blocks loopback, RFC 1918, 169.254.169.254 metadata, Tailscale 100.64/10 and v4-mapped v6
  • connections whose actual connected peer is re-checked
  • redirects that are followed manually and re-validated
  • files up to a 25 MB stream cap that begin with the %PDF- magic bytes

kit.py applies the same rules as the TypeScript kit:

  • Trust comes from the path token.
  • Client IP is the last XFF entry.
  • Limits fail closed.
  • The kill-switch file is read on every request.
  • No pricing copy is shown.
  • Document text is wrapped in <<<untrusted>>>.
  • Logs never contain cell values.

Correctness fix found along the way

PyMuPDF 1.27's find_tables().extract() splits "37.50" into "3750\n." and drops spaces. After cleanup, that is a silent 100× on every amount, and reconciliation can't catch it because the error is uniform. Cells are now read with page.get_textbox(cell_rect), and a regression test is pinned. Core pdfmux still calls extract() in extractors/fast.py:66 and segment.py:260; that's a follow-up task.

Tests

  • Remote: 62 tests (tests/test_remote_tables.py, tests/test_remote_server.py).
  • Full suite locally: 803 pass. The only failures are the MCP test files, because the local venv lacks mcp>=2; CI has it.
  • Packaging: new remote extra (starlette, uvicorn, httpx, openpyxl).

🤖 Generated with Claude Code

NameetP and others added 5 commits October 2, 2026 17:33
…/dates, balance reconciliation)

No models, no network, no paid fallback. 30 tests on generated PDFs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…x, XLSX, kit

- extract_tables tool: file (openai/fileParams) or pdf_url → CSV inline (wrapped untrusted) +
  15-minute signed XLSX link + "rows verified N of M"; neutral copy, annotations.
- fetch.py: https-only, every resolved address must be global (blocks loopback, RFC1918,
  169.254.169.254 metadata, 100.64/10 Tailscale, v4-mapped v6), connected peer re-checked
  (rebinding), manual re-validated redirects, 25 MB streaming cap, %PDF- magic.
- sandbox.py + worker.py: parse in a separate process — docker run --network none --read-only
  --cap-drop ALL --user nobody with mem/cpu/pid limits in production, rlimits locally; timeout
  enforced from outside (SIGALRM can't stop C code). Front end never parses; worker never fetches.
- outputs.py: CSV/XLSX from typed values (numbers stay numbers, dates real dates, summary sheet).
- kit.py: Python port of chatgpt-plugin-kit (token trust, last-XFF IP, fail-closed limits,
  kill-switch file, banned copy, untrusted wrapper, JSONL logs without cell values).
- FIX: PyMuPDF 1.27 find_tables().extract() splits "37.50" into "3750\n." and drops spaces —
  a silent 100x on every amount. Cells are now read via page.get_textbox(cell rect). Regression
  test pinned. (Core pdfmux's two extract() call sites are a follow-up.)

Tests: 62 remote tests. Full suite: 803 passed; the only failures are the MCP test files,
which need mcp>=2 that this local venv lacks (pre-existing, unrelated).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ontainer user

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xtract_tables demo/tests

Force-added past the *.pdf ignore on purpose: it's a deliberate public fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NameetP and others added 3 commits October 2, 2026 23:39
…sheet)

- remote/tables.py drops its private _cell_texts copy for pdfmux.table_cells (merged in #43).
- outputs.py: wb.worksheets[0] instead of Optional wb.active (pyright reportOptionalSubscript).
- ci.yml: install the remote extra so test_remote_server can import httpx/starlette/openpyxl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… runs pytest, not python -m)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NameetP
NameetP merged commit 070ced4 into main Oct 6, 2026
7 checks passed
@NameetP
NameetP deleted the feat/chatgpt-remote branch October 6, 2026 02:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant