@chatpanel/gateway 0.6.66 → 0.6.68

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/server.js +37 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chatpanel/gateway",
3
- "version": "0.6.66",
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.",
3
+ "version": "0.6.68",
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": {
7
7
  "chatpanel-gateway": "bin/chatpanel-gateway.js"
package/src/server.js CHANGED
@@ -55,7 +55,7 @@ import * as openai from './openai.js';
55
55
  import * as responses from './responses.js';
56
56
  import * as anthropic from './anthropic.js';
57
57
 
58
- export const VERSION = '0.6.66';
58
+ export const VERSION = '0.6.68';
59
59
 
60
60
  // WARM search tier — SQLite + FTS5 record store (falls back to an encrypted-JSON
61
61
  // store if SQLite can't load), fed by the extension's ingest sync + backup-ingest.
@@ -660,6 +660,10 @@ export function createGateway(cfg = loadConfig()) {
660
660
  const tts = ttsEngine.health();
661
661
  return sendJson(res, 200, {
662
662
  ok: true, version: VERSION, backend: cfg.backend, tier: cfg.redaction.tier,
663
+ // WHO STARTED THIS PROCESS — additive. ChatPanel Desktop sets CHATPANEL_MANAGED_BY=desktop
664
+ // on the login service it registers, so a client can say "provided by the desktop app"
665
+ // and stop offering install.sh for a gateway that is already installed. Absent otherwise.
666
+ ...(process.env.CHATPANEL_MANAGED_BY ? { managedBy: String(process.env.CHATPANEL_MANAGED_BY).slice(0, 32) } : {}),
663
667
  // `runtime` = 'native' (npm, fast quantized) | 'wasm' (binary, slow fp32) —
664
668
  // the extension uses it to advise the far-faster native gateway.
665
669
  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 },
@@ -676,6 +680,7 @@ export function createGateway(cfg = loadConfig()) {
676
680
  const health = await probeNerHealth(cfg); // live GET /health on the detector
677
681
  return sendJson(res, 200, {
678
682
  ok: true, version: VERSION, backend: cfg.backend, tier: cfg.redaction.tier,
683
+ ...(process.env.CHATPANEL_MANAGED_BY ? { managedBy: String(process.env.CHATPANEL_MANAGED_BY).slice(0, 32) } : {}), // see /health
679
684
  ner: {
680
685
  autostart: !!cfg.ner?.autostart,
681
686
  configured: health.configured,
@@ -1552,6 +1557,37 @@ export function createGateway(cfg = loadConfig()) {
1552
1557
  }
1553
1558
  }
1554
1559
 
1560
+ // ONE skill, with the prompt the list deliberately leaves out.
1561
+ //
1562
+ // /skills answers `promptChars` — a COUNT — because a list of ninety-five skills carrying
1563
+ // ninety-five prompt bodies is a megabyte nobody asked for. A client that wants to scope a
1564
+ // task to a skill needs the body itself, and the only other way to get it was to go
1565
+ // straight at the bridge, which is the habit the /skills comment above exists to prevent.
1566
+ //
1567
+ // The id is passed through as ONE path segment, encoded. A skill id is a file name on the
1568
+ // user's disk, so letting a slash or a `..` reach the bridge's reader would be asking it
1569
+ // to open something else; the bridge validates too, and this is the half we own.
1570
+ if (req.method === 'GET' && /^\/skills\/[^/]+$/.test(pathname)) {
1571
+ const id = decodeURIComponent(pathname.slice('/skills/'.length));
1572
+ if (!id || id === '.' || id === '..' || id.includes('/') || id.includes('\\')) {
1573
+ return sendJson(res, 400, { error: { message: 'not a skill id', type: 'invalid_request' } });
1574
+ }
1575
+ const base = String(cfg.bridge?.url || '').replace(/\/$/, '');
1576
+ if (!base) return sendJson(res, 503, { error: { message: 'no bridge is configured', type: 'no_bridge' } });
1577
+ const token = readBridgeToken(cfg.bridge?.token);
1578
+ try {
1579
+ const r = await fetch(`${base}/skills/${encodeURIComponent(id)}`, {
1580
+ headers: { Accept: 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
1581
+ signal: AbortSignal.timeout(8000),
1582
+ });
1583
+ const data = await r.json().catch(() => ({}));
1584
+ if (!r.ok) return sendJson(res, r.status, { error: { message: data?.error || `bridge ${r.status}`, type: 'bridge_error' } });
1585
+ return sendJson(res, 200, { skill: data?.skill || null });
1586
+ } catch (e) {
1587
+ return sendJson(res, 502, { error: { message: `bridge unreachable: ${e.message}`, type: 'bridge_unreachable' } });
1588
+ }
1589
+ }
1590
+
1555
1591
  // Model discovery — aggregate every destination's models.
1556
1592
  if (req.method === 'GET' && /\/models$/.test(pathname)) {
1557
1593
  return sendJson(res, 200, await aggregateModelsAsync(cfg));