neoagent 3.2.1-beta.1 → 3.2.1-beta.10

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.
Files changed (172) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +654 -90
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +111 -20
  6. package/flutter_app/lib/main_integrations.dart +607 -8
  7. package/flutter_app/lib/main_models.dart +3 -0
  8. package/flutter_app/lib/main_operations.dart +334 -321
  9. package/flutter_app/lib/main_security.dart +266 -112
  10. package/flutter_app/lib/main_settings.dart +4 -3
  11. package/flutter_app/lib/main_shared.dart +14 -11
  12. package/flutter_app/lib/src/backend_client.dart +78 -0
  13. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  14. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  15. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  16. package/landing/index.html +3 -1
  17. package/lib/manager.js +106 -89
  18. package/lib/schema_migrations.js +115 -13
  19. package/package.json +30 -15
  20. package/runtime/paths.js +49 -5
  21. package/server/db/database.js +2 -2
  22. package/server/guest-agent.cli.package.json +13 -0
  23. package/server/guest_agent.js +85 -40
  24. package/server/http/middleware.js +24 -0
  25. package/server/http/routes.js +11 -6
  26. package/server/public/.last_build_id +1 -1
  27. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  28. package/server/public/flutter_bootstrap.js +2 -2
  29. package/server/public/main.dart.js +73083 -72209
  30. package/server/routes/admin.js +1 -1
  31. package/server/routes/android.js +30 -34
  32. package/server/routes/browser.js +23 -15
  33. package/server/routes/desktop.js +18 -1
  34. package/server/routes/integrations.js +107 -1
  35. package/server/routes/memory.js +1 -0
  36. package/server/routes/settings.js +16 -5
  37. package/server/routes/social_reach.js +12 -3
  38. package/server/routes/social_video.js +4 -0
  39. package/server/services/agents/manager.js +1 -1
  40. package/server/services/ai/capabilityHealth.js +62 -96
  41. package/server/services/ai/compaction.js +7 -2
  42. package/server/services/ai/history.js +45 -6
  43. package/server/services/ai/integrated_tools/http_request.js +8 -0
  44. package/server/services/ai/loop/agent_engine_core.js +390 -162
  45. package/server/services/ai/loop/blank_recovery.js +5 -4
  46. package/server/services/ai/loop/callbacks.js +1 -0
  47. package/server/services/ai/loop/completion_judge.js +121 -5
  48. package/server/services/ai/loop/conversation_loop.js +510 -332
  49. package/server/services/ai/loop/messaging_delivery.js +129 -57
  50. package/server/services/ai/loop/model_call_guard.js +91 -0
  51. package/server/services/ai/loop/model_io.js +20 -45
  52. package/server/services/ai/loop/progress_classification.js +2 -0
  53. package/server/services/ai/loop/tool_dispatch.js +19 -8
  54. package/server/services/ai/loopPolicy.js +48 -21
  55. package/server/services/ai/messagingFallback.js +17 -17
  56. package/server/services/ai/model_discovery.js +227 -0
  57. package/server/services/ai/model_failure_cache.js +108 -0
  58. package/server/services/ai/model_identity.js +71 -0
  59. package/server/services/ai/models.js +68 -163
  60. package/server/services/ai/providerRetry.js +17 -59
  61. package/server/services/ai/provider_selector.js +166 -0
  62. package/server/services/ai/providers/anthropic.js +2 -2
  63. package/server/services/ai/providers/claudeCode.js +21 -33
  64. package/server/services/ai/providers/githubCopilot.js +41 -20
  65. package/server/services/ai/providers/google.js +135 -97
  66. package/server/services/ai/providers/grok.js +4 -3
  67. package/server/services/ai/providers/grokOauth.js +19 -27
  68. package/server/services/ai/providers/nvidia.js +10 -5
  69. package/server/services/ai/providers/ollama.js +111 -84
  70. package/server/services/ai/providers/ollama_stream.js +142 -0
  71. package/server/services/ai/providers/openai.js +39 -5
  72. package/server/services/ai/providers/openaiCodex.js +11 -4
  73. package/server/services/ai/providers/openrouter.js +29 -7
  74. package/server/services/ai/providers/provider_error.js +36 -0
  75. package/server/services/ai/settings.js +26 -2
  76. package/server/services/ai/systemPrompt.js +19 -12
  77. package/server/services/ai/taskAnalysis.js +58 -10
  78. package/server/services/ai/terminal_reply.js +18 -0
  79. package/server/services/ai/toolEvidence.js +350 -29
  80. package/server/services/ai/tools.js +190 -111
  81. package/server/services/android/controller.js +770 -237
  82. package/server/services/android/process.js +140 -0
  83. package/server/services/android/sdk_download.js +143 -0
  84. package/server/services/android/uia.js +6 -5
  85. package/server/services/artifacts/store.js +24 -0
  86. package/server/services/browser/controller.js +843 -385
  87. package/server/services/browser/extension/gateway.js +40 -16
  88. package/server/services/browser/extension/protocol.js +15 -1
  89. package/server/services/browser/extension/provider.js +71 -47
  90. package/server/services/browser/extension/registry.js +155 -34
  91. package/server/services/cli/executor.js +62 -9
  92. package/server/services/credentials/bitwarden_cli.js +322 -0
  93. package/server/services/credentials/broker.js +594 -0
  94. package/server/services/desktop/gateway.js +41 -4
  95. package/server/services/desktop/protocol.js +3 -0
  96. package/server/services/desktop/provider.js +39 -42
  97. package/server/services/desktop/registry.js +137 -52
  98. package/server/services/integrations/bitwarden/constants.js +14 -0
  99. package/server/services/integrations/bitwarden/provider.js +197 -0
  100. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  101. package/server/services/integrations/figma/provider.js +78 -12
  102. package/server/services/integrations/github/common.js +11 -6
  103. package/server/services/integrations/github/provider.js +52 -53
  104. package/server/services/integrations/google/provider.js +55 -19
  105. package/server/services/integrations/home_assistant/network.js +17 -20
  106. package/server/services/integrations/home_assistant/provider.js +7 -5
  107. package/server/services/integrations/home_assistant/tools.js +17 -5
  108. package/server/services/integrations/http.js +51 -0
  109. package/server/services/integrations/manager.js +159 -53
  110. package/server/services/integrations/microsoft/provider.js +80 -13
  111. package/server/services/integrations/neoarchive/provider.js +55 -29
  112. package/server/services/integrations/neorecall/client.js +17 -10
  113. package/server/services/integrations/neorecall/provider.js +20 -11
  114. package/server/services/integrations/notion/provider.js +16 -13
  115. package/server/services/integrations/oauth_provider.js +115 -51
  116. package/server/services/integrations/registry.js +2 -0
  117. package/server/services/integrations/slack/provider.js +98 -9
  118. package/server/services/integrations/spotify/provider.js +67 -71
  119. package/server/services/integrations/trello/provider.js +21 -7
  120. package/server/services/integrations/weather/provider.js +18 -12
  121. package/server/services/integrations/whatsapp/provider.js +76 -16
  122. package/server/services/manager.js +110 -1
  123. package/server/services/memory/embedding_index.js +20 -8
  124. package/server/services/memory/embeddings.js +151 -90
  125. package/server/services/memory/ingestion.js +50 -9
  126. package/server/services/memory/ingestion_documents.js +13 -3
  127. package/server/services/memory/manager.js +52 -19
  128. package/server/services/messaging/automation.js +85 -10
  129. package/server/services/messaging/formatting_guides.js +7 -4
  130. package/server/services/messaging/http_platforms.js +33 -13
  131. package/server/services/messaging/inbound_queue.js +78 -24
  132. package/server/services/messaging/inbound_store.js +224 -0
  133. package/server/services/messaging/manager.js +326 -51
  134. package/server/services/messaging/typing_keepalive.js +5 -2
  135. package/server/services/messaging/whatsapp.js +22 -14
  136. package/server/services/network/http.js +210 -0
  137. package/server/services/network/safe_request.js +307 -0
  138. package/server/services/runtime/backends/local-vm.js +227 -67
  139. package/server/services/runtime/docker-vm-manager.js +9 -0
  140. package/server/services/runtime/guest_bootstrap.js +30 -4
  141. package/server/services/runtime/guest_image.js +43 -12
  142. package/server/services/runtime/manager.js +77 -23
  143. package/server/services/runtime/validation.js +7 -6
  144. package/server/services/security/tool_categories.js +6 -0
  145. package/server/services/social_reach/channels/github.js +10 -4
  146. package/server/services/social_reach/channels/reddit.js +4 -4
  147. package/server/services/social_reach/channels/rss.js +2 -2
  148. package/server/services/social_reach/channels/social_video.js +12 -7
  149. package/server/services/social_reach/channels/v2ex.js +21 -8
  150. package/server/services/social_reach/channels/x.js +2 -2
  151. package/server/services/social_reach/channels/xueqiu.js +5 -5
  152. package/server/services/social_reach/service.js +9 -6
  153. package/server/services/social_reach/utils.js +65 -14
  154. package/server/services/social_video/service.js +160 -50
  155. package/server/services/tasks/integration_runtime.js +18 -8
  156. package/server/services/tasks/runtime.js +39 -4
  157. package/server/services/voice/agentBridge.js +17 -4
  158. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  159. package/server/services/voice/liveSession.js +31 -0
  160. package/server/services/voice/message.js +1 -1
  161. package/server/services/voice/openaiSpeech.js +33 -8
  162. package/server/services/voice/providers.js +233 -151
  163. package/server/services/voice/runtime.js +2 -2
  164. package/server/services/voice/runtimeManager.js +118 -20
  165. package/server/services/voice/turnRunner.js +6 -0
  166. package/server/services/wearable/firmware_manifest.js +51 -13
  167. package/server/services/wearable/service.js +1 -0
  168. package/server/utils/abort.js +96 -0
  169. package/server/utils/cloud-security.js +110 -3
  170. package/server/utils/files.js +31 -0
  171. package/server/utils/image_payload.js +95 -0
  172. package/server/utils/retry.js +107 -0
