feat(indexing): index-url — index a document set from an allow-listed http(s) URL in both transports (#208) - #210
Draft
adityamparikh wants to merge 6 commits into
Draft
adityamparikh wants to merge 6 commits into
adityamparikh wants to merge 6 commits into
Conversation
…che#208) Rewrites the URL-ingestion spec on top of upstream main. index-url fetches an http(s) URL whose host is on an operator allow-list (GitHub raw content by default), refuses link-local and cloud-metadata addresses on every redirect hop, sends no credentials or caller headers, caps the body at 10 MB by default, and hands the decoded payload to the existing document creators through a shared indexPayload method. Datasets over the cap are routed to bin/solr post or Solr's /update handler by the index-data prompt. Drops the streaming spine, the idle watchdog and the STDIO-only index-file from the earlier design; that work is preserved on wip/index-url-streaming and the reasons are recorded in the spec. Every default, message, exception type and boundary is stated exactly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
…ex-url (apache#208) UrlTargetPolicy is pure: absolute http(s) with a host and no embedded credentials, host on the operator allow-list (exact, *.suffix or *), and no link-local or cloud-metadata address even with *. UrlFetcher runs it on every redirect hop, uses Spring's RestClient over HttpURLConnection with redirect-following disabled so the policy sees each hop, follows five redirects and refuses the sixth and any https-to-http downgrade, sends only Accept and User-Agent, rejects non-2xx before reading a byte, and caps the body by Content-Length and by reading. UrlIndexingProperties carries the allow-list, timeouts and cap with a startup check on the cap. IndexingService gains indexPayload, the one parse-then-index path the four inline tools now share; IndexingServiceTest passes unmodified. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
UrlIndexingService fetches an allow-listed http(s) URL through UrlFetcher, resolves the format from an explicit argument, then the requested and final URL path extensions, then the Content-Type (text/plain and text/html never resolve), decodes the body with its declared charset and hands it to IndexingService.indexPayload. Every fetch or parse failure reports that nothing was indexed, because indexing starts only after the whole body has parsed. Registered in both transports with idempotent and openWorld hints. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
…pache#208) UrlIndexingIntegrationTest indexes all four formats from a JDK HTTP server into Testcontainers Solr with the allow-list bound from configuration, resolves the format from extension before query string and from media type without one, follows a redirect, and proves that text/plain, HTML, 404, an over-cap body and a host off the allow-list each fail and leave the collection empty. Both MCP client transports assert index-url and its hints through the shared base and add a round trip plus a refused metadata address. The index-data prompt and the server instructions route URLs under the cap to index-url, larger datasets to bin/solr post or the /update handler, and pasted data to the inline tools. README, tutorial and both security docs describe the allow-list, the cap and the timeouts. THREAT_MODEL.md narrows §8.5 to the Solr backend, discloses the allow-listed fetch and the DNS-rebinding window in §9, and updates §1, §2, §5a, §11, §11a, §12 and §13. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
… message (apache#208) A Location header the JDK cannot parse used to surface its raw parse message to the model; it now returns "The URL redirected to an invalid location. Use the final URL directly." with a test. Drops the wall-clock assertion from the off-list redirect test, which could flake on a cold JVM and was redundant: the allow-list message already proves no DNS lookup happened. Adds UrlIndexingPropertiesTest for the cap's range check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
…hem, and harden the policy (apache#208) Review findings from three independent passes over PR apache#210. Spring's response close() drains an unread body to keep the connection reusable, so a refused over-cap or non-2xx response was downloaded in full after the decision to refuse it. UrlFetcher now closes the body stream before returning on every non-body path, which makes the JDK drop the connection; a 2 MB body streamed at 10 KB/s must now fail inside a 5-second preemptive timeout. The policy refused only link-local addresses and the AWS IPv6 metadata literal while the docs claimed cloud-metadata addresses in general; Alibaba Cloud (100.100.100.200) and Azure WireServer (168.63.129.16) are now refused too, and every doc site says the known cloud-metadata addresses. A port above 65535 is rejected as an invalid URL instead of leaking HttpURLConnection's message; a non-numeric Content-Length counts as unknown; syntactic failures on a redirect hop report the redirect message rather than telling the caller to fix a URL they never supplied. Tests: every redirect status, a 3xx without Location, a quoted charset, SolrException in the Solr row, csv and markdown media types, a blank explicit format, an exact IPv6 allow-list entry, exact messages in the integration test, and the field-name summary that pins the indexPayload refactor. THREAT_MODEL §9 records the per-read timeout, header size and DNS-cache facts; FAQ and AGENTS.md tool counts and service lists updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
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.
Implements #208: an
index-urltool that indexes a JSON, CSV, XML or Markdown document set from an http(s) URL into a Solr collection without the payload passing through the model, registered in both the STDIO and HTTP transports.Design record with every decision and its reason:
docs/superpowers/specs/2026-09-15-url-ingestion-design.md(in this PR).Why
The four inline indexing tools take the payload as a tool-call argument, so the model emits every byte. Measured when #197 was closed: 61 records took over two minutes, of which Solr took under a second. The server's differentiator is the onboarding loop (look at data, design schema, index, verify, search) and it broke at the indexing step, with no workaround in chat-only clients such as Claude Desktop. A URL argument is ~20 tokens regardless of payload size.
What
SOLR_INDEX_URL_ALLOWED_HOSTStakes exact hosts,*.suffixpatterns or*. Default:raw.githubusercontent.com,*.githubusercontent.com,github.com, so the tutorial URL works with no configuration and an HTTP deployment is not an SSRF primitive on day one.*.user:pass@is rejected. OnlyAcceptandUser-Agentare sent.https→httprefused.SOLR_INDEX_URL_MAX_BYTES, default 10 MB, checked fromContent-Lengthand while reading. Over the cap, the error points atbin/solr post/ the/updatehandler.format> requested URL extension (query string ignored) > final URL extension >Content-Type;text/plainandtext/htmlnever resolve.IndexingService.indexPayload, which the four inline tools now also use (IndexingServiceTestpasses unmodified). Nothing is indexed unless the whole document parses, so every fetch or parse error truthfully says so.RestClientoverHttpURLConnection(already on the classpath), whose read timeout covers the body; redirect following disabled at the connection so the policy sees each hop.SOLR_INDEX_URL_CONNECT_TIMEOUT10s,SOLR_INDEX_URL_READ_TIMEOUT30s.index-dataprompt and the server instructions route URLs under the cap toindex-url, larger datasets tobin/solr postorcurlagainst/update(run where the file is, which also covers Claude Desktop local files), and pasted data to the inline tools.Relationship to earlier decisions
index-filefor giving the transports different surfaces. This PR adds no transport-specific tool and reverses nothing recorded there; the local-file case is served bybin/solr postguidance.index-urlcarries no payload, so one tool with an optionalformatsuffices.indexPayload. After refactor(indexing): forward CSV and XML to Solr's own update handlers #205 merges, a follow-up can proxy the fetched body straight to Solr's/updatefor JSON/CSV/XML and drop the cap for those formats with the same tool contract.index-file, and allowed any host by default is preserved atwip/index-url-streamingand is not proposed.Threat model
THREAT_MODEL.md§8.5 said the client "cannot inject a target URL" at critical severity; this tool exercises that deliberately behind an allow-list. The PR narrows §8.5 to the Solr backend and its credentials, adds the allow-listed fetch and the DNS-rebinding window as a §9 bounded property, adds the*-on-an-internal-network misuse to §11, theindex-urlnon-finding to §11a, a dated entry to §12, qualifies §13, adds the three knobs to §5a, corrects the tool counts in §1/§5a, and updatesdocs/security/stdio.mdandhttp.md.Two defaults are the author's choice and need maintainer confirmation: the allow-list contents and the 10 MB cap. Both are single constants.
Tests
UrlTargetPolicyTest(21),IndexFormatsTest(14),UrlFetcherTest(17, real JDKHttpServer),UrlIndexingIntegrationTest(5, Testcontainers Solr).UrlIndexingServiceTest(14, Mockito, the only class skipped natively).index-urland its hints through the shared base and add a URL round trip plus a refused metadata address.IndexingServiceTestunmodified.Review pass (three independent reviewers: security, correctness, tests/docs)
No critical findings. Fixed in
94897cd:close()drains an unread body for connection reuse, so an over-cap or non-2xx response was pulled in full after being refused.UrlFetchernow closes the body stream before returning on every non-body path, which makes the JDK drop the connection. Proven by a 2 MB body streamed at 10 KB/s that must fail inside a 5-second preemptive timeout.100.100.100.200) and Azure WireServer (168.63.129.16) now are too, and every document says "the known cloud-metadata addresses".HttpURLConnectionmessage; a non-numericContent-Lengthcounts as unknown; syntactic failures on a redirect hop report the redirect message.Location, quoted charset,SolrException, csv/markdown media types, blank explicit format, exact IPv6 allow-list entry, exact messages in the integration test, and the field-name summary that pins theindexPayloadrefactor.THREAT_MODEL.md§9 now also records that the read timeout is per read (a slow allow-listed sender can hold one call open), that response headers are not size-capped, and that the JDK DNS cache narrows the rebinding window. FAQ and AGENTS.md tool counts and service lists updated.Accepted and documented rather than built: a total deadline per fetch (it would break legitimate slow downloads, and the default allow-list is not attacker-controlled), and a concurrency limit on fetches (same exposure class as the inline tools).
Verification
Manual STDIO run of the fat jar against a local Solr, indexing the raw GitHub
shows.json:🤖 Generated with Claude Code