neoagent 2.1.18-beta.21 → 2.1.18-beta.23
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/docs/capabilities.md +3 -1
- package/docs/configuration.md +2 -0
- package/extensions/chrome-browser/background.mjs +209 -0
- package/extensions/chrome-browser/manifest.json +17 -0
- package/extensions/chrome-browser/popup.css +69 -0
- package/extensions/chrome-browser/popup.html +28 -0
- package/extensions/chrome-browser/popup.js +81 -0
- package/extensions/chrome-browser/protocol.mjs +309 -0
- package/package.json +4 -2
- package/server/config/origins.js +10 -0
- package/server/db/database.js +29 -0
- package/server/http/routes.js +1 -0
- package/server/index.js +2 -0
- package/server/middleware/auth.js +4 -2
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +37671 -37552
- package/server/routes/browser_extension.js +133 -0
- package/server/services/ai/capabilityHealth.js +27 -0
- package/server/services/browser/extension/gateway.js +65 -0
- package/server/services/browser/extension/protocol.js +49 -0
- package/server/services/browser/extension/provider.js +178 -0
- package/server/services/browser/extension/registry.js +353 -0
- package/server/services/browser/extension/zip.js +133 -0
- package/server/services/manager.js +18 -0
- package/server/services/runtime/manager.js +7 -31
- package/server/services/runtime/settings.js +2 -6
package/docs/capabilities.md
CHANGED
|
@@ -121,4 +121,6 @@ Runtime settings let operators choose where higher-risk work runs:
|
|
|
121
121
|
|
|
122
122
|
Remote execution uses `remote_worker_base_url` and an encrypted `remote_worker_token`. Production policy can require the secure VM profile and a strong VM guest token.
|
|
123
123
|
|
|
124
|
-
These controls matter operationally: the browser, Android emulator, local files, and shell commands run wherever the NeoAgent backend
|
|
124
|
+
These controls matter operationally: the browser, Android emulator, local files, and shell commands run wherever the NeoAgent backend, configured worker, or paired browser extension is running, not necessarily on the computer where you are reading the docs. Logs from a different server or remote browser may not match the logs on the local machine.
|
|
125
|
+
|
|
126
|
+
For extension-only remote browser control, download `/api/browser-extension/download` from NeoAgent, unzip it on the remote machine, load the folder in `chrome://extensions`, and pair after logging in. The extension uses Chrome's debugger permission for full browser control, so Chrome will show its normal debugging warning while attached. The popup can check whether the server has a newer extension bundle, but unpacked Developer Mode installs still need a manual download and reload.
|
package/docs/configuration.md
CHANGED
|
@@ -89,6 +89,8 @@ Production policy can require the VM backend. In that case, set a strong `NEOAGE
|
|
|
89
89
|
|
|
90
90
|
Remote worker settings are stored through the app as `remote_worker_base_url` and encrypted `remote_worker_token` values. Use an `http` or `https` worker URL only.
|
|
91
91
|
|
|
92
|
+
The browser backend can also be set to `extension`. In that mode, browser actions use the paired Chrome extension connection rather than the server-local Puppeteer browser. To install only the extension on a remote machine, open NeoAgent, download `/api/browser-extension/download`, unzip it, load the folder through `chrome://extensions` with Developer mode enabled, then pair after logging in to NeoAgent. Unpacked Chrome extensions cannot replace themselves automatically; use the extension popup's update check to compare against the server bundle, then download and reload the latest ZIP when needed.
|
|
93
|
+
|
|
92
94
|
## Secrets Guidance
|
|
93
95
|
|
|
94
96
|
Treat `SESSION_SECRET`, provider API keys, OAuth client secrets, messaging credentials, and Telnyx tokens as sensitive. Do not commit them, print them in logs, or expose them in client-side code. Store them in server-side environment variables or a secrets manager, restrict access to operators who need them, and rotate them immediately if you suspect exposure.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { createBrowserProtocol } from './protocol.mjs';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEYS = ['serverUrl', 'token', 'pairingId', 'pairingSecret', 'approvalUrl', 'status'];
|
|
4
|
+
const protocol = createBrowserProtocol(chrome);
|
|
5
|
+
let socket = null;
|
|
6
|
+
let reconnectTimer = null;
|
|
7
|
+
|
|
8
|
+
function getStorage(keys = STORAGE_KEYS) {
|
|
9
|
+
return chrome.storage.local.get(keys);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function setStorage(values) {
|
|
13
|
+
return chrome.storage.local.set(values);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function removeStorage(keys) {
|
|
17
|
+
return chrome.storage.local.remove(keys);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeServerUrl(value) {
|
|
21
|
+
return String(value || '').trim().replace(/\/+$/, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function websocketUrl(serverUrl, token) {
|
|
25
|
+
const url = new URL('/api/browser-extension/ws', serverUrl);
|
|
26
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
27
|
+
url.searchParams.set('token', token);
|
|
28
|
+
return url.toString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function compareVersions(a, b) {
|
|
32
|
+
const left = String(a || '0').split('.').map((part) => Number(part) || 0);
|
|
33
|
+
const right = String(b || '0').split('.').map((part) => Number(part) || 0);
|
|
34
|
+
const length = Math.max(left.length, right.length);
|
|
35
|
+
for (let i = 0; i < length; i += 1) {
|
|
36
|
+
const delta = (left[i] || 0) - (right[i] || 0);
|
|
37
|
+
if (delta !== 0) return delta;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function updateStatus(status) {
|
|
43
|
+
await setStorage({ status });
|
|
44
|
+
chrome.runtime.sendMessage({ type: 'status', status }).catch(() => {});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function connect() {
|
|
48
|
+
const { serverUrl, token } = await getStorage(['serverUrl', 'token']);
|
|
49
|
+
if (!serverUrl || !token) {
|
|
50
|
+
await updateStatus('not_paired');
|
|
51
|
+
return { connected: false };
|
|
52
|
+
}
|
|
53
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
54
|
+
return { connected: true };
|
|
55
|
+
}
|
|
56
|
+
if (socket) {
|
|
57
|
+
try { socket.close(); } catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
socket = new WebSocket(websocketUrl(serverUrl, token));
|
|
61
|
+
await updateStatus('connecting');
|
|
62
|
+
|
|
63
|
+
socket.addEventListener('open', () => {
|
|
64
|
+
updateStatus('connected');
|
|
65
|
+
});
|
|
66
|
+
socket.addEventListener('close', () => {
|
|
67
|
+
updateStatus('disconnected');
|
|
68
|
+
clearTimeout(reconnectTimer);
|
|
69
|
+
reconnectTimer = setTimeout(() => connect().catch(() => {}), 5000);
|
|
70
|
+
});
|
|
71
|
+
socket.addEventListener('error', () => {
|
|
72
|
+
updateStatus('disconnected');
|
|
73
|
+
});
|
|
74
|
+
socket.addEventListener('message', (event) => {
|
|
75
|
+
handleSocketMessage(event.data).catch((error) => {
|
|
76
|
+
console.error('NeoAgent command handling failed', error);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return { connected: false };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleSocketMessage(raw) {
|
|
84
|
+
let message;
|
|
85
|
+
try {
|
|
86
|
+
message = JSON.parse(raw);
|
|
87
|
+
} catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!message || message.type !== 'command' || !message.id) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const result = await protocol.run(message.command, message.payload || {});
|
|
95
|
+
socket?.send(JSON.stringify({ type: 'result', id: message.id, ok: true, result }));
|
|
96
|
+
} catch (error) {
|
|
97
|
+
socket?.send(JSON.stringify({
|
|
98
|
+
type: 'result',
|
|
99
|
+
id: message.id,
|
|
100
|
+
ok: false,
|
|
101
|
+
error: error?.message || String(error),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function startPairing(serverUrl) {
|
|
107
|
+
const normalized = normalizeServerUrl(serverUrl);
|
|
108
|
+
if (!normalized) throw new Error('NeoAgent server URL required.');
|
|
109
|
+
const response = await fetch(`${normalized}/api/browser-extension/pairing/request`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'content-type': 'application/json' },
|
|
112
|
+
body: JSON.stringify({ extensionName: 'NeoAgent Browser' }),
|
|
113
|
+
});
|
|
114
|
+
const payload = await response.json().catch(() => ({}));
|
|
115
|
+
if (!response.ok) throw new Error(payload.error || `Pairing failed: ${response.status}`);
|
|
116
|
+
await setStorage({
|
|
117
|
+
serverUrl: normalized,
|
|
118
|
+
pairingId: payload.pairingId,
|
|
119
|
+
pairingSecret: payload.pairingSecret,
|
|
120
|
+
approvalUrl: payload.approvalUrl,
|
|
121
|
+
status: 'approval_pending',
|
|
122
|
+
});
|
|
123
|
+
await chrome.tabs.create({ url: payload.approvalUrl, active: true });
|
|
124
|
+
return payload;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function claimPairing() {
|
|
128
|
+
const { serverUrl, pairingId, pairingSecret } = await getStorage(['serverUrl', 'pairingId', 'pairingSecret']);
|
|
129
|
+
if (!serverUrl || !pairingId || !pairingSecret) {
|
|
130
|
+
throw new Error('No pending pairing request.');
|
|
131
|
+
}
|
|
132
|
+
const response = await fetch(`${serverUrl}/api/browser-extension/pairing/${encodeURIComponent(pairingId)}/claim`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: { 'content-type': 'application/json' },
|
|
135
|
+
body: JSON.stringify({ pairingSecret, extensionName: 'NeoAgent Browser' }),
|
|
136
|
+
});
|
|
137
|
+
const payload = await response.json().catch(() => ({}));
|
|
138
|
+
if (!response.ok) throw new Error(payload.error || `Claim failed: ${response.status}`);
|
|
139
|
+
await setStorage({
|
|
140
|
+
token: payload.token,
|
|
141
|
+
tokenId: payload.tokenId,
|
|
142
|
+
status: 'paired',
|
|
143
|
+
});
|
|
144
|
+
await removeStorage(['pairingId', 'pairingSecret', 'approvalUrl']);
|
|
145
|
+
await connect();
|
|
146
|
+
return payload;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function disconnect() {
|
|
150
|
+
clearTimeout(reconnectTimer);
|
|
151
|
+
reconnectTimer = null;
|
|
152
|
+
if (socket) {
|
|
153
|
+
try { socket.close(); } catch {}
|
|
154
|
+
}
|
|
155
|
+
socket = null;
|
|
156
|
+
await removeStorage(['token', 'tokenId', 'pairingId', 'pairingSecret', 'approvalUrl']);
|
|
157
|
+
await updateStatus('not_paired');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function checkForUpdates() {
|
|
161
|
+
const { serverUrl } = await getStorage(['serverUrl']);
|
|
162
|
+
if (!serverUrl) throw new Error('NeoAgent server URL required.');
|
|
163
|
+
const response = await fetch(`${serverUrl}/api/browser-extension/latest`);
|
|
164
|
+
const latest = await response.json().catch(() => ({}));
|
|
165
|
+
if (!response.ok) throw new Error(latest.error || `Update check failed: ${response.status}`);
|
|
166
|
+
const currentVersion = chrome.runtime.getManifest().version;
|
|
167
|
+
return {
|
|
168
|
+
currentVersion,
|
|
169
|
+
latestVersion: latest.version || currentVersion,
|
|
170
|
+
downloadUrl: latest.downloadUrl || `${serverUrl}/api/browser-extension/download`,
|
|
171
|
+
updateAvailable: compareVersions(latest.version, currentVersion) > 0,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function openDownload() {
|
|
176
|
+
const { serverUrl } = await getStorage(['serverUrl']);
|
|
177
|
+
if (!serverUrl) throw new Error('NeoAgent server URL required.');
|
|
178
|
+
await chrome.tabs.create({ url: `${serverUrl}/api/browser-extension/download`, active: true });
|
|
179
|
+
return { success: true };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
183
|
+
const run = async () => {
|
|
184
|
+
switch (message?.type) {
|
|
185
|
+
case 'startPairing':
|
|
186
|
+
return startPairing(message.serverUrl);
|
|
187
|
+
case 'claimPairing':
|
|
188
|
+
return claimPairing();
|
|
189
|
+
case 'connect':
|
|
190
|
+
return connect();
|
|
191
|
+
case 'disconnect':
|
|
192
|
+
return disconnect();
|
|
193
|
+
case 'checkForUpdates':
|
|
194
|
+
return checkForUpdates();
|
|
195
|
+
case 'openDownload':
|
|
196
|
+
return openDownload();
|
|
197
|
+
case 'getState':
|
|
198
|
+
return getStorage([...STORAGE_KEYS, 'tokenId']);
|
|
199
|
+
default:
|
|
200
|
+
return { error: 'unknown message' };
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
run()
|
|
204
|
+
.then((result) => sendResponse({ ok: true, result }))
|
|
205
|
+
.catch((error) => sendResponse({ ok: false, error: error.message || String(error) }));
|
|
206
|
+
return true;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
connect().catch(() => {});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "NeoAgent Browser",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Connect this Chrome browser to NeoAgent for browser automation.",
|
|
6
|
+
"minimum_chrome_version": "118",
|
|
7
|
+
"permissions": ["debugger", "storage", "tabs"],
|
|
8
|
+
"host_permissions": ["http://*/*", "https://*/*"],
|
|
9
|
+
"action": {
|
|
10
|
+
"default_title": "NeoAgent Browser",
|
|
11
|
+
"default_popup": "popup.html"
|
|
12
|
+
},
|
|
13
|
+
"background": {
|
|
14
|
+
"service_worker": "background.mjs",
|
|
15
|
+
"type": "module"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
body {
|
|
2
|
+
margin: 0;
|
|
3
|
+
width: 320px;
|
|
4
|
+
background: #0f172a;
|
|
5
|
+
color: #e5e7eb;
|
|
6
|
+
font: 14px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
main {
|
|
10
|
+
padding: 16px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
h1 {
|
|
14
|
+
margin: 0 0 10px;
|
|
15
|
+
font-size: 18px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#status {
|
|
19
|
+
margin: 0 0 12px;
|
|
20
|
+
color: #38bdf8;
|
|
21
|
+
font-weight: 700;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
label {
|
|
25
|
+
display: grid;
|
|
26
|
+
gap: 6px;
|
|
27
|
+
color: #cbd5e1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
input {
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
width: 100%;
|
|
33
|
+
padding: 9px 10px;
|
|
34
|
+
border: 1px solid #334155;
|
|
35
|
+
border-radius: 8px;
|
|
36
|
+
background: #111827;
|
|
37
|
+
color: #f8fafc;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.actions {
|
|
41
|
+
display: grid;
|
|
42
|
+
gap: 8px;
|
|
43
|
+
margin-top: 12px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
button {
|
|
47
|
+
border: 0;
|
|
48
|
+
border-radius: 8px;
|
|
49
|
+
padding: 9px 10px;
|
|
50
|
+
background: #0284c7;
|
|
51
|
+
color: white;
|
|
52
|
+
font: inherit;
|
|
53
|
+
font-weight: 700;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
button.secondary {
|
|
58
|
+
background: #334155;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.hint,
|
|
62
|
+
.message {
|
|
63
|
+
color: #94a3b8;
|
|
64
|
+
font-size: 12px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.message {
|
|
68
|
+
min-height: 1.2em;
|
|
69
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>NeoAgent Browser</title>
|
|
6
|
+
<link rel="stylesheet" href="popup.css">
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main>
|
|
10
|
+
<h1>NeoAgent Browser</h1>
|
|
11
|
+
<p id="status">Loading...</p>
|
|
12
|
+
<label>
|
|
13
|
+
Server URL
|
|
14
|
+
<input id="serverUrl" type="url" placeholder="https://neoagent.example.com">
|
|
15
|
+
</label>
|
|
16
|
+
<div class="actions">
|
|
17
|
+
<button id="pair">Pair after login</button>
|
|
18
|
+
<button id="claim">Finish pairing</button>
|
|
19
|
+
<button id="checkUpdate" class="secondary">Check for update</button>
|
|
20
|
+
<button id="download" class="secondary">Download latest ZIP</button>
|
|
21
|
+
<button id="disconnect" class="secondary">Disconnect</button>
|
|
22
|
+
</div>
|
|
23
|
+
<p class="hint">Pairing opens NeoAgent so you can log in there. This extension never stores your NeoAgent password.</p>
|
|
24
|
+
<p id="message" class="message"></p>
|
|
25
|
+
</main>
|
|
26
|
+
<script src="popup.js" type="module"></script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const statusEl = document.querySelector('#status');
|
|
2
|
+
const serverUrlEl = document.querySelector('#serverUrl');
|
|
3
|
+
const messageEl = document.querySelector('#message');
|
|
4
|
+
|
|
5
|
+
function send(type, payload = {}) {
|
|
6
|
+
return chrome.runtime.sendMessage({ type, ...payload }).then((response) => {
|
|
7
|
+
if (!response?.ok) throw new Error(response?.error || 'Extension action failed.');
|
|
8
|
+
return response.result;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function setMessage(text) {
|
|
13
|
+
messageEl.textContent = text || '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function refresh() {
|
|
17
|
+
const state = await send('getState');
|
|
18
|
+
statusEl.textContent = `Status: ${state.status || 'not_paired'}`;
|
|
19
|
+
if (state.serverUrl) serverUrlEl.value = state.serverUrl;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
document.querySelector('#pair').addEventListener('click', async () => {
|
|
23
|
+
try {
|
|
24
|
+
setMessage('');
|
|
25
|
+
await send('startPairing', { serverUrl: serverUrlEl.value });
|
|
26
|
+
await refresh();
|
|
27
|
+
setMessage('Log in and approve the extension in the opened NeoAgent tab.');
|
|
28
|
+
} catch (error) {
|
|
29
|
+
setMessage(error.message);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
document.querySelector('#claim').addEventListener('click', async () => {
|
|
34
|
+
try {
|
|
35
|
+
setMessage('');
|
|
36
|
+
await send('claimPairing');
|
|
37
|
+
await refresh();
|
|
38
|
+
setMessage('Connected.');
|
|
39
|
+
} catch (error) {
|
|
40
|
+
setMessage(error.message);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
document.querySelector('#disconnect').addEventListener('click', async () => {
|
|
45
|
+
try {
|
|
46
|
+
setMessage('');
|
|
47
|
+
await send('disconnect');
|
|
48
|
+
await refresh();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
setMessage(error.message);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
document.querySelector('#checkUpdate').addEventListener('click', async () => {
|
|
55
|
+
try {
|
|
56
|
+
setMessage('');
|
|
57
|
+
const result = await send('checkForUpdates');
|
|
58
|
+
setMessage(
|
|
59
|
+
result.updateAvailable
|
|
60
|
+
? `Update available: ${result.currentVersion} -> ${result.latestVersion}.`
|
|
61
|
+
: `Current version ${result.currentVersion} is up to date.`,
|
|
62
|
+
);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
setMessage(error.message);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
document.querySelector('#download').addEventListener('click', async () => {
|
|
69
|
+
try {
|
|
70
|
+
setMessage('');
|
|
71
|
+
await send('openDownload');
|
|
72
|
+
} catch (error) {
|
|
73
|
+
setMessage(error.message);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
chrome.runtime.onMessage.addListener((message) => {
|
|
78
|
+
if (message?.type === 'status') refresh().catch(() => {});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
refresh().catch((error) => setMessage(error.message));
|