@@ -30,20 +30,41 @@ function migrateMemoryEmbeddingIndex(db) {
30
30
  .map((column) => column.name),
31
31
  );
32
32
  if (!columns.has('index_version')) {
33
- db.exec('ALTER TABLE memory_embedding_bands ADD COLUMN index_version INTEGER');
34
- }
35
- db.exec(`
36
- DROP INDEX IF EXISTS idx_memory_embedding_bands_lookup;
37
- CREATE INDEX idx_memory_embedding_bands_lookup
38
- ON memory_embedding_bands(
39
- user_id,
40
- agent_id,
41
- dimension,
42
- index_version,
43
- band_index,
44
- band_value
33
+ try {
34
+ db.exec('ALTER TABLE memory_embedding_bands ADD COLUMN index_version INTEGER');
35
+ } catch (error) {
36
+ const refreshedColumns = new Set(
37
+ db.prepare('PRAGMA table_info(memory_embedding_bands)').all()
38
+ .map((column) => column.name),
45
39
  );
46
- `);
40
+ if (!refreshedColumns.has('index_version')) throw error;
41
+ }
42
+ }
43
+ const expectedIndexColumns = [
44
+ 'user_id',
45
+ 'agent_id',
46
+ 'dimension',
47
+ 'index_version',
48
+ 'band_index',
49
+ 'band_value',
50
+ ];
51
+ const indexColumns = db.prepare(
52
+ 'PRAGMA index_info(idx_memory_embedding_bands_lookup)',
53
+ ).all().map((column) => column.name);
54
+ if (indexColumns.join('\0') !== expectedIndexColumns.join('\0')) {
55
+ db.exec(`
56
+ DROP INDEX IF EXISTS idx_memory_embedding_bands_lookup;
57
+ CREATE INDEX IF NOT EXISTS idx_memory_embedding_bands_lookup
58
+ ON memory_embedding_bands(
59
+ user_id,
60
+ agent_id,
61
+ dimension,
62
+ index_version,
63
+ band_index,
64
+ band_value
65
+ );
66
+ `);
67
+ }
47
68
  }
