neoagent 2.3.1-beta.71 → 2.3.1-beta.73
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/configuration.md +2 -0
- package/docs/getting-started.md +17 -0
- package/flutter_app/lib/main_account_settings.dart +62 -0
- package/flutter_app/lib/main_controller.dart +30 -6
- package/flutter_app/lib/main_settings.dart +2 -9
- package/flutter_app/lib/src/backend_client.dart +10 -1
- package/package.json +1 -1
- package/runtime/paths.js +4 -3
- package/server/db/database.js +13 -0
- package/server/guest-agent.package.json +16 -0
- package/server/guest_agent.js +13 -1
- package/server/index.js +11 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +33084 -32987
- package/server/routes/account.js +26 -1
- package/server/routes/android.js +30 -1
- package/server/routes/auth.js +6 -4
- package/server/routes/browser.js +29 -0
- package/server/routes/settings.js +8 -17
- package/server/services/android/android_bootstrap_worker.js +47 -0
- package/server/services/android/controller.js +297 -57
- package/server/services/bootstrap_helpers.js +0 -34
- package/server/services/browser/controller.js +67 -9
- package/server/services/cli/executor.js +36 -1
- package/server/services/runtime/backends/local-vm.js +99 -22
- package/server/services/runtime/guest_bootstrap.js +450 -0
- package/server/services/runtime/manager.js +11 -0
- package/server/services/runtime/qemu.js +273 -16
package/docs/configuration.md
CHANGED
|
@@ -134,6 +134,8 @@ Runtime profile and backend selection are stored in user settings, not normally
|
|
|
134
134
|
|
|
135
135
|
Production policy can require the VM backend. In that case, set a strong `NEOAGENT_VM_GUEST_TOKEN` of at least 32 characters and avoid placeholder values.
|
|
136
136
|
|
|
137
|
+
The VM backend requires QEMU on the host machine. It runs an x86_64 Ubuntu guest so the Android emulator can run in the same isolated runtime as the browser. On macOS, install QEMU with `brew install qemu`. On Ubuntu or Debian, install `qemu-system` and `qemu-utils` from apt before starting NeoAgent.
|
|
138
|
+
|
|
137
139
|
The app exposes two browser backend choices: VM and Chrome extension. VM uses the local isolated runtime. Chrome extension uses the paired extension connection on the remote machine instead of the server-local 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.
|
|
138
140
|
|
|
139
141
|
## Secrets Guidance
|
package/docs/getting-started.md
CHANGED
|
@@ -5,10 +5,27 @@ NeoAgent installs as a Node CLI and runs a self-hosted server with a bundled Flu
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
- Node.js 20 or newer.
|
|
8
|
+
- QEMU for VM-backed browser and Android runs.
|
|
8
9
|
- A reachable server URL if you want OAuth callbacks, mobile access, or messaging webhooks.
|
|
9
10
|
- At least one hosted AI provider API key, unless you only use local Ollama.
|
|
10
11
|
- Android Studio or a Flutter Android toolchain if you build the Android client yourself.
|
|
11
12
|
|
|
13
|
+
### QEMU Installation
|
|
14
|
+
|
|
15
|
+
NeoAgent uses a per-user x86_64 VM for browser and Android execution. Install QEMU before starting the service:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# macOS
|
|
19
|
+
brew install qemu
|
|
20
|
+
|
|
21
|
+
# Ubuntu / Debian
|
|
22
|
+
sudo apt-get update
|
|
23
|
+
sudo apt-get install -y qemu-system qemu-utils
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The first VM boot also downloads the Ubuntu base image and seeds the guest runtime automatically.
|
|
27
|
+
That guest bootstrap installs the browser and Android runtime dependencies it needs, including Java and the emulator support packages.
|
|
28
|
+
|
|
12
29
|
## Install
|
|
13
30
|
|
|
14
31
|
```bash
|
|
@@ -167,6 +167,7 @@ class AccountSettingsPanel extends StatefulWidget {
|
|
|
167
167
|
|
|
168
168
|
class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
169
169
|
late AccountSettingsTab _selectedTab;
|
|
170
|
+
late final TextEditingController _displayNameController;
|
|
170
171
|
late final TextEditingController _emailController;
|
|
171
172
|
late final TextEditingController _emailPasswordController;
|
|
172
173
|
late final TextEditingController _setupPasswordController;
|
|
@@ -178,6 +179,8 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
178
179
|
late final TextEditingController _confirmNewPasswordController;
|
|
179
180
|
Map<String, dynamic>? _pendingSetup;
|
|
180
181
|
List<String> _recoveryCodes = const <String>[];
|
|
182
|
+
String? _displayNameSuccessMessage;
|
|
183
|
+
String? _displayNameInlineError;
|
|
181
184
|
String? _emailSuccessMessage;
|
|
182
185
|
String? _emailInlineError;
|
|
183
186
|
String? _passwordSuccessMessage;
|
|
@@ -187,6 +190,9 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
187
190
|
void initState() {
|
|
188
191
|
super.initState();
|
|
189
192
|
_selectedTab = widget.initialTab ?? AccountSettingsTab.account;
|
|
193
|
+
_displayNameController = TextEditingController(
|
|
194
|
+
text: widget.controller.user?['display_name']?.toString() ?? '',
|
|
195
|
+
);
|
|
190
196
|
_emailController = TextEditingController(
|
|
191
197
|
text: widget.controller.user?['email']?.toString() ?? '',
|
|
192
198
|
);
|
|
@@ -208,6 +214,11 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
208
214
|
oldWidget.initialTab != widget.initialTab) {
|
|
209
215
|
_selectedTab = widget.initialTab!;
|
|
210
216
|
}
|
|
217
|
+
final displayName =
|
|
218
|
+
widget.controller.user?['display_name']?.toString() ?? '';
|
|
219
|
+
if (_displayNameController.text.isEmpty && displayName.isNotEmpty) {
|
|
220
|
+
_displayNameController.text = displayName;
|
|
221
|
+
}
|
|
211
222
|
final email = widget.controller.user?['email']?.toString() ?? '';
|
|
212
223
|
if (_emailController.text.isEmpty && email.isNotEmpty) {
|
|
213
224
|
_emailController.text = email;
|
|
@@ -216,6 +227,7 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
216
227
|
|
|
217
228
|
@override
|
|
218
229
|
void dispose() {
|
|
230
|
+
_displayNameController.dispose();
|
|
219
231
|
_emailController.dispose();
|
|
220
232
|
_emailPasswordController.dispose();
|
|
221
233
|
_setupPasswordController.dispose();
|
|
@@ -409,6 +421,56 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
409
421
|
const SizedBox(height: 12),
|
|
410
422
|
_MetaPill(label: username, icon: Icons.person_outline),
|
|
411
423
|
const SizedBox(height: 18),
|
|
424
|
+
TextField(
|
|
425
|
+
controller: _displayNameController,
|
|
426
|
+
decoration: const InputDecoration(
|
|
427
|
+
labelText: 'Display name',
|
|
428
|
+
helperText: 'Shown in the sidebar. Leave blank to use your username.',
|
|
429
|
+
),
|
|
430
|
+
),
|
|
431
|
+
if (_displayNameInlineError != null) ...<Widget>[
|
|
432
|
+
const SizedBox(height: 10),
|
|
433
|
+
_InlineError(message: _displayNameInlineError!),
|
|
434
|
+
],
|
|
435
|
+
if (_displayNameSuccessMessage != null) ...<Widget>[
|
|
436
|
+
const SizedBox(height: 10),
|
|
437
|
+
_InlineSuccess(message: _displayNameSuccessMessage!),
|
|
438
|
+
],
|
|
439
|
+
const SizedBox(height: 14),
|
|
440
|
+
FilledButton.icon(
|
|
441
|
+
onPressed: controller.isSavingAccountSettings
|
|
442
|
+
? null
|
|
443
|
+
: () async {
|
|
444
|
+
setState(() {
|
|
445
|
+
_displayNameInlineError = null;
|
|
446
|
+
_displayNameSuccessMessage = null;
|
|
447
|
+
});
|
|
448
|
+
final trimmed = _displayNameController.text.trim();
|
|
449
|
+
if (trimmed.length > 64) {
|
|
450
|
+
setState(() {
|
|
451
|
+
_displayNameInlineError =
|
|
452
|
+
'Display name must be 64 characters or fewer.';
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
final saved = await controller.updateAccountDisplayName(
|
|
457
|
+
displayName: trimmed,
|
|
458
|
+
);
|
|
459
|
+
if (saved && mounted) {
|
|
460
|
+
setState(() {
|
|
461
|
+
_displayNameSuccessMessage = 'Display name saved.';
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
icon: controller.isSavingAccountSettings
|
|
466
|
+
? const SizedBox.square(
|
|
467
|
+
dimension: 16,
|
|
468
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
469
|
+
)
|
|
470
|
+
: Icon(Icons.save_outlined),
|
|
471
|
+
label: Text('Save name'),
|
|
472
|
+
),
|
|
473
|
+
const SizedBox(height: 22),
|
|
412
474
|
Text('Current email: $currentEmail'),
|
|
413
475
|
const SizedBox(height: 16),
|
|
414
476
|
TextField(
|
|
@@ -4510,7 +4510,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4510
4510
|
notifyListeners();
|
|
4511
4511
|
|
|
4512
4512
|
final payload = <String, dynamic>{
|
|
4513
|
-
'headless_browser':
|
|
4513
|
+
'headless_browser': true,
|
|
4514
4514
|
'browser_backend': browserBackend,
|
|
4515
4515
|
'smarter_model_selector': smarterSelector,
|
|
4516
4516
|
'enabled_models': enabledModels,
|
|
@@ -4645,6 +4645,29 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4645
4645
|
}
|
|
4646
4646
|
}
|
|
4647
4647
|
|
|
4648
|
+
Future<bool> updateAccountDisplayName({
|
|
4649
|
+
required String displayName,
|
|
4650
|
+
}) async {
|
|
4651
|
+
isSavingAccountSettings = true;
|
|
4652
|
+
errorMessage = null;
|
|
4653
|
+
notifyListeners();
|
|
4654
|
+
try {
|
|
4655
|
+
_applyAccountResponse(
|
|
4656
|
+
await _backendClient.updateAccountDisplayName(
|
|
4657
|
+
baseUrl: backendUrl,
|
|
4658
|
+
displayName: displayName,
|
|
4659
|
+
),
|
|
4660
|
+
);
|
|
4661
|
+
return true;
|
|
4662
|
+
} catch (error) {
|
|
4663
|
+
errorMessage = _friendlyErrorMessage(error);
|
|
4664
|
+
return false;
|
|
4665
|
+
} finally {
|
|
4666
|
+
isSavingAccountSettings = false;
|
|
4667
|
+
notifyListeners();
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
|
|
4648
4671
|
Future<void> linkAccountProvider(String provider) async {
|
|
4649
4672
|
isSavingAccountSettings = true;
|
|
4650
4673
|
errorMessage = null;
|
|
@@ -5957,9 +5980,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
5957
5980
|
details.length <= 800;
|
|
5958
5981
|
}
|
|
5959
5982
|
|
|
5960
|
-
bool get headlessBrowser =>
|
|
5961
|
-
settings['headless_browser'] != false &&
|
|
5962
|
-
settings['headless_browser'] != 'false';
|
|
5983
|
+
bool get headlessBrowser => true;
|
|
5963
5984
|
|
|
5964
5985
|
String get browserBackend =>
|
|
5965
5986
|
settings['browser_backend']?.toString().trim().toLowerCase() ?? 'vm';
|
|
@@ -6099,8 +6120,11 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
6099
6120
|
|
|
6100
6121
|
DateTime? get liveVoiceCaptureStartedAt => _liveVoiceCaptureStartedAt;
|
|
6101
6122
|
|
|
6102
|
-
String get accountLabel
|
|
6103
|
-
|
|
6123
|
+
String get accountLabel {
|
|
6124
|
+
final displayName = user?['display_name']?.toString().trim() ?? '';
|
|
6125
|
+
if (displayName.isNotEmpty) return displayName;
|
|
6126
|
+
return user?['username']?.toString() ?? username.ifEmpty('NeoAgent User');
|
|
6127
|
+
}
|
|
6104
6128
|
|
|
6105
6129
|
String get modelIndicator {
|
|
6106
6130
|
if (defaultChatModel != 'auto') {
|
|
@@ -456,7 +456,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
456
456
|
onPressed: controller.isSavingSettings
|
|
457
457
|
? null
|
|
458
458
|
: () => controller.saveSettings(
|
|
459
|
-
headlessBrowser:
|
|
459
|
+
headlessBrowser: true,
|
|
460
460
|
browserBackend: _browserBackend == 'extension'
|
|
461
461
|
? 'extension'
|
|
462
462
|
: 'vm',
|
|
@@ -593,14 +593,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
593
593
|
),
|
|
594
594
|
),
|
|
595
595
|
const SizedBox(height: 12),
|
|
596
|
-
|
|
597
|
-
title: 'Run browser headless',
|
|
598
|
-
subtitle:
|
|
599
|
-
'Keep browser automation off-screen when visible windows are not needed.',
|
|
600
|
-
value: _headlessBrowser,
|
|
601
|
-
onChanged: (value) => setState(() => _headlessBrowser = value),
|
|
602
|
-
),
|
|
603
|
-
const SizedBox(height: 12),
|
|
596
|
+
|
|
604
597
|
DropdownButtonFormField<String>(
|
|
605
598
|
initialValue: _browserBackend,
|
|
606
599
|
decoration: const InputDecoration(
|
|
@@ -10,7 +10,7 @@ class BackendClient {
|
|
|
10
10
|
BackendClient({AppHttpClient? httpClient})
|
|
11
11
|
: _httpClient = httpClient ?? createAppHttpClient();
|
|
12
12
|
|
|
13
|
-
static const Duration _requestTimeout = Duration(seconds:
|
|
13
|
+
static const Duration _requestTimeout = Duration(seconds: 60);
|
|
14
14
|
|
|
15
15
|
final AppHttpClient _httpClient;
|
|
16
16
|
|
|
@@ -246,6 +246,15 @@ class BackendClient {
|
|
|
246
246
|
});
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
Future<Map<String, dynamic>> updateAccountDisplayName({
|
|
250
|
+
required String baseUrl,
|
|
251
|
+
required String displayName,
|
|
252
|
+
}) async {
|
|
253
|
+
return putMap(baseUrl, '/api/account/display-name', <String, dynamic>{
|
|
254
|
+
'displayName': displayName,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
249
258
|
Future<Map<String, dynamic>> beginTwoFactorSetup({
|
|
250
259
|
required String baseUrl,
|
|
251
260
|
required String currentPassword,
|
package/package.json
CHANGED
package/runtime/paths.js
CHANGED
|
@@ -135,7 +135,7 @@ function generateSecret(bytes = 32) {
|
|
|
135
135
|
return crypto.randomBytes(bytes).toString('hex');
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
function getDefaultVmBaseImageUrl(arch =
|
|
138
|
+
function getDefaultVmBaseImageUrl(arch = 'x64') {
|
|
139
139
|
return arch === 'arm64' ? DEFAULT_VM_BASE_IMAGE_URLS.arm64 : DEFAULT_VM_BASE_IMAGE_URLS.x64;
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -173,8 +173,9 @@ function ensureSecureRuntimeEnv({ envFile = ENV_FILE, env = process.env, logger
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
let vmBaseImageUrl = String(env.NEOAGENT_VM_BASE_IMAGE_URL || parsed.get('NEOAGENT_VM_BASE_IMAGE_URL') || '').trim();
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
const preferredVmBaseImageUrl = getDefaultVmBaseImageUrl();
|
|
177
|
+
if (!vmBaseImageUrl || /arm64|aarch64/i.test(vmBaseImageUrl)) {
|
|
178
|
+
vmBaseImageUrl = preferredVmBaseImageUrl;
|
|
178
179
|
env.NEOAGENT_VM_BASE_IMAGE_URL = vmBaseImageUrl;
|
|
179
180
|
upsertEnvValue(envFile, 'NEOAGENT_VM_BASE_IMAGE_URL', vmBaseImageUrl);
|
|
180
181
|
changes.push('NEOAGENT_VM_BASE_IMAGE_URL');
|
package/server/db/database.js
CHANGED
|
@@ -1589,6 +1589,19 @@ function migrateUsersOnboarding() {
|
|
|
1589
1589
|
}
|
|
1590
1590
|
migrateUsersOnboarding();
|
|
1591
1591
|
|
|
1592
|
+
function migrateUsersDisplayName() {
|
|
1593
|
+
try {
|
|
1594
|
+
const columns = db.pragma('table_info(users)');
|
|
1595
|
+
const hasDisplayNameCol = columns.some((c) => c.name === 'display_name');
|
|
1596
|
+
if (!hasDisplayNameCol) {
|
|
1597
|
+
db.exec('ALTER TABLE users ADD COLUMN display_name TEXT');
|
|
1598
|
+
}
|
|
1599
|
+
} catch (err) {
|
|
1600
|
+
console.warn('Could not add display_name column:', err.message);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
migrateUsersDisplayName();
|
|
1604
|
+
|
|
1592
1605
|
try {
|
|
1593
1606
|
db.exec(`
|
|
1594
1607
|
INSERT OR REPLACE INTO conversation_history_fts(rowid, content, role, user_id, agent_id, agent_run_id)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "neoagent-guest-agent",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Minimal guest runtime for NeoAgent VM browser, CLI, and Android services (uses puppeteer-core with playwright-chromium browser binaries)",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"express": "^4.21.2",
|
|
11
|
+
"node-pty": "^1.0.0",
|
|
12
|
+
"playwright-chromium": "^1.59.1",
|
|
13
|
+
"proper-lockfile": "^4.1.2",
|
|
14
|
+
"puppeteer-core": "^24.40.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/server/guest_agent.js
CHANGED
|
@@ -10,7 +10,19 @@ const { AndroidController } = require('./services/android/controller');
|
|
|
10
10
|
const { RUNTIME_HOME } = require('../runtime/paths');
|
|
11
11
|
|
|
12
12
|
const PORT = Number(process.env.NEOAGENT_GUEST_AGENT_PORT || 8421);
|
|
13
|
-
|
|
13
|
+
function resolveGuestToken() {
|
|
14
|
+
const raw = String(process.env.NEOAGENT_VM_GUEST_TOKEN || '').trim();
|
|
15
|
+
if (raw) return raw;
|
|
16
|
+
const b64 = String(process.env.NEOAGENT_VM_GUEST_TOKEN_B64 || '').trim();
|
|
17
|
+
if (!b64) return '';
|
|
18
|
+
try {
|
|
19
|
+
return Buffer.from(b64, 'base64').toString('utf8').trim();
|
|
20
|
+
} catch {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const AUTH_TOKEN = resolveGuestToken();
|
|
14
26
|
const FILE_ROOT = path.join(RUNTIME_HOME, 'guest-agent-files');
|
|
15
27
|
const MAX_APK_STREAM_BYTES = Number(process.env.NEOAGENT_GUEST_MAX_APK_STREAM_BYTES || 512 * 1024 * 1024);
|
|
16
28
|
|
package/server/index.js
CHANGED
|
@@ -210,8 +210,17 @@ function closeHttpServer(server, sockets, timeoutMs = 5000) {
|
|
|
210
210
|
});
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
function normalizeShutdownExitCode(value) {
|
|
214
|
+
const code = Number(value);
|
|
215
|
+
if (Number.isFinite(code)) {
|
|
216
|
+
return code;
|
|
217
|
+
}
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
213
221
|
async function shutdown(exitCode = 0) {
|
|
214
|
-
|
|
222
|
+
const normalizedExitCode = normalizeShutdownExitCode(exitCode);
|
|
223
|
+
shutdownExitCode = Math.max(shutdownExitCode, normalizedExitCode);
|
|
215
224
|
if (shuttingDown) return;
|
|
216
225
|
shuttingDown = true;
|
|
217
226
|
|
|
@@ -224,7 +233,7 @@ async function shutdown(exitCode = 0) {
|
|
|
224
233
|
]);
|
|
225
234
|
|
|
226
235
|
db.close();
|
|
227
|
-
process.exit(shutdownExitCode);
|
|
236
|
+
process.exit(normalizeShutdownExitCode(shutdownExitCode));
|
|
228
237
|
}
|
|
229
238
|
|
|
230
239
|
httpServer.listen(PORT, () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
af7040916c02919dd1df9c5d1b46d6a6
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "2814524395" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|