@chatpanel/gateway 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/gateway",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Local privacy gateway — redacts PII out of OpenAI/Anthropic API traffic before it reaches a model, then restores it in the reply. Point opencode, codex, aider, Claude Code, etc. at it.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/anthropic.js CHANGED
@@ -9,6 +9,15 @@ export function matches(pathname) {
9
9
  return /\/messages$/.test(pathname);
10
10
  }
11
11
 
12
+ // Append a (non-redacted) instruction to the system prompt.
13
+ export function injectSystemNote(body, note) {
14
+ if (!note || !body) return body;
15
+ if (typeof body.system === 'string') body.system += `\n\n${note}`;
16
+ else if (Array.isArray(body.system)) body.system.push({ type: 'text', text: note });
17
+ else body.system = note;
18
+ return body;
19
+ }
20
+
12
21
  // Push segments for a content field that may be a string or an array of blocks
13
22
  // ({type:'text',text}, {type:'tool_result',content}, …).
14
23
  function collectContent(content, segs) {
package/src/openai.js CHANGED
@@ -11,6 +11,16 @@ export function matches(pathname) {
11
11
  return /\/chat\/completions$/.test(pathname) || /\/completions$/.test(pathname);
12
12
  }
13
13
 
14
+ // Append a (non-redacted) instruction to the system prompt — e.g. the placeholder
15
+ // note that tells the model placeholders are auto-restored for tools.
16
+ export function injectSystemNote(body, note) {
17
+ if (!note || !body || !Array.isArray(body.messages)) return body;
18
+ const sys = body.messages.find((m) => m && m.role === 'system');
19
+ if (sys && typeof sys.content === 'string') sys.content += `\n\n${note}`;
20
+ else body.messages.unshift({ role: 'system', content: note });
21
+ return body;
22
+ }
23
+
14
24
  // Collect segments from messages[].content (string or multimodal parts). System
15
25
  // messages are included unless redactSystem is false.
16
26
  export function collectSegments(body, redactionCfg) {
package/src/responses.js CHANGED
@@ -9,6 +9,13 @@ export function matches(pathname) {
9
9
  return /\/responses$/.test(pathname);
10
10
  }
11
11
 
12
+ // Append a (non-redacted) instruction to the Responses-API instructions field.
13
+ export function injectSystemNote(body, note) {
14
+ if (!note || !body) return body;
15
+ body.instructions = body.instructions ? `${body.instructions}\n\n${note}` : note;
16
+ return body;
17
+ }
18
+
12
19
  // Redactable text in a Responses request lives in `instructions` (system) and
13
20
  // `input` (a string, or an array of items whose content parts carry text).
14
21
  function collectInputItem(item, segs) {
package/src/server.js CHANGED
@@ -33,7 +33,7 @@ import * as openai from './openai.js';
33
33
  import * as responses from './responses.js';
34
34
  import * as anthropic from './anthropic.js';
35
35
 
36
- export const VERSION = '0.5.1';
36
+ export const VERSION = '0.5.2';
37
37
 
38
38
  const KNOWN_AGENTS = new Set(['codex', 'claude', 'opencode', 'pi', 'kiro', 'antigravity']);
39
39
 
@@ -122,6 +122,18 @@ function pickAgent(model, cfg) {
122
122
  return KNOWN_AGENTS.has(model) ? model : cfg.bridge.agent;
123
123
  }
124
124
 
125
+ // A follow-up request carrying a tool result for a PARKED relay session. Such a
126
+ // request must NOT be redacted here: the relay owns redaction/restore through its
127
+ // OWN (round-1) vault, so re-redacting with a fresh vault would put the tool
128
+ // result's new tokens in the wrong vault and leave them unrestored in the reply.
129
+ function isRelayResume(body, kind) {
130
+ if (kind !== 'openai' || !body) return false;
131
+ const tr = openai.extractLatestToolResult(body);
132
+ if (!tr) return false;
133
+ const parsed = parseToolCallId(tr.tool_call_id);
134
+ return !!(parsed && getRelaySession(parsed.gwId));
135
+ }
136
+
125
137
  // Guard against an api destination pointing back at THIS gateway (loopback host +
126
138
  // our own port) — forwarding there would loop forever.
127
139
  function isSelfUrl(baseUrl, cfg) {
@@ -208,14 +220,11 @@ async function startRelay(req, res, { kind, adapter, agent }, body, vault, cfg,
208
220
  const redactOpts = { tier: effectiveTier({ tier: cfg.redaction.tier }, isPro), dictionary: gatedDictionary(cfg.redaction, isPro), entities: [] };
209
221
  const s = createRelaySession({ vault, redactOpts, bridgeUrl: cfg.bridge.url, token, harness });
210
222
  const ttl = setTimeout(() => endRelaySession(s.id), 135_000); // bridge tool-call timeout is 120s
211
- // Tell the agent placeholders are auto-restored for tools, so privacy-aware agents
212
- // (Codex/Claude) USE them instead of refusing. Appended after redaction.
213
- const sysWithNote = (vault && tools?.length)
214
- ? `${system || ''}\n\n${placeholderToolNote({ toolData: cfg.tools?.toolData })}`.trim()
215
- : system;
223
+ // The placeholder note is already in `system` (injected into the body after
224
+ // redaction in the main handler), so toTurn() carried it here nothing to add.
216
225
  let resp;
217
226
  try {
218
- resp = await openBridgeChat({ bridgeUrl: cfg.bridge.url, agent, token, messages, system: sysWithNote, specs: toolsToSpecs(tools), options: {}, signal: undefined });
227
+ resp = await openBridgeChat({ bridgeUrl: cfg.bridge.url, agent, token, messages, system, specs: toolsToSpecs(tools), options: {}, signal: undefined });
219
228
  } catch (e) { clearTimeout(ttl); endRelaySession(s.id); return sendJson(res, 502, { error: { message: `bridge: ${e.message}`, type: 'bridge_error' } }); }
220
229
  s.reader = resp.body.getReader();
221
230
  res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
@@ -434,7 +443,11 @@ export function createGateway(cfg = loadConfig()) {
434
443
  let isPro = true;
435
444
  if (r.redactable && req.method === 'POST' && raw.length) {
436
445
  try { body = JSON.parse(raw.toString('utf8')); } catch { body = null; }
437
- if (body) {
446
+ if (body && isRelayResume(body, r.kind)) {
447
+ // Relay tool-result follow-up: do NOT redact here — the parked session
448
+ // redacts the tool result + restores the reply with ITS vault. Pass raw.
449
+ outBody = raw;
450
+ } else if (body) {
438
451
  // Auto-narrow tools to the top-K most relevant for this turn (speed) —
439
452
  // same shared ranker as the extension's AUTO mode. Only MCP-named tools
440
453
  // are narrowed; the client's core tools (bash/read/edit…) are always kept,
@@ -463,6 +476,13 @@ export function createGateway(cfg = loadConfig()) {
463
476
  const { vault: v, count } = await redactSegments(segs, cfg.redaction, { signal: ac.signal, isPro });
464
477
  vault = v;
465
478
  redactedCount = count;
479
+ // When tools are armed, tell the model placeholders are auto-restored for
480
+ // tools (so privacy-aware models USE them instead of refusing). Injected
481
+ // AFTER redaction so the note isn't itself redacted. Covers BOTH the API
482
+ // forward and the relay (which reads system from this same body).
483
+ if (Array.isArray(body.tools) && body.tools.length && typeof r.adapter.injectSystemNote === 'function') {
484
+ r.adapter.injectSystemNote(body, placeholderToolNote({ toolData: cfg.tools?.toolData }));
485
+ }
466
486
  outBody = Buffer.from(JSON.stringify(body), 'utf8');
467
487
  }
468
488
  }