promethios-bridge 1.0.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/README.md +55 -0
- package/package.json +44 -0
- package/src/bridge.js +194 -0
- package/src/cli.js +44 -0
- package/src/executor.js +176 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# promethios-bridge
|
|
2
|
+
|
|
3
|
+
Run Promethios agent frameworks locally on your computer — with full access to your files, terminal, and browser.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx promethios-bridge --token <your-setup-token>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Get your setup token from **Promethios → Settings → Local Bridge → Generate Setup Token**.
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
The bridge connects your computer to Promethios so that agent frameworks can execute tools locally instead of (or in addition to) the cloud sandbox.
|
|
16
|
+
|
|
17
|
+
Once running, open any thread in Promethios and use the **Cloud / My Computer / Hybrid** toggle in the thread header.
|
|
18
|
+
|
|
19
|
+
| Mode | What runs where |
|
|
20
|
+
|---|---|
|
|
21
|
+
| ☁️ Cloud | All tools run in Promethios cloud sandbox (default) |
|
|
22
|
+
| 💻 My Computer | All tools run on your machine via this bridge |
|
|
23
|
+
| ⚡ Hybrid | Agent decides per-tool — local for files/terminal, cloud for APIs |
|
|
24
|
+
|
|
25
|
+
## Permissions
|
|
26
|
+
|
|
27
|
+
The first time a framework requests local execution, Promethios shows you exactly what it wants access to. You approve or deny each capability before anything runs.
|
|
28
|
+
|
|
29
|
+
Capabilities the bridge supports:
|
|
30
|
+
- `filesystem.read` — Read files on your machine
|
|
31
|
+
- `filesystem.write` — Write files on your machine
|
|
32
|
+
- `terminal.execute` — Run shell commands
|
|
33
|
+
- `terminal.readonly` — Run read-only commands (ls, cat, etc.)
|
|
34
|
+
- `browser.open` — Open URLs in your browser
|
|
35
|
+
- `network.http` — Make HTTP requests (useful for localhost APIs)
|
|
36
|
+
|
|
37
|
+
## Options
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
--token <token> Setup token from Promethios (required on first run)
|
|
41
|
+
--api <url> Promethios API base URL (default: https://api.promethios.ai)
|
|
42
|
+
--port <port> Local port for the bridge server (default: 7823)
|
|
43
|
+
--dev Verbose debug logging
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Security
|
|
47
|
+
|
|
48
|
+
- The bridge only listens on `127.0.0.1` (localhost) — it is not accessible from the internet.
|
|
49
|
+
- All tool calls are authenticated and signed by the Promethios cloud.
|
|
50
|
+
- Destructive commands (`rm -rf /`, `mkfs`, etc.) are blocked at the executor level.
|
|
51
|
+
- You can disconnect at any time with `Ctrl+C`.
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "promethios-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Run Promethios agent frameworks locally on your computer with full file, terminal, and browser access.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"promethios-bridge": "src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/cli.js",
|
|
11
|
+
"dev": "node src/cli.js --dev"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["promethios", "ai", "agent", "local", "bridge", "framework"],
|
|
14
|
+
"author": "Promethios <hello@promethios.ai>",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/wesheets/promethios.git",
|
|
19
|
+
"directory": "promethios-bridge"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://promethios.ai/local-bridge",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/wesheets/promethios/issues"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src/",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"axios": "^1.6.0",
|
|
34
|
+
"chalk": "^4.1.2",
|
|
35
|
+
"commander": "^11.1.0",
|
|
36
|
+
"express": "^4.18.2",
|
|
37
|
+
"open": "^8.4.2",
|
|
38
|
+
"ora": "^5.4.1",
|
|
39
|
+
"node-fetch": "^2.7.0"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/bridge.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promethios Local Bridge — Core
|
|
3
|
+
*
|
|
4
|
+
* 1. Authenticates with Promethios using a setup token
|
|
5
|
+
* 2. Starts a local HTTP server on the configured port
|
|
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
|
|
9
|
+
* 6. Sends heartbeats every 30s to keep the connection alive
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const express = require('express');
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
const ora = require('ora');
|
|
15
|
+
const fetch = require('node-fetch');
|
|
16
|
+
const { executeLocalTool } = require('./executor');
|
|
17
|
+
|
|
18
|
+
const HEARTBEAT_INTERVAL = 30_000; // 30s
|
|
19
|
+
|
|
20
|
+
async function startBridge({ setupToken, apiBase, port, dev }) {
|
|
21
|
+
const log = dev
|
|
22
|
+
? (...args) => console.log(chalk.gray(' [debug]'), ...args)
|
|
23
|
+
: () => {};
|
|
24
|
+
|
|
25
|
+
// ── Step 1: Authenticate ─────────────────────────────────────────────────
|
|
26
|
+
const spinner = ora(' Authenticating with Promethios...').start();
|
|
27
|
+
|
|
28
|
+
let authToken;
|
|
29
|
+
try {
|
|
30
|
+
authToken = await authenticate(setupToken, apiBase, dev);
|
|
31
|
+
spinner.succeed(chalk.green(' Authenticated'));
|
|
32
|
+
} catch (err) {
|
|
33
|
+
spinner.fail(chalk.red(' Authentication failed: ' + err.message));
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(chalk.yellow(' To get a setup token:'));
|
|
36
|
+
console.log(chalk.white(' 1. Open Promethios → Settings → Local Bridge'));
|
|
37
|
+
console.log(chalk.white(' 2. Click "Generate Setup Token"'));
|
|
38
|
+
console.log(chalk.white(' 3. Run: npx promethios-bridge --token <your-token>'));
|
|
39
|
+
console.log('');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Step 2: Start local HTTP server ──────────────────────────────────────
|
|
44
|
+
const app = express();
|
|
45
|
+
app.use(express.json());
|
|
46
|
+
|
|
47
|
+
// Health check
|
|
48
|
+
app.get('/health', (req, res) => res.json({ ok: true, version: require('../package.json').version }));
|
|
49
|
+
|
|
50
|
+
// Tool call endpoint — called by the Promethios cloud relay
|
|
51
|
+
app.post('/tool-call', async (req, res) => {
|
|
52
|
+
const { toolName, args, frameworkId, callId } = req.body;
|
|
53
|
+
log('Tool call received:', toolName, JSON.stringify(args).slice(0, 100));
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result = await executeLocalTool({ toolName, args, frameworkId, dev });
|
|
57
|
+
log('Tool result:', JSON.stringify(result).slice(0, 200));
|
|
58
|
+
res.json({ success: true, result, callId });
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(chalk.red(` ✗ Tool "${toolName}" failed:`), err.message);
|
|
61
|
+
res.status(500).json({ success: false, error: err.message, callId });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Capability query — cloud can ask what this bridge supports
|
|
66
|
+
app.get('/capabilities', (req, res) => {
|
|
67
|
+
res.json({ capabilities: getSupportedCapabilities() });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await new Promise((resolve, reject) => {
|
|
71
|
+
const server = app.listen(port, '127.0.0.1', () => resolve(server));
|
|
72
|
+
server.on('error', reject);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const callbackUrl = `http://127.0.0.1:${port}`;
|
|
76
|
+
console.log(chalk.green(` ✓ Local server listening on port ${port}`));
|
|
77
|
+
|
|
78
|
+
// ── Step 3: Register with Promethios API ─────────────────────────────────
|
|
79
|
+
const regSpinner = ora(' Registering bridge with Promethios...').start();
|
|
80
|
+
try {
|
|
81
|
+
await registerBridge({ authToken, apiBase, callbackUrl, port, dev });
|
|
82
|
+
regSpinner.succeed(chalk.green(' Bridge registered'));
|
|
83
|
+
} catch (err) {
|
|
84
|
+
regSpinner.fail(chalk.red(' Registration failed: ' + err.message));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Step 4: Show status and keep alive ───────────────────────────────────
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(chalk.bold.green(' ✓ Promethios Local Bridge is running'));
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(chalk.white(' Your computer is now connected to Promethios.'));
|
|
93
|
+
console.log(chalk.white(' Open any thread and switch to "My Computer" mode.'));
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(chalk.gray(' Press Ctrl+C to disconnect.'));
|
|
96
|
+
console.log('');
|
|
97
|
+
|
|
98
|
+
// Heartbeat loop
|
|
99
|
+
const heartbeatTimer = setInterval(async () => {
|
|
100
|
+
try {
|
|
101
|
+
await fetch(`${apiBase}/api/local-bridge/heartbeat`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json' },
|
|
104
|
+
});
|
|
105
|
+
log('Heartbeat sent');
|
|
106
|
+
} catch (err) {
|
|
107
|
+
log('Heartbeat failed:', err.message);
|
|
108
|
+
}
|
|
109
|
+
}, HEARTBEAT_INTERVAL);
|
|
110
|
+
|
|
111
|
+
// Graceful shutdown
|
|
112
|
+
const shutdown = async (signal) => {
|
|
113
|
+
console.log('');
|
|
114
|
+
console.log(chalk.yellow(` Disconnecting (${signal})...`));
|
|
115
|
+
clearInterval(heartbeatTimer);
|
|
116
|
+
try {
|
|
117
|
+
await fetch(`${apiBase}/api/local-bridge/unregister`, {
|
|
118
|
+
method: 'DELETE',
|
|
119
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
120
|
+
});
|
|
121
|
+
console.log(chalk.green(' ✓ Bridge disconnected cleanly'));
|
|
122
|
+
} catch { /* ignore */ }
|
|
123
|
+
process.exit(0);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
127
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
131
|
+
// Auth: exchange setup token for a session bearer token
|
|
132
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
133
|
+
async function authenticate(setupToken, apiBase, dev) {
|
|
134
|
+
if (!setupToken) {
|
|
135
|
+
throw new Error('No setup token provided. Use --token <token>');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const res = await fetch(`${apiBase}/api/local-bridge/auth/exchange`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
body: JSON.stringify({ setupToken }),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
const body = await res.text();
|
|
146
|
+
throw new Error(`${res.status}: ${body}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const data = await res.json();
|
|
150
|
+
return data.bearerToken;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
154
|
+
// Register the local bridge endpoint with the Promethios API
|
|
155
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
156
|
+
async function registerBridge({ authToken, apiBase, callbackUrl, port, dev }) {
|
|
157
|
+
const deviceId = require('crypto').randomBytes(8).toString('hex');
|
|
158
|
+
const capabilities = getSupportedCapabilities();
|
|
159
|
+
|
|
160
|
+
const res = await fetch(`${apiBase}/api/local-bridge/register`, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: {
|
|
163
|
+
Authorization: `Bearer ${authToken}`,
|
|
164
|
+
'Content-Type': 'application/json',
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify({
|
|
167
|
+
deviceId,
|
|
168
|
+
callbackUrl,
|
|
169
|
+
capabilities,
|
|
170
|
+
bridgeVersion: require('../package.json').version,
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
const body = await res.text();
|
|
176
|
+
throw new Error(`${res.status}: ${body}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
181
|
+
// What this bridge can do (declared to the cloud for permission manifest display)
|
|
182
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
183
|
+
function getSupportedCapabilities() {
|
|
184
|
+
return [
|
|
185
|
+
'filesystem.read',
|
|
186
|
+
'filesystem.write',
|
|
187
|
+
'terminal.execute',
|
|
188
|
+
'terminal.readonly',
|
|
189
|
+
'browser.open',
|
|
190
|
+
'network.http',
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { startBridge };
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* promethios-bridge CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx promethios-bridge # Interactive setup
|
|
7
|
+
* npx promethios-bridge --token <token> # Pre-authenticated (from Promethios UI)
|
|
8
|
+
* npx promethios-bridge --port 7823 # Custom local port
|
|
9
|
+
* npx promethios-bridge --dev # Dev mode (verbose logging)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { program } = require('commander');
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
const ora = require('ora');
|
|
15
|
+
const { startBridge } = require('./bridge');
|
|
16
|
+
|
|
17
|
+
const VERSION = require('../package.json').version;
|
|
18
|
+
|
|
19
|
+
console.log('');
|
|
20
|
+
console.log(chalk.bold.cyan(' 🌉 Promethios Local Bridge') + chalk.gray(` v${VERSION}`));
|
|
21
|
+
console.log(chalk.gray(' Run AI agent frameworks on your computer'));
|
|
22
|
+
console.log('');
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('promethios-bridge')
|
|
26
|
+
.description('Connect your computer to Promethios so agent frameworks can run locally')
|
|
27
|
+
.version(VERSION)
|
|
28
|
+
.option('--token <token>', 'Setup token from Promethios (get it from Settings → Local Bridge)')
|
|
29
|
+
.option('--api <url>', 'Promethios API base URL', 'https://api.promethios.ai')
|
|
30
|
+
.option('--port <port>', 'Local port for the bridge server', '7823')
|
|
31
|
+
.option('--dev', 'Enable verbose debug logging')
|
|
32
|
+
.parse(process.argv);
|
|
33
|
+
|
|
34
|
+
const opts = program.opts();
|
|
35
|
+
|
|
36
|
+
startBridge({
|
|
37
|
+
setupToken: opts.token,
|
|
38
|
+
apiBase: opts.api,
|
|
39
|
+
port: parseInt(opts.port, 10),
|
|
40
|
+
dev: !!opts.dev,
|
|
41
|
+
}).catch((err) => {
|
|
42
|
+
console.error(chalk.red('\n ✗ Bridge failed to start:'), err.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
package/src/executor.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promethios Local Bridge — Tool Executor
|
|
3
|
+
*
|
|
4
|
+
* Executes tool calls locally on the user's machine.
|
|
5
|
+
* Each tool type is sandboxed to only the capabilities the user approved.
|
|
6
|
+
*
|
|
7
|
+
* Built-in tool types:
|
|
8
|
+
* - read_file Read a local file
|
|
9
|
+
* - write_file Write/append to a local file
|
|
10
|
+
* - list_directory List files in a directory
|
|
11
|
+
* - run_command Execute a shell command (with approval)
|
|
12
|
+
* - open_browser Open a URL in the default browser
|
|
13
|
+
* - http_request Make an HTTP request (same as cloud but runs locally)
|
|
14
|
+
* - custom Run developer-written implementation code from the framework
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs').promises;
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { execSync, exec } = require('child_process');
|
|
20
|
+
const { promisify } = require('util');
|
|
21
|
+
const execAsync = promisify(exec);
|
|
22
|
+
|
|
23
|
+
async function executeLocalTool({ toolName, args, frameworkId, dev }) {
|
|
24
|
+
const log = dev ? (...a) => console.log('[executor]', ...a) : () => {};
|
|
25
|
+
|
|
26
|
+
switch (toolName) {
|
|
27
|
+
// ── Filesystem ────────────────────────────────────────────────────────
|
|
28
|
+
case 'read_file': {
|
|
29
|
+
const filePath = resolveSafePath(args.path);
|
|
30
|
+
log('read_file', filePath);
|
|
31
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
32
|
+
return { content, path: filePath, size: content.length };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
case 'write_file': {
|
|
36
|
+
const filePath = resolveSafePath(args.path);
|
|
37
|
+
log('write_file', filePath);
|
|
38
|
+
const mode = args.append ? 'a' : 'w';
|
|
39
|
+
await fs.writeFile(filePath, args.content || '', { flag: mode, encoding: 'utf8' });
|
|
40
|
+
return { success: true, path: filePath, bytesWritten: (args.content || '').length };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case 'list_directory': {
|
|
44
|
+
const dirPath = resolveSafePath(args.path || '.');
|
|
45
|
+
log('list_directory', dirPath);
|
|
46
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
47
|
+
return {
|
|
48
|
+
path: dirPath,
|
|
49
|
+
entries: entries.map((e) => ({
|
|
50
|
+
name: e.name,
|
|
51
|
+
type: e.isDirectory() ? 'directory' : 'file',
|
|
52
|
+
})),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case 'stat_file': {
|
|
57
|
+
const filePath = resolveSafePath(args.path);
|
|
58
|
+
const stat = await fs.stat(filePath);
|
|
59
|
+
return {
|
|
60
|
+
path: filePath,
|
|
61
|
+
size: stat.size,
|
|
62
|
+
isDirectory: stat.isDirectory(),
|
|
63
|
+
modified: stat.mtime.toISOString(),
|
|
64
|
+
created: stat.birthtime.toISOString(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Terminal ──────────────────────────────────────────────────────────
|
|
69
|
+
case 'run_command': {
|
|
70
|
+
const cmd = args.command;
|
|
71
|
+
if (!cmd) throw new Error('command is required');
|
|
72
|
+
// Block obviously dangerous commands
|
|
73
|
+
if (/rm\s+-rf\s+\/|mkfs|dd\s+if=|:(){ :|:& };:/i.test(cmd)) {
|
|
74
|
+
throw new Error('Command blocked: potentially destructive operation');
|
|
75
|
+
}
|
|
76
|
+
log('run_command', cmd);
|
|
77
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
78
|
+
timeout: 30_000,
|
|
79
|
+
cwd: args.cwd ? resolveSafePath(args.cwd) : process.env.HOME,
|
|
80
|
+
maxBuffer: 1024 * 1024, // 1MB output limit
|
|
81
|
+
});
|
|
82
|
+
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Browser ───────────────────────────────────────────────────────────
|
|
86
|
+
case 'open_browser': {
|
|
87
|
+
const url = args.url;
|
|
88
|
+
if (!url || !/^https?:\/\//.test(url)) throw new Error('Valid http/https URL required');
|
|
89
|
+
log('open_browser', url);
|
|
90
|
+
// Use platform-appropriate open command
|
|
91
|
+
const openCmd = process.platform === 'darwin' ? 'open' :
|
|
92
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
93
|
+
await execAsync(`${openCmd} "${url.replace(/"/g, '')}"`);
|
|
94
|
+
return { success: true, url };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── HTTP (runs locally, useful for localhost APIs) ────────────────────
|
|
98
|
+
case 'http_request': {
|
|
99
|
+
const { url, method = 'GET', headers = {}, body } = args;
|
|
100
|
+
if (!url) throw new Error('url is required');
|
|
101
|
+
log('http_request', method, url);
|
|
102
|
+
|
|
103
|
+
const fetchOptions = {
|
|
104
|
+
method,
|
|
105
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
106
|
+
};
|
|
107
|
+
if (body && method !== 'GET') {
|
|
108
|
+
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const res = await fetch(url, fetchOptions);
|
|
112
|
+
const responseText = await res.text();
|
|
113
|
+
let responseData;
|
|
114
|
+
try { responseData = JSON.parse(responseText); } catch { responseData = responseText; }
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
status: res.status,
|
|
118
|
+
ok: res.ok,
|
|
119
|
+
data: responseData,
|
|
120
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Custom tool (developer-written code from framework definition) ────
|
|
125
|
+
case 'custom':
|
|
126
|
+
default: {
|
|
127
|
+
// For custom tools, the framework definition's implementation code is
|
|
128
|
+
// passed as args.__implementation. We run it in a vm context with
|
|
129
|
+
// access to the full Node.js environment (unlike the cloud sandbox).
|
|
130
|
+
const impl = args.__implementation;
|
|
131
|
+
if (!impl) {
|
|
132
|
+
return { error: `Unknown tool: ${toolName}`, stub: true };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
log('custom tool execution', toolName);
|
|
136
|
+
const vm = require('vm');
|
|
137
|
+
const context = vm.createContext({
|
|
138
|
+
args: { ...args },
|
|
139
|
+
fetch,
|
|
140
|
+
require,
|
|
141
|
+
process: { env: process.env, platform: process.platform },
|
|
142
|
+
console,
|
|
143
|
+
Buffer,
|
|
144
|
+
URL,
|
|
145
|
+
URLSearchParams,
|
|
146
|
+
crypto: require('crypto'),
|
|
147
|
+
fs: require('fs').promises,
|
|
148
|
+
path: require('path'),
|
|
149
|
+
result: undefined,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const script = new vm.Script(`
|
|
153
|
+
(async () => {
|
|
154
|
+
${impl}
|
|
155
|
+
})().then(r => { result = r; });
|
|
156
|
+
`);
|
|
157
|
+
|
|
158
|
+
await script.runInContext(context);
|
|
159
|
+
// Wait for async to settle
|
|
160
|
+
await new Promise(r => setTimeout(r, 100));
|
|
161
|
+
return context.result !== undefined ? context.result : { success: true };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
+
// Resolve a path safely — expand ~ and normalize
|
|
168
|
+
// Does NOT restrict to a specific directory (user approved full access)
|
|
169
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
170
|
+
function resolveSafePath(inputPath) {
|
|
171
|
+
if (!inputPath) throw new Error('Path is required');
|
|
172
|
+
const expanded = inputPath.replace(/^~/, process.env.HOME || '/home');
|
|
173
|
+
return path.resolve(expanded);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { executeLocalTool };
|