@chatpanel/gateway 0.6.74 → 0.6.76
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 +2 -2
- package/src/config.js +8 -0
- package/src/configstore.js +15 -1
- package/src/server.js +25 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.76",
|
|
4
4
|
"description": "Local privacy gateway \u2014 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": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"node": ">=18"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@chatpanel/pii": "^0.7.
|
|
30
|
+
"@chatpanel/pii": "^0.7.2",
|
|
31
31
|
"@huggingface/transformers": "^4.2.0",
|
|
32
32
|
"onnxruntime-web": "1.26.0-dev.20260416-b7804b056c",
|
|
33
33
|
"phonemizer": "^1.2.1"
|
package/src/config.js
CHANGED
|
@@ -57,6 +57,14 @@ export const DEFAULTS = {
|
|
|
57
57
|
// Bearer token for the bridge's privileged /chat route. Empty = read the
|
|
58
58
|
// per-install token from ~/.chatpanel/bridge-token.
|
|
59
59
|
token: '',
|
|
60
|
+
// What a CLI agent driven THROUGH the gateway may do — the same three modes the
|
|
61
|
+
// extension offers per agent: 'default' (read-only, asks before writing),
|
|
62
|
+
// 'acceptEdits' (edits files in its workspace on its own), 'bypassPermissions' (full
|
|
63
|
+
// access and shell). Absent, the bridge ran every gateway turn read-only and told the
|
|
64
|
+
// model to "set Permissions" in a settings page the desktop did not have.
|
|
65
|
+
permissionMode: 'default',
|
|
66
|
+
// Where the agent works. Empty = the bridge's ChatPanel workspace.
|
|
67
|
+
workingDir: '',
|
|
60
68
|
},
|
|
61
69
|
|
|
62
70
|
upstreams: {
|
package/src/configstore.js
CHANGED
|
@@ -46,7 +46,7 @@ export function publicConfig(cfg, { proUnlocked = false } = {}) {
|
|
|
46
46
|
backend: cfg.backend,
|
|
47
47
|
// Strip per-destination apiKey (write-only).
|
|
48
48
|
destinations: (Array.isArray(cfg.destinations) ? cfg.destinations : []).map((d) => { const { apiKey, ...rest } = d; return { ...rest, hasKey: !!apiKey }; }),
|
|
49
|
-
bridge: { url: cfg.bridge?.url, agent: cfg.bridge?.agent, hasToken: !!cfg.bridge?.token },
|
|
49
|
+
bridge: { url: cfg.bridge?.url, agent: cfg.bridge?.agent, hasToken: !!cfg.bridge?.token, permissionMode: cfg.bridge?.permissionMode || 'default', workingDir: cfg.bridge?.workingDir || '' },
|
|
50
50
|
upstreams: cfg.upstreams,
|
|
51
51
|
redaction: {
|
|
52
52
|
tier: cfg.redaction?.tier,
|
|
@@ -98,6 +98,18 @@ export function isSelfDestination(d, cfg = {}) {
|
|
|
98
98
|
return loop && port === (Number(cfg.port) || 4320);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
export const PERMISSION_MODES = ['default', 'acceptEdits', 'bypassPermissions'];
|
|
102
|
+
|
|
103
|
+
/** The per-turn options a gateway-driven agent receives — the bridge's own vocabulary. */
|
|
104
|
+
export function bridgeAgentOptions(cfg, extra = {}) {
|
|
105
|
+
const b = cfg?.bridge || {};
|
|
106
|
+
return {
|
|
107
|
+
...(PERMISSION_MODES.includes(b.permissionMode) && b.permissionMode !== 'default' ? { permissionMode: b.permissionMode } : {}),
|
|
108
|
+
...(typeof b.workingDir === 'string' && b.workingDir.trim() ? { workingDir: b.workingDir.trim() } : {}),
|
|
109
|
+
...extra,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
101
113
|
export function applyConfigPatch(cfg, patch = {}) {
|
|
102
114
|
if (patch.backend === 'bridge' || patch.backend === 'api') cfg.backend = patch.backend;
|
|
103
115
|
if (Array.isArray(patch.destinations)) {
|
|
@@ -122,6 +134,8 @@ export function applyConfigPatch(cfg, patch = {}) {
|
|
|
122
134
|
if (patch.bridge && typeof patch.bridge === 'object') {
|
|
123
135
|
if (typeof patch.bridge.url === 'string') cfg.bridge.url = patch.bridge.url;
|
|
124
136
|
if (typeof patch.bridge.agent === 'string') cfg.bridge.agent = patch.bridge.agent;
|
|
137
|
+
if (PERMISSION_MODES.includes(patch.bridge.permissionMode)) cfg.bridge.permissionMode = patch.bridge.permissionMode;
|
|
138
|
+
if (typeof patch.bridge.workingDir === 'string') cfg.bridge.workingDir = patch.bridge.workingDir.trim().slice(0, 1024);
|
|
125
139
|
}
|
|
126
140
|
// api backend: where redacted traffic is forwarded (the client still picks the
|
|
127
141
|
// model + sends its own key).
|
package/src/server.js
CHANGED
|
@@ -47,7 +47,7 @@ import { rawOrtAvailable } from './ort.js';
|
|
|
47
47
|
import * as ttsVoices from './tts-voices.js';
|
|
48
48
|
import { resolveTtsVoice } from './tts-voice-resolve.js';
|
|
49
49
|
import { resolvePro, checkQuota, consume, usage } from './freegate.js';
|
|
50
|
-
import { publicConfig, applyConfigPatch, applyNerModelSelection, persistConfig, configPath } from './configstore.js';
|
|
50
|
+
import { publicConfig, applyConfigPatch, applyNerModelSelection, persistConfig, configPath, bridgeAgentOptions } from './configstore.js';
|
|
51
51
|
import { resolveDestination, aggregateModelsAsync, listDestinations } from './router.js';
|
|
52
52
|
import { makeAccessEvent } from './observability.js';
|
|
53
53
|
import { createPersistentAccessLog } from './access-log-store.js';
|
|
@@ -56,7 +56,7 @@ import * as openai from './openai.js';
|
|
|
56
56
|
import * as responses from './responses.js';
|
|
57
57
|
import * as anthropic from './anthropic.js';
|
|
58
58
|
|
|
59
|
-
export const VERSION = '0.6.
|
|
59
|
+
export const VERSION = '0.6.76';
|
|
60
60
|
|
|
61
61
|
// WARM search tier — SQLite + FTS5 record store (falls back to an encrypted-JSON
|
|
62
62
|
// store if SQLite can't load), fed by the extension's ingest sync + backup-ingest.
|
|
@@ -401,7 +401,7 @@ async function startRelay(req, res, { kind, adapter, agent }, body, vault, cfg,
|
|
|
401
401
|
// redaction in the main handler), so toTurn() carried it here — nothing to add.
|
|
402
402
|
let resp;
|
|
403
403
|
try {
|
|
404
|
-
resp = await openBridgeChat({ bridgeUrl, agent, token, messages, system, specs: toolsToSpecs(tools), options:
|
|
404
|
+
resp = await openBridgeChat({ bridgeUrl, agent, token, messages, system, specs: toolsToSpecs(tools), options: bridgeAgentOptions(cfg), signal: undefined });
|
|
405
405
|
} catch (e) { clearTimeout(ttl); endRelaySession(s.id); trace?.commit(); return sendJson(res, 502, { error: { message: `bridge: ${e.message}`, type: 'bridge_error' } }); }
|
|
406
406
|
s.reader = resp.body.getReader();
|
|
407
407
|
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
|
|
@@ -453,14 +453,23 @@ async function handleBridge(req, res, { kind, adapter, redactable, pathname, age
|
|
|
453
453
|
const shaper = shaperFor(kind, body?.model || agent);
|
|
454
454
|
const token = readBridgeToken(cfg.bridge.token);
|
|
455
455
|
const ac = new AbortController();
|
|
456
|
-
|
|
456
|
+
// STOP MUST REACH THE AGENT. The client pressing Stop aborts ITS request; the bridge only
|
|
457
|
+
// kills the CLI when the gateway's request to it goes away, and that happened only if the
|
|
458
|
+
// gateway noticed. It listened on `req 'close'`, which on this Node fires when the REQUEST
|
|
459
|
+
// is complete — the body was read long ago — not when the socket is cut mid-response. So
|
|
460
|
+
// Stop in the desktop stopped nothing: Codex wrote its whole essay to nobody (measured:
|
|
461
|
+
// the process outlived the abort by the length of the answer). The RESPONSE closing is
|
|
462
|
+
// the signal for a severed connection; a response that finished normally is not a stop.
|
|
463
|
+
res.on('close', () => { if (!res.writableFinished) ac.abort(); });
|
|
457
464
|
|
|
458
465
|
// The model half of `claude/opus`, handed to the CLI as its `--model`. Absent for a bare
|
|
459
466
|
// agent id, which leaves the agent on its own default exactly as before.
|
|
460
467
|
const { agentModel } = parseAgentModel(body?.model, cfg);
|
|
461
468
|
const turn = {
|
|
462
469
|
bridgeUrl: await resolveBridgeUrl(cfg), agent, token, messages, system, signal: ac.signal,
|
|
463
|
-
|
|
470
|
+
// Permissions and working directory from the gateway's config (the desktop's Settings →
|
|
471
|
+
// Engine → Agents), plus the model half of `claude/opus` when the caller named one.
|
|
472
|
+
options: bridgeAgentOptions(cfg, agentModel ? { model: agentModel } : {}),
|
|
464
473
|
};
|
|
465
474
|
|
|
466
475
|
if (!wantStream) {
|
|
@@ -1764,7 +1773,7 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
1764
1773
|
if (!redactionOff) await ensureNer(cfg);
|
|
1765
1774
|
const segs = redactionOff ? [] : r.adapter.collectSegments(body, cfg.redaction);
|
|
1766
1775
|
const ac = new AbortController();
|
|
1767
|
-
|
|
1776
|
+
res.on('close', () => { if (!res.writableFinished) ac.abort(); });
|
|
1768
1777
|
const rd0 = trace ? trace.clock() : 0;
|
|
1769
1778
|
// Redact at the configured tier for everyone (free users get name/org
|
|
1770
1779
|
// redaction within their allowance); the custom dictionary stays capped for
|
|
@@ -1799,7 +1808,16 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
1799
1808
|
// AFTER redaction so the note isn't itself redacted. Covers BOTH the API
|
|
1800
1809
|
// forward and the relay (which reads system from this same body).
|
|
1801
1810
|
if (!redactionOff && Array.isArray(body.tools) && body.tools.length && typeof r.adapter.injectSystemNote === 'function') {
|
|
1802
|
-
|
|
1811
|
+
// A relayed CLI agent brings tools of its own that run PAST this proxy and get the
|
|
1812
|
+
// placeholder literally; an API model has only the tools in this request. The note
|
|
1813
|
+
// has to say which is which, so the destination is looked up here — the same
|
|
1814
|
+
// resolution as below, on the same hint, just earlier. No destination means the
|
|
1815
|
+
// legacy bridge path, which is an agent.
|
|
1816
|
+
const early = resolveDestination(body?.model, cfg, r.kind, {
|
|
1817
|
+
destination: String(req.headers['x-chatpanel-destination'] || body?.chatpanel?.destination || '').trim(),
|
|
1818
|
+
});
|
|
1819
|
+
const ownTools = !(early && early.type === 'api');
|
|
1820
|
+
r.adapter.injectSystemNote(body, placeholderToolNote({ toolData: cfg.tools?.toolData, ownTools }));
|
|
1803
1821
|
}
|
|
1804
1822
|
outBody = Buffer.from(JSON.stringify(body), 'utf8');
|
|
1805
1823
|
}
|