Skip to content

feat(pi): optional multi-provider web search for the coding agent - #5951

Open
BillLeoutsakosvl346 wants to merge 6 commits into
stagingfrom
feature/pi-search
Open

feat(pi): optional multi-provider web search for the coding agent#5951
BillLeoutsakosvl346 wants to merge 6 commits into
stagingfrom
feature/pi-search

Conversation

@BillLeoutsakosvl346

Copy link
Copy Markdown
Contributor

Summary

Adds an optional internet search capability to the Pi Coding Agent block: a Search Provider dropdown (None, Exa, Serper, Parallel, Firecrawl), off by default, available in all three modes — Create PR, Review Code, and Local Dev.

  • Keys follow the block's existing pattern. The selected provider's key comes from the block field, falling back to Workspace Settings → BYOK, exactly like the model selector. A Sim-hosted key is never spent on search in any mode, so a missing key fails the run with a setup message rather than quietly billing Sim. Switching providers clears the key field, so the previous provider's key cannot be sent to the new one.
  • One tool, two execution paths. Local Dev and Review Code register a host-side web_search tool that reuses the existing exa_search / serper_search / parallel_search / firecrawl_search tools. Create PR has no host in the agent loop, so it gets a generated Pi extension written into the sandbox that makes its own bounded fetch calls. Both paths derive their requests from the same normalizer in search/normalize.ts, and search/parity.test.ts asserts the two produce byte-identical URLs, headers, and bodies per provider — the sandbox copy cannot import Sim's code, so that test is what keeps it from drifting.
  • Results are normalized and treated as untrusted. Every provider collapses to title, URL, snippet, and optional publication date, capped per field and per envelope, with a prompt guideline telling the model the content is untrusted data rather than instructions.
  • Bounded. 20 searches per run (Pi's agent loop has no turn ceiling of its own), a 10s per-call timeout, and failures classified as unauthorized / rate limited / timed out / unreachable instead of echoing provider text, which can carry injected instructions.

Two drive-by fixes it needed: serper_search was dropping the date field on organic results, and Sim's redaction patterns did not match keys named *ApiKey or passphrase.

Test plan

  • search/{normalize,tool,extension-source,parity}.test.ts — 62 tests covering argument clamping, per-provider normalization and fallback chains, envelope byte caps, key-source precedence, failure classification, the per-run budget, and host/sandbox request parity
  • pi-handler, keys, local-backend, cloud-backend, cloud-review-backend, blocks/blocks/pi, redaction, tools/serper — 826 tests pass, including backward compatibility for Pi blocks saved before the fields existed
  • tsc --noEmit, biome check, repo lint and format clean on the rebased base
  • Manual: run each mode against a real key for one provider, and confirm a missing key fails with the setup message

Reviewer notes

Two things worth a second opinion, both deliberate:

  1. Create PR delivers the search key to the sandbox as an env var, which the agent can read — it holds bash. This matches how the model provider key already reaches that mode, and is the reason search refuses a Sim-hosted key everywhere: only a key the user supplied themselves can get there.
  2. Review Code gains an outbound network channel in a mode that otherwise has none, so a malicious PR could in principle exfiltrate repository text through search queries. Bounded at roughly 10 KB per run by the call and query-length caps, and documented in pi.mdx as a reason not to enable search when reviewing untrusted forks of private repos.

Made with Cursor

Adds a search provider dropdown (Exa, Serper, Parallel, Firecrawl) to the Pi
block, off by default. The selected provider's key comes from the block field or
Workspace Settings → BYOK; a Sim-hosted key is never spent, so a missing key
fails the run with a setup message instead of quietly billing Sim.

Search is available in all three modes. Local Dev and Review Code register a
host-side tool that goes through the existing provider tools, while Create PR has
no host in the loop and gets a generated Pi extension in the sandbox. Both paths
derive their requests from one normalizer and are held together by a parity test,
since the sandbox copy cannot import Sim's code.

Results are normalized to title, URL, snippet, and publication date, capped per
field and per envelope, marked untrusted in the prompt, and limited to 20
searches per run so a tool loop cannot drain the workspace's quota.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Error Error Jul 28, 2026 8:13pm

Request Review

@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds optional, BYOK-only web search to every Pi Coding Agent mode.

  • Introduces provider selection, credential resolution, permission preflight, and secret redaction for Exa, Serper, Parallel AI, and Firecrawl.
  • Registers a normalized host-side search tool for Local Dev and Review Code and generates an equivalent sandbox extension for Create PR.
  • Bounds search calls, request duration, query/result sizes, and normalizes provider failures and result envelopes.
  • Documents the new network boundary and adds provider parity, backend, block configuration, redaction, and normalization tests.
  • Preserves Serper organic-result publication dates and expands sensitive-key detection.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete blocking or independently actionable non-blocking defects identified.

The provider mappings, credential boundaries, permission checks, execution-mode integrations, bounded request behavior, normalization paths, and secret-redaction boundaries are internally consistent and covered by focused parity and backend tests.

Important Files Changed

Filename Overview
apps/sim/executor/handlers/pi/pi-handler.ts Resolves optional search configuration, enforces tool permissions before credential access, and dispatches mode-specific tool implementations.
apps/sim/executor/handlers/pi/search/normalize.ts Defines bounded provider-neutral arguments, request mappings, result normalization, and serialized output limits.
apps/sim/executor/handlers/pi/search/tool.ts Implements the host-side search adapter using existing registered tools, explicit user credentials, failure classification, and a per-run call budget.
apps/sim/executor/handlers/pi/search/extension-source.ts Generates the Create PR sandbox extension with equivalent provider requests, normalization, timeout, response-size, and call-count controls.
apps/sim/executor/handlers/pi/cloud-backend.ts Installs and activates the sandbox search extension when configured while extending credential redaction across surfaced outputs and errors.
apps/sim/executor/handlers/pi/cloud-review-backend.ts Registers host-side web search for Review Code and adjusts its sealed capability prompt and tool allowlist.
apps/sim/executor/handlers/pi/keys.ts Adds strict provider parsing and resolves search credentials exclusively from block input or workspace BYOK.
apps/sim/lib/core/security/redaction.ts Extends sensitive-field recognition to prefixed API-key names and passphrases.
apps/sim/tools/serper/search.ts Preserves the optional date supplied on Serper organic search results.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Block[Pi block inputs] --> Handler[Pi handler]
  Handler --> Permission[Search permission preflight]
  Permission --> Keys[Block key or workspace BYOK]
  Keys --> Mode{Execution mode}
  Mode -->|Local Dev| Host[Host-side web_search tool]
  Mode -->|Review Code| Host
  Mode -->|Create PR| Sandbox[Generated sandbox extension]
  Host --> Tools[Existing provider ToolConfig]
  Sandbox --> APIs[Provider API]
  Tools --> Normalize[Normalized bounded results]
  APIs --> Normalize
  Normalize --> Agent[Pi agent]
Loading

Reviews (1): Last reviewed commit: "feat(pi): optional multi-provider web se..." | Re-trigger Greptile

`check:utils` bans `JSON.parse(JSON.stringify(...))`. The round-trip was
normalizing the host body to its wire form, which buys nothing here: the bodies
are plain JSON and `toEqual` already ignores undefined members.

Co-authored-by: Cursor <cursoragent@cursor.com>
@BillLeoutsakosvl346

Copy link
Copy Markdown
Contributor Author

Video demo:

Screen.Recording.2026-07-26.at.12.48.51.PM.mp4

Create PR streams the whole Pi run through one Connect server-stream
(`commands.run` -> envd `Process.Start`), held open for the full
`PI_TIMEOUT_MS`. Mid-stream it could die with:

  [internal] protocol error: received unsupported compressed output

That string is `@connectrpc/connect-web`, not Pi — Pi has no Connect
dependency at all. connect's `compressedFlag` is `0b00000001` and gzip's
magic first byte is `0x1f`; `0x1f & 0x01 === 1`, so a raw gzip body fed to
the envelope reader trips this on byte one. It reads as "the server sent a
compressed envelope" but really means "this was never a Connect envelope" —
an HTTP-level gzip that was not transparently decompressed.

e2b 2.30.0 pinned `@connectrpc/connect-web@2.0.0-rc.3` and drove envd
through undici 7 with `allowH2: true`. e2b 2.36.1 moves to stable
connect-web 2.1.2 and loads undici 8.8.0 when Node >= 22.19.0 — exactly our
engine floor — so the failing path gets a different HTTP stack.

The connect-web upgrade alone is not the fix: 2.0.0-rc.3 and 2.1.2 ship a
byte-identical `connect-transport.js` (bar the copyright year), and
connect-web still has no `acceptCompression` option by design. The undici 8
swap is the part that matters.

`@e2b/code-interpreter@2.7.0` only asks for `e2b: ^2.28.0`, so the override
pins the floor we actually need. Verified API-compatible: every method we
call (`Sandbox.create`, `runCode`, `commands.run`, `files.read/write`,
`kill`, `Template`, `defaultBuildLogger`, `waitForTimeout`) has an identical
signature across the two versions, and we never touch `SandboxPaginator`,
the one type that changed.
…ted scope

Follow-ups from review of the web-search work. Each fix lands in both the host
adapter (`normalize.ts`) and the Create PR sandbox copy (`extension-source.ts`),
with the extension test asserting the two produce byte-identical envelopes.

- `usableUrl` was the one provider-controlled field not whitespace-bounded:
  title/snippet/date all go through `collapseWhitespace`, `url` only trimmed.
  Up to 2048 chars of newlines and control characters could ride into the
  envelope. Dropped rather than collapsed — `url` must stay byte-exact to stay
  resolvable, so collapsing would emit a different, still-dead link, and a URL
  carrying raw whitespace is already malformed under RFC 3986.

- `numResults: null` (or `''`, or `[]`) returned 1 result, not the documented
  default of 5: `Number(null)` is a finite 0, so the clamp floor won rather
  than the default. Only a real number or a non-blank numeric string now counts
  as the model having asked for a count.

- Envelope truncation was silent. When results were dropped to fit the 50 KB
  ceiling the model read the short list as the complete answer. It now carries
  a message saying so, and the message is inside what gets measured so the note
  cannot push a truncated envelope back over the ceiling.

- The budget is per *block execution*, not per workflow run: the counter lives
  in the tool spec and both adapters build a fresh one per execution, so a Pi
  block inside a Loop gets the full allowance every iteration. The constant,
  the agent-facing message, and the docs all claimed "per run". Renamed to
  `PI_SEARCH_MAX_CALLS_PER_EXECUTION` and corrected the wording rather than
  tightening the cap, since a shared ceiling would fail late iterations of a
  legitimate fan-out.

- The Search API Key tooltip promised "switching providers clears this field".
  That clear is driven through the collaborative editor setter, so a workflow
  imported, forked, or updated via the API keeps the previous provider's key —
  exactly the case where sending it to a new vendor matters.

Docs also gain a warning that Create PR hands both the model key and the search
key to the agent as environment variables, which Pi copies into every bash
child. That matters most for Settings > BYOK keys: those are workspace-scoped,
only admins can manage them, and the API only ever returns them masked — yet
anyone who can run a Pi block in Create PR mode can read the raw value.
The "you cannot add a provider without mirroring it" story rested on two
mechanisms that did not hold. Verified by adding a fifth provider to
`PI_SEARCH_PROVIDERS` and running the build: it produced only two errors, and
every test still passed.

- `normalizePiSearchRecords` assigns to `let built` inside its switch rather
  than returning, so unlike its two siblings a missing case was not a type
  error — it silently normalized the new provider to zero results. Added an
  explicit `never` check.

- The sandbox copy's `normalizeRecords` used a trailing `else` for Firecrawl,
  so an unmirrored provider was silently normalized with Firecrawl's field
  names; `extractRecords` did the same with its `payload.data` tail. Both now
  test for `firecrawl` explicitly and throw otherwise.

- `Record<PiSearchProvider, ...>` on the `TOOLS` and `payloads` fixtures looked
  like exhaustiveness guards but are inert: `apps/sim/tsconfig.json` excludes
  `**/*.test.ts`, and vitest transpiles without typechecking. Both suites drive
  their providers off `Object.keys(fixture)`, so a missing provider was skipped
  rather than failed. Each suite now asserts its fixture covers the registry.

Re-running the same experiment now yields three compile errors plus two test
failures naming the missing fixtures.
A fallback exists so a key has somewhere to go when the field is unavailable.
The Search API Key field is unconditionally available: unlike the model key,
whose visibility runs through `shouldRequireApiKeyForModel` and its `isHosted`
branch, `getSearchApiKeyCondition` gates only on whether a provider is
selected. So the fallback never had a configuration to cover.

Removing it also closes an escalation. Workspace BYOK keys are admin-managed
and the API only ever returns them masked, yet `resolvePiSearchKey` would
resolve one for any member who could run the block — and in Create PR that key
is handed to the sandbox as an environment variable, which Pi copies into every
bash child. A member could read a credential the product deliberately never
shows them. Requiring the key on the block keeps the sandbox exposure to a key
its author already holds.

Nothing depends on the fallback: it has never shipped.

- `resolvePiSearchKey` is now synchronous and returns the key, since there is
  no lookup left to await. `byokProviderId` leaves the search registry and
  `PiSearchKeySource` / `PiSearchKeyResolution` are gone — with one source,
  `keySource` carried no information, and the logging rationale for it (a block
  field silently shadowing a stored key) no longer exists.
- The field is now `required`. Safe alongside its condition: the serializer's
  required check returns early for fields that are not visible, so a Pi block
  with search off still validates. Pinned by a test.

Docs and the block's tooltip, placeholder, and best practices updated. The
Create PR key-exposure callout now explains the missing fallback rather than
recommending the block field as a way around it.
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.

2 participants