Skip to content

feat(plugin): implement chunked fetch for large responses and add tests - #694

Merged
pikann merged 4 commits into
masterfrom
feature/implement-chunked-body-fetching-for-plugin
Nov 3, 2026
Merged

pikann merged 4 commits into
masterfrom
feature/implement-chunked-body-fetching-for-plugin

Conversation

@pikann

@pikann pikann commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Two fixes that make agents and plugins work with large Git data:

  1. Plugin runtime: large paca.fetch responses. A response whose JSON envelope did not fit a plugin's fixed 1 MiB buffer came back as an empty response, which plugins reported as a network failure ("GitHub is unreachable"). The host now delivers it in chunks, or says plainly that it does not fit.
  2. Agent runner: provider-CLI agents lost their MCP servers. The runner wrote a Claude/Codex/Cursor/Gemini agent's MCP config to the wrong path on current Docker daemons, so the agent started with no Paca MCP tools at all (no issue_list, no task or repo tools).

Part of Paca-AI/plugin-sdk-go#16. The SDK half is Paca-AI/plugin-sdk-go#18, released as plugin-sdk-go v0.4.2.

1. Chunked fetch and explicit size errors (services/api/internal/platform/plugin)

Problem. Host results are written into the guest's fixed 1 MiB arena through paca_malloc. When a fetch envelope did not fit, writeJSONResult returned {0,0}; even the error message could not be delivered, so the guest saw an empty response. A real GitHub issue-list page (1.1 MB) hit this.

Changes

  • fetch_chunks.go (new): a per-plugin store of response bodies parked for chunked reads (at most 2 unread bodies, 60 s TTL, 1 MiB per chunk).
  • paca.fetch accepts "chunked": true. If the envelope does not fit, the host parks the body and answers with the status, headers, a body_handle and body_size. The plugin then reads {body_handle, offset, limit} pieces; each chunk is base64 in the JSON envelope. The handle is released after the last chunk is delivered, and a chunk that does not fit leaves the handle in place so the caller can retry with a smaller limit.
  • Without chunked (older SDKs), an oversized response now returns a small explicit error envelope, {"status":0,"error":"…does not fit…","code":"response_too_large"}, not an empty response.
  • The same explicit error is used for db_query2 (through its existing error pointer) and settings_get, so a zero-length result there can no longer be mistaken for "no rows" or "no settings". New helpers: writeJSONResultChecked, responseTooLargeMessage, errCodeResponseTooLarge.
  • Other host calls that return data keep returning nothing when they do not fit (they have no error channel); the SDK words that case so it no longer reads like a network failure.

2. Provider-CLI config sync (services/agent-runner/internal/executor)

Problem. For a provider_cli agent, syncProviderCLIConfig writes the CLI's MCP config to ~/.claude/.claude.json (and the Codex, Cursor and Gemini equivalents), where ~/.<cli> is an absolute symlink onto the persistent volume. The tar was extracted at /home/goose and relied on the extraction following that symlink. Docker 29.x resolves an absolute symlink against the extraction directory instead of the container root, so the file was written to /home/goose/home/paca/workspaces/.cli-home/<cli>/... and the CLI never read it. The runner logged nothing.

Change. persistedCLIEntries maps each file onto its real volume path (home/paca/workspaces/.cli-home/<cli>/...) and the tar is extracted from /, so no symlink is involved. The volume directories already exist (the bootstrap step creates them), so they are excluded from the generated directory entries and only genuinely new subdirectories (such as skills/<name>) get headers. This also fixes skill files synced the same way.

Compatibility

  • Older SDK on this host: no chunked flag, so an oversized response gets the explicit response_too_large error.
  • This SDK (v0.4.2) on an older host: chunked is ignored and the failure message is the clearer "host returned no data".
  • No change to the Git provider SPI, the plugin manifest or the database.
  • The new extraction path does not depend on how a Docker daemon resolves symlinks.

Notes

  • Merged with master: master's private-network paca.fetch hosts and redirect guard (feat(plugins): allow private network hosts for paca.fetch #693) touch the same handler. Chunk reads only serve a body parked by a fetch that already passed the domain, private-host and redirect checks. Master's runtime_fetch_test.go is kept as is; the chunking tests moved to runtime_fetch_chunk_test.go (both files were added with the same name).
  • Limits unchanged: 50 MiB per fetch response, held in host memory while parked.