48
69
 
49
70
  function removeRetiredCaptureData(db) {
@@ -661,6 +682,83 @@ function migrateAgentRunLifecycle(db) {
661
682
  `);
662
683
  }
663
684
 
685
+ function migrateMessagingInboundJobs(db) {
686
+ db.exec(`
687
+ CREATE TABLE IF NOT EXISTS messaging_inbound_jobs (
688
+ id TEXT PRIMARY KEY,
689
+ message_id INTEGER NOT NULL UNIQUE,
690
+ user_id INTEGER NOT NULL,
691
+ agent_id TEXT,
692
+ platform TEXT NOT NULL,
693
+ platform_msg_id TEXT,
694
+ platform_chat_id TEXT,
695
+ payload_json TEXT NOT NULL,
696
+ status TEXT NOT NULL DEFAULT 'pending'
697
+ CHECK(status IN ('pending', 'processing', 'completed', 'failed')),
698
+ attempts INTEGER NOT NULL DEFAULT 0,
699
+ run_id TEXT,
700
+ last_error TEXT,
701
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
702
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
703
+ completed_at TEXT,
704
+ FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
705
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
706
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE SET NULL
707
+ );
708
+
709
+ CREATE INDEX IF NOT EXISTS idx_messaging_inbound_jobs_pending
710
+ ON messaging_inbound_jobs(status, platform, user_id, agent_id, created_at);
711
+ CREATE INDEX IF NOT EXISTS idx_messaging_inbound_jobs_run
712
+ ON messaging_inbound_jobs(run_id);
713
+ `);
714
+ }
715
+
716
+ function migrateCredentialBroker(db) {
717
+ db.exec(`
718
+ CREATE TABLE IF NOT EXISTS credential_bindings (
719
+ id TEXT PRIMARY KEY,
720
+ user_id INTEGER NOT NULL,
721
+ agent_id TEXT NOT NULL,
722
+ provider_key TEXT NOT NULL,
723
+ connection_id INTEGER NOT NULL,
724
+ alias TEXT NOT NULL,
725
+ usage_type TEXT NOT NULL CHECK(usage_type IN ('browser', 'http')),
726
+ item_ref_encrypted TEXT NOT NULL,
727
+ field_config_encrypted TEXT NOT NULL,
728
+ target_config_json TEXT NOT NULL DEFAULT '{}',
729
+ created_at TEXT DEFAULT (datetime('now')),
730
+ updated_at TEXT DEFAULT (datetime('now')),
731
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
732
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE,
733
+ FOREIGN KEY (connection_id) REFERENCES integration_connections(id) ON DELETE CASCADE,
734
+ UNIQUE(user_id, agent_id, alias)
735
+ );
736
+
737
+ CREATE INDEX IF NOT EXISTS idx_credential_bindings_scope
738
+ ON credential_bindings(user_id, agent_id, provider_key, usage_type);
739
+
740
+ CREATE TABLE IF NOT EXISTS credential_usage_audit (
741
+ id TEXT PRIMARY KEY,
742
+ user_id INTEGER NOT NULL,
743
+ agent_id TEXT NOT NULL,
744
+ run_id TEXT,
745
+ binding_id TEXT,
746
+ operation TEXT NOT NULL,
747
+ target TEXT,
748
+ outcome TEXT NOT NULL,
749
+ error_code TEXT,
750
+ created_at TEXT DEFAULT (datetime('now')),
751
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
752
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE,
753
+ FOREIGN KEY (run_id) REFERENCES agent_runs(id) ON DELETE SET NULL,
754
+ FOREIGN KEY (binding_id) REFERENCES credential_bindings(id) ON DELETE SET NULL
755
+ );
756
+
757
+ CREATE INDEX IF NOT EXISTS idx_credential_usage_audit_scope
758
+ ON credential_usage_audit(user_id, agent_id, created_at DESC);
759
+ `);
760
+ }
761
+
664
762
  function runSchemaMigrations(db) {
665
763
  removeRetiredCaptureData(db);
666
764
  migrateMemoryEmbeddingIndex(db);
@@ -676,6 +774,8 @@ function runSchemaMigrations(db) {
676
774
  migrateToolPoliciesAllowAlways(db);
677
775
  migrateBilling(db);
678
776
  migrateAgentRunLifecycle(db);
777
+ migrateMessagingInboundJobs(db);
778
+ migrateCredentialBroker(db);
679
779
  }
680
780
 
681
781
  module.exports = {
@@ -694,5 +794,7 @@ module.exports = {
694
794
  migrateToolPoliciesAllowAlways,
695
795
  migrateBilling,
696
796
  migrateAgentRunLifecycle,
797
+ migrateMessagingInboundJobs,
798
+ migrateCredentialBroker,
697
799
  runSchemaMigrations,
698
800
  };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "3.2.1-beta.1",
3
+ "version": "3.2.1-beta.10",
4
4
  "description": "Self-hosted AI agent for long-running tasks, automation, messaging, device control, and local memory",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
7
7
  "engines": {
8
- "node": ">=18.0.0"
8
+ "node": ">=20.19.0"
9
9
  },
10
10
  "bin": {
11
11
  "neoagent": "bin/neoagent.js"
@@ -67,10 +67,11 @@
67
67
  "homepage": "https://github.com/NeoLabs-Systems/NeoAgent#readme",
68
68
  "dependencies": {
69
69
  "@anthropic-ai/sdk": "^0.39.0",
70
- "@google/generative-ai": "^0.24.0",
71
- "@modelcontextprotocol/sdk": "^1.12.1",
70
+ "@bitwarden/cli": "^2026.6.0",
71
+ "@google/genai": "^2.13.0",
72
+ "@modelcontextprotocol/sdk": "^1.29.0",
72
73
  "@remotion/cli": "^4.0.459",
73
- "@slidev/cli": "^52.15.2",
74
+ "@slidev/cli": "^52.18.0",
74
75
  "@slidev/theme-default": "^0.25.0",
75
76
  "baileys": "^6.7.21",
76
77
  "bcrypt": "^6.0.0",
@@ -78,7 +79,7 @@
78
79
  "better-sqlite3-session-store": "^0.1.0",
79
80
  "cheerio": "^1.0.0-rc.12",
80
81
  "cors": "^2.8.5",
81
- "discord.js": "^14.25.1",
82
+ "discord.js": "^14.27.0",
82
83
  "dotenv": "^16.4.7",
83
84
  "express": "^4.21.2",
84
85
  "express-rate-limit": "^7.5.0",
@@ -86,10 +87,10 @@
86
87
  "geoip-lite": "^1.4.10",
87
88
  "googleapis": "^150.0.1",
88
89
  "helmet": "^8.0.0",
89
- "multer": "^1.4.5-lts.1",
90
- "node-cron": "^3.0.3",
90
+ "multer": "^2.2.0",
91
+ "node-cron": "^4.6.0",
91
92
  "node-pty": "^1.0.0",
92
- "nodemailer": "^8.0.5",
93
+ "nodemailer": "^9.0.3",
93
94
  "openai": "^4.85.4",
94
95
  "otplib": "^13.4.0",
95
96
  "playwright-chromium": "^1.59.1",
@@ -99,7 +100,7 @@
99
100
  "puppeteer-extra-plugin-stealth": "^2.11.2",
100
101
  "qrcode": "^1.5.4",
101
102
  "remotion": "^4.0.459",
102
- "sharp": "^0.34.5",
103
+ "sharp": "^0.35.3",
103
104
  "socket.io": "^4.8.1",
104
105
  "stripe": "^22.2.1",
105
106
  "telegraf": "^4.16.3",
@@ -109,19 +110,33 @@
109
110
  "ws": "^8.21.0"
110
111
  },
111
112
  "overrides": {
113
+ "@bitwarden/cli": {
114
+ "form-data": "4.0.6",
115
+ "inquirer": "8.2.7",
116
+ "multer": "2.2.0"
117
+ },
118
+ "@hono/node-server": "^2.0.11",
112
119
  "axios": "^1.15.2",
113
120
  "basic-ftp": "^5.3.0",
121
+ "dompurify": "^3.4.12",
122
+ "fast-uri": "^3.1.4",
123
+ "form-data": "4.0.6",
114
124
  "follow-redirects": "^1.16.0",
115
- "hono": "^4.12.14",
116
- "protobufjs": "^7.5.5",
125
+ "hono": "^4.12.31",
126
+ "ip-address": "^10.2.0",
127
+ "linkify-it": "^5.0.2",
128
+ "protobufjs": "^7.6.5",
117
129
  "serialize-javascript": "^7.0.5",
118
- "undici": "^6.24.0",
130
+ "tmp": "0.2.7",
131
+ "undici": "^6.27.0",
132
+ "uuid": "$uuid",
119
133
  "webpackbar": "^7.0.0",
120
134
  "ws": "^8.21.0"
121
135
  },
122
136
  "devDependencies": {
123
- "@docusaurus/core": "3.10.0",
124
- "@docusaurus/preset-classic": "3.10.0",
137
+ "@docusaurus/core": "3.10.2",
138
+ "@docusaurus/preset-classic": "3.10.2",
139
+ "@types/node": "^20.19.0",
125
140
  "autocannon": "^7.15.0",
126
141
  "react": "18.3.1",
127
142
  "react-dom": "18.3.1",
package/runtime/paths.js CHANGED
@@ -109,26 +109,69 @@ function readEnvFileRaw(envFile = ENV_FILE) {
109
109
  }
110
110
  }
111
111
 
112
+ function writeEnvFileAtomic(envFile, content) {
113
+ const resolved = path.resolve(envFile);
114
+ const directory = path.dirname(resolved);
115
+ const temporary = path.join(
116
+ directory,
117
+ `.${path.basename(resolved)}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`,
118
+ );
119
+ fs.mkdirSync(directory, { recursive: true });
120
+ let descriptor = null;
121
+ try {
122
+ descriptor = fs.openSync(temporary, 'wx', 0o600);
123
+ fs.writeFileSync(descriptor, content, 'utf8');
124
+ fs.fsyncSync(descriptor);
125
+ fs.closeSync(descriptor);
126
+ descriptor = null;
127
+ fs.renameSync(temporary, resolved);
128
+ } finally {
129
+ if (descriptor !== null) {
130
+ try { fs.closeSync(descriptor); } catch {}
131
+ }
132
+ try { fs.rmSync(temporary, { force: true }); } catch {}
133
+ }
134
+ }
135
+
136
+ function normalizeEnvKey(key) {
137
+ const normalized = String(key).replace(/[\r\n]/g, '');
138
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(normalized)) {
139
+ throw new Error(`Invalid environment variable name: ${normalized || '(empty)'}`);
140
+ }
141
+ return normalized;
142
+ }
143
+
112
144
  function upsertEnvValue(envFile, key, value) {
145
+ const safeKey = normalizeEnvKey(key);
146
+ const safeValue = String(value).replace(/[\r\n]/g, '');
113
147
  const raw = readEnvFileRaw(envFile);
114
148
  const lines = raw ? raw.split('\n') : [];
115
149
  let replaced = false;
116
150
 
117
151
  for (let i = 0; i < lines.length; i++) {
118
- if (lines[i].startsWith(`${key}=`)) {
119
- lines[i] = `${key}=${value}`;
152
+ if (lines[i].startsWith(`${safeKey}=`)) {
153
+ lines[i] = `${safeKey}=${safeValue}`;
120
154
  replaced = true;
121
155
  break;
122
156
  }
123
157
  }
124
158
 
125
159
  if (!replaced) {
126
- lines.push(`${key}=${value}`);
160
+ lines.push(`${safeKey}=${safeValue}`);
127
161
  }
128
162
 
129
163
  const output = lines.filter((line, idx, arr) => idx !== arr.length - 1 || line !== '').join('\n') + '\n';
130
- fs.mkdirSync(path.dirname(envFile), { recursive: true });
131
- fs.writeFileSync(envFile, output, { mode: 0o600 });
164
+ writeEnvFileAtomic(envFile, output);
165
+ }
166
+
167
+ function removeEnvValue(envFile, key) {
168
+ const safeKey = normalizeEnvKey(key);
169
+ const raw = readEnvFileRaw(envFile);
170
+ if (!raw) return false;
171
+ const lines = raw.split('\n').filter((line) => !line.startsWith(`${safeKey}=`));
172
+ const output = lines.filter((line, idx, arr) => idx !== arr.length - 1 || line !== '').join('\n') + '\n';
173
+ writeEnvFileAtomic(envFile, output);
174
+ return true;
132
175
  }
133
176
 
134
177
  function generateSecret(bytes = 32) {
@@ -247,6 +290,7 @@ module.exports = {
247
290
  ensureSecureRuntimeEnv,
248
291
  getDefaultVmBaseImageUrl,
249
292
  migrateLegacyRuntime,
293
+ removeEnvValue,
250
294
  upsertEnvValue,
251
295
  readEnvFileRaw,
252
296
  };
@@ -1311,8 +1311,8 @@ function getMainAgentId(userId) {
1311
1311
  id, user_id, slug, display_name, description, responsibilities, instructions,
1312
1312
  is_default, can_delegate, can_be_delegated_to, delegate_targets_json
1313
1313
  )
1314
- VALUES (?, ?, 'main', 'Main', 'Default personal assistant and fallback agent.',
1315
- 'Handle general requests and delegate to specialist agents only when there is a clear match.',
1314
+ VALUES (?, ?, 'main', 'Main', 'Default personal AI contact and fallback agent.',
1315
+ 'Handle general requests like a proactive favorite contact and delegate to specialist agents only when there is a clear match.',
1316
1316
  '', 1, 1, 0, '[]'
1317
1317
  )`
1318
1318
  ).run(id, userId);
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "neoagent-guest-agent-cli",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "description": "Guest runtime for isolated NeoAgent CLI services",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "dependencies": {
10
+ "express": "^4.21.2",
11
+ "proper-lockfile": "^4.1.2"
12
+ }
13
+ }
@@ -4,7 +4,6 @@ const express = require('express');
4
4
  const fs = require('fs');
