promethios-bridge 1.1.0 → 1.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promethios-bridge",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Run Promethios agent frameworks locally on your computer with full file, terminal, and browser access.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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. Listens for tool-call requests from the cloud orchestrator
8
- * 5. Executes tools locally and returns results
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
- // Tool call endpoint called by the Promethios cloud relay
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 — cloud can ask what this bridge supports
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
  // ─────────────────────────────────────────────────────────────────────────────
@@ -157,6 +246,15 @@ async function registerBridge({ authToken, apiBase, callbackUrl, port, dev }) {
157
246
  const deviceId = require('crypto').randomBytes(8).toString('hex');
158
247
  const capabilities = getSupportedCapabilities();
159
248
 
249
+ // Detect OS and shell so the cloud can inject OS-aware guidance into the agent
250
+ const osModule = require('os');
251
+ const platform = process.platform; // 'win32' | 'darwin' | 'linux'
252
+ const shell = platform === 'win32'
253
+ ? 'cmd'
254
+ : (process.env.SHELL || '/bin/zsh').split('/').pop();
255
+ const homeDir = osModule.homedir();
256
+ const username = osModule.userInfo().username;
257
+
160
258
  const res = await fetch(`${apiBase}/api/local-bridge/register`, {
161
259
  method: 'POST',
162
260
  headers: {
@@ -168,6 +266,10 @@ async function registerBridge({ authToken, apiBase, callbackUrl, port, dev }) {
168
266
  callbackUrl,
169
267
  capabilities,
170
268
  bridgeVersion: require('../package.json').version,
269
+ os: platform, // 'win32' | 'darwin' | 'linux'
270
+ shell, // 'cmd' | 'zsh' | 'bash' etc.
271
+ homeDir, // e.g. 'C:\\Users\\ted' or '/Users/ted'
272
+ username, // e.g. 'ted'
171
273
  }),
172
274
  });
173
275
 
@@ -191,4 +293,8 @@ function getSupportedCapabilities() {
191
293
  ];
192
294
  }
193
295
 
296
+ function sleep(ms) {
297
+ return new Promise(resolve => setTimeout(resolve, ms));
298
+ }
299
+
194
300
  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: 30_000,
114
+ timeout: timeoutMs,
100
115
  cwd: args.cwd ? resolveSafePath(args.cwd) : process.env.HOME,
101
116
  maxBuffer: 1024 * 1024, // 1MB output limit
102
117
  });