Tests

  • runtime_fetch_chunk_test.go, against the real wazero runtime and fetch handler with a small-arena fixture (testdata/fetchplugin, 64 KiB):
    • an envelope that exactly fills the free space succeeds, and one byte more returns response_too_large (the 1,048,535 / 1,048,536 boundary from the report);
    • a 5× oversized response returns the error, and the plugin's next small fetch still works;
    • earlier un-reset allocations reduce the capacity;
    • a body larger than the arena is reassembled intact in 16 KiB chunks and the handle is released after the last one;
    • a small body stays inline; a request without chunked still gets the explicit error;
    • a chunk that is too large can be retried with a smaller limit;
    • the chunk store's eviction, expiry and bounds.
  • provider_cli_sync_test.go: for all four adapters, every synced file is under the real volume path and not under the symlinked ~/.<cli>, no directory header is written for existing directories, and nested skill files keep their subdirectories.
  • Also run by hand:
    • an SDK-built plugin through the real runtime fetched a 5 MB body and the real 1.1 MB GitHub issue page, and returned a 4 MB result, all intact;
    • on a Docker 29.9.2 daemon, a tar built this way lands on the volume with nothing at the doubled path;
  • go vet, golangci-lint (0 issues) and go test for internal/platform/plugin and internal/executor/... pass.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one minor suggestion inline.

Reviewed changes

  • Chunked paca.fetch — new host-side fetchChunkStore (fetch_chunks.go) parks an oversized response body and returns body_handle/body_size; a follow-up fetch with {body_handle, offset, limit} streams base64 chunks and releases the handle after the final read. Bounded at 2 handles per plugin with a 60s TTL and a 1 MiB raw cap per chunk.
  • Explicit response_too_large — writeJSONResultChecked surfaces allocator failures; fetch, settings_get, and db_query2 now return an actionable error envelope (with code: "response_too_large") instead of a silent empty response.
  • provider-cli config sync — new persistedCLIEntries rewrites $HOME-relative config paths onto the real persistent-volume path (HomeRoot/<Name()>) and the tar is extracted from /, avoiding the ~/.claude absolute-symlink resolution bug; the persisted ancestors are excluded from generated directory headers.
  • Tests — runtime_fetch_test.go (arena boundary, chunk reassembly/retry/release, store TTL + eviction) and provider_cli_sync_test.go.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

…r-plugin

Resolves the add/add conflict in
services/api/internal/platform/plugin/runtime_fetch_test.go: master's file
(private-network host and redirect guard tests, #693) is kept as is, and the
chunked-fetch tests move to runtime_fetch_chunk_test.go.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues — the delta since the last review is the master merge, which is clean. The earlier minor wording suggestion is still open, so this review stays non-approving to leave it actionable.

Reviewed changes

  • Merged master into the branch, threading r.services.FetchPrivateHosts into isAllowedFetchDomain (runtime.go:2276) so the chunk-aware fetch handler keeps master's private-network and redirect protections.
  • Renamed the PR's test file from runtime_fetch_test.go to runtime_fetch_chunk_test.go to avoid colliding with master's same-named file; no duplicate top-level symbols, and the package compiles and passes.
  • Confirmed the chunk read path has no new behavioral surface: req.BodyHandle != "" returns before domain validation, and a handle only exists after the fetch that parked it already passed the domain, private-host, and redirect checks — and the store is per-plugin.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

The shared response-too-large message told every caller to "request less
data (e.g. a smaller page size)", which a plugin cannot do for a third-party
paca.fetch response. The message now takes a remedy per operation:
db_query and settings_get keep the page-size hint, a fetch that did not opt
in is told to repeat the request with "chunked": true, a chunk read is told
to use a smaller limit, and a chunked fetch whose headers alone do not fit
says so.

Addresses the review comment on #694.
@pikann
pikann force-pushed the feature/implement-chunked-body-fetching-for-plugin branch from a8b3c79 to 7df45f3 Compare November 3, 2026 11:32

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. The only prior open thread is now addressed.

Reviewed changes

  • Pointed paca.fetch size errors at the escape hatch: responseTooLargeMessage now takes a per-operation remedy, so a non-chunked oversized fetch says repeat the request with "chunked": true instead of suggesting a smaller page, while db_query/settings_get keep request less data (e.g. a smaller page size).
  • Differentiated the chunked fetch failures: a chunked fetch whose metadata itself does not fit falls through to remedyHeaders (the response headers alone are too large to deliver), and an oversized body_handle read uses remedySmallerChunk (read it again with a smaller limit) — the right remedy for each path.
  • Added TestResponseTooLargeMessage_NamesTheRemedyForEachOperation and extended the fetch/chunk assertions to pin the remedy wording (fetch no longer mentions a smaller page).

This addresses the one open inline suggestion from the prior review; the thread has been resolved.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pikann
pikann merged commit 8be753e into master Nov 3, 2026
10 checks passed
@pikann
pikann deleted the feature/implement-chunked-body-fetching-for-plugin branch November 3, 2026 11:42
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