kiku-agent 1.3.2 → 1.3.3
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/README.md +69 -12
- package/package.json +2 -1
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/modules/evaluation/runner.js +4 -14
- package/server/agent/eval/modules/evaluation/scoring.js +15 -3
- package/server/agent/eval/modules/server/evalApi.js +5 -5
- package/server/agent/eval/modules/ui/evalUi.js +7 -4
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentFollowUps.test.js +128 -2
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +1 -1
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/bilibiliEmbed.test.js +45 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/clarifyingQuestions.test.js +33 -0
- package/server/agent/tests/cliAutomation.test.js +38 -1
- package/server/agent/tests/cloudProviders.test.js +91 -3
- package/server/agent/tests/commands.test.js +92 -3
- package/server/agent/tests/contextIntelligence.test.js +50 -3
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/envCommand.test.js +69 -0
- package/server/agent/tests/evalMetrics.test.js +8 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/hybridWebSearch.test.js +2 -0
- package/server/agent/tests/lanDiscovery.test.js +5 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +32 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/nextGenArchitecture.test.js +4 -2
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +10 -5
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +16 -9
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +4 -4
- package/server/agent/tests/sessionAndCopy.test.js +39 -4
- package/server/agent/tests/sidebarControls.test.js +34 -0
- package/server/agent/tests/statefulMemory.test.js +8 -1
- package/server/agent/tests/textProtocolLoop.test.js +71 -0
- package/server/agent/tests/textToolParsing.test.js +48 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
- package/server/agent/tests/unifiedEndpoints.test.js +13 -6
- package/server/agent/tests/v110Features.test.js +1 -1
- package/server/agent/tests/vramEstimator.test.js +75 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +256 -5
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webExtract.test.js +23 -0
- package/server/agent/tests/webSources.test.js +46 -12
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +14 -8
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +18 -18
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
|
@@ -40,7 +40,11 @@ export function findFutureDates(text, now = new Date()) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function hasLiveVerification(tools = []) {
|
|
43
|
-
return tools.some(tool =>
|
|
43
|
+
return tools.some(tool => {
|
|
44
|
+
const name = tool?.name || tool?.function?.name || '';
|
|
45
|
+
if (tool?.capabilities?.includes('live_verification') || tool?.capabilities?.includes('catalog_verification')) return true;
|
|
46
|
+
return /^(?:web_search|web_extract|fetch_webpage|verify_claim|search_news|search_chinese_web|search_bilibili|search_encyclopedia|search_works|get_work_detail|get_career_timeline|global_search)$/.test(name);
|
|
47
|
+
});
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
function redactUnverifiedFutureDates(text, now) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const MUSIC_CONTROL_ACTIONS = new Map([
|
|
2
|
+
['play_music', 'play'],
|
|
3
|
+
['pause_music', 'pause'],
|
|
4
|
+
['resume_music', 'resume'],
|
|
5
|
+
['stop_music', 'stop'],
|
|
6
|
+
['next_track', 'next'],
|
|
7
|
+
['prev_track', 'prev'],
|
|
8
|
+
['set_volume', 'set_volume'],
|
|
9
|
+
['mute_music', 'mute'],
|
|
10
|
+
['unmute_music', 'unmute'],
|
|
11
|
+
['seek_music', 'seek'],
|
|
12
|
+
['set_repeat_mode', 'set_repeat'],
|
|
13
|
+
['set_shuffle_mode', 'set_shuffle'],
|
|
14
|
+
['toggle_video_mode', 'toggle_video'],
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function text(value, maxLength = 500) {
|
|
18
|
+
return typeof value === 'string' ? value.slice(0, maxLength) : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeSong(song) {
|
|
22
|
+
if (!song || typeof song !== 'object') return undefined;
|
|
23
|
+
const youtubeId = text(song.youtubeId, 32);
|
|
24
|
+
return {
|
|
25
|
+
id: text(song.id, 200),
|
|
26
|
+
title: text(song.title),
|
|
27
|
+
titleTh: text(song.titleTh),
|
|
28
|
+
titleCn: text(song.titleCn),
|
|
29
|
+
titleEn: text(song.titleEn),
|
|
30
|
+
youtubeId: youtubeId && /^[a-zA-Z0-9_-]{11}$/.test(youtubeId) ? youtubeId : undefined,
|
|
31
|
+
duration: text(song.duration, 32),
|
|
32
|
+
category: text(song.category, 100),
|
|
33
|
+
dramaName: text(song.dramaName),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createMusicControlAction(toolName, result) {
|
|
38
|
+
const fallbackAction = MUSIC_CONTROL_ACTIONS.get(toolName);
|
|
39
|
+
if (!fallbackAction || !result || typeof result !== 'object') return null;
|
|
40
|
+
|
|
41
|
+
const action = fallbackAction;
|
|
42
|
+
const event = {
|
|
43
|
+
type: 'ui_action',
|
|
44
|
+
target: 'music_player',
|
|
45
|
+
tool: toolName,
|
|
46
|
+
action,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (result.status === 'error') return event;
|
|
50
|
+
|
|
51
|
+
const playing = text(result.playing);
|
|
52
|
+
const currentTrack = text(result.currentTrack);
|
|
53
|
+
const song = normalizeSong(result.song);
|
|
54
|
+
if (playing !== undefined) event.playing = playing;
|
|
55
|
+
if (currentTrack !== undefined) event.currentTrack = currentTrack;
|
|
56
|
+
if (song !== undefined) event.song = song;
|
|
57
|
+
if (action === 'set_volume') event.volume = Math.max(0, Math.min(100, Number(result.volume) || 0));
|
|
58
|
+
if (action === 'seek') event.seconds = Math.max(0, Number(result.seconds) || 0);
|
|
59
|
+
if (action === 'set_repeat') event.mode = ['off', 'one', 'all'].includes(result.mode) ? result.mode : 'off';
|
|
60
|
+
if (action === 'set_shuffle') event.enabled = Boolean(result.enabled);
|
|
61
|
+
if (action === 'toggle_video') event.isVideoOpen = Boolean(result.isVideoOpen);
|
|
62
|
+
|
|
63
|
+
return event;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function collectMusicControlActions(tools = []) {
|
|
67
|
+
return tools
|
|
68
|
+
.map((tool) => createMusicControlAction(tool?.name, tool?.result))
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
}
|
|
@@ -43,10 +43,10 @@ export function applySecurityHeaders(req, res) {
|
|
|
43
43
|
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
44
44
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
45
45
|
res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
|
|
46
|
-
res.setHeader('Permissions-Policy', 'camera=(), microphone=(self), geolocation=(), payment=(), usb=()');
|
|
46
|
+
res.setHeader('Permissions-Policy', 'camera=(), microphone=(self), geolocation=(self), payment=(), usb=()');
|
|
47
47
|
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
48
48
|
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
|
|
49
|
-
res.setHeader('Content-Security-Policy', "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'unsafe-inline' https://www.youtube.com https://www.youtube-nocookie.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob: https:; media-src 'self' blob: https:; frame-src https://www.youtube.com https://www.youtube-nocookie.com https://player.bilibili.com https://www.bilibili.com; connect-src 'self' http://localhost:* http://127.0.0.1:* https:;");
|
|
49
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self' 'unsafe-inline' https://www.youtube.com https://www.youtube-nocookie.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net data:; img-src 'self' data: blob: https:; media-src 'self' blob: https:; frame-src https://www.youtube.com https://www.youtube-nocookie.com https://player.bilibili.com https://www.bilibili.com https://*.openstreetmap.org https://openstreetmap.org; connect-src 'self' http://localhost:* http://127.0.0.1:* https:;");
|
|
50
50
|
}
|
|
51
51
|
return !origin || Boolean(allowedOrigin);
|
|
52
52
|
}
|
|
@@ -64,13 +64,23 @@ export function extractApiKey(req, urlObj = null) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export function resolveClientIp(req) {
|
|
67
|
-
const directIp = req?.socket?.remoteAddress || '
|
|
67
|
+
const directIp = String(req?.socket?.remoteAddress || 'unknown');
|
|
68
68
|
const trustProxy = process.env.VERCEL === '1' || process.env.KIKU_TRUST_PROXY === 'true';
|
|
69
|
-
if (!trustProxy) return
|
|
69
|
+
if (!trustProxy) return directIp;
|
|
70
70
|
const forwarded = req?.headers?.['x-forwarded-for'];
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
if (typeof forwarded === 'string' && forwarded.trim()) {
|
|
72
|
+
const parts = forwarded.split(',').map(s => s.trim()).filter(Boolean);
|
|
73
|
+
const candidate = parts[0] || directIp;
|
|
74
|
+
const isCandidateLoopback = candidate === '127.0.0.1' || candidate === '::1' || candidate === 'localhost' || candidate.startsWith('::ffff:127.');
|
|
75
|
+
const isDirectLoopback = directIp === '127.0.0.1' || directIp === '::1' || directIp === 'localhost' || directIp.startsWith('::ffff:127.');
|
|
76
|
+
|
|
77
|
+
if (isCandidateLoopback && !isDirectLoopback) {
|
|
78
|
+
const nonLoopback = parts.find(p => p !== '127.0.0.1' && p !== '::1' && p !== 'localhost' && !p.startsWith('::ffff:127.'));
|
|
79
|
+
return nonLoopback || directIp;
|
|
80
|
+
}
|
|
81
|
+
return candidate;
|
|
82
|
+
}
|
|
83
|
+
return directIp;
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
function constantTimeEqual(left, right) {
|
|
@@ -80,41 +90,42 @@ function constantTimeEqual(left, right) {
|
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
export function isWebChatAdmin(req) {
|
|
83
|
-
|
|
84
|
-
// otherwise a stray NODE_ENV=test in production would disable admin auth.
|
|
93
|
+
|
|
85
94
|
if (process.env.WEBCHAT_ALLOW_TEST_ADMIN === 'true' && process.env.NODE_ENV !== 'production') return true;
|
|
86
95
|
const expected = process.env.WEBCHAT_ADMIN_KEY || '';
|
|
87
96
|
if (!expected) return false;
|
|
88
97
|
return constantTimeEqual(extractApiKey(req), expected);
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
export function
|
|
92
|
-
const ip = clientIp || 'unknown';
|
|
100
|
+
export function checkRateLimit(clientIp, bucket = 'default', limit = MAX_CHAT_REQUESTS_PER_MINUTE) {
|
|
101
|
+
const ip = bucket + ':' + (clientIp || 'unknown');
|
|
93
102
|
const now = Date.now();
|
|
94
103
|
const entry = rateLimitStore.get(ip);
|
|
95
104
|
|
|
96
105
|
if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
|
|
97
106
|
rateLimitStore.set(ip, { count: 1, windowStart: now });
|
|
98
|
-
return { allowed: true, remaining:
|
|
107
|
+
return { allowed: true, remaining: limit - 1 };
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
entry.count++;
|
|
102
|
-
if (entry.count >
|
|
111
|
+
if (entry.count > limit) {
|
|
103
112
|
const retryAfter = Math.ceil((RATE_LIMIT_WINDOW_MS - (now - entry.windowStart)) / 1000);
|
|
104
113
|
return { allowed: false, retryAfter };
|
|
105
114
|
}
|
|
106
115
|
|
|
107
|
-
return { allowed: true, remaining:
|
|
116
|
+
return { allowed: true, remaining: limit - entry.count };
|
|
108
117
|
}
|
|
109
118
|
|
|
110
|
-
export
|
|
119
|
+
export function checkChatRateLimit(ip) { return checkRateLimit(ip, 'chat', 60); }
|
|
120
|
+
|
|
121
|
+
export async function checkChatRateLimitDistributed(clientIp, bucket = 'chat', limit = 60) {
|
|
111
122
|
const redisUrl = process.env.UPSTASH_REDIS_REST_URL;
|
|
112
123
|
const redisToken = process.env.UPSTASH_REDIS_REST_TOKEN;
|
|
113
|
-
if (!redisUrl || !redisToken) return
|
|
124
|
+
if (!redisUrl || !redisToken) return checkRateLimit(clientIp, bucket, limit);
|
|
114
125
|
|
|
115
126
|
const now = Date.now();
|
|
116
127
|
const identity = createHash('sha256').update(String(clientIp || 'unknown')).digest('hex');
|
|
117
|
-
const key = `webchat-rate:${identity}:${Math.floor(now / RATE_LIMIT_WINDOW_MS)}`;
|
|
128
|
+
const key = `webchat-rate:${bucket}:${identity}:${Math.floor(now / RATE_LIMIT_WINDOW_MS)}`;
|
|
118
129
|
try {
|
|
119
130
|
const response = await fetch(`${redisUrl.replace(/\/$/, '')}/pipeline`, {
|
|
120
131
|
method: 'POST',
|
|
@@ -126,13 +137,13 @@ export async function checkChatRateLimitDistributed(clientIp) {
|
|
|
126
137
|
const result = await response.json();
|
|
127
138
|
const count = Number(result?.[0]?.result) || 1;
|
|
128
139
|
return {
|
|
129
|
-
allowed: count <=
|
|
130
|
-
remaining: Math.max(0,
|
|
131
|
-
retryAfter: count <=
|
|
140
|
+
allowed: count <= limit,
|
|
141
|
+
remaining: Math.max(0, limit - count),
|
|
142
|
+
retryAfter: count <= limit ? 0 : 60,
|
|
132
143
|
};
|
|
133
144
|
}
|
|
134
145
|
} catch {}
|
|
135
|
-
return
|
|
146
|
+
return checkRateLimit(clientIp, bucket, limit);
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
|
|
@@ -13,19 +13,19 @@ export function makeInspectableToolPayload(value, options = {}) {
|
|
|
13
13
|
if (typeof item === 'bigint') return String(item);
|
|
14
14
|
if (typeof item === 'string') {
|
|
15
15
|
return item.length > maxStringLength
|
|
16
|
-
? item.slice(0, maxStringLength) + '\n…
|
|
16
|
+
? item.slice(0, maxStringLength) + '\n… ตัดข้อความที่ยาวเกินไป · ย่อแล้ว'
|
|
17
17
|
: item;
|
|
18
18
|
}
|
|
19
|
-
if (typeof item === 'undefined') return '
|
|
19
|
+
if (typeof item === 'undefined') return '—';
|
|
20
20
|
if (typeof item !== 'object') return String(item);
|
|
21
|
-
if (depth >= maxDepth) return '
|
|
22
|
-
if (seen.has(item)) return '
|
|
21
|
+
if (depth >= maxDepth) return '… ข้อมูลซ้อนลึก · ย่อแล้ว';
|
|
22
|
+
if (seen.has(item)) return '○ อ้างอิงซ้ำ';
|
|
23
23
|
seen.add(item);
|
|
24
24
|
|
|
25
25
|
if (Array.isArray(item)) {
|
|
26
26
|
const output = item.slice(0, maxArrayItems).map(entry => visit(entry, depth + 1));
|
|
27
27
|
if (item.length > maxArrayItems) {
|
|
28
|
-
output.push(`…
|
|
28
|
+
output.push(`… ย่ออีก ${item.length - maxArrayItems} รายการ`);
|
|
29
29
|
}
|
|
30
30
|
return output;
|
|
31
31
|
}
|
|
@@ -33,7 +33,7 @@ export function makeInspectableToolPayload(value, options = {}) {
|
|
|
33
33
|
const output = {};
|
|
34
34
|
const entries = Object.entries(item);
|
|
35
35
|
entries.slice(0, maxObjectKeys).forEach(([key, entry]) => {
|
|
36
|
-
output[key] = SENSITIVE_TOOL_FIELD.test(key) ? '
|
|
36
|
+
output[key] = SENSITIVE_TOOL_FIELD.test(key) ? '···' : visit(entry, depth + 1);
|
|
37
37
|
});
|
|
38
38
|
if (entries.length > maxObjectKeys) {
|
|
39
39
|
output.__truncated__ = `ตัดอีก ${entries.length - maxObjectKeys} ฟิลด์`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { lookup } from 'node:dns/promises';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
import { parseIPv4, isPrivateIPv4, isPrivateIPv6 } from '../../agent/websearch/security/ssrf.js';
|
|
4
|
+
import { isWebChatAdmin, resolveClientIp } from './security.js';
|
|
5
|
+
|
|
6
|
+
function ipv4Address(host) {
|
|
7
|
+
const mapped = host.match(/^::ffff:([a-f0-9]{1,4}):([a-f0-9]{1,4})$/i);
|
|
8
|
+
if (mapped) return parseIPv4(String(parseInt(mapped[1], 16) * 65536 + parseInt(mapped[2], 16)));
|
|
9
|
+
return parseIPv4(host.replace(/^::ffff:/i, ''));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isTrustedRequest(req) {
|
|
13
|
+
const ip = resolveClientIp(req).toLowerCase();
|
|
14
|
+
const v4 = ipv4Address(ip);
|
|
15
|
+
return ip === '::1' || Boolean(v4?.startsWith('127.')) || isWebChatAdmin(req);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function validateInferenceBaseUrl(rawUrl, { callerIsTrusted = false, trusted = callerIsTrusted, lookupHost = lookup } = {}) {
|
|
19
|
+
if (typeof rawUrl !== 'string' || !rawUrl.trim() || rawUrl.length > 2048) return { ok: false, reason: 'Invalid baseUrl length' };
|
|
20
|
+
let url;
|
|
21
|
+
try { url = new URL(rawUrl.trim()); } catch { return { ok: false, reason: 'Invalid baseUrl' }; }
|
|
22
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) return { ok: false, reason: 'baseUrl must use HTTP(S), without credentials or fragment' };
|
|
23
|
+
const host = url.hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '').toLowerCase();
|
|
24
|
+
if (host === 'metadata.google.internal' || host.endsWith('.metadata.google.internal')) return { ok: false, reason: 'Metadata endpoint is forbidden' };
|
|
25
|
+
let addresses;
|
|
26
|
+
try {
|
|
27
|
+
addresses = isIP(host) ? [{ address: host }] : await lookupHost(host, { all: true, verbatim: true });
|
|
28
|
+
} catch { return { ok: false, reason: 'Cannot resolve baseUrl hostname' }; }
|
|
29
|
+
if (!addresses.length) return { ok: false, reason: 'Cannot resolve baseUrl hostname' };
|
|
30
|
+
for (const { address } of addresses) {
|
|
31
|
+
const v4 = ipv4Address(address);
|
|
32
|
+
if (v4?.startsWith('169.254.') || /^fe[89ab]/i.test(address) || address.toLowerCase() === 'fd00:ec2::254') return { ok: false, reason: 'Metadata/link-local endpoint is forbidden' };
|
|
33
|
+
if (!trusted && (v4 ? isPrivateIPv4(v4) : isPrivateIPv6(address))) return { ok: false, reason: 'Private/loopback baseUrl requires a local or admin caller' };
|
|
34
|
+
}
|
|
35
|
+
return { ok: true, normalized: url.href.replace(/\/+$/, '') };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function sanitizeInferenceError(error, trusted = false) {
|
|
39
|
+
if (!trusted) return 'Inference provider unavailable. Check provider settings or contact the administrator.';
|
|
40
|
+
return String(error?.message || error || 'Inference provider unavailable')
|
|
41
|
+
.replace(/Bearer\s+[^\s]+/gi, 'Bearer [redacted]')
|
|
42
|
+
.replace(/(?:sk-|AIza)[A-Za-z0-9_-]+/g, '[redacted]')
|
|
43
|
+
.replace(/https?:\/\/[^\s]+/g, '[endpoint]')
|
|
44
|
+
.replace(/[\r\n\x00-\x1f]+/g, ' ').slice(0, 200);
|
|
45
|
+
}
|