5
5
  const os = require('os');
6
6
  const path = require('path');
7
- const { CLIExecutor } = require('./services/cli/executor');
8
7
  const { RUNTIME_HOME, DATA_DIR } = require('../runtime/paths');
9
8
 
10
9
  const PORT = Number(process.env.NEOAGENT_GUEST_AGENT_PORT || 8421);
@@ -21,8 +20,9 @@ function resolveGuestToken() {
21
20
  }
22
21
 
23
22
  const AUTH_TOKEN = resolveGuestToken();
24
- const GUEST_PROFILE = String(process.env.NEOAGENT_GUEST_PROFILE || 'browser_cli').trim() === 'android'
25
- ? 'android'
23
+ const RAW_GUEST_PROFILE = String(process.env.NEOAGENT_GUEST_PROFILE || 'browser_cli').trim();
24
+ const GUEST_PROFILE = ['android', 'browser', 'cli', 'browser_cli'].includes(RAW_GUEST_PROFILE)
25
+ ? RAW_GUEST_PROFILE
26
26
  : 'browser_cli';
27
27
  const FILE_ROOT = path.join(RUNTIME_HOME, 'guest-agent-files');
28
28
  const MAX_APK_STREAM_BYTES = Number(process.env.NEOAGENT_GUEST_MAX_APK_STREAM_BYTES || 512 * 1024 * 1024);
