Files
Jack Kavanagh 5ff4186a79 feat(templating): (H1) route user-plugin response hooks through the sandbox (PR 10b) (#10286)
* feat(templating): (H1) in-sandbox response-hook API + marshaling

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.

* feat(templating): (H1) route user-plugin response hooks through the sandbox

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).

* fix(templating): (H1) revive bridged Buffer in response getBody + reject 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.
2026-07-24 10:57:40 -04:00
..