neoagent 2.4.1-beta.9 → 2.4.1
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/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +36 -4
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -3
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +13 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +2 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +197 -42
- package/flutter_app/lib/main_devices.dart +436 -4
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +673 -871
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +46 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +42 -3
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88537 -85936
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +10 -2
- package/server/routes/stream.js +1 -1
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +78 -13
- package/server/services/browser/extension/registry.js +94 -9
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -53
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/streaming/android-stream.js +293 -40
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
package/server/routes/mcp.js
CHANGED
|
@@ -6,6 +6,7 @@ const { sanitizeError } = require('../utils/security');
|
|
|
6
6
|
const { validateRemoteMcpEndpoint } = require('../services/runtime/mcp');
|
|
7
7
|
const { getAgentIdFromRequest, isMainAgent, resolveAgentId } = require('../services/agents/manager');
|
|
8
8
|
const { resolvePublicBaseUrl } = require('../services/integrations/env');
|
|
9
|
+
const { consumeOAuthState } = require('../services/mcp/client_support');
|
|
9
10
|
|
|
10
11
|
const MCP_OAUTH_STATE_RE = /^(\d+)::[a-f0-9]{32}$/;
|
|
11
12
|
|
|
@@ -45,8 +46,11 @@ router.get('/', (req, res) => {
|
|
|
45
46
|
config: JSON.parse(s.config || '{}'),
|
|
46
47
|
agentId: s.agent_id || null,
|
|
47
48
|
enabled: !!s.enabled,
|
|
48
|
-
status: liveStatuses[s.id]?.status || 'stopped',
|
|
49
|
-
toolCount: liveStatuses[s.id]?.toolCount || 0
|
|
49
|
+
status: liveStatuses[s.id]?.status || s.status || 'stopped',
|
|
50
|
+
toolCount: liveStatuses[s.id]?.toolCount || 0,
|
|
51
|
+
error: liveStatuses[s.id]?.error || null,
|
|
52
|
+
consecutiveFails: liveStatuses[s.id]?.consecutiveFails || 0,
|
|
53
|
+
nextRetryAt: liveStatuses[s.id]?.nextRetryAt || null,
|
|
50
54
|
}));
|
|
51
55
|
|
|
52
56
|
res.json(result);
|
|
@@ -70,20 +74,26 @@ router.post('/', (req, res) => {
|
|
|
70
74
|
});
|
|
71
75
|
|
|
72
76
|
// Update an MCP server
|
|
73
|
-
router.put('/:id', (req, res) => {
|
|
77
|
+
router.put('/:id', async (req, res) => {
|
|
74
78
|
try {
|
|
75
79
|
const server = db.prepare('SELECT * FROM mcp_servers WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
|
|
76
80
|
if (!server) return res.status(404).json({ error: 'Server not found' });
|
|
77
81
|
|
|
82
|
+
const mcpClient = req.app.locals.mcpClient;
|
|
83
|
+
const wasLive = Boolean(mcpClient.getStatus(req.session.userId)[server.id]);
|
|
78
84
|
const { name, command, config, enabled } = req.body;
|
|
79
85
|
const agentId = (req.body.agentId !== undefined || req.body.agent_id !== undefined)
|
|
80
86
|
? resolveAgentId(req.session.userId, getAgentIdFromRequest(req))
|
|
81
87
|
: (server.agent_id || resolveAgentId(req.session.userId, null));
|
|
82
88
|
const endpoint = command ? validateRemoteMcpEndpoint(command) : server.command;
|
|
83
|
-
db.prepare(
|
|
89
|
+
db.prepare("UPDATE mcp_servers SET agent_id = ?, name = ?, command = ?, config = ?, enabled = ?, status = 'stopped' WHERE id = ?")
|
|
84
90
|
.run(agentId, name || server.name, endpoint, JSON.stringify(config || JSON.parse(server.config)), enabled !== undefined ? (enabled ? 1 : 0) : server.enabled, server.id);
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
if (wasLive) {
|
|
93
|
+
await mcpClient.stopServer(server.id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
res.json({ success: true, status: 'stopped' });
|
|
87
97
|
} catch (err) {
|
|
88
98
|
res.status(400).json({ error: sanitizeError(err) });
|
|
89
99
|
}
|
|
@@ -109,9 +119,8 @@ router.post('/:id/start', async (req, res) => {
|
|
|
109
119
|
|
|
110
120
|
const mcpClient = req.app.locals.mcpClient;
|
|
111
121
|
const result = await mcpClient.startServer(server.id, server.command, server.name, req.session.userId, { agentId: server.agent_id });
|
|
112
|
-
const tools = await mcpClient.listTools(server.id, req.session.userId);
|
|
113
122
|
|
|
114
|
-
res.json(
|
|
123
|
+
res.json(result);
|
|
115
124
|
} catch (err) {
|
|
116
125
|
if (err.message && err.message.startsWith('OAUTH_REDIRECT:')) {
|
|
117
126
|
const url = err.message.substring(15);
|
|
@@ -152,20 +161,27 @@ router.get('/:id/tools', async (req, res) => {
|
|
|
152
161
|
// OAuth Callback
|
|
153
162
|
router.get('/oauth/callback', async (req, res) => {
|
|
154
163
|
const { code, state, error } = req.query;
|
|
155
|
-
if (!state) return res.status(400).send('Missing state parameter');
|
|
156
|
-
if (!error && !code) return res.status(400).send('Missing code parameter');
|
|
157
|
-
if (error) return res.status(400).send(`OAuth Error: ${error}`);
|
|
164
|
+
if (!state) return res.status(400).type('text/plain').send('Missing state parameter');
|
|
158
165
|
|
|
159
166
|
const stateMatch = String(state).match(MCP_OAUTH_STATE_RE);
|
|
160
|
-
if (!stateMatch) return res.status(400).send('Invalid state format');
|
|
167
|
+
if (!stateMatch) return res.status(400).type('text/plain').send('Invalid state format');
|
|
161
168
|
|
|
162
169
|
const serverId = Number.parseInt(stateMatch[1], 10);
|
|
163
170
|
if (!Number.isInteger(serverId) || serverId <= 0) {
|
|
164
|
-
return res.status(400).send('Invalid state format');
|
|
171
|
+
return res.status(400).type('text/plain').send('Invalid state format');
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
const server = db.prepare('SELECT id FROM mcp_servers WHERE id = ? AND user_id = ?').get(serverId, req.session.userId);
|
|
168
|
-
if (!server) return res.status(404).send('Server not found');
|
|
175
|
+
if (!server) return res.status(404).type('text/plain').send('Server not found');
|
|
176
|
+
if (!error && !code) {
|
|
177
|
+
return res.status(400).type('text/plain').send('Missing code parameter');
|
|
178
|
+
}
|
|
179
|
+
if (!consumeOAuthState(serverId, state)) {
|
|
180
|
+
return res.status(400).type('text/plain').send('Invalid or expired OAuth state');
|
|
181
|
+
}
|
|
182
|
+
if (error) {
|
|
183
|
+
return res.status(400).type('text/plain').send(`OAuth error: ${String(error)}`);
|
|
184
|
+
}
|
|
169
185
|
|
|
170
186
|
const mcpClient = req.app.locals.mcpClient;
|
|
171
187
|
const trustedOrigin = JSON.stringify(getTrustedPostMessageOrigin(req));
|
package/server/routes/memory.js
CHANGED
|
@@ -111,13 +111,19 @@ router.get('/', (req, res) => {
|
|
|
111
111
|
const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
|
|
112
112
|
const coreMemory = { ...(mm.getCoreMemory(userId, { agentId }) || {}) };
|
|
113
113
|
delete coreMemory.active_context;
|
|
114
|
+
const knowledgeViews = mm.listKnowledgeViews(userId, { agentId, limit: 12 });
|
|
114
115
|
res.json({
|
|
115
116
|
agentId,
|
|
116
117
|
assistantBehaviorNotes: mm.getAssistantBehaviorNotes(userId, { agentId }),
|
|
117
118
|
assistantSelfState: mm.getAssistantSelfState(userId, { agentId }),
|
|
118
119
|
dailyLogs: mm.listDailyLogs(7, userId),
|
|
119
120
|
apiKeys: Object.keys(mm.readApiKeys(userId)),
|
|
120
|
-
coreMemory
|
|
121
|
+
coreMemory,
|
|
122
|
+
stats: mm.getMemoryStats(userId, { agentId }),
|
|
123
|
+
entities: mm.listEntities(userId, { agentId, limit: 12 }),
|
|
124
|
+
knowledgeViews,
|
|
125
|
+
ingestionOverview: mm.getIngestionOverview(userId, { agentId }),
|
|
126
|
+
recentKnowledgeChanges: mm.listRecentKnowledgeChanges(userId, { agentId, limit: 8 }),
|
|
121
127
|
});
|
|
122
128
|
});
|
|
123
129
|
|
|
@@ -157,6 +163,7 @@ router.get('/ingestion/status', (req, res) => {
|
|
|
157
163
|
jobs: mm.listIngestionJobs(userId, { agentId }),
|
|
158
164
|
recentChanges: mm.listRecentKnowledgeChanges(userId, { agentId }),
|
|
159
165
|
connections: ingestionService?.listConnectionStatuses?.(userId, { agentId }) || [],
|
|
166
|
+
service: ingestionService?.getStatus?.() || { state: 'unavailable' },
|
|
160
167
|
});
|
|
161
168
|
} catch (err) {
|
|
162
169
|
res.status(500).json({ error: sanitizeError(err) });
|
|
@@ -204,6 +211,21 @@ router.get('/knowledge-views', (req, res) => {
|
|
|
204
211
|
}
|
|
205
212
|
});
|
|
206
213
|
|
|
214
|
+
router.get('/entities', (req, res) => {
|
|
215
|
+
const mm = req.app.locals.memoryManager;
|
|
216
|
+
const userId = req.session.userId;
|
|
217
|
+
const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
|
|
218
|
+
try {
|
|
219
|
+
res.json(mm.listEntities(userId, {
|
|
220
|
+
agentId,
|
|
221
|
+
query: req.query.query || null,
|
|
222
|
+
limit: req.query.limit,
|
|
223
|
+
}));
|
|
224
|
+
} catch (err) {
|
|
225
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
207
229
|
router.post('/knowledge-views/materialize', (req, res) => {
|
|
208
230
|
const mm = req.app.locals.memoryManager;
|
|
209
231
|
const userId = req.session.userId;
|
|
@@ -80,6 +80,8 @@ router.post('/disconnect', async (req, res) => {
|
|
|
80
80
|
try {
|
|
81
81
|
const { platform } = req.body;
|
|
82
82
|
const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
83
|
+
if (!platform) return res.status(400).json({ error: 'Platform is required' });
|
|
84
|
+
|
|
83
85
|
const manager = req.app.locals.messagingManager;
|
|
84
86
|
const result = await manager.disconnectPlatform(req.session.userId, platform, { agentId });
|
|
85
87
|
res.json(result);
|
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
const express = require('express');
|
|
4
4
|
const db = require('../db/database');
|
|
5
|
+
const { buildFtsQuery } = require('../db/ftsQuery');
|
|
6
|
+
const { requireAuth } = require('../middleware/auth');
|
|
5
7
|
const { getErrorMessage } = require('../services/bootstrap_helpers');
|
|
6
8
|
|
|
7
9
|
const router = express.Router();
|
|
8
10
|
|
|
11
|
+
router.use(requireAuth);
|
|
12
|
+
|
|
9
13
|
router.get('/search', (req, res) => {
|
|
10
14
|
const { q, limit = 50, offset = 0 } = req.query;
|
|
11
|
-
|
|
12
|
-
if (!req.user || !req.user.id) {
|
|
13
|
-
return res.status(401).json({ error: 'Unauthorized' });
|
|
14
|
-
}
|
|
15
|
+
const userId = req.session.userId;
|
|
15
16
|
|
|
16
17
|
try {
|
|
17
18
|
let results = [];
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const ftsQuery = q ? buildFtsQuery(q) : null;
|
|
20
|
+
if (ftsQuery) {
|
|
21
|
+
// Full text search. buildFtsQuery sanitizes user input so FTS5 operator
|
|
22
|
+
// characters (hyphens, AND/OR/NOT) don't throw and 500 the request.
|
|
20
23
|
results = db.prepare(`
|
|
21
24
|
SELECT s.id, s.timestamp, s.app_name, s.text_content
|
|
22
25
|
FROM screen_history_fts fts
|
|
@@ -24,7 +27,10 @@ router.get('/search', (req, res) => {
|
|
|
24
27
|
WHERE screen_history_fts MATCH ? AND s.user_id = ?
|
|
25
28
|
ORDER BY s.timestamp DESC
|
|
26
29
|
LIMIT ? OFFSET ?
|
|
27
|
-
`).all(
|
|
30
|
+
`).all(ftsQuery, userId, Number(limit), Number(offset));
|
|
31
|
+
} else if (q) {
|
|
32
|
+
// Query had no usable search tokens — return no matches rather than error.
|
|
33
|
+
results = [];
|
|
28
34
|
} else {
|
|
29
35
|
// Recent history
|
|
30
36
|
results = db.prepare(`
|
|
@@ -33,7 +39,7 @@ router.get('/search', (req, res) => {
|
|
|
33
39
|
WHERE user_id = ?
|
|
34
40
|
ORDER BY timestamp DESC
|
|
35
41
|
LIMIT ? OFFSET ?
|
|
36
|
-
`).all(
|
|
42
|
+
`).all(userId, Number(limit), Number(offset));
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
res.json({ results });
|
|
@@ -562,8 +562,16 @@ router.delete('/:key', (req, res) => {
|
|
|
562
562
|
res.json({ success: true });
|
|
563
563
|
});
|
|
564
564
|
|
|
565
|
+
function requireAdminSession(req, res, next) {
|
|
566
|
+
if (req.session?.isAdmin === true) return next();
|
|
567
|
+
return res.status(403).json({
|
|
568
|
+
success: false,
|
|
569
|
+
error: 'Server updates are only available from the admin dashboard.',
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
565
573
|
// Trigger auto-update script
|
|
566
|
-
router.post('/update', requireAuth, updateTriggerLimiter, (req, res) => {
|
|
574
|
+
router.post('/update', requireAuth, requireAdminSession, updateTriggerLimiter, (req, res) => {
|
|
567
575
|
if (isManagedDeployment()) {
|
|
568
576
|
return res.status(403).json({
|
|
569
577
|
success: false,
|
|
@@ -611,7 +619,7 @@ router.post('/update', requireAuth, updateTriggerLimiter, (req, res) => {
|
|
|
611
619
|
res.json({ success: true, message: 'Update triggered', pid: child.pid });
|
|
612
620
|
});
|
|
613
621
|
|
|
614
|
-
router.put('/update/channel', (req, res) => {
|
|
622
|
+
router.put('/update/channel', requireAuth, requireAdminSession, (req, res) => {
|
|
615
623
|
if (isManagedDeployment()) {
|
|
616
624
|
return res.status(403).json({
|
|
617
625
|
success: false,
|
package/server/routes/stream.js
CHANGED
|
@@ -111,7 +111,7 @@ router.post('/start', async (req, res) => {
|
|
|
111
111
|
const platform = normalizePlatform(req.body?.platform);
|
|
112
112
|
const resolved = await resolveStartDeviceId(platform, req);
|
|
113
113
|
const deviceId = resolved.deviceId;
|
|
114
|
-
const fps = boundedInt(req.body?.fps, platform === 'android' ?
|
|
114
|
+
const fps = boundedInt(req.body?.fps, platform === 'android' ? 12 : 15, 1, platform === 'android' ? 30 : 20);
|
|
115
115
|
const quality = boundedInt(req.body?.quality, platform === 'android' ? 75 : 80, 30, 95);
|
|
116
116
|
const hub = streamHub(req);
|
|
117
117
|
await hub.stopStream(userId, platform, deviceId, 'restart');
|
|
@@ -13,10 +13,10 @@ router.post('/geofence', async (req, res) => {
|
|
|
13
13
|
const { label, latitude, longitude, radius_meters, action } = req.body;
|
|
14
14
|
|
|
15
15
|
try {
|
|
16
|
-
const userRow = db.prepare('SELECT id FROM users WHERE id = ?').get(req.
|
|
16
|
+
const userRow = db.prepare('SELECT id FROM users WHERE id = ?').get(req.session.userId);
|
|
17
17
|
if (!userRow) return res.status(401).json({ error: 'Unauthorized' });
|
|
18
18
|
|
|
19
|
-
console.log(`[Triggers] Geofence entered: ${label} by user ${req.
|
|
19
|
+
console.log(`[Triggers] Geofence entered: ${label} by user ${req.session.userId}`);
|
|
20
20
|
|
|
21
21
|
res.json({ success: true, message: 'Geofence trigger processed' });
|
|
22
22
|
} catch (err) {
|
|
@@ -29,14 +29,14 @@ router.post('/geofence', async (req, res) => {
|
|
|
29
29
|
Promise.resolve().then(() => {
|
|
30
30
|
const agentEngine = req.app.locals.agentEngine;
|
|
31
31
|
if (!agentEngine) return;
|
|
32
|
-
const defaultAgentId = db.prepare('SELECT id FROM agents WHERE user_id = ? ORDER BY is_default DESC LIMIT 1').get(req.
|
|
32
|
+
const defaultAgentId = db.prepare('SELECT id FROM agents WHERE user_id = ? ORDER BY is_default DESC LIMIT 1').get(req.session.userId)?.id;
|
|
33
33
|
if (!defaultAgentId) return;
|
|
34
34
|
const prompt = [
|
|
35
35
|
`A geofence event was triggered.`,
|
|
36
36
|
`Location label: ${label || 'unknown'}`,
|
|
37
37
|
action ? `Suggested action: ${action}` : 'Check if there are any active reminders or tasks related to this location.',
|
|
38
38
|
].join('\n');
|
|
39
|
-
return agentEngine.run(req.
|
|
39
|
+
return agentEngine.run(req.session.userId, prompt, {
|
|
40
40
|
agentId: defaultAgentId,
|
|
41
41
|
triggerSource: 'tasks',
|
|
42
42
|
context: { source: 'geofence', label, latitude, longitude, radius_meters },
|
|
@@ -4,7 +4,8 @@ const { requireAuth } = require('../middleware/auth');
|
|
|
4
4
|
const { sanitizeError } = require('../utils/security');
|
|
5
5
|
const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
|
|
6
6
|
const { TurnCoordinator } = require('../services/voice/turnCoordinator');
|
|
7
|
-
const { normalizeVoiceSynthesisOptions } = require('../services/voice/providers');
|
|
7
|
+
const { normalizeVoiceSynthesisOptions, transcribeVoiceInput } = require('../services/voice/providers');
|
|
8
|
+
const { writeTempAudioFile, removeTempFile } = require('../services/voice/liveAudio');
|
|
8
9
|
const { runVoiceTranscriptTurn } = require('../services/voice/turnRunner');
|
|
9
10
|
|
|
10
11
|
const router = express.Router();
|
|
@@ -135,4 +136,38 @@ router.post('/respond', async (req, res) => {
|
|
|
135
136
|
}
|
|
136
137
|
});
|
|
137
138
|
|
|
139
|
+
router.post('/transcribe', async (req, res) => {
|
|
140
|
+
try {
|
|
141
|
+
const audioBase64 = String(req.body?.audioBase64 || '').trim();
|
|
142
|
+
const mimeType = String(req.body?.mimeType || 'audio/pcm;rate=16000;channels=1').trim();
|
|
143
|
+
|
|
144
|
+
if (!audioBase64) {
|
|
145
|
+
return res.status(400).json({ error: 'audioBase64 is required.' });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const approxBytes = (audioBase64.length * 3) / 4;
|
|
149
|
+
if (approxBytes > 25 * 1024 * 1024) {
|
|
150
|
+
return res.status(400).json({ error: 'Audio exceeds maximum size of 25MB.' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const audioBytes = Buffer.from(audioBase64, 'base64');
|
|
154
|
+
const { filePath, mimeType: fileMimeType } = await writeTempAudioFile(audioBytes, mimeType);
|
|
155
|
+
let transcript = '';
|
|
156
|
+
try {
|
|
157
|
+
transcript = await transcribeVoiceInput(filePath, {
|
|
158
|
+
mimeType: fileMimeType,
|
|
159
|
+
userId: req.session.userId,
|
|
160
|
+
timeoutMs: 30000,
|
|
161
|
+
});
|
|
162
|
+
} finally {
|
|
163
|
+
await removeTempFile(filePath);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return res.json({ transcript: String(transcript || '').trim() });
|
|
167
|
+
} catch (err) {
|
|
168
|
+
const message = sanitizeError(err);
|
|
169
|
+
return res.status(500).json({ error: message });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
138
173
|
module.exports = router;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const { requireAuth } = require('../middleware/auth');
|
|
5
|
+
const { sanitizeError } = require('../utils/security');
|
|
6
|
+
|
|
7
|
+
const router = express.Router();
|
|
8
|
+
|
|
9
|
+
const MAX_PATH_LENGTH = 2048;
|
|
10
|
+
const MAX_EDIT_BYTES = 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
router.use(requireAuth);
|
|
13
|
+
|
|
14
|
+
function badRequest(message) {
|
|
15
|
+
const error = new Error(message);
|
|
16
|
+
error.status = 400;
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseWorkspacePath(value, fallback = '.') {
|
|
21
|
+
const normalized = String(value ?? fallback).trim();
|
|
22
|
+
if (normalized.length > MAX_PATH_LENGTH) {
|
|
23
|
+
throw badRequest('path is too long.');
|
|
24
|
+
}
|
|
25
|
+
return normalized || fallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getWorkspace(req) {
|
|
29
|
+
const workspace = req.app?.locals?.workspaceManager;
|
|
30
|
+
if (!workspace) {
|
|
31
|
+
const error = new Error('Workspace service is unavailable.');
|
|
32
|
+
error.status = 503;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
return workspace;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handleWorkspaceAction(req, res, action) {
|
|
39
|
+
try {
|
|
40
|
+
const result = action(getWorkspace(req));
|
|
41
|
+
return res.json(result);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return res.status(err.status || 500).json({ error: sanitizeError(err), code: err.code || null });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
router.get('/files', (req, res) => handleWorkspaceAction(req, res, (workspace) =>
|
|
48
|
+
workspace.listExplorerDirectory(req.session.userId, {
|
|
49
|
+
path: parseWorkspacePath(req.query?.path, '.'),
|
|
50
|
+
})));
|
|
51
|
+
|
|
52
|
+
router.get('/files/content', (req, res) => handleWorkspaceAction(req, res, (workspace) =>
|
|
53
|
+
workspace.readExplorerFile(req.session.userId, {
|
|
54
|
+
path: parseWorkspacePath(req.query?.path, ''),
|
|
55
|
+
maxBytes: MAX_EDIT_BYTES,
|
|
56
|
+
})));
|
|
57
|
+
|
|
58
|
+
router.put('/files/content', (req, res) => handleWorkspaceAction(req, res, (workspace) => {
|
|
59
|
+
const content = String(req.body?.content ?? '');
|
|
60
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_EDIT_BYTES) {
|
|
61
|
+
throw badRequest('content is too large.');
|
|
62
|
+
}
|
|
63
|
+
return workspace.writeExplorerFile(req.session.userId, {
|
|
64
|
+
path: parseWorkspacePath(req.body?.path, ''),
|
|
65
|
+
content,
|
|
66
|
+
});
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
router.get('/files/download', (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
const workspace = getWorkspace(req);
|
|
72
|
+
const file = workspace.getExplorerDownload(req.session.userId, {
|
|
73
|
+
path: parseWorkspacePath(req.query?.path, ''),
|
|
74
|
+
});
|
|
75
|
+
res.setHeader('Cache-Control', 'private, no-store');
|
|
76
|
+
return res.download(file.absolutePath, file.filename, (err) => {
|
|
77
|
+
if (!err || res.headersSent) return;
|
|
78
|
+
const status = err.code === 'ENOENT' || err.code === 'EACCES' ? 404 : 500;
|
|
79
|
+
res.status(status).json({ error: status === 404 ? 'File not found' : 'Failed to download file' });
|
|
80
|
+
});
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return res.status(err.status || 500).json({ error: sanitizeError(err), code: err.code || null });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
module.exports = router;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const bcrypt = require('bcrypt');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { generateSecret, generateURI, verifySync } = require('otplib');
|
|
6
|
+
const db = require('../../db/database');
|
|
7
|
+
const { encryptValue, decryptValue } = require('../integrations/secrets');
|
|
8
|
+
|
|
9
|
+
const TOTP_OPTS = { strategy: 'totp', digits: 6, period: 30, epochTolerance: 30 };
|
|
10
|
+
const RECOVERY_ALPHA = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
11
|
+
|
|
12
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function getRow() {
|
|
15
|
+
return db.prepare('SELECT * FROM admin_two_factor WHERE id = 1').get() || null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeCode(v) {
|
|
19
|
+
return String(v || '').trim().replace(/\s+/g, '').toUpperCase();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function verifyTotpCode(secret, code) {
|
|
23
|
+
const token = String(code || '').trim().replace(/\s+/g, '');
|
|
24
|
+
if (!/^\d{6}$/.test(token)) return false;
|
|
25
|
+
return verifySync({ ...TOTP_OPTS, secret, token })?.valid === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeRecoveryCode() {
|
|
29
|
+
let raw = '';
|
|
30
|
+
while (raw.length < 10) raw += RECOVERY_ALPHA[crypto.randomInt(RECOVERY_ALPHA.length)];
|
|
31
|
+
return `${raw.slice(0, 5)}-${raw.slice(5)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function getStatus() {
|
|
37
|
+
const row = getRow();
|
|
38
|
+
const codesLeft = db.prepare('SELECT COUNT(*) AS n FROM admin_recovery_codes WHERE used_at IS NULL').get().n;
|
|
39
|
+
return {
|
|
40
|
+
enabled: row?.enabled === 1,
|
|
41
|
+
pending: Boolean(row?.pending_secret),
|
|
42
|
+
enabledAt: row?.enabled_at || null,
|
|
43
|
+
recoveryCodesRemaining: codesLeft,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function beginSetup() {
|
|
48
|
+
if (!String(process.env.SESSION_SECRET || '').trim()) {
|
|
49
|
+
throw Object.assign(new Error('SESSION_SECRET must be configured before enabling 2FA'), { statusCode: 500 });
|
|
50
|
+
}
|
|
51
|
+
const secret = generateSecret();
|
|
52
|
+
const otpauthUrl = generateURI({ ...TOTP_OPTS, issuer: 'NeoAgent Admin', label: 'admin', secret });
|
|
53
|
+
db.prepare(`
|
|
54
|
+
INSERT INTO admin_two_factor (id, pending_secret, enabled, created_at, updated_at)
|
|
55
|
+
VALUES (1, ?, COALESCE((SELECT enabled FROM admin_two_factor WHERE id = 1), 0), datetime('now'), datetime('now'))
|
|
56
|
+
ON CONFLICT(id) DO UPDATE SET pending_secret = excluded.pending_secret, updated_at = datetime('now')
|
|
57
|
+
`).run(encryptValue(secret));
|
|
58
|
+
return { otpauthUrl, manualKey: secret };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function enable(code) {
|
|
62
|
+
const row = getRow();
|
|
63
|
+
if (!row?.pending_secret) throw Object.assign(new Error('No 2FA setup pending'), { statusCode: 400 });
|
|
64
|
+
const secret = decryptValue(row.pending_secret);
|
|
65
|
+
if (!verifyTotpCode(secret, code)) throw Object.assign(new Error('Invalid code — try again'), { statusCode: 401 });
|
|
66
|
+
|
|
67
|
+
const codes = Array.from({ length: 10 }, makeRecoveryCode);
|
|
68
|
+
const hashes = await Promise.all(codes.map((c) => bcrypt.hash(normalizeCode(c).replace(/-/g, ''), 12)));
|
|
69
|
+
|
|
70
|
+
db.transaction(() => {
|
|
71
|
+
db.prepare('DELETE FROM admin_recovery_codes').run();
|
|
72
|
+
const ins = db.prepare("INSERT INTO admin_recovery_codes (code_hash, created_at) VALUES (?, datetime('now'))");
|
|
73
|
+
for (const h of hashes) ins.run(h);
|
|
74
|
+
db.prepare(`
|
|
75
|
+
UPDATE admin_two_factor
|
|
76
|
+
SET secret = pending_secret, pending_secret = NULL, enabled = 1,
|
|
77
|
+
enabled_at = datetime('now'), updated_at = datetime('now')
|
|
78
|
+
WHERE id = 1
|
|
79
|
+
`).run();
|
|
80
|
+
})();
|
|
81
|
+
|
|
82
|
+
return { recoveryCodes: codes };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Verifies a TOTP code or recovery code.
|
|
87
|
+
* Returns true if valid (or if 2FA is not enabled).
|
|
88
|
+
*/
|
|
89
|
+
async function verifyCode(code) {
|
|
90
|
+
const row = getRow();
|
|
91
|
+
if (!row?.enabled || !row.secret) return true; // 2FA not configured
|
|
92
|
+
|
|
93
|
+
const secret = decryptValue(row.secret);
|
|
94
|
+
if (verifyTotpCode(secret, code)) return true;
|
|
95
|
+
|
|
96
|
+
// Try recovery code
|
|
97
|
+
const normalized = normalizeCode(String(code || '')).replace(/-/g, '');
|
|
98
|
+
if (normalized.length < 6) return false;
|
|
99
|
+
const rows = db.prepare('SELECT id, code_hash FROM admin_recovery_codes WHERE used_at IS NULL ORDER BY id').all();
|
|
100
|
+
for (const r of rows) {
|
|
101
|
+
if (await bcrypt.compare(normalized, r.code_hash)) {
|
|
102
|
+
db.prepare("UPDATE admin_recovery_codes SET used_at = datetime('now') WHERE id = ?").run(r.id);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function disable(code) {
|
|
110
|
+
const row = getRow();
|
|
111
|
+
if (!row?.enabled) return;
|
|
112
|
+
if (!await verifyCode(code)) throw Object.assign(new Error('Invalid 2FA code'), { statusCode: 401 });
|
|
113
|
+
db.transaction(() => {
|
|
114
|
+
db.prepare(`UPDATE admin_two_factor SET enabled = 0, secret = NULL, pending_secret = NULL,
|
|
115
|
+
enabled_at = NULL, updated_at = datetime('now') WHERE id = 1`).run();
|
|
116
|
+
db.prepare('DELETE FROM admin_recovery_codes').run();
|
|
117
|
+
})();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function regenerateCodes(code) {
|
|
121
|
+
if (!await verifyCode(code)) throw Object.assign(new Error('Invalid 2FA code'), { statusCode: 401 });
|
|
122
|
+
const codes = Array.from({ length: 10 }, makeRecoveryCode);
|
|
123
|
+
const hashes = await Promise.all(codes.map((c) => bcrypt.hash(normalizeCode(c).replace(/-/g, ''), 12)));
|
|
124
|
+
db.transaction(() => {
|
|
125
|
+
db.prepare('DELETE FROM admin_recovery_codes').run();
|
|
126
|
+
const ins = db.prepare("INSERT INTO admin_recovery_codes (code_hash, created_at) VALUES (?, datetime('now'))");
|
|
127
|
+
for (const h of hashes) ins.run(h);
|
|
128
|
+
})();
|
|
129
|
+
return { recoveryCodes: codes };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { getStatus, beginSetup, enable, verifyCode, disable, regenerateCodes };
|
|
@@ -113,7 +113,12 @@ function evaluatePasswordStrength(password, context = {}) {
|
|
|
113
113
|
label,
|
|
114
114
|
length,
|
|
115
115
|
hasMinimumLength: length >= MIN_PASSWORD_LENGTH,
|
|
116
|
-
isAcceptable: length >= MIN_PASSWORD_LENGTH
|
|
116
|
+
isAcceptable: length >= MIN_PASSWORD_LENGTH
|
|
117
|
+
&& score >= MIN_PASSWORD_SCORE
|
|
118
|
+
&& !containsPersonalInfo
|
|
119
|
+
&& !usesCommonPattern
|
|
120
|
+
&& !sequential
|
|
121
|
+
&& !repeatedRuns,
|
|
117
122
|
feedback,
|
|
118
123
|
};
|
|
119
124
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Decides whether the agent may accept a `task_complete` signal, balancing the
|
|
4
|
+
// model's self-reported confidence against the confidence the run requires.
|
|
5
|
+
|
|
6
|
+
function normalizeCompletionConfidence(value) {
|
|
7
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
8
|
+
if (normalized === 'high' || normalized === 'medium' || normalized === 'low') return normalized;
|
|
9
|
+
return 'medium';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function completionConfidenceRank(value) {
|
|
13
|
+
const normalized = normalizeCompletionConfidence(value);
|
|
14
|
+
if (normalized === 'high') return 3;
|
|
15
|
+
if (normalized === 'medium') return 2;
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function shouldAcceptTaskComplete({ confidence, requiredConfidence, iteration, maxIterations }) {
|
|
20
|
+
const required = normalizeCompletionConfidence(requiredConfidence || 'medium');
|
|
21
|
+
const actual = normalizeCompletionConfidence(confidence || 'medium');
|
|
22
|
+
if (completionConfidenceRank(actual) >= completionConfidenceRank(required)) {
|
|
23
|
+
return { accept: true, reason: '' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const iterationsRemaining = Math.max(0, Number(maxIterations || 0) - Number(iteration || 0));
|
|
27
|
+
if (iterationsRemaining <= 1) {
|
|
28
|
+
return {
|
|
29
|
+
accept: true,
|
|
30
|
+
reason: `Accepted ${actual}-confidence completion at the iteration limit; final verifier will calibrate the answer.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
accept: false,
|
|
36
|
+
reason: `Completion confidence "${actual}" is below required "${required}". Continue with verification, recovery, or a narrower truthful result before completing.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
normalizeCompletionConfidence,
|
|
42
|
+
completionConfidenceRank,
|
|
43
|
+
shouldAcceptTaskComplete,
|
|
44
|
+
};
|