@@ -32,8 +32,10 @@ fs.mkdirSync(FILE_ROOT, { recursive: true });
32
32
  const app = express();
33
33
  app.use(express.json({ limit: '100mb' }));
34
34
 
35
- const cliExecutor = new CLIExecutor();
36
- const browserController = GUEST_PROFILE === 'browser_cli'
35
+ const cliExecutor = ['cli', 'browser_cli', 'android'].includes(GUEST_PROFILE)
36
+ ? new (require('./services/cli/executor').CLIExecutor)()
37
+ : null;
38
+ const browserController = ['browser', 'browser_cli'].includes(GUEST_PROFILE)
37
39
  ? new (require('./services/browser/controller').BrowserController)({ runtimeBackend: 'vm' })
38
40
  : null;
39
41
  const androidController = GUEST_PROFILE === 'android'
@@ -105,6 +107,26 @@ async function handle(res, work) {
105
107
  }
106
108
  }
107
109
 
110
+ async function handleRequest(req, res, work) {
111
+ const controller = new AbortController();
112
+ const abort = () => {
113
+ if (!res.writableEnded) controller.abort('Guest runtime request disconnected.');
114
+ };
115
+ req.once('aborted', abort);
116
+ res.once('close', abort);
117
+ try {
118
+ const result = await work(controller.signal);
119
+ if (!res.headersSent && !res.writableEnded) res.json(result);
120
+ } catch (err) {
121
+ if (!res.headersSent && !res.writableEnded) {
122
+ res.status(controller.signal.aborted ? 499 : 500).json({ error: sanitizeError(err) });
123
+ }
124
+ } finally {
125
+ req.removeListener('aborted', abort);
126
+ res.removeListener('close', abort);
127
+ }
128
+ }
129
+
108
130
  app.use(requireToken);
