@chatpanel/gateway 0.6.91 → 0.6.93
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/README.md +23 -4
- package/bin/chatpanel-gateway.js +12 -2
- package/package.json +3 -2
- package/src/bridge-supervisor.js +176 -0
- package/src/bridge.js +17 -8
- package/src/local-status.js +1 -1
- package/src/server.js +14 -9
package/README.md
CHANGED
|
@@ -37,14 +37,19 @@ The redaction engine is the **same code** the ChatPanel extension runs — the
|
|
|
37
37
|
[`@chatpanel/pii`](https://github.com/chatpanel/chatpanel-pii) package is the
|
|
38
38
|
single source of truth, so a privacy feature added once is shared everywhere.
|
|
39
39
|
|
|
40
|
-
## Quick start
|
|
40
|
+
## Quick start — the one thing to install
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
The gateway **carries the [bridge](https://github.com/chatpanel/chatpanel-bridge)**
|
|
43
|
+
(0.6.92+): `@chatpanel/bridge` is a dependency, bundled into the same binary, and the
|
|
44
|
+
gateway starts it as a child process when nothing already answers on 4319 — or adopts a
|
|
45
|
+
bridge that is already running (the desktop app's, or a standalone you installed; a newer
|
|
46
|
+
standalone is preferred so bridge fixes keep their own cadence). Two processes on purpose:
|
|
47
|
+
the bridge spawns your CLIs and holds SCM tokens; the model runtimes live here. Log into
|
|
48
|
+
the CLIs (`claude`, `codex`, …) as you normally would — that is all.
|
|
44
49
|
|
|
45
50
|
```bash
|
|
46
51
|
# Standalone binary — no Node.js required:
|
|
47
|
-
curl -fsSL https://dl.chatpanel.net/
|
|
52
|
+
curl -fsSL https://dl.chatpanel.net/install.sh | bash # macOS / Linux
|
|
48
53
|
# Windows (PowerShell): irm https://dl.chatpanel.net/gateway/install.ps1 | iex
|
|
49
54
|
|
|
50
55
|
# Or via npm (needs Node):
|
|
@@ -52,8 +57,22 @@ npm install -g @chatpanel/gateway
|
|
|
52
57
|
chatpanel-gateway
|
|
53
58
|
# → ChatPanel Privacy Gateway on http://127.0.0.1:4320
|
|
54
59
|
# backend : bridge (agent: codex, via http://127.0.0.1:4319)
|
|
60
|
+
# bridge : starting the embedded bridge (v0.11.20)
|
|
55
61
|
```
|
|
56
62
|
|
|
63
|
+
On Windows, if `chatpanel-gateway` is "not recognized" right after `npm i -g`, npm's global bin
|
|
64
|
+
folder is not on that shell's PATH yet: open a new PowerShell, or run it by path —
|
|
65
|
+
`node "$(npm root -g)/@chatpanel/gateway/bin/chatpanel-gateway.js" --install`. npm may also warn
|
|
66
|
+
that `boolean@3.2.0` is deprecated and that `onnxruntime-node` / `sharp` / `protobufjs` run install
|
|
67
|
+
scripts: all three come through `@huggingface/transformers` (the native model runtime) and are
|
|
68
|
+
expected.
|
|
69
|
+
|
|
70
|
+
`GET /health` says which bridge it runs: `bridge.mode` is `embedded`, `standalone`,
|
|
71
|
+
`adopted` or `off`. `CHATPANEL_BRIDGE_MANAGED=off` (or `bridge.managed: false`) turns the
|
|
72
|
+
supervision off for a host that runs its own bridge — the desktop app does. A
|
|
73
|
+
`bridge.url` that is not on this machine is left alone: nothing is started for a remote
|
|
74
|
+
bridge.
|
|
75
|
+
|
|
57
76
|
> **Binary vs. npm — same features, very different local-AI speed.** Both run
|
|
58
77
|
> identical redaction/routing. But the standalone binary runs the local models
|
|
59
78
|
> (speech-to-text, diarization, NER) on the **WASM** runtime — **fp32-only,
|
package/bin/chatpanel-gateway.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// chatpanel-gateway --uninstall remove login auto-start
|
|
13
13
|
// chatpanel-gateway --status is auto-start registered?
|
|
14
14
|
// chatpanel-gateway --version print version
|
|
15
|
+
// chatpanel-gateway --bridge run the embedded bridge (the supervisor's child; not for hands)
|
|
15
16
|
//
|
|
16
17
|
// Config comes from gateway.config.json / env (see src/config.js).
|
|
17
18
|
export {}; // mark as an ES module (all imports below are dynamic)
|
|
@@ -19,7 +20,16 @@ export {}; // mark as an ES module (all imports below are dynamic)
|
|
|
19
20
|
const arg = process.argv[2];
|
|
20
21
|
|
|
21
22
|
try {
|
|
22
|
-
if (arg === '
|
|
23
|
+
if (arg === '--bridge') {
|
|
24
|
+
// THE EMBEDDED BRIDGE. The gateway carries @chatpanel/bridge and starts it as a child of
|
|
25
|
+
// itself (src/bridge-supervisor.js) when nothing answers on 4319 — one install, two
|
|
26
|
+
// processes. The flag is consumed here; what remains of argv is the bridge's own
|
|
27
|
+
// (`--version` for the supervisor's check). CHATPANEL_BRIDGE_EMBEDDED tells the bridge its
|
|
28
|
+
// execPath is the gateway, so it never self-updates over it.
|
|
29
|
+
process.env.CHATPANEL_BRIDGE_EMBEDDED = '1';
|
|
30
|
+
process.argv.splice(2, 1);
|
|
31
|
+
await import('@chatpanel/bridge/src/server.js');
|
|
32
|
+
} else if (arg === 'mcp') {
|
|
23
33
|
// Its own path: proxies to the running gateway over HTTP and must NOT import
|
|
24
34
|
// server.js (which would open a second handle on the warm SQLite store).
|
|
25
35
|
const { runMcpServer } = await import('../src/mcp.js');
|
|
@@ -60,7 +70,7 @@ try {
|
|
|
60
70
|
start();
|
|
61
71
|
break;
|
|
62
72
|
default:
|
|
63
|
-
console.error(`unknown option: ${arg}\nUsage: chatpanel-gateway [mcp|tools list|tools schema <tool>|call <tool> '<json>'|local|connect|--install|--uninstall|--status|--version]`);
|
|
73
|
+
console.error(`unknown option: ${arg}\nUsage: chatpanel-gateway [mcp|tools list|tools schema <tool>|call <tool> '<json>'|local|connect|--install|--uninstall|--status|--version|--bridge]`);
|
|
64
74
|
process.exit(2);
|
|
65
75
|
}
|
|
66
76
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Local privacy gateway
|
|
3
|
+
"version": "0.6.93",
|
|
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": {
|
|
7
7
|
"chatpanel-gateway": "bin/chatpanel-gateway.js"
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"node": ">=18"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@chatpanel/bridge": "^0.11.21",
|
|
30
31
|
"@chatpanel/pii": "^0.7.4",
|
|
31
32
|
"@huggingface/transformers": "^4.2.0",
|
|
32
33
|
"onnxruntime-web": "1.26.0-dev.20260416-b7804b056c",
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// The gateway carries the bridge — one install, two processes.
|
|
2
|
+
//
|
|
3
|
+
// A user installs ONE thing (this gateway: the binary, or `npm i -g @chatpanel/gateway`) and
|
|
4
|
+
// gets the bridge with it: `@chatpanel/bridge` is a dependency, bundled into the same binary,
|
|
5
|
+
// and the gateway STARTS it as a child process when nothing already answers on the bridge
|
|
6
|
+
// port. Two processes on purpose: the bridge spawns CLIs, holds SCM tokens and runs shells —
|
|
7
|
+
// a small zero-dependency process with its own failure domain — and the NER / STT / TTS
|
|
8
|
+
// runtimes that crash or eat memory live here, not there. A gateway restart never takes a
|
|
9
|
+
// running Claude Code task down with it.
|
|
10
|
+
//
|
|
11
|
+
// What is already there is ADOPTED, never fought: a bridge the desktop app installed, a
|
|
12
|
+
// standalone the user put in ~/.local/bin (preferred when it is NEWER than the embedded copy,
|
|
13
|
+
// so a bridge fix keeps shipping on its own tag), a remote bridge named in config. The plan
|
|
14
|
+
// is a pure function (`planBridge`) so the rule is testable without spawning anything.
|
|
15
|
+
//
|
|
16
|
+
// An embedded child is started with CHATPANEL_BRIDGE_EMBEDDED=1: its `process.execPath` is
|
|
17
|
+
// the GATEWAY binary, so the bridge's self-update is disabled there (it would overwrite the
|
|
18
|
+
// gateway with a bridge) and its /health says `update.embedded: 'gateway'`. Both children get
|
|
19
|
+
// CHATPANEL_MANAGED_BY=gateway so a client can say who runs it instead of offering install.sh.
|
|
20
|
+
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { existsSync } from 'node:fs';
|
|
24
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
25
|
+
import { resolveLaunch } from './service.js';
|
|
26
|
+
import { DEFAULT_BRIDGE_URL } from './bridge.js';
|
|
27
|
+
|
|
28
|
+
const LOOPBACK = /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:(\d+))?\/?$/i;
|
|
29
|
+
const BACKOFF_MS = [1000, 2000, 5000, 10000, 30000];
|
|
30
|
+
const STABLE_MS = 60_000; // a child up this long resets the back-off
|
|
31
|
+
const PROBE_TIMEOUT_MS = 1200;
|
|
32
|
+
|
|
33
|
+
/** Where the standalone bridge installer puts its binary (scripts/install.sh / install.ps1 in the bridge repo). */
|
|
34
|
+
export function standaloneCandidates({ home = os.homedir(), platform = process.platform, env = process.env } = {}) {
|
|
35
|
+
if (platform === 'win32') return [path.join(env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'ChatPanel', 'chatpanel-bridge.exe')];
|
|
36
|
+
return [path.join(home, '.local', 'bin', 'chatpanel-bridge')];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** semver-ish: 1 when a > b, -1 when a < b, 0 when equal or unreadable. */
|
|
40
|
+
export function compareVersions(a, b) {
|
|
41
|
+
const pa = String(a || '').split('.').map((x) => parseInt(x, 10)); const pb = String(b || '').split('.').map((x) => parseInt(x, 10));
|
|
42
|
+
if (pa.some(Number.isNaN) || pb.some(Number.isNaN) || pa.length < 3 || pb.length < 3) return 0;
|
|
43
|
+
for (let i = 0; i < 3; i += 1) { if (pa[i] !== pb[i]) return pa[i] > pb[i] ? 1 : -1; }
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What to do about the bridge — pure.
|
|
49
|
+
* managed: false / 'off' → off (the host runs its own, e.g. the desktop app)
|
|
50
|
+
* a non-loopback bridge.url → off (a remote bridge is the user's; never start one here)
|
|
51
|
+
* something answers the port → adopt
|
|
52
|
+
* a standalone newer than the embedded copy → spawn-standalone
|
|
53
|
+
* an embedded copy → spawn-embedded
|
|
54
|
+
* only a standalone → spawn-standalone
|
|
55
|
+
*/
|
|
56
|
+
export function planBridge({ managed = true, cfgUrl = '', healthy = null, standalone = null, embeddedVersion = null } = {}) {
|
|
57
|
+
if (managed === false || managed === 'off') return { action: 'off', why: 'bridge supervision is off (the host runs its own)' };
|
|
58
|
+
const url = String(cfgUrl || DEFAULT_BRIDGE_URL);
|
|
59
|
+
if (!LOOPBACK.test(url)) return { action: 'off', why: `bridge.url ${url} is not on this machine; nothing to start here` };
|
|
60
|
+
if (healthy) return { action: 'adopt', why: `a bridge already answers at ${url}${healthy.version ? ` (v${healthy.version}${healthy.managedBy ? `, run by ${healthy.managedBy}` : ''})` : ''}` };
|
|
61
|
+
const sv = standalone?.version || null;
|
|
62
|
+
if (standalone?.path && embeddedVersion && compareVersions(sv, embeddedVersion) > 0) return { action: 'spawn-standalone', why: `the installed bridge (v${sv}) is newer than the embedded one (v${embeddedVersion})`, path: standalone.path };
|
|
63
|
+
if (embeddedVersion) return { action: 'spawn-embedded', why: `starting the embedded bridge (v${embeddedVersion})${standalone?.path ? ` — the installed one is v${sv || '?'}` : ''}` };
|
|
64
|
+
if (standalone?.path) return { action: 'spawn-standalone', why: `starting the installed bridge${sv ? ` (v${sv})` : ''}`, path: standalone.path };
|
|
65
|
+
return { action: 'off', why: 'no bridge to start (no embedded copy, no standalone install)' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** `GET /health` on the bridge, or null. */
|
|
69
|
+
export async function probeBridge(url, { timeoutMs = PROBE_TIMEOUT_MS, fetchImpl = fetch } = {}) {
|
|
70
|
+
const ctrl = new AbortController();
|
|
71
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetchImpl(`${String(url).replace(/\/$/, '')}/health`, { signal: ctrl.signal });
|
|
74
|
+
if (!res.ok) return null;
|
|
75
|
+
const j = await res.json().catch(() => null);
|
|
76
|
+
return j && j.ok !== false ? { version: j.version || null, managedBy: j.managedBy || null } : null;
|
|
77
|
+
} catch { return null; } finally { clearTimeout(t); }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `<program> [...args] --version`, or null — one process, bounded. */
|
|
81
|
+
export function versionOf(program, args = [], { timeoutMs = 8000 } = {}) {
|
|
82
|
+
try {
|
|
83
|
+
const r = spawnSync(program, [...args, '--version'], { encoding: 'utf8', timeout: timeoutMs, env: { ...process.env, CHATPANEL_BRIDGE_EMBEDDED: '1' } });
|
|
84
|
+
const v = String(r.stdout || '').trim().split('\n').pop();
|
|
85
|
+
return /^\d+\.\d+\.\d+/.test(v) ? v : null;
|
|
86
|
+
} catch { return null; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** How THIS gateway starts its embedded bridge: itself, with `--bridge` (bin/chatpanel-gateway.js). */
|
|
90
|
+
export function embeddedLaunch() {
|
|
91
|
+
const { program, args } = resolveLaunch();
|
|
92
|
+
return { program, args: [...args, '--bridge'] };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const portOf = (url) => { const m = LOOPBACK.exec(String(url || DEFAULT_BRIDGE_URL)); return m?.[3] || '4319'; };
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Make sure a bridge is running for this gateway, and keep it running. Returns a controller:
|
|
99
|
+
* `status()` → `{ mode: 'adopted'|'embedded'|'standalone'|'off', version, pid, restarts, why }`,
|
|
100
|
+
* `stop()` ends a child we started. Everything that touches the machine is injectable so the
|
|
101
|
+
* rule can be tested without a process: `probe`, `spawnImpl`, `version`, `candidates`, `exists`.
|
|
102
|
+
*/
|
|
103
|
+
export async function ensureBridge(cfg, {
|
|
104
|
+
log = (line) => console.log(line),
|
|
105
|
+
probe = probeBridge,
|
|
106
|
+
spawnImpl = spawn,
|
|
107
|
+
version = versionOf,
|
|
108
|
+
candidates = standaloneCandidates(),
|
|
109
|
+
exists = existsSync,
|
|
110
|
+
launch = embeddedLaunch,
|
|
111
|
+
managed = process.env.CHATPANEL_BRIDGE_MANAGED === 'off' ? false : cfg?.bridge?.managed,
|
|
112
|
+
waitForSiblingMs = 2500,
|
|
113
|
+
now = Date.now,
|
|
114
|
+
setTimer = setTimeout,
|
|
115
|
+
} = {}) {
|
|
116
|
+
const url = String(cfg?.bridge?.url || DEFAULT_BRIDGE_URL).replace(/\/$/, '');
|
|
117
|
+
const state = { mode: 'off', version: null, pid: null, restarts: 0, why: '', child: null, stopping: false, backoff: 0 };
|
|
118
|
+
const settle = (mode, why, extra = {}) => { Object.assign(state, { mode, why }, extra); log(` bridge : ${why}`); };
|
|
119
|
+
|
|
120
|
+
// Someone else (the desktop's supervisor, launchd) may be starting a bridge this very
|
|
121
|
+
// second; wait a moment before deciding it is absent, or two bridges race for one port.
|
|
122
|
+
let healthy = await probe(url);
|
|
123
|
+
if (!healthy && managed !== false && managed !== 'off' && LOOPBACK.test(url) && waitForSiblingMs > 0) {
|
|
124
|
+
const until = now() + waitForSiblingMs;
|
|
125
|
+
while (!healthy && now() < until) { await new Promise((r) => setTimer(r, 500)); healthy = await probe(url); }
|
|
126
|
+
}
|
|
127
|
+
const emb = launch();
|
|
128
|
+
const embeddedVersion = managed === false || managed === 'off' || healthy ? null : version(emb.program, emb.args);
|
|
129
|
+
const standalonePath = candidates.find((p) => exists(p)) || null;
|
|
130
|
+
const standalone = standalonePath && !healthy ? { path: standalonePath, version: version(standalonePath, []) } : null;
|
|
131
|
+
const plan = planBridge({ managed, cfgUrl: url, healthy, standalone, embeddedVersion });
|
|
132
|
+
|
|
133
|
+
if (plan.action === 'off') { settle('off', plan.why); return controller(); }
|
|
134
|
+
if (plan.action === 'adopt') { settle('adopted', plan.why, { version: healthy.version }); return controller(); }
|
|
135
|
+
|
|
136
|
+
const spec = plan.action === 'spawn-embedded'
|
|
137
|
+
? { ...emb, env: { CHATPANEL_BRIDGE_EMBEDDED: '1' }, mode: 'embedded', version: embeddedVersion }
|
|
138
|
+
: { program: plan.path, args: [], env: {}, mode: 'standalone', version: standalone?.version || null };
|
|
139
|
+
let startedAt = 0;
|
|
140
|
+
|
|
141
|
+
const start = () => {
|
|
142
|
+
if (state.stopping) return;
|
|
143
|
+
const child = spawnImpl(spec.program, spec.args, {
|
|
144
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
145
|
+
env: { ...process.env, ...spec.env, CHATPANEL_MANAGED_BY: 'gateway', CHATPANEL_BRIDGE_PORT: portOf(url) },
|
|
146
|
+
});
|
|
147
|
+
startedAt = now();
|
|
148
|
+
state.child = child; state.pid = child.pid || null;
|
|
149
|
+
const relay = (stream, tag) => stream?.on('data', (buf) => { for (const line of String(buf).split('\n')) if (line.trim()) log(`[bridge] ${line}`); });
|
|
150
|
+
relay(child.stdout, 'out'); relay(child.stderr, 'err');
|
|
151
|
+
child.on('error', (e) => log(`[bridge] could not start ${spec.program}: ${e?.message || e}`));
|
|
152
|
+
child.on('exit', async (code, sig) => {
|
|
153
|
+
state.child = null; state.pid = null;
|
|
154
|
+
if (state.stopping) return;
|
|
155
|
+
// The port may have been taken by a bridge someone else started while ours came up —
|
|
156
|
+
// that is not a failure to restart from, it is a bridge to adopt.
|
|
157
|
+
const other = await probe(url);
|
|
158
|
+
if (other) { settle('adopted', `a bridge already answers at ${url} (v${other.version || '?'}) — ours stepped aside`, { version: other.version }); return; }
|
|
159
|
+
if (now() - startedAt > STABLE_MS) state.backoff = 0;
|
|
160
|
+
const delay = BACKOFF_MS[Math.min(state.backoff, BACKOFF_MS.length - 1)];
|
|
161
|
+
state.backoff += 1; state.restarts += 1;
|
|
162
|
+
log(`[bridge] exited (${sig || code}); restarting in ${delay / 1000}s`);
|
|
163
|
+
setTimer(start, delay);
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
settle(spec.mode, plan.why, { version: spec.version });
|
|
167
|
+
start();
|
|
168
|
+
return controller();
|
|
169
|
+
|
|
170
|
+
function controller() {
|
|
171
|
+
return {
|
|
172
|
+
status: () => ({ mode: state.mode, version: state.version, pid: state.pid, restarts: state.restarts, why: state.why }),
|
|
173
|
+
stop: () => { state.stopping = true; if (state.child && !state.child.killed) { try { state.child.kill('SIGTERM'); } catch { /* gone */ } } },
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
package/src/bridge.js
CHANGED
|
@@ -158,13 +158,25 @@ export async function openBridgeChat({ bridgeUrl, agent, token, messages, system
|
|
|
158
158
|
options: options || {},
|
|
159
159
|
...(Array.isArray(specs) && specs.length ? { pageTools: { specs } } : {}),
|
|
160
160
|
}), signal, tokenPath);
|
|
161
|
-
if (!res.ok || !res.body)
|
|
162
|
-
const detail = await res.text().catch(() => '');
|
|
163
|
-
throw new Error(`bridge /chat HTTP ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`);
|
|
164
|
-
}
|
|
161
|
+
if (!res.ok || !res.body) throw new Error(await bridgeRefusal(res));
|
|
165
162
|
return res;
|
|
166
163
|
}
|
|
167
164
|
|
|
165
|
+
/**
|
|
166
|
+
* What the bridge said when it refused a turn, as a sentence. The bridge answers
|
|
167
|
+
* `{ error: { message } }` — and for the commonest first-run failure that message is the
|
|
168
|
+
* whole story ("Claude Code isn't signed in … run `claude`, type `/login` …"). Wrapping it in
|
|
169
|
+
* `bridge /chat HTTP 503: {"error":{"message":"…` and cutting it at 200 characters turned the
|
|
170
|
+
* one useful sentence into JSON debris.
|
|
171
|
+
*/
|
|
172
|
+
export async function bridgeRefusal(res) {
|
|
173
|
+
const text = await res.text().catch(() => '');
|
|
174
|
+
let message = '';
|
|
175
|
+
try { const j = JSON.parse(text); message = String(j?.error?.message || j?.error || j?.message || '').trim(); } catch { /* not JSON */ }
|
|
176
|
+
if (message) return message.slice(0, 600);
|
|
177
|
+
return `bridge /chat HTTP ${res.status}${text ? `: ${text.slice(0, 200)}` : ''}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
168
180
|
// Stream a turn through the bridge. Calls onText(restorableChunk) for each delta, and
|
|
169
181
|
// onActivity(event) for everything else the agent reports — status lines, the working
|
|
170
182
|
// directory, tool calls, reasoning.
|
|
@@ -177,10 +189,7 @@ export async function streamBridgeChat({ bridgeUrl, agent, token, messages, syst
|
|
|
177
189
|
options: options || {},
|
|
178
190
|
}), signal, tokenPath);
|
|
179
191
|
|
|
180
|
-
if (!res.ok || !res.body)
|
|
181
|
-
const detail = await res.text().catch(() => '');
|
|
182
|
-
throw new Error(`bridge /chat HTTP ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`);
|
|
183
|
-
}
|
|
192
|
+
if (!res.ok || !res.body) throw new Error(await bridgeRefusal(res));
|
|
184
193
|
|
|
185
194
|
const reader = res.body.getReader();
|
|
186
195
|
const decoder = new TextDecoder();
|
package/src/local-status.js
CHANGED
|
@@ -101,5 +101,5 @@ export async function bridgePresenceNote(brOverride) {
|
|
|
101
101
|
const n = (br.data.skills?.count ?? null);
|
|
102
102
|
return `bridge detected at ${url} (v${br.data.version}${n != null ? `, ${n} skills` : ''}) — its agents and skills are available through this gateway.`;
|
|
103
103
|
}
|
|
104
|
-
return `bridge not detected at ${url} —
|
|
104
|
+
return `bridge not detected at ${url} — a running gateway (0.6.92+) starts its embedded bridge itself; if this one did not, check ~/.chatpanel/gateway.log. The gateway runs fine without it, minus local agents/skills.`;
|
|
105
105
|
}
|
package/src/server.js
CHANGED
|
@@ -39,6 +39,7 @@ import { createScorecardStore } from './scorecard-store.js';
|
|
|
39
39
|
import { createEngineLedgerStore } from './engine-ledger-store.js';
|
|
40
40
|
import { createProjectStore } from './project-store.js';
|
|
41
41
|
import { applicationsFor, recruitPass } from './recruiting.js';
|
|
42
|
+
import { ensureBridge } from './bridge-supervisor.js';
|
|
42
43
|
import { createHistoryStore } from './sqlite-store.js';
|
|
43
44
|
import { ingestBackups } from './backup-ingest.js';
|
|
44
45
|
import * as nerEngine from './ner-engine.js';
|
|
@@ -62,7 +63,7 @@ import * as openai from './openai.js';
|
|
|
62
63
|
import * as responses from './responses.js';
|
|
63
64
|
import * as anthropic from './anthropic.js';
|
|
64
65
|
|
|
65
|
-
export const VERSION = '0.6.
|
|
66
|
+
export const VERSION = '0.6.93';
|
|
66
67
|
|
|
67
68
|
// WARM search tier — SQLite + FTS5 record store (falls back to an encrypted-JSON
|
|
68
69
|
// store if SQLite can't load), fed by the extension's ingest sync + backup-ingest.
|
|
@@ -745,6 +746,10 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
745
746
|
// on the login service it registers, so a client can say "provided by the desktop app"
|
|
746
747
|
// and stop offering install.sh for a gateway that is already installed. Absent otherwise.
|
|
747
748
|
...(process.env.CHATPANEL_MANAGED_BY ? { managedBy: String(process.env.CHATPANEL_MANAGED_BY).slice(0, 32) } : {}),
|
|
749
|
+
// THE BRIDGE THIS GATEWAY RUNS OR ADOPTED — additive. `mode` is adopted | embedded |
|
|
750
|
+
// standalone | off; a client can say "bridge: provided by the gateway" and stop
|
|
751
|
+
// offering a second installer.
|
|
752
|
+
...(bridgeSupervisor ? { bridge: bridgeSupervisor.status() } : {}),
|
|
748
753
|
// `runtime` = 'native' (npm, fast quantized) | 'wasm' (binary, slow fp32) —
|
|
749
754
|
// the extension uses it to advise the far-faster native gateway.
|
|
750
755
|
stt: { enabled: cfg.stt?.enabled !== false, state: stt.state, ready: stt.ok, model: stt.model || cfg.stt?.model || DEFAULT_STT_MODEL, runtime: stt.runtime, dtype: stt.dtype },
|
|
@@ -2208,6 +2213,8 @@ function readRunHint(header, legacy) {
|
|
|
2208
2213
|
return Object.keys(out).length ? out : null;
|
|
2209
2214
|
}
|
|
2210
2215
|
|
|
2216
|
+
let bridgeSupervisor = null; // the running gateway's bridge controller (bridge-supervisor.js), for /health and shutdown
|
|
2217
|
+
|
|
2211
2218
|
export function start(cfg = loadConfig()) {
|
|
2212
2219
|
installTimestampedConsole(); // every gateway log line gets a clock — before anything logs
|
|
2213
2220
|
ensureGatewayToken(); // M2: load/create the admin-route token (best-effort)
|
|
@@ -2234,13 +2241,11 @@ export function start(cfg = loadConfig()) {
|
|
|
2234
2241
|
server.listen(cfg.port, cfg.host, () => {
|
|
2235
2242
|
console.log(`ChatPanel Privacy Gateway v${VERSION} on http://${cfg.host}:${cfg.port}`);
|
|
2236
2243
|
console.log(` backend : ${cfg.backend}` + (cfg.backend === 'bridge' ? ` (agent: ${cfg.bridge.agent}, via ${cfg.bridge.url})` : ''));
|
|
2237
|
-
//
|
|
2238
|
-
//
|
|
2239
|
-
//
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
.then((note) => console.log(` bridge : ${note}`))
|
|
2243
|
-
.catch(() => {});
|
|
2244
|
+
// THE GATEWAY CARRIES THE BRIDGE (bridge-supervisor.js): adopt one that already answers
|
|
2245
|
+
// (the desktop's, a standalone, launchd's), else start the embedded copy as a child and
|
|
2246
|
+
// keep it up. One install for the user; two processes on purpose. Best-effort and
|
|
2247
|
+
// non-fatal: the gateway runs without a bridge, it just has no local agents.
|
|
2248
|
+
ensureBridge(cfg).then((c) => { bridgeSupervisor = c; }).catch((e) => console.log(` bridge : not supervised (${e?.message || e})`));
|
|
2244
2249
|
console.log(` redaction: ${cfg.redaction.tier}` + (cfg.redaction.detection?.backend && cfg.redaction.detection.backend !== 'off'
|
|
2245
2250
|
? ` + ${cfg.redaction.detection.backend} detector` : (cfg.ner?.autostart ? ' (+ NER starting…)' : '')));
|
|
2246
2251
|
// M7: a non-loopback bind exposes the gateway on the LAN, where the per-request
|
|
@@ -2258,7 +2263,7 @@ export function start(cfg = loadConfig()) {
|
|
|
2258
2263
|
.then((r) => { if (r?.ok) console.log(` warm : seeded ${r.ingested} records from ${r.file}`); })
|
|
2259
2264
|
.catch(() => {});
|
|
2260
2265
|
}
|
|
2261
|
-
const shutdown = () => { ner?.stop(); entitlement.stop(); server.close(() => process.exit(0)); };
|
|
2266
|
+
const shutdown = () => { ner?.stop(); entitlement.stop(); bridgeSupervisor?.stop(); server.close(() => process.exit(0)); };
|
|
2262
2267
|
process.on('SIGINT', shutdown);
|
|
2263
2268
|
process.on('SIGTERM', shutdown);
|
|
2264
2269
|
return server;
|