feat(templating): (H1) route user-plugin response hooks through the sandbox (PR 10b)#10286
Conversation
✅ Circular References ReportGenerated at: 2026-07-24T14:34:15.700Z Summary
Click to view all circular references in PR (9)Click to view all circular references in base branch (9)Analysis✅ No Change: This PR does not introduce or remove any circular references. This report was generated automatically by comparing against the |
8ad4f12 to
033d6d5
Compare
b1eff93 to
62bf5ef
Compare
033d6d5 to
045b1c2
Compare
236b349 to
369f56e
Compare
369f56e to
57ecee8
Compare
There was a problem hiding this comment.
Pull request overview
Routes user-plugin response hooks through the QuickJS sandbox (H1 phase), so response hooks can read response metadata/body and rewrite the body only via capability-gated host bridges; bundled plugins remain in-process and behavior is gated by templateTagSandboxEnabled.
Changes:
- Add an in-sandbox
context.responseAPI (__buildResponseApi) and wire__invokeHookto support response hooks with a read-onlycontext.request. - Add host bridge support for
response.setBodyplus host-side sandbox runners/IPC wiring to execute user response hooks from both node runtime and plugin window paths. - Add unit + smoke coverage for sandboxed response hook execution and body rewrite behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/insomnia/src/templating/sandbox/sandbox-hooks.test.ts | Adds a unit test covering sandboxed response hooks and the new body rewrite bridge. |
| packages/insomnia/src/templating/sandbox/in-sandbox-bootstrap.ts | Implements the in-sandbox response API and wires response hooks through __invokeHook. |
| packages/insomnia/src/templating/sandbox/host-bridge.ts | Registers the new response.setBody bridge path under capability gating. |
| packages/insomnia/src/runtimes/network/network-adapter.node.ts | Executes user response hooks in the sandbox (when available/enabled) in the node runtime send pipeline. |
| packages/insomnia/src/plugins/invoke-method.ts | Executes user response hooks via the templating-worker-database protocol when sandboxing is enabled. |
| packages/insomnia/src/main/templating-worker-database.ts | Adds runResponseHookInSandbox, plugin.runUserResponseHook, and the response.setBody host handler. |
| packages/insomnia/src/common/templating/types.ts | Extends PluginToMainAPIPaths for the new bridge endpoints. |
| packages/insomnia-smoke-test/tests/smoke/sandbox-template-tags.test.ts | Adds E2E coverage ensuring sandboxed response hooks rewrite the visible response body with the flag on/off. |
Comments suppressed due to low confidence (1)
packages/insomnia/src/templating/sandbox/in-sandbox-bootstrap.ts:466
context.response.setBody()treatsundefined/nullbody as an empty payload (body || []) and will write an empty file, whereas the in-process implementation ultimately throws if you passundefinedtofs.writeFileSync. Add an explicit null/undefined guard and avoid silently coercing missing bodies to empty bytes.
' setBody: function (body) {',
' if (!resp.bodyPath) { throw new Error("Could not set body without existing body path"); }',
' resp.bytesContent = (body && body.length) || 0;',
' var b64 = (typeof body === "string") ? Buffer.from(body, "utf8").toString("base64") : Buffer.from(body || []).toString("base64");',
' return __bridge("response.setBody", { bodyPath: resp.bodyPath, bodyBase64: b64 });',
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ect empty setBody Addresses Copilot review on #10286: - in-sandbox-bootstrap.ts: context.response.getBody() now revives the JSON-marshaled Buffer shape ({ type: 'Buffer', data: [...] }) back into a (shimmed) Buffer, so hooks consume it exactly like the in-process response API instead of a plain object. - templating-worker-database.ts: response.setBody rejects a missing/non-string bodyBase64 (TypeError) instead of defaulting to '' — a write primitive must not silently truncate the body to zero bytes. An empty string stays a valid empty body. - sandbox-hooks.test.ts: the getBodyBuffer stub now returns the real marshaled Buffer shape and the hook decodes with toString('utf8'), exercising the revive end-to-end.
kwburns-kong
left a comment
There was a problem hiding this comment.
@jackkav, PR #10294 covers a few more things. I've started to look into a bit of the capabilities / bridge handlers, we should add a final step within the sandbox plan to identify and correctly scope what each can directly do.
First increment of PR 10b (response hooks). Adds the sandbox-side machinery to run a user plugin's response hook, without yet wiring the network adapters: - __buildResponseApi(resp): faithful ES5 rebuild of plugins/context/response.ts getters (status/headers/time, case-insensitive getHeader returning string|string[]|null). getBody() bridges to the existing response.getBodyBuffer path; getBodyStream() throws (a Node Readable can't cross the sandbox); setBody() base64-encodes the bytes over a new response.setBody bridge path (no fs in the sandbox) and updates bytesContent locally. - __invokeHook: for response hooks, attaches context.response and a READ-ONLY context.request (matches pluginRequest.init(..., true)), and marshals both the request and response back out. - host-bridge: response.setBody mapped at baseline (models.read, grouped with the other response ops) — a bounded write to the host-set bodyPath, matching the ungated in-process behavior; not a general fs write. Unit test drives a response hook that reads status/headers, reads the body via the bridge, and rewrites it via setBody (asserting the base64 round-trip and the read-only request). Host runner + adapter integration + e2e are the next increment.
…andbox Wires the response-hook sandbox core into both hook paths so a user plugin's response hook runs in QuickJS (bundle plugins and the flag-off path unchanged). - templating-worker-database: runResponseHookInSandbox + the plugin.runUserResponseHook handler; the response.setBody bridge handler (base64-decode -> fs.writeFileSync) guarded to the responses directory as defense in depth (bodyPath is host-set); pickHookResponseFields. - network-adapter.node.ts (main/CLI): runs the response hook in the sandbox for user plugins, merging the returned response fields onto newResponse. - invoke-method.ts (plugin window): reaches the same runner over the protocol. E2E: a user plugin response hook rewrites the body via setBody; the echo response pane shows the rewrite. Flag off it reports ranin-mainprocess (control), flag on ranin-sandboxed. Completes H1 (request hooks in PR 10a).
…ect empty setBody Addresses Copilot review on #10286: - in-sandbox-bootstrap.ts: context.response.getBody() now revives the JSON-marshaled Buffer shape ({ type: 'Buffer', data: [...] }) back into a (shimmed) Buffer, so hooks consume it exactly like the in-process response API instead of a plain object. - templating-worker-database.ts: response.setBody rejects a missing/non-string bodyBase64 (TypeError) instead of defaulting to '' — a write primitive must not silently truncate the body to zero bytes. An empty string stays a valid empty body. - sandbox-hooks.test.ts: the getBodyBuffer stub now returns the real marshaled Buffer shape and the hook decodes with toString('utf8'), exercising the revive end-to-end.
78eb899 to
60f6667
Compare
Summary
Phase 2, PR 10b — H1 (response hooks), completing H1 (request hooks landed in #10279). Routes a user plugin's response hooks through the QuickJS sandbox: the hook reads the response and can rewrite its body, but reaches the host only through capability-gated bridges. Bundle plugins stay in-process; gated on
templateTagSandboxEnabled. Stacked on #10279.How it works
__buildResponseApi(in-sandbox): faithful ES5 rebuild ofplugins/context/response.ts— getters (status/headers/time;getHeaderreturnsstring | string[] | null, case-insensitive).getBody()bridges to the existingresponse.getBodyBufferpath.getBodyStream()throws (a NodeReadablecan't cross the sandbox).setBody()base64-encodes the bytes over a newresponse.setBodybridge and updatesbytesContent.__invokeHook: for response hooks, attachescontext.response+ a read-onlycontext.request(matchespluginRequest.init(..., true)), and marshals both back out.response.setBodyhost handler: base64-decode →fs.writeFileSync, contained to the responses directory as defense in depth (bodyPathis host-set, never plugin-chosen). Mapped at baseline (models.read, grouped with the other response ops) — a bounded write matching the ungated in-process behavior; not a general fs write.network-adapter.node.ts(main/CLI) callsrunResponseHookInSandboxdirectly;invoke-method.ts(plugin window) over the protocol. Per-plugin hook-index recovery; the returned response fields merge ontonewResponseand the body change persists via the on-disk write.Tests
sandbox-hooks.test.ts): a response hook reads status/headers, reads the body via the bridge, and rewrites it viasetBody— asserting the base64 round-trip,bytesContent, and that the request view is read-only.sandbox-hook-collection.yaml): a response hook rewrites the body; the echo response pane shows it. Flag off reportsranin-mainprocess(control); flag on reportsranin-sandboxed.Local: 23 sandbox/worker-db unit tests + type-check + lint (incl. smoke workspace) green. Electron smoke runs in CI.
Note on scope
getBodyStream()intentionally throws in the sandbox (a Node stream is unmarshalable) — response hooks should usegetBody(). This is the one deliberate behavior difference from the in-process path.