109
131
 
110
132
  app.get('/health', (_req, res) => {
@@ -118,36 +140,40 @@ app.get('/health', (_req, res) => {
118
140
  });
119
141
 
120
142
  app.post('/exec', async (req, res) => {
121
- await handle(res, async () => {
143
+ await handleRequest(req, res, async (signal) => {
144
+ const executor = requireCapability(cliExecutor, 'cli');
122
145
  const command = String(req.body?.command || '').trim();
123
146
  if (!command) {
124
147
  return { error: 'command is required' };
125
148
  }
126
149
  if (req.body?.pty) {
127
- return cliExecutor.executeInteractive(command, req.body?.inputs || [], {
150
+ return executor.executeInteractive(command, req.body?.inputs || [], {
128
151
  cwd: req.body?.cwd,
129
152
  timeout: req.body?.timeout,
153
+ signal,
130
154
  });
131
155
  }
132
- return cliExecutor.execute(command, {
156
+ return executor.execute(command, {
133
157
  cwd: req.body?.cwd,
134
158
  timeout: req.body?.timeout,
135
159
  stdinInput: req.body?.stdin_input,
160
+ signal,
136
161
  });
137
162
  });
138
163
  });
139
164
 
140
165
  app.post('/exec/kill', async (req, res) => {
141
166
  await handle(res, async () => {
167
+ const executor = requireCapability(cliExecutor, 'cli');
142
168
  const pid = Number(req.body?.pid);
143
169
  if (!Number.isInteger(pid) || pid <= 0) {
144
170
  return { error: 'pid is required' };
145
171
  }
146
172
  const reason = String(req.body?.reason || 'aborted').trim();
147
- if (typeof cliExecutor.isManaged === 'function' && !cliExecutor.isManaged(pid)) {
173
+ if (typeof executor.isManaged === 'function' && !executor.isManaged(pid)) {
148
174
  return { error: 'pid not managed' };
149
175
  }
150
- const killed = cliExecutor.kill(pid, reason || 'aborted');
176
+ const killed = executor.kill(pid, reason || 'aborted');
151
177
  return { success: killed, pid };
152
178
  });
153
179
  });
@@ -185,6 +211,22 @@ app.get('/browser/status', async (_req, res) => {
185
211
  };
186
212
  });
187
213
  });
214
+
215
+ app.use('/browser', (req, res, next) => {
216
+ if (!browserController?.hasProtectedCredentialFill?.()) return next();
217
+ const allowed = new Set([
218
+ '/status',
219
+ '/credential-submit',
220
+ '/credential-cancel',
221
+ '/close',
222
+ ]);
223
+ if (allowed.has(req.path)) return next();
224
+ return res.status(423).json({
225
+ error: 'Browser control is paused while a protected credential fill is active. Submit or cancel it first.',
226
+ code: 'PROTECTED_CREDENTIAL_FILL_ACTIVE',
227
+ });
228
+ });
229
+
188
230
  function requireCapability(controller, name) {
189
231
  if (!controller) {
190
232
  throw new Error(`${name} runtime is unavailable in this guest profile.`);
@@ -192,43 +234,46 @@ function requireCapability(controller, name) {
192
234
  return controller;
193
235
  }
194
236
 
195
- app.post('/browser/launch', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').launch(req.body || {})));
196
- app.post('/browser/navigate', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').navigate(req.body?.url, req.body || {})));
197
- app.post('/browser/screenshot', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').screenshot(req.body || {})));
198
- app.post('/browser/screenshot-jpeg', async (req, res) => handle(res, async () => {
199
- const jpeg = await requireCapability(browserController, 'browser').screenshotJpeg(req.body?.quality, req.body || {});
237
+ app.post('/browser/launch', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').launch({ ...(req.body || {}), signal })));
238
+ app.post('/browser/navigate', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').navigate(req.body?.url, { ...(req.body || {}), signal })));
239
+ app.post('/browser/screenshot', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').screenshot({ ...(req.body || {}), signal })));
240
+ app.post('/browser/screenshot-jpeg', async (req, res) => handleRequest(req, res, async (signal) => {
241
+ const jpeg = await requireCapability(browserController, 'browser').screenshotJpeg(req.body?.quality, { ...(req.body || {}), signal });
200
242
  return {
201
243
  contentType: 'image/jpeg',
202
244
  contentBase64: Buffer.from(jpeg).toString('base64'),
203
245
  };
204
246
  }));
205
- app.post('/browser/click', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').click(req.body?.selector, req.body?.text, req.body?.screenshot !== false)));
206
- app.post('/browser/click-point', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').clickPoint(req.body?.x, req.body?.y, req.body?.screenshot !== false)));
207
- app.post('/browser/fill', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').type(req.body?.selector, String(req.body?.value ?? req.body?.text ?? ''), req.body || {})));
208
- app.post('/browser/type-text', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').typeText(String(req.body?.text || ''), req.body || {})));
209
- app.post('/browser/press-key', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').pressKey(req.body?.key, req.body?.screenshot !== false)));
210
- app.post('/browser/scroll', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').scroll(req.body?.deltaX ?? 0, req.body?.deltaY ?? 0, req.body?.screenshot !== false)));
211
- app.post('/browser/extract', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').extractContent(req.body || {})));
212
- app.post('/browser/execute', async (req, res) => handle(res, () => requireCapability(browserController, 'browser').executeJS(req.body?.code)));
247
+ app.post('/browser/click', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').click(req.body?.selector, req.body?.text, req.body?.screenshot !== false, { signal })));
248
+ app.post('/browser/click-point', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').clickPoint(req.body?.x, req.body?.y, req.body?.screenshot !== false, { signal })));
249
+ app.post('/browser/fill', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').type(req.body?.selector, String(req.body?.value ?? req.body?.text ?? ''), { ...(req.body || {}), signal })));
250
+ app.post('/browser/credential-fill', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').fillCredential(req.body || {}, { signal })));
251
+ app.post('/browser/credential-submit', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').submitProtectedCredential(req.body?.protectedFillId, { signal })));
252
+ app.post('/browser/credential-cancel', async (req, res) => handleRequest(req, res, () => requireCapability(browserController, 'browser').cancelProtectedCredential(req.body?.protectedFillId)));
253
+ app.post('/browser/type-text', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').typeText(String(req.body?.text || ''), { ...(req.body || {}), signal })));
254
+ app.post('/browser/press-key', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').pressKey(req.body?.key, req.body?.screenshot !== false, { signal })));
255
+ app.post('/browser/scroll', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').scroll(req.body?.deltaX ?? 0, req.body?.deltaY ?? 0, req.body?.screenshot !== false, { signal })));
256
+ app.post('/browser/extract', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').extractContent({ ...(req.body || {}), signal })));
257
+ app.post('/browser/execute', async (req, res) => handleRequest(req, res, (signal) => requireCapability(browserController, 'browser').executeJS(req.body?.code, { signal })));
213
258
  app.post('/browser/close', async (_req, res) => handle(res, () => requireCapability(browserController, 'browser').closeBrowser().then(() => ({ success: true }))));
214
259
 
215
260
  app.get('/android/status', async (_req, res) => handle(res, () => requireCapability(androidController, 'android').getStatus()));
216
- app.post('/android/start', async (req, res) => handle(res, () => requireCapability(androidController, 'android').requestStartEmulator(req.body || {})));
217
- app.post('/android/stop', async (_req, res) => handle(res, () => requireCapability(androidController, 'android').stopEmulator()));
218
- app.get('/android/devices', async (_req, res) => handle(res, async () => ({ devices: await requireCapability(androidController, 'android').listDevices() })));
219
- app.post('/android/screenshot', async (req, res) => handle(res, () => requireCapability(androidController, 'android').screenshot(req.body || {})));
220
- app.post('/android/observe', async (req, res) => handle(res, () => requireCapability(androidController, 'android').observe(req.body || {})));
221
- app.post('/android/ui-dump', async (req, res) => handle(res, () => requireCapability(androidController, 'android').dumpUi(req.body || {})));
222
- app.get('/android/apps', async (req, res) => handle(res, () => requireCapability(androidController, 'android').listApps({ includeSystem: req.query.includeSystem === 'true' })));
223
- app.post('/android/open-app', async (req, res) => handle(res, () => requireCapability(androidController, 'android').openApp(req.body || {})));
224
- app.post('/android/open-intent', async (req, res) => handle(res, () => requireCapability(androidController, 'android').openIntent(req.body || {})));
225
- app.post('/android/tap', async (req, res) => handle(res, () => requireCapability(androidController, 'android').tap(req.body || {})));
226
- app.post('/android/long-press', async (req, res) => handle(res, () => requireCapability(androidController, 'android').longPress(req.body || {})));
227
- app.post('/android/type', async (req, res) => handle(res, () => requireCapability(androidController, 'android').type(req.body || {})));
228
- app.post('/android/swipe', async (req, res) => handle(res, () => requireCapability(androidController, 'android').swipe(req.body || {})));
229
- app.post('/android/press-key', async (req, res) => handle(res, () => requireCapability(androidController, 'android').pressKey(req.body || {})));
230
- app.post('/android/wait-for', async (req, res) => handle(res, () => requireCapability(androidController, 'android').waitFor(req.body || {})));
231
- app.post('/android/shell', async (req, res) => handle(res, () => requireCapability(androidController, 'android').shell(req.body || {})));
261
+ app.post('/android/start', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').requestStartEmulator({ ...(req.body || {}), signal })));
262
+ app.post('/android/stop', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').stopEmulator({ signal })));
263
+ app.get('/android/devices', async (req, res) => handleRequest(req, res, async (signal) => ({ devices: await requireCapability(androidController, 'android').listDevices({ signal }) })));
264
+ app.post('/android/screenshot', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').screenshot({ ...(req.body || {}), signal })));
265
+ app.post('/android/observe', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').observe({ ...(req.body || {}), signal })));
266
+ app.post('/android/ui-dump', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').dumpUi({ ...(req.body || {}), signal })));
267
+ app.get('/android/apps', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').listApps({ includeSystem: req.query.includeSystem === 'true', signal })));
268
+ app.post('/android/open-app', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').openApp({ ...(req.body || {}), signal })));
269
+ app.post('/android/open-intent', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').openIntent({ ...(req.body || {}), signal })));
270
+ app.post('/android/tap', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').tap({ ...(req.body || {}), signal })));
271
+ app.post('/android/long-press', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').longPress({ ...(req.body || {}), signal })));
272
+ app.post('/android/type', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').type({ ...(req.body || {}), signal })));
273
+ app.post('/android/swipe', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').swipe({ ...(req.body || {}), signal })));
274
+ app.post('/android/press-key', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').pressKey({ ...(req.body || {}), signal })));
275
+ app.post('/android/wait-for', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').waitFor({ ...(req.body || {}), signal })));
276
+ app.post('/android/shell', async (req, res) => handleRequest(req, res, (signal) => requireCapability(androidController, 'android').shell({ ...(req.body || {}), signal })));
232
277
  app.post('/android/install-apk', async (req, res) => {
233
278
  await handle(res, async () => {
234
279
  requireCapability(androidController, 'android');
@@ -325,7 +370,7 @@ async function shutdown() {
325
370
  } catch (err) {
326
371
  console.warn('[GuestAgent] Failed to close android controller:', err?.message);
327
372
  }
328
- cliExecutor.killAll('shutdown');
373
+ cliExecutor?.killAll?.('shutdown');
329
374
  server.close(() => process.exit(0));
330
375
  }
331
376
 
@@ -145,6 +145,28 @@ function createSessionMiddleware({ secureCookies, trustProxy }) {
145
145
  });
146
146
  }
147
147
 
148
+ function attachRequestSignal(req, res, next) {
149
+ const controller = new AbortController();
150
+ const abort = () => {
151
+ if (!res.writableEnded && !controller.signal.aborted) {
152
+ const error = new Error('HTTP client disconnected.');
153
+ error.name = 'AbortError';
154
+ error.code = 'ABORT_ERR';
155
+ controller.abort(error);
156
+ }
157
+ };
158
+ const cleanup = () => {
159
+ req.removeListener('aborted', abort);
160
+ res.removeListener('close', abort);
161
+ res.removeListener('finish', cleanup);
162
+ };
163
+ req.signal = controller.signal;
164
+ req.once('aborted', abort);
165
+ res.once('close', abort);
166
+ res.once('finish', cleanup);
167
+ next();
168
+ }
169
+
148
170
  function applyHttpMiddleware(app, { secureCookies, trustProxy, sessionMiddleware, validateOrigin }) {
149
171
  const jsonBody = require('express').json({
150
172
  limit: '10mb',
@@ -179,6 +201,7 @@ function applyHttpMiddleware(app, { secureCookies, trustProxy, sessionMiddleware
179
201
  }
180
202
 
181
203
  app.use(helmet(buildHelmetOptions({ secureCookies })));
204
+ app.use(attachRequestSignal);
182
205
  app.use(
183
206
  cors((req, callback) => {
184
207
  const requestPath = `${req.originalUrl || req.url || req.path || ''}`.split('?')[0];
@@ -255,6 +278,7 @@ function applyHttpMiddleware(app, { secureCookies, trustProxy, sessionMiddleware
255
278
  }
256
279
 
257
280
  module.exports = {
281
+ attachRequestSignal,
258
282
  applyHttpMiddleware,
259
283
  createSessionMiddleware
260
284
  };