promethios-bridge 1.1.0 → 1.2.0
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 +1 -1
- package/src/bridge.js +100 -7
- package/src/executor.js +16 -1
package/package.json
CHANGED
package/src/bridge.js
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
* Promethios Local Bridge — Core
|
|
3
3
|
*
|
|
4
4
|
* 1. Authenticates with Promethios using a setup token
|
|
5
|
-
* 2. Starts a local HTTP server on the configured port
|
|
5
|
+
* 2. Starts a local HTTP server on the configured port (for health checks)
|
|
6
6
|
* 3. Registers the bridge endpoint with the Promethios API
|
|
7
|
-
* 4.
|
|
8
|
-
* 5. Executes tools locally and
|
|
7
|
+
* 4. Polls the Promethios API for pending tool calls (Firestore relay)
|
|
8
|
+
* 5. Executes tools locally and posts results back to the API
|
|
9
9
|
* 6. Sends heartbeats every 30s to keep the connection alive
|
|
10
|
+
*
|
|
11
|
+
* ── Why polling instead of direct HTTP? ─────────────────────────────────────
|
|
12
|
+
* The bridge only listens on 127.0.0.1 (localhost) for security — it is NOT
|
|
13
|
+
* reachable from the internet. Instead of requiring a tunnel, the bridge polls
|
|
14
|
+
* the Promethios API for pending tool calls queued by the cloud orchestrator.
|
|
15
|
+
* This works behind firewalls, NAT, and corporate proxies with no setup.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
const express = require('express');
|
|
@@ -16,6 +22,7 @@ const fetch = require('node-fetch');
|
|
|
16
22
|
const { executeLocalTool } = require('./executor');
|
|
17
23
|
|
|
18
24
|
const HEARTBEAT_INTERVAL = 30_000; // 30s
|
|
25
|
+
const POLL_INTERVAL = 1_000; // 1s — poll for pending tool calls
|
|
19
26
|
|
|
20
27
|
async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
21
28
|
const log = dev
|
|
@@ -40,17 +47,18 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
40
47
|
process.exit(1);
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
// ── Step 2: Start local HTTP server
|
|
50
|
+
// ── Step 2: Start local HTTP server (health check only) ──────────────────
|
|
44
51
|
const app = express();
|
|
45
52
|
app.use(express.json());
|
|
46
53
|
|
|
47
54
|
// Health check
|
|
48
55
|
app.get('/health', (req, res) => res.json({ ok: true, version: require('../package.json').version }));
|
|
49
56
|
|
|
50
|
-
//
|
|
57
|
+
// Legacy direct tool-call endpoint (kept for backward compatibility with
|
|
58
|
+
// older backend versions that call callbackUrl/tool-call directly)
|
|
51
59
|
app.post('/tool-call', async (req, res) => {
|
|
52
60
|
const { toolName, args, frameworkId, callId } = req.body;
|
|
53
|
-
log('Tool call received:', toolName, JSON.stringify(args).slice(0, 100));
|
|
61
|
+
log('Tool call received (direct):', toolName, JSON.stringify(args).slice(0, 100));
|
|
54
62
|
|
|
55
63
|
try {
|
|
56
64
|
const result = await executeLocalTool({ toolName, args, frameworkId, dev });
|
|
@@ -62,7 +70,7 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
62
70
|
}
|
|
63
71
|
});
|
|
64
72
|
|
|
65
|
-
// Capability query
|
|
73
|
+
// Capability query
|
|
66
74
|
app.get('/capabilities', (req, res) => {
|
|
67
75
|
res.json({ capabilities: getSupportedCapabilities() });
|
|
68
76
|
});
|
|
@@ -108,10 +116,48 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
108
116
|
}
|
|
109
117
|
}, HEARTBEAT_INTERVAL);
|
|
110
118
|
|
|
119
|
+
// ── Step 5: Poll for pending tool calls (Firestore relay) ─────────────────
|
|
120
|
+
// The cloud backend queues tool calls in Firestore when the agent uses
|
|
121
|
+
// local_shell, local_file_read, or local_file_write. We poll for them here,
|
|
122
|
+
// execute locally, and post results back. This works behind firewalls/NAT.
|
|
123
|
+
let pollActive = true;
|
|
124
|
+
|
|
125
|
+
const pollLoop = async () => {
|
|
126
|
+
while (pollActive) {
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetch(`${apiBase}/api/local-bridge/pending-calls`, {
|
|
129
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (res.ok) {
|
|
133
|
+
const { calls } = await res.json();
|
|
134
|
+
if (calls && calls.length > 0) {
|
|
135
|
+
log(`Got ${calls.length} pending tool call(s)`);
|
|
136
|
+
// Execute all pending calls concurrently
|
|
137
|
+
await Promise.all(calls.map(call => executePendingCall({ call, authToken, apiBase, dev })));
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
log('Pending-calls poll returned', res.status);
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
log('Poll error:', err.message);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Wait before next poll
|
|
147
|
+
await sleep(POLL_INTERVAL);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Start polling in background (don't await — runs until shutdown)
|
|
152
|
+
pollLoop().catch(err => {
|
|
153
|
+
console.error(chalk.red(' ✗ Poll loop crashed:'), err.message);
|
|
154
|
+
});
|
|
155
|
+
|
|
111
156
|
// Graceful shutdown
|
|
112
157
|
const shutdown = async (signal) => {
|
|
113
158
|
console.log('');
|
|
114
159
|
console.log(chalk.yellow(` Disconnecting (${signal})...`));
|
|
160
|
+
pollActive = false;
|
|
115
161
|
clearInterval(heartbeatTimer);
|
|
116
162
|
try {
|
|
117
163
|
await fetch(`${apiBase}/api/local-bridge/unregister`, {
|
|
@@ -127,6 +173,49 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
|
127
173
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
128
174
|
}
|
|
129
175
|
|
|
176
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
177
|
+
// Execute a single pending tool call and post the result back
|
|
178
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
179
|
+
async function executePendingCall({ call, authToken, apiBase, dev }) {
|
|
180
|
+
const log = dev ? (...a) => console.log(chalk.gray(' [debug]'), ...a) : () => {};
|
|
181
|
+
const { callId, toolName, args } = call;
|
|
182
|
+
|
|
183
|
+
log(`Executing pending call ${callId}: ${toolName}`);
|
|
184
|
+
console.log(chalk.cyan(` → Running ${toolName}...`));
|
|
185
|
+
|
|
186
|
+
let result, error;
|
|
187
|
+
try {
|
|
188
|
+
result = await executeLocalTool({ toolName, args, dev });
|
|
189
|
+
log(`Result for ${callId}:`, JSON.stringify(result).slice(0, 200));
|
|
190
|
+
console.log(chalk.green(` ✓ ${toolName} completed`));
|
|
191
|
+
} catch (err) {
|
|
192
|
+
error = err.message;
|
|
193
|
+
console.error(chalk.red(` ✗ ${toolName} failed:`), err.message);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Post result back to the API
|
|
197
|
+
try {
|
|
198
|
+
const body = error ? { error } : { result };
|
|
199
|
+
const res = await fetch(`${apiBase}/api/local-bridge/tool-result/${callId}`, {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: {
|
|
202
|
+
Authorization: `Bearer ${authToken}`,
|
|
203
|
+
'Content-Type': 'application/json',
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify(body),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!res.ok) {
|
|
209
|
+
const text = await res.text();
|
|
210
|
+
log(`Failed to post result for ${callId}: ${res.status} ${text}`);
|
|
211
|
+
} else {
|
|
212
|
+
log(`Result posted for ${callId}`);
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
log(`Error posting result for ${callId}:`, err.message);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
130
219
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
131
220
|
// Auth: exchange setup token for a session bearer token
|
|
132
221
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -191,4 +280,8 @@ function getSupportedCapabilities() {
|
|
|
191
280
|
];
|
|
192
281
|
}
|
|
193
282
|
|
|
283
|
+
function sleep(ms) {
|
|
284
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
285
|
+
}
|
|
286
|
+
|
|
194
287
|
module.exports = { startBridge };
|
package/src/executor.js
CHANGED
|
@@ -23,6 +23,20 @@ const execAsync = promisify(exec);
|
|
|
23
23
|
async function executeLocalTool({ toolName, args, frameworkId, dev }) {
|
|
24
24
|
const log = dev ? (...a) => console.log('[executor]', ...a) : () => {};
|
|
25
25
|
|
|
26
|
+
// ── Aliases for the new universal bridge tool names ─────────────────────
|
|
27
|
+
// The Promethios backend injects local_shell, local_file_read, local_file_write
|
|
28
|
+
// into the agent's tool list when the bridge is connected. Map them to the
|
|
29
|
+
// existing executor handlers.
|
|
30
|
+
if (toolName === 'local_shell') {
|
|
31
|
+
return executeLocalTool({ toolName: 'run_command', args: { command: args.command, cwd: args.cwd, timeout: args.timeout }, frameworkId, dev });
|
|
32
|
+
}
|
|
33
|
+
if (toolName === 'local_file_read') {
|
|
34
|
+
return executeLocalTool({ toolName: 'read_file', args: { path: args.path, encoding: args.encoding }, frameworkId, dev });
|
|
35
|
+
}
|
|
36
|
+
if (toolName === 'local_file_write') {
|
|
37
|
+
return executeLocalTool({ toolName: 'write_file', args: { path: args.path, content: args.content, encoding: args.encoding }, frameworkId, dev });
|
|
38
|
+
}
|
|
39
|
+
|
|
26
40
|
// ── local_execute is the built-in tool injected by the backend when the bridge
|
|
27
41
|
// is connected. It uses an `action` field to dispatch to the right handler.
|
|
28
42
|
if (toolName === 'local_execute') {
|
|
@@ -95,8 +109,9 @@ async function executeLocalTool({ toolName, args, frameworkId, dev }) {
|
|
|
95
109
|
throw new Error('Command blocked: potentially destructive operation');
|
|
96
110
|
}
|
|
97
111
|
log('run_command', cmd);
|
|
112
|
+
const timeoutMs = args.timeout ? Math.min(args.timeout * 1000, 120_000) : 30_000;
|
|
98
113
|
const { stdout, stderr } = await execAsync(cmd, {
|
|
99
|
-
timeout:
|
|
114
|
+
timeout: timeoutMs,
|
|
100
115
|
cwd: args.cwd ? resolveSafePath(args.cwd) : process.env.HOME,
|
|
101
116
|
maxBuffer: 1024 * 1024, // 1MB output limit
|
|
102
117
|
});
|