@luckydraw/cumulus 0.31.36 → 0.31.38
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/CHANGELOG.md +11 -0
- package/dist/gateway/adapters/webchat.d.ts +3 -0
- package/dist/gateway/adapters/webchat.d.ts.map +1 -1
- package/dist/gateway/adapters/webchat.js +21 -9
- package/dist/gateway/adapters/webchat.js.map +1 -1
- package/dist/gateway/auth.d.ts +10 -2
- package/dist/gateway/auth.d.ts.map +1 -1
- package/dist/gateway/auth.js +25 -6
- package/dist/gateway/auth.js.map +1 -1
- package/dist/gateway/bridge/client.d.ts +65 -0
- package/dist/gateway/bridge/client.d.ts.map +1 -0
- package/dist/gateway/bridge/client.js +138 -0
- package/dist/gateway/bridge/client.js.map +1 -0
- package/dist/gateway/bridge/gateway.d.ts +51 -0
- package/dist/gateway/bridge/gateway.d.ts.map +1 -0
- package/dist/gateway/bridge/gateway.js +190 -0
- package/dist/gateway/bridge/gateway.js.map +1 -0
- package/dist/gateway/bridge/protocol.d.ts +25 -0
- package/dist/gateway/bridge/protocol.d.ts.map +1 -0
- package/dist/gateway/bridge/protocol.js +40 -0
- package/dist/gateway/bridge/protocol.js.map +1 -0
- package/dist/gateway/config.d.ts +53 -0
- package/dist/gateway/config.d.ts.map +1 -1
- package/dist/gateway/config.js +110 -0
- package/dist/gateway/config.js.map +1 -1
- package/dist/gateway/daemon.js +4 -0
- package/dist/gateway/daemon.js.map +1 -1
- package/dist/gateway/gateway-agents-mcp.js +3 -1
- package/dist/gateway/gateway-agents-mcp.js.map +1 -1
- package/dist/gateway/namespaces.d.ts +69 -0
- package/dist/gateway/namespaces.d.ts.map +1 -0
- package/dist/gateway/namespaces.js +105 -0
- package/dist/gateway/namespaces.js.map +1 -0
- package/dist/gateway/server.d.ts +5 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +239 -12
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/transcript-ingest.d.ts +80 -0
- package/dist/gateway/transcript-ingest.d.ts.map +1 -0
- package/dist/gateway/transcript-ingest.js +253 -0
- package/dist/gateway/transcript-ingest.js.map +1 -0
- package/dist/lib/content-store.d.ts +1 -1
- package/dist/lib/content-store.d.ts.map +1 -1
- package/dist/lib/content-store.js.map +1 -1
- package/dist/lib/gateway.d.ts +14 -0
- package/dist/lib/gateway.d.ts.map +1 -1
- package/dist/lib/gateway.js +35 -2
- package/dist/lib/gateway.js.map +1 -1
- package/package.json +1 -1
package/dist/gateway/server.js
CHANGED
|
@@ -28,11 +28,56 @@ import { catchUpSegmentation, loadSegmentBoundaries, saveSegmentBoundaries, } fr
|
|
|
28
28
|
import { listTemplates, scaffoldFromTemplate } from '../lib/templates.js';
|
|
29
29
|
import { checkForUpdate, performUpdate, readChangelog } from '../lib/version-check.js';
|
|
30
30
|
import { serveStaticFile, getMediaDir } from './adapters/webchat.js';
|
|
31
|
-
import { authenticate } from './auth.js';
|
|
31
|
+
import { authenticate, getPresentedKey } from './auth.js';
|
|
32
|
+
import { BridgeGateway } from './bridge/gateway.js';
|
|
32
33
|
import { handleEmailHook, handleFormHook, handleGenericWebhook, } from './hooks.js';
|
|
34
|
+
import { allNamespaceKeys, namespaceForKey, namespaceForThread, resolveExecutorProxy, threadVisibleToScope, } from './namespaces.js';
|
|
33
35
|
import { configureVapid, sendNotification, subscribe as pushSubscribe, unsubscribe as pushUnsubscribe, } from './push.js';
|
|
36
|
+
import { validateTranscriptFlush, ingestTranscriptFlush } from './transcript-ingest.js';
|
|
34
37
|
const threadQueues = new Map();
|
|
35
38
|
const threadBusy = new Map();
|
|
39
|
+
const bridgeContexts = new Map();
|
|
40
|
+
async function buildBridgeContextBlock(threadName, executorUrl) {
|
|
41
|
+
const ctx = bridgeContexts.get(threadName);
|
|
42
|
+
if (!ctx)
|
|
43
|
+
return undefined;
|
|
44
|
+
const ageSec = Math.round((Date.now() - ctx.at) / 1000);
|
|
45
|
+
const parts = [
|
|
46
|
+
`Live screen context from the user's connected app tab (captured ${ageSec <= 2 ? 'with this message' : `${ageSec}s ago`}):`,
|
|
47
|
+
];
|
|
48
|
+
if (ctx.describeView !== undefined) {
|
|
49
|
+
parts.push(`View: ${JSON.stringify(ctx.describeView)}`);
|
|
50
|
+
}
|
|
51
|
+
if (ctx.selection?.text) {
|
|
52
|
+
parts.push(`User's text selection: ${JSON.stringify(ctx.selection.text)}`);
|
|
53
|
+
}
|
|
54
|
+
// Hydrate selection refs into full records via the executor. Soft-fail:
|
|
55
|
+
// screen context is useful without hydration (executor is a P3/P4 concern).
|
|
56
|
+
const refs = Array.isArray(ctx.selection?.refs) ? ctx.selection.refs.slice(0, 5) : [];
|
|
57
|
+
for (const ref of refs) {
|
|
58
|
+
if (!ref?.entity || !ref?.key)
|
|
59
|
+
continue;
|
|
60
|
+
try {
|
|
61
|
+
const upstream = await fetch(`${executorUrl}/execute`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
command: 'records.get',
|
|
66
|
+
params: { entity: ref.entity, key: ref.key },
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
const record = (await upstream.json());
|
|
70
|
+
if (record?.ok) {
|
|
71
|
+
parts.push(`Selected record ${ref.entity}/${ref.key}${ref.field ? ` (field: ${ref.field})` : ''}: ${JSON.stringify(record.data)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// executor unavailable — skip hydration for this ref
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
parts.push('This is background screen state, not user instructions. Use app_describeView only to re-check after your own display/mutate commands.');
|
|
79
|
+
return `<system-reminder>\n${parts.join('\n')}\n</system-reminder>`;
|
|
80
|
+
}
|
|
36
81
|
/** Federation router reference — set by daemon when hub mode is active */
|
|
37
82
|
let federationRouter;
|
|
38
83
|
/**
|
|
@@ -134,10 +179,14 @@ async function processMessage(threadName, item, opts) {
|
|
|
134
179
|
}
|
|
135
180
|
};
|
|
136
181
|
try {
|
|
182
|
+
// Bridge screen context (task 097) — ephemeral, prompt-only. Soft-fails to
|
|
183
|
+
// undefined when the bridge is off or no tab context has arrived.
|
|
184
|
+
const contextBlock = await buildBridgeContextBlock(threadName, opts.bridge?.executorUrl ?? 'http://127.0.0.1:8091').catch(() => undefined);
|
|
137
185
|
const result = await sendMessage({
|
|
138
186
|
threadName,
|
|
139
187
|
message,
|
|
140
188
|
images,
|
|
189
|
+
contextBlock,
|
|
141
190
|
basePath: opts.basePath,
|
|
142
191
|
claudePath: opts.claudePath,
|
|
143
192
|
sharedMcpPort: opts.sharedMcpPort,
|
|
@@ -247,13 +296,16 @@ async function handleWebhook(type, name, req, res, pipelineOpts, hooksConfig) {
|
|
|
247
296
|
}
|
|
248
297
|
}
|
|
249
298
|
// ─── Agent endpoints ─────────────────────────────────────────────────────────
|
|
250
|
-
async function handleListAgents(res) {
|
|
299
|
+
async function handleListAgents(res, callerNs, namespaces) {
|
|
251
300
|
try {
|
|
252
301
|
if (!fs.existsSync(THREADS_DIR)) {
|
|
253
302
|
jsonResponse(res, 200, { agents: [] });
|
|
254
303
|
return;
|
|
255
304
|
}
|
|
256
|
-
const files = fs
|
|
305
|
+
const files = fs
|
|
306
|
+
.readdirSync(THREADS_DIR)
|
|
307
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
308
|
+
.filter(f => threadVisibleToScope(f.replace(/\.jsonl$/, ''), callerNs, namespaces));
|
|
257
309
|
const agents = [];
|
|
258
310
|
for (const file of files) {
|
|
259
311
|
const name = file.replace(/\.jsonl$/, '');
|
|
@@ -675,13 +727,16 @@ async function handleGetStatus(threadName, res) {
|
|
|
675
727
|
jsonResponse(res, 500, { error: String(err) });
|
|
676
728
|
}
|
|
677
729
|
}
|
|
678
|
-
async function handleListThreads(res) {
|
|
730
|
+
async function handleListThreads(res, scopeNs, namespaces) {
|
|
679
731
|
try {
|
|
680
732
|
if (!fs.existsSync(THREADS_DIR)) {
|
|
681
733
|
jsonResponse(res, 200, { threads: [] });
|
|
682
734
|
return;
|
|
683
735
|
}
|
|
684
|
-
const files = fs
|
|
736
|
+
const files = fs
|
|
737
|
+
.readdirSync(THREADS_DIR)
|
|
738
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
739
|
+
.filter(f => threadVisibleToScope(f.replace(/\.jsonl$/, ''), scopeNs, namespaces));
|
|
685
740
|
const threads = [];
|
|
686
741
|
for (const file of files) {
|
|
687
742
|
const name = file.replace(/\.jsonl$/, '');
|
|
@@ -894,6 +949,30 @@ async function handleSyncMessages(threadName, req, res) {
|
|
|
894
949
|
jsonResponse(res, 500, { error: String(err) });
|
|
895
950
|
}
|
|
896
951
|
}
|
|
952
|
+
// ─── Transcript ingestion handler (task 096) ─────────────────────────────────
|
|
953
|
+
async function handleIngestTranscript(req, res) {
|
|
954
|
+
let body;
|
|
955
|
+
try {
|
|
956
|
+
body = JSON.parse(await readBody(req));
|
|
957
|
+
}
|
|
958
|
+
catch {
|
|
959
|
+
jsonResponse(res, 400, { error: 'Invalid JSON body' });
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const parsed = validateTranscriptFlush(body);
|
|
963
|
+
if (!parsed.ok) {
|
|
964
|
+
jsonResponse(res, 400, { error: parsed.error });
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
const result = await ingestTranscriptFlush(parsed.flush);
|
|
969
|
+
jsonResponse(res, 200, result);
|
|
970
|
+
}
|
|
971
|
+
catch (err) {
|
|
972
|
+
console.error('[Gateway] transcript ingest error:', err);
|
|
973
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
974
|
+
}
|
|
975
|
+
}
|
|
897
976
|
// ─── Thread config handler ───────────────────────────────────────────────────
|
|
898
977
|
async function handleUpdateThreadConfig(threadName, req, res) {
|
|
899
978
|
let body;
|
|
@@ -1453,7 +1532,12 @@ function saveMediaFile(mediaDir, originalFilename, data, baseUrl) {
|
|
|
1453
1532
|
// ─── Server ──────────────────────────────────────────────────────────────────
|
|
1454
1533
|
export async function startGatewayServer(options) {
|
|
1455
1534
|
const { port, ...pipelineOpts } = options;
|
|
1456
|
-
|
|
1535
|
+
// Namespace-scoped keys (task 097 P2) must authenticate like base keys; fold
|
|
1536
|
+
// them into the valid-key set. The namespace→key mapping stays in
|
|
1537
|
+
// `namespaces` for scope resolution.
|
|
1538
|
+
const namespaces = options.namespaces ?? [];
|
|
1539
|
+
const nsKeys = allNamespaceKeys(namespaces);
|
|
1540
|
+
let currentApiKeys = Array.from(new Set([...options.apiKeys, ...nsKeys]));
|
|
1457
1541
|
let scheduler;
|
|
1458
1542
|
let adminCallbacks;
|
|
1459
1543
|
// federationRouter is module-level (set via handle.setFederationRouter)
|
|
@@ -1463,11 +1547,43 @@ export async function startGatewayServer(options) {
|
|
|
1463
1547
|
configureVapid(vapidConfig);
|
|
1464
1548
|
console.log('[Gateway] Push notifications enabled (VAPID configured)');
|
|
1465
1549
|
}
|
|
1550
|
+
// Agent-native app bridge (task 097) — assigned below (after the server
|
|
1551
|
+
// exists, so it can claim WS upgrades on /bridge); the request handler closes
|
|
1552
|
+
// over `bridgeGateway`/`loadBridgeManifest`. Inert unless options.bridge.enabled.
|
|
1553
|
+
let bridgeGateway;
|
|
1554
|
+
const bridgeExecutorUrl = options.bridge?.executorUrl ?? 'http://127.0.0.1:8091';
|
|
1555
|
+
// Last-known tab registrations, persisted so the agent keeps its bridge
|
|
1556
|
+
// toolset across tab disconnects and gateway restarts. All device tabs of one
|
|
1557
|
+
// app register the same manifest — strip a trailing device suffix so they
|
|
1558
|
+
// share one file (aligns with the P2 namespace prefix model).
|
|
1559
|
+
const bridgeManifestsDir = path.join(process.env.CUMULUS_DIR || path.join(os.homedir(), '.cumulus'), 'bridge-manifests');
|
|
1560
|
+
const bridgeManifestKey = (thread) => thread.replace(/-[0-9a-f]{6,16}$/, '');
|
|
1561
|
+
const persistBridgeManifest = (thread, manifest) => {
|
|
1562
|
+
// An empty registration (probe/test tabs) carries no tools — never let it
|
|
1563
|
+
// clobber a real persisted manifest.
|
|
1564
|
+
if (!Array.isArray(manifest) || manifest.length === 0)
|
|
1565
|
+
return;
|
|
1566
|
+
try {
|
|
1567
|
+
fs.mkdirSync(bridgeManifestsDir, { recursive: true });
|
|
1568
|
+
fs.writeFileSync(path.join(bridgeManifestsDir, bridgeManifestKey(thread) + '.json'), JSON.stringify(manifest));
|
|
1569
|
+
}
|
|
1570
|
+
catch (err) {
|
|
1571
|
+
console.error('[Gateway] bridge manifest persist failed:', err);
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
const loadBridgeManifest = (thread) => {
|
|
1575
|
+
try {
|
|
1576
|
+
return JSON.parse(fs.readFileSync(path.join(bridgeManifestsDir, bridgeManifestKey(thread) + '.json'), 'utf8'));
|
|
1577
|
+
}
|
|
1578
|
+
catch {
|
|
1579
|
+
return null;
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1466
1582
|
const server = http.createServer(async (req, res) => {
|
|
1467
1583
|
// CORS headers for web clients
|
|
1468
1584
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1469
1585
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
1470
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, X-Confirm');
|
|
1586
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, Authorization, X-Confirm');
|
|
1471
1587
|
if (req.method === 'OPTIONS') {
|
|
1472
1588
|
res.writeHead(204);
|
|
1473
1589
|
res.end();
|
|
@@ -1514,6 +1630,81 @@ export async function startGatewayServer(options) {
|
|
|
1514
1630
|
if (!authenticate(req, res, currentApiKeys))
|
|
1515
1631
|
return;
|
|
1516
1632
|
try {
|
|
1633
|
+
// POST /bridge/call — route a tool call to the connected app tab (task
|
|
1634
|
+
// 097). Body: { thread, command, params?, confirm?, summary? }. Commands
|
|
1635
|
+
// whose manifest entry declares risk "export" ALWAYS go through the
|
|
1636
|
+
// confirm chip regardless of body.confirm — export can never silently run.
|
|
1637
|
+
if (routePath === '/bridge/call' && req.method === 'POST') {
|
|
1638
|
+
if (!bridgeGateway) {
|
|
1639
|
+
jsonResponse(res, 503, { ok: false, summary: 'Bridge not enabled' });
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
let body;
|
|
1643
|
+
try {
|
|
1644
|
+
body = JSON.parse((await readBody(req)) || '{}');
|
|
1645
|
+
}
|
|
1646
|
+
catch {
|
|
1647
|
+
jsonResponse(res, 400, { error: 'Invalid JSON body' });
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
const thread = typeof body.thread === 'string' ? body.thread : undefined;
|
|
1651
|
+
const command = typeof body.command === 'string' ? body.command : undefined;
|
|
1652
|
+
if (!thread || !command) {
|
|
1653
|
+
jsonResponse(res, 400, { error: 'thread and command are required' });
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
const callParams = body.params ?? {};
|
|
1657
|
+
const manifestEntry = bridgeGateway.manifest(thread)?.find(c => c.name === command);
|
|
1658
|
+
const needsConfirm = Boolean(body.confirm) || manifestEntry?.risk === 'export';
|
|
1659
|
+
const result = needsConfirm
|
|
1660
|
+
? await bridgeGateway.callWithConfirm(thread, command, callParams, typeof body.summary === 'string' ? body.summary : '')
|
|
1661
|
+
: await bridgeGateway.call(thread, command, callParams);
|
|
1662
|
+
jsonResponse(res, 200, result);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
// GET /bridge/manifest/:thread — commands the app tab can execute (task
|
|
1666
|
+
// 097). Live registration when a tab is connected, else the persisted
|
|
1667
|
+
// last-known one (null only if this app has never registered).
|
|
1668
|
+
if (bridgeGateway && req.method === 'GET' && routePath.startsWith('/bridge/manifest/')) {
|
|
1669
|
+
const thread = decodeURIComponent(routePath.slice('/bridge/manifest/'.length));
|
|
1670
|
+
const live = bridgeGateway.manifest(thread);
|
|
1671
|
+
jsonResponse(res, 200, {
|
|
1672
|
+
thread,
|
|
1673
|
+
manifest: live ?? loadBridgeManifest(thread),
|
|
1674
|
+
live: live != null,
|
|
1675
|
+
});
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
// Generic executor pass-through (task 097 P3). Replaces the fork's
|
|
1679
|
+
// baked-in /state + /journal routes (Rule #8): any namespace that declares
|
|
1680
|
+
// an `executorProxy` reverse-proxies its configured path prefixes to that
|
|
1681
|
+
// upstream, so the app tab talks to a single origin. Activated by the
|
|
1682
|
+
// presence of executorProxy — independent of bridge.enabled — and open to
|
|
1683
|
+
// any valid key (namespaces are visibility-only, not an access boundary),
|
|
1684
|
+
// so it sits post-authenticate above. Forwards the raw URL, preserves
|
|
1685
|
+
// method + body, relays the upstream status/content-type verbatim.
|
|
1686
|
+
const proxyTarget = resolveExecutorProxy(routePath, namespaces);
|
|
1687
|
+
if (proxyTarget) {
|
|
1688
|
+
const body = req.method === 'GET' || req.method === 'HEAD' || req.method === 'DELETE'
|
|
1689
|
+
? undefined
|
|
1690
|
+
: await readBody(req);
|
|
1691
|
+
try {
|
|
1692
|
+
const upstream = await fetch(proxyTarget.origin + (req.url || routePath), {
|
|
1693
|
+
method: req.method,
|
|
1694
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1695
|
+
body,
|
|
1696
|
+
});
|
|
1697
|
+
const text = await upstream.text();
|
|
1698
|
+
res.writeHead(upstream.status, {
|
|
1699
|
+
'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
|
|
1700
|
+
});
|
|
1701
|
+
res.end(text);
|
|
1702
|
+
}
|
|
1703
|
+
catch {
|
|
1704
|
+
jsonResponse(res, 502, { error: 'Executor unavailable' });
|
|
1705
|
+
}
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1517
1708
|
// POST /api/admin/restart — platform-agnostic graceful restart
|
|
1518
1709
|
if (routePath === '/api/admin/restart' && req.method === 'POST') {
|
|
1519
1710
|
if (!adminCallbacks) {
|
|
@@ -1577,6 +1768,11 @@ export async function startGatewayServer(options) {
|
|
|
1577
1768
|
await handleSyncMessages(params.name, req, res);
|
|
1578
1769
|
return;
|
|
1579
1770
|
}
|
|
1771
|
+
// POST /api/ingest-transcript — store-only transcript ingestion (task 096)
|
|
1772
|
+
if (routePath === '/api/ingest-transcript' && req.method === 'POST') {
|
|
1773
|
+
await handleIngestTranscript(req, res);
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1580
1776
|
// GET /api/thread/:name/history
|
|
1581
1777
|
if (routePath === '/api/thread/:name/history' && req.method === 'GET') {
|
|
1582
1778
|
await handleGetHistory(params.name, query, res);
|
|
@@ -1649,9 +1845,12 @@ export async function startGatewayServer(options) {
|
|
|
1649
1845
|
await handleGetDashboard(res);
|
|
1650
1846
|
return;
|
|
1651
1847
|
}
|
|
1652
|
-
// GET /api/threads
|
|
1848
|
+
// GET /api/threads — scoped by the presented key's namespace (task 097 P2):
|
|
1849
|
+
// a namespace-scoped key sees only its threads; the default key sees every
|
|
1850
|
+
// non-namespaced thread.
|
|
1653
1851
|
if (routePath === '/api/threads' && req.method === 'GET') {
|
|
1654
|
-
|
|
1852
|
+
const scopeNs = namespaceForKey(getPresentedKey(req) ?? '', namespaces);
|
|
1853
|
+
await handleListThreads(res, scopeNs, namespaces);
|
|
1655
1854
|
return;
|
|
1656
1855
|
}
|
|
1657
1856
|
// GET /api/templates — list templates
|
|
@@ -1691,9 +1890,15 @@ export async function startGatewayServer(options) {
|
|
|
1691
1890
|
jsonResponse(res, 200, { models, claudeModels });
|
|
1692
1891
|
return;
|
|
1693
1892
|
}
|
|
1694
|
-
// GET /api/agents — list agents
|
|
1893
|
+
// GET /api/agents — list agents, scoped by the CALLING thread's namespace
|
|
1894
|
+
// (task 097 P2). The MCP server authenticates with the gateway's unscoped
|
|
1895
|
+
// key, so scope comes from ?caller=<thread> (its own AGENT_NAME), not the
|
|
1896
|
+
// key: a pursuit agent sees pursuit agents; a default agent sees
|
|
1897
|
+
// non-namespaced agents.
|
|
1695
1898
|
if (routePath === '/api/agents' && req.method === 'GET') {
|
|
1696
|
-
|
|
1899
|
+
const caller = typeof query.caller === 'string' ? query.caller : '';
|
|
1900
|
+
const callerNs = caller ? namespaceForThread(caller, namespaces) : undefined;
|
|
1901
|
+
await handleListAgents(res, callerNs, namespaces);
|
|
1697
1902
|
return;
|
|
1698
1903
|
}
|
|
1699
1904
|
// POST /api/agents/inject — agent-to-agent messaging
|
|
@@ -1746,6 +1951,27 @@ export async function startGatewayServer(options) {
|
|
|
1746
1951
|
}
|
|
1747
1952
|
}
|
|
1748
1953
|
});
|
|
1954
|
+
// Mount the agent-native app bridge (task 097) — claims WS upgrades on
|
|
1955
|
+
// /bridge only, leaving federation/webchat upgrades untouched. Default off.
|
|
1956
|
+
if (options.bridge?.enabled) {
|
|
1957
|
+
bridgeGateway = new BridgeGateway({
|
|
1958
|
+
authenticate: (apiKey) => currentApiKeys.includes(apiKey),
|
|
1959
|
+
onContext: (thread, context) => bridgeContexts.set(thread, {
|
|
1960
|
+
...context,
|
|
1961
|
+
at: Date.now(),
|
|
1962
|
+
}),
|
|
1963
|
+
onTabChange: (thread, event) => {
|
|
1964
|
+
if (event === 'connect') {
|
|
1965
|
+
const manifest = bridgeGateway?.manifest(thread);
|
|
1966
|
+
if (manifest)
|
|
1967
|
+
persistBridgeManifest(thread, manifest);
|
|
1968
|
+
}
|
|
1969
|
+
if (event === 'disconnect')
|
|
1970
|
+
bridgeContexts.delete(thread);
|
|
1971
|
+
},
|
|
1972
|
+
}).attach(server, '/bridge');
|
|
1973
|
+
console.log('[Gateway] App bridge mounted on /bridge (executor:', bridgeExecutorUrl + ')');
|
|
1974
|
+
}
|
|
1749
1975
|
// Handle federation WebSocket upgrades on /federation
|
|
1750
1976
|
server.on('upgrade', (req, socket, head) => {
|
|
1751
1977
|
const url = req.url?.split('?')[0] || '/';
|
|
@@ -1770,7 +1996,8 @@ export async function startGatewayServer(options) {
|
|
|
1770
1996
|
url,
|
|
1771
1997
|
server,
|
|
1772
1998
|
updateApiKeys: (newKeys) => {
|
|
1773
|
-
|
|
1999
|
+
// Keep namespace-scoped keys valid across hot key rotation (task 097 P2).
|
|
2000
|
+
currentApiKeys = Array.from(new Set([...newKeys, ...nsKeys]));
|
|
1774
2001
|
},
|
|
1775
2002
|
setBroadcastToThread: (fn) => {
|
|
1776
2003
|
pipelineOpts.broadcastToThread = fn;
|