neoagent 3.2.1-beta.0 → 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 (175) hide show
  1. package/docs/agent-run-lifecycle.md +10 -0
  2. package/extensions/chrome-browser/background.mjs +318 -88
  3. package/extensions/chrome-browser/http.mjs +136 -0
  4. package/extensions/chrome-browser/protocol.mjs +654 -90
  5. package/flutter_app/lib/main_chat.dart +118 -739
  6. package/flutter_app/lib/main_controller.dart +111 -20
  7. package/flutter_app/lib/main_integrations.dart +607 -8
  8. package/flutter_app/lib/main_models.dart +3 -0
  9. package/flutter_app/lib/main_operations.dart +334 -321
  10. package/flutter_app/lib/main_security.dart +266 -112
  11. package/flutter_app/lib/main_settings.dart +4 -3
  12. package/flutter_app/lib/main_shared.dart +14 -11
  13. package/flutter_app/lib/src/backend_client.dart +78 -0
  14. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  15. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  16. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  17. package/landing/index.html +3 -1
  18. package/lib/manager.js +106 -89
  19. package/lib/schema_migrations.js +145 -13
  20. package/package.json +30 -15
  21. package/runtime/paths.js +49 -5
  22. package/server/db/database.js +4 -4
  23. package/server/guest-agent.cli.package.json +13 -0
  24. package/server/guest_agent.js +85 -40
  25. package/server/http/middleware.js +24 -0
  26. package/server/http/routes.js +11 -6
  27. package/server/public/.last_build_id +1 -1
  28. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  29. package/server/public/flutter_bootstrap.js +2 -2
  30. package/server/public/main.dart.js +73083 -72209
  31. package/server/routes/admin.js +1 -1
  32. package/server/routes/agents.js +35 -2
  33. package/server/routes/android.js +30 -34
  34. package/server/routes/browser.js +23 -15
  35. package/server/routes/desktop.js +18 -1
  36. package/server/routes/integrations.js +107 -1
  37. package/server/routes/memory.js +1 -0
  38. package/server/routes/settings.js +16 -5
  39. package/server/routes/social_reach.js +12 -3
  40. package/server/routes/social_video.js +4 -0
  41. package/server/services/agents/manager.js +1 -1
  42. package/server/services/ai/capabilityHealth.js +62 -96
  43. package/server/services/ai/compaction.js +7 -2
  44. package/server/services/ai/history.js +45 -6
  45. package/server/services/ai/integrated_tools/http_request.js +8 -0
  46. package/server/services/ai/loop/agent_engine_core.js +496 -166
  47. package/server/services/ai/loop/blank_recovery.js +5 -4
  48. package/server/services/ai/loop/callbacks.js +1 -0
  49. package/server/services/ai/loop/completion_judge.js +121 -5
  50. package/server/services/ai/loop/conversation_loop.js +620 -340
  51. package/server/services/ai/loop/lifecycle.js +108 -0
  52. package/server/services/ai/loop/messaging_delivery.js +129 -57
  53. package/server/services/ai/loop/model_call_guard.js +91 -0
  54. package/server/services/ai/loop/model_io.js +48 -56
  55. package/server/services/ai/loop/progress_classification.js +2 -0
  56. package/server/services/ai/loop/tool_dispatch.js +28 -8
  57. package/server/services/ai/loopPolicy.js +48 -21
  58. package/server/services/ai/messagingFallback.js +17 -17
  59. package/server/services/ai/model_discovery.js +227 -0
  60. package/server/services/ai/model_failure_cache.js +108 -0
  61. package/server/services/ai/model_identity.js +71 -0
  62. package/server/services/ai/models.js +68 -163
  63. package/server/services/ai/providerRetry.js +17 -59
  64. package/server/services/ai/provider_selector.js +166 -0
  65. package/server/services/ai/providers/anthropic.js +4 -4
  66. package/server/services/ai/providers/claudeCode.js +21 -33
  67. package/server/services/ai/providers/githubCopilot.js +41 -20
  68. package/server/services/ai/providers/google.js +135 -97
  69. package/server/services/ai/providers/grok.js +6 -5
  70. package/server/services/ai/providers/grokOauth.js +19 -27
  71. package/server/services/ai/providers/nvidia.js +12 -7
  72. package/server/services/ai/providers/ollama.js +114 -86
  73. package/server/services/ai/providers/ollama_stream.js +142 -0
  74. package/server/services/ai/providers/openai.js +41 -7
  75. package/server/services/ai/providers/openaiCodex.js +13 -4
  76. package/server/services/ai/providers/openrouter.js +31 -9
  77. package/server/services/ai/providers/provider_error.js +36 -0
  78. package/server/services/ai/settings.js +26 -2
  79. package/server/services/ai/systemPrompt.js +19 -12
  80. package/server/services/ai/taskAnalysis.js +58 -10
  81. package/server/services/ai/terminal_reply.js +18 -0
  82. package/server/services/ai/toolEvidence.js +350 -29
  83. package/server/services/ai/tools.js +190 -111
  84. package/server/services/android/controller.js +770 -237
  85. package/server/services/android/process.js +140 -0
  86. package/server/services/android/sdk_download.js +143 -0
  87. package/server/services/android/uia.js +6 -5
  88. package/server/services/artifacts/store.js +24 -0
  89. package/server/services/browser/controller.js +843 -385
  90. package/server/services/browser/extension/gateway.js +40 -16
  91. package/server/services/browser/extension/protocol.js +15 -1
  92. package/server/services/browser/extension/provider.js +71 -47
  93. package/server/services/browser/extension/registry.js +155 -34
  94. package/server/services/cli/executor.js +62 -9
  95. package/server/services/credentials/bitwarden_cli.js +322 -0
  96. package/server/services/credentials/broker.js +594 -0
  97. package/server/services/desktop/gateway.js +41 -4
  98. package/server/services/desktop/protocol.js +3 -0
  99. package/server/services/desktop/provider.js +39 -42
  100. package/server/services/desktop/registry.js +137 -52
  101. package/server/services/integrations/bitwarden/constants.js +14 -0
  102. package/server/services/integrations/bitwarden/provider.js +197 -0
  103. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  104. package/server/services/integrations/figma/provider.js +78 -12
  105. package/server/services/integrations/github/common.js +11 -6
  106. package/server/services/integrations/github/provider.js +52 -53
  107. package/server/services/integrations/google/provider.js +55 -19
  108. package/server/services/integrations/home_assistant/network.js +17 -20
  109. package/server/services/integrations/home_assistant/provider.js +7 -5
  110. package/server/services/integrations/home_assistant/tools.js +17 -5
  111. package/server/services/integrations/http.js +51 -0
  112. package/server/services/integrations/manager.js +159 -53
  113. package/server/services/integrations/microsoft/provider.js +80 -13
  114. package/server/services/integrations/neoarchive/provider.js +55 -29
  115. package/server/services/integrations/neorecall/client.js +17 -10
  116. package/server/services/integrations/neorecall/provider.js +20 -11
  117. package/server/services/integrations/notion/provider.js +16 -13
  118. package/server/services/integrations/oauth_provider.js +115 -51
  119. package/server/services/integrations/registry.js +2 -0
  120. package/server/services/integrations/slack/provider.js +98 -9
  121. package/server/services/integrations/spotify/provider.js +67 -71
  122. package/server/services/integrations/trello/provider.js +21 -7
  123. package/server/services/integrations/weather/provider.js +18 -12
  124. package/server/services/integrations/whatsapp/provider.js +76 -16
  125. package/server/services/manager.js +110 -1
  126. package/server/services/memory/embedding_index.js +20 -8
  127. package/server/services/memory/embeddings.js +151 -90
  128. package/server/services/memory/ingestion.js +50 -9
  129. package/server/services/memory/ingestion_documents.js +13 -3
  130. package/server/services/memory/manager.js +52 -19
  131. package/server/services/messaging/automation.js +85 -10
  132. package/server/services/messaging/formatting_guides.js +7 -4
  133. package/server/services/messaging/http_platforms.js +33 -13
  134. package/server/services/messaging/inbound_queue.js +78 -24
  135. package/server/services/messaging/inbound_store.js +224 -0
  136. package/server/services/messaging/manager.js +326 -51
  137. package/server/services/messaging/typing_keepalive.js +5 -2
  138. package/server/services/messaging/whatsapp.js +22 -14
  139. package/server/services/network/http.js +210 -0
  140. package/server/services/network/safe_request.js +307 -0
  141. package/server/services/runtime/backends/local-vm.js +227 -67
  142. package/server/services/runtime/docker-vm-manager.js +9 -0
  143. package/server/services/runtime/guest_bootstrap.js +30 -4
  144. package/server/services/runtime/guest_image.js +43 -12
  145. package/server/services/runtime/manager.js +77 -23
  146. package/server/services/runtime/validation.js +7 -6
  147. package/server/services/security/tool_categories.js +6 -0
  148. package/server/services/social_reach/channels/github.js +10 -4
  149. package/server/services/social_reach/channels/reddit.js +4 -4
  150. package/server/services/social_reach/channels/rss.js +2 -2
  151. package/server/services/social_reach/channels/social_video.js +12 -7
  152. package/server/services/social_reach/channels/v2ex.js +21 -8
  153. package/server/services/social_reach/channels/x.js +2 -2
  154. package/server/services/social_reach/channels/xueqiu.js +5 -5
  155. package/server/services/social_reach/service.js +9 -6
  156. package/server/services/social_reach/utils.js +65 -14
  157. package/server/services/social_video/service.js +160 -50
  158. package/server/services/tasks/integration_runtime.js +18 -8
  159. package/server/services/tasks/runtime.js +39 -4
  160. package/server/services/voice/agentBridge.js +17 -4
  161. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  162. package/server/services/voice/liveSession.js +31 -0
  163. package/server/services/voice/message.js +1 -1
  164. package/server/services/voice/openaiSpeech.js +33 -8
  165. package/server/services/voice/providers.js +233 -151
  166. package/server/services/voice/runtime.js +2 -2
  167. package/server/services/voice/runtimeManager.js +118 -20
  168. package/server/services/voice/turnRunner.js +6 -0
  169. package/server/services/wearable/firmware_manifest.js +51 -13
  170. package/server/services/wearable/service.js +1 -0
  171. package/server/utils/abort.js +96 -0
  172. package/server/utils/cloud-security.js +110 -3
  173. package/server/utils/files.js +31 -0
  174. package/server/utils/image_payload.js +95 -0
  175. 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) {
@@ -633,6 +654,111 @@ function migrateBilling(db) {
633
654
  `);
634
655
  }
635
656
 
657
+ function migrateAgentRunLifecycle(db) {
658
+ db.exec(`
659
+ CREATE TABLE IF NOT EXISTS agent_run_controls (
660
+ run_id TEXT PRIMARY KEY,
661
+ user_id INTEGER NOT NULL,
662
+ action TEXT NOT NULL CHECK(action IN ('pause', 'stop', 'interrupt')),
663
+ reason TEXT DEFAULT '',
664
+ requested_at TEXT DEFAULT (datetime('now')),
665
+ consumed_at TEXT,
666
+ FOREIGN KEY (run_id) REFERENCES agent_runs(id) ON DELETE CASCADE,
667
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
668
+ );
669
+
670
+ CREATE TABLE IF NOT EXISTS agent_run_checkpoints (
671
+ run_id TEXT PRIMARY KEY,
672
+ version INTEGER NOT NULL DEFAULT 1,
673
+ phase TEXT NOT NULL,
674
+ state_json TEXT NOT NULL DEFAULT '{}',
675
+ created_at TEXT DEFAULT (datetime('now')),
676
+ updated_at TEXT DEFAULT (datetime('now')),
677
+ FOREIGN KEY (run_id) REFERENCES agent_runs(id) ON DELETE CASCADE
678
+ );
679
+
680
+ CREATE INDEX IF NOT EXISTS idx_agent_run_controls_pending
681
+ ON agent_run_controls(user_id, consumed_at, requested_at);
682
+ `);
683
+ }
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
+
636
762
  function runSchemaMigrations(db) {
637
763
  removeRetiredCaptureData(db);
638
764
  migrateMemoryEmbeddingIndex(db);
@@ -647,6 +773,9 @@ function runSchemaMigrations(db) {
647
773
  migrateApprovalPersistence(db);
648
774
  migrateToolPoliciesAllowAlways(db);
649
775
  migrateBilling(db);
776
+ migrateAgentRunLifecycle(db);
777
+ migrateMessagingInboundJobs(db);
778
+ migrateCredentialBroker(db);
650
779
  }
651
780
 
652
781
  module.exports = {
@@ -664,5 +793,8 @@ module.exports = {
664
793
  migrateApprovalPersistence,
665
794
  migrateToolPoliciesAllowAlways,
666
795
  migrateBilling,
796
+ migrateAgentRunLifecycle,
797
+ migrateMessagingInboundJobs,
798
+ migrateCredentialBroker,
667
799
  runSchemaMigrations,
668
800
  };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "3.2.1-beta.0",
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
  };
@@ -1021,7 +1021,7 @@ function interruptStaleAgentRuns(reason = STALE_RUN_INTERRUPTED_ERROR) {
1021
1021
  const staleRunIds = db.prepare(
1022
1022
  `SELECT id
1023
1023
  FROM agent_runs
1024
- WHERE status = 'running'`
1024
+ WHERE status IN ('running', 'pausing', 'paused', 'resuming')`
1025
1025
  ).all().map((row) => row.id);
1026
1026
  const runsResult = db.prepare(
1027
1027
  `UPDATE agent_runs
@@ -1029,7 +1029,7 @@ function interruptStaleAgentRuns(reason = STALE_RUN_INTERRUPTED_ERROR) {
1029
1029
  error = COALESCE(NULLIF(error, ''), ?),
1030
1030
  updated_at = datetime('now'),
1031
1031
  completed_at = COALESCE(completed_at, datetime('now'))
1032
- WHERE status = 'running'`
1032
+ WHERE status IN ('running', 'pausing', 'paused', 'resuming')`
1033
1033
  ).run(normalizedReason);
1034
1034
 
1035
1035
  db.prepare(
@@ -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