skimpyclaw 0.3.14 → 0.4.0
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 +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +101 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +349 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
package/dist/providers/openai.js
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
// OpenAI-Compatible Provider (OpenAI, OpenRouter, Groq, etc.)
|
|
2
|
-
import { startObservation } from '@langfuse/tracing';
|
|
3
|
-
import { stripProvider, toOpenAITools, truncateToolResult } from './utils.js';
|
|
4
|
-
import { compactOpenAIMessages } from './context-manager.js';
|
|
5
|
-
import { toOpenAIContent } from './content.js';
|
|
6
|
-
import { toUsageDetails, toCostDetails } from './observability.js';
|
|
7
|
-
import { getToolDefinitions, executeTool } from '../tools.js';
|
|
8
|
-
import { toErrorMessage } from '../utils.js';
|
|
9
|
-
import { ToolCallGuard } from './tool-guard.js';
|
|
10
|
-
import { addEvent } from '../audit.js';
|
|
11
|
-
import { buildUsageRecord, recordUsage } from '../usage.js';
|
|
12
|
-
// Map of provider name → OpenAI client
|
|
13
|
-
const openaiClients = new Map();
|
|
14
|
-
export function addOpenAIClient(name, client) {
|
|
15
|
-
openaiClients.set(name, client);
|
|
16
|
-
}
|
|
17
|
-
export function getOpenAIClient(name) {
|
|
18
|
-
return openaiClients.get(name);
|
|
19
|
-
}
|
|
20
|
-
export function hasOpenAIClient(name) {
|
|
21
|
-
return openaiClients.has(name);
|
|
22
|
-
}
|
|
23
|
-
export function clearOpenAIClients() {
|
|
24
|
-
openaiClients.clear();
|
|
25
|
-
}
|
|
26
|
-
export function resetOpenAIProviderState() {
|
|
27
|
-
openaiClients.clear();
|
|
28
|
-
}
|
|
29
|
-
export function isOpenAIAvailable(provider) {
|
|
30
|
-
return openaiClients.has(provider);
|
|
31
|
-
}
|
|
32
|
-
const LANGFUSE_APP_NAME = 'skimpyclaw';
|
|
33
|
-
function recordOpenAIUsage(params) {
|
|
34
|
-
const usage = params.usage;
|
|
35
|
-
let inputTokens = typeof usage?.prompt_tokens === 'number'
|
|
36
|
-
? usage.prompt_tokens
|
|
37
|
-
: (typeof usage?.input_tokens === 'number' ? usage.input_tokens : 0);
|
|
38
|
-
const outputTokens = typeof usage?.completion_tokens === 'number'
|
|
39
|
-
? usage.completion_tokens
|
|
40
|
-
: (typeof usage?.output_tokens === 'number' ? usage.output_tokens : 0);
|
|
41
|
-
// Some OpenAI-compatible providers only return total_tokens.
|
|
42
|
-
if (inputTokens === 0 && outputTokens === 0 && typeof usage?.total_tokens === 'number') {
|
|
43
|
-
inputTokens = usage.total_tokens;
|
|
44
|
-
}
|
|
45
|
-
const cost = toCostDetails(params.model, usage);
|
|
46
|
-
recordUsage(buildUsageRecord({
|
|
47
|
-
model: params.model,
|
|
48
|
-
provider: params.provider,
|
|
49
|
-
inputTokens,
|
|
50
|
-
outputTokens,
|
|
51
|
-
inputCost: cost?.input ?? 0,
|
|
52
|
-
outputCost: cost?.output ?? 0,
|
|
53
|
-
totalCost: cost?.total ?? 0,
|
|
54
|
-
trigger: params.trigger || 'api',
|
|
55
|
-
agentId: params.agentId,
|
|
56
|
-
}));
|
|
57
|
-
}
|
|
58
|
-
async function startGenerationObservation(name, attributes) {
|
|
59
|
-
const { isLangfuseEnabled } = await import('../langfuse.js');
|
|
60
|
-
if (!isLangfuseEnabled())
|
|
61
|
-
return null;
|
|
62
|
-
attributes.metadata = { app: LANGFUSE_APP_NAME, ...attributes.metadata };
|
|
63
|
-
return startObservation(name, attributes, { asType: 'generation' });
|
|
64
|
-
}
|
|
65
|
-
export async function chatOpenAI(params, provider) {
|
|
66
|
-
const client = openaiClients.get(provider);
|
|
67
|
-
if (!client) {
|
|
68
|
-
throw new Error(`OpenAI client not initialized for provider: ${provider}`);
|
|
69
|
-
}
|
|
70
|
-
const { messages, options, config } = params;
|
|
71
|
-
const modelId = stripProvider(options.model, openaiClients);
|
|
72
|
-
const providerBaseURL = config.models.providers[provider]?.baseURL || '';
|
|
73
|
-
const isKimiLike = providerBaseURL.includes('kimi.com') || providerBaseURL.includes('moonshot.ai');
|
|
74
|
-
const kimiRequestExtras = isKimiLike
|
|
75
|
-
? { extra_body: { interleaved: { field: 'reasoning_content' } } }
|
|
76
|
-
: {};
|
|
77
|
-
const openaiMessages = messages.map(m => ({
|
|
78
|
-
role: m.role,
|
|
79
|
-
content: toOpenAIContent(m.content),
|
|
80
|
-
}));
|
|
81
|
-
const genObs = await startGenerationObservation(`${provider}:${modelId}`, {
|
|
82
|
-
input: { messages: openaiMessages },
|
|
83
|
-
model: modelId,
|
|
84
|
-
modelParameters: {
|
|
85
|
-
max_tokens: options.maxTokens || 4096,
|
|
86
|
-
temperature: options.temperature,
|
|
87
|
-
},
|
|
88
|
-
metadata: { provider },
|
|
89
|
-
});
|
|
90
|
-
try {
|
|
91
|
-
const response = await client.chat.completions.create({
|
|
92
|
-
model: modelId,
|
|
93
|
-
messages: openaiMessages,
|
|
94
|
-
max_tokens: options.maxTokens || 4096,
|
|
95
|
-
temperature: options.temperature,
|
|
96
|
-
...kimiRequestExtras,
|
|
97
|
-
});
|
|
98
|
-
let content = response.choices[0]?.message?.content || '';
|
|
99
|
-
// Strip <think>...</think> reasoning blocks (e.g. MiniMax M2.x)
|
|
100
|
-
content = content.replace(/<think>[\s\S]*?<\/think>\s*/g, '').trim();
|
|
101
|
-
recordOpenAIUsage({ model: modelId, provider, usage: response.usage, trigger: 'api' });
|
|
102
|
-
genObs?.update({
|
|
103
|
-
output: response.choices[0]?.message,
|
|
104
|
-
usageDetails: toUsageDetails(response.usage),
|
|
105
|
-
costDetails: toCostDetails(modelId, response.usage),
|
|
106
|
-
});
|
|
107
|
-
genObs?.end();
|
|
108
|
-
return content;
|
|
109
|
-
}
|
|
110
|
-
catch (err) {
|
|
111
|
-
const errorMessage = toErrorMessage(err);
|
|
112
|
-
genObs?.update({ level: 'ERROR', statusMessage: errorMessage, output: { error: errorMessage } });
|
|
113
|
-
genObs?.end();
|
|
114
|
-
throw err;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
export async function chatWithToolsOpenAI(params, provider) {
|
|
118
|
-
const client = openaiClients.get(provider);
|
|
119
|
-
if (!client) {
|
|
120
|
-
throw new Error(`OpenAI client not initialized for provider: ${provider}`);
|
|
121
|
-
}
|
|
122
|
-
const { messages, options, config, toolConfig, toolContext } = params;
|
|
123
|
-
const modelId = stripProvider(options.model, openaiClients);
|
|
124
|
-
const maxIterations = toolConfig.maxIterations || 20;
|
|
125
|
-
// Resolve tools once at start
|
|
126
|
-
const includeSpawn = !!(toolContext?.fullConfig && (toolContext?.chatId || toolContext?.isCronJob));
|
|
127
|
-
const toolDefs = await getToolDefinitions(toolConfig, {
|
|
128
|
-
includeAgentTools: includeSpawn,
|
|
129
|
-
includeMcp: false,
|
|
130
|
-
projects: toolContext?.fullConfig?.projects
|
|
131
|
-
});
|
|
132
|
-
const openaiTools = toOpenAITools(toolDefs);
|
|
133
|
-
// Kimi requires interleaved reasoning content when replaying tool calls.
|
|
134
|
-
const providerBaseURL = config.models.providers[provider]?.baseURL || '';
|
|
135
|
-
const requiresReasoningContent = providerBaseURL.includes('kimi.com') || providerBaseURL.includes('moonshot.ai');
|
|
136
|
-
const kimiRequestExtras = requiresReasoningContent
|
|
137
|
-
? { extra_body: { interleaved: { field: 'reasoning_content' } } }
|
|
138
|
-
: {};
|
|
139
|
-
// Build messages for OpenAI format — preserve images for vision models
|
|
140
|
-
const apiMessages = messages.map(m => ({
|
|
141
|
-
role: m.role,
|
|
142
|
-
content: toOpenAIContent(m.content),
|
|
143
|
-
}));
|
|
144
|
-
const toolLog = [];
|
|
145
|
-
// Guard: spin detection, no-progress detection, token budget
|
|
146
|
-
const guard = new ToolCallGuard(toolConfig.maxTurnTokens);
|
|
147
|
-
for (let i = 0; i < maxIterations; i++) {
|
|
148
|
-
// Check abort signal
|
|
149
|
-
if (toolContext?.abortSignal?.aborted) {
|
|
150
|
-
return {
|
|
151
|
-
response: `[Cancelled after ${toolLog.length} tool calls]`,
|
|
152
|
-
toolCalls: toolLog,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
// Compact old tool results if context is growing large
|
|
156
|
-
const compactionResult = await compactOpenAIMessages(apiMessages, toolConfig.contextManagement, i + 1, config);
|
|
157
|
-
const messagesForApi = compactionResult.messages;
|
|
158
|
-
if (compactionResult.compacted) {
|
|
159
|
-
const method = compactionResult.method === 'llm' ? 'LLM summary' : 'truncation';
|
|
160
|
-
const detail = `~${Math.round((compactionResult.tokensBefore || 0) / 1000)}k → ~${Math.round((compactionResult.tokensAfter || 0) / 1000)}k tokens`;
|
|
161
|
-
toolLog.push(`[context compacted via ${method}: ${detail}]`);
|
|
162
|
-
}
|
|
163
|
-
console.log(`[agent:openai-tools] Iteration ${i + 1}/${maxIterations} (provider: ${provider}, model: ${modelId})`);
|
|
164
|
-
const genObs = await startGenerationObservation(`${provider}:${modelId}`, {
|
|
165
|
-
input: { messages: apiMessages },
|
|
166
|
-
model: modelId,
|
|
167
|
-
modelParameters: {
|
|
168
|
-
max_tokens: options.maxTokens || 4096,
|
|
169
|
-
temperature: options.temperature,
|
|
170
|
-
},
|
|
171
|
-
metadata: { provider, iteration: i + 1 },
|
|
172
|
-
});
|
|
173
|
-
let completion;
|
|
174
|
-
try {
|
|
175
|
-
completion = await client.chat.completions.create({
|
|
176
|
-
model: modelId,
|
|
177
|
-
messages: messagesForApi,
|
|
178
|
-
tools: openaiTools,
|
|
179
|
-
max_tokens: options.maxTokens || 4096,
|
|
180
|
-
temperature: options.temperature,
|
|
181
|
-
...kimiRequestExtras,
|
|
182
|
-
});
|
|
183
|
-
recordOpenAIUsage({
|
|
184
|
-
model: modelId,
|
|
185
|
-
provider,
|
|
186
|
-
usage: completion.usage,
|
|
187
|
-
trigger: toolContext?.trigger || 'api',
|
|
188
|
-
agentId: toolContext?.agentId,
|
|
189
|
-
});
|
|
190
|
-
genObs?.update({
|
|
191
|
-
output: completion.choices[0]?.message,
|
|
192
|
-
usageDetails: toUsageDetails(completion.usage),
|
|
193
|
-
costDetails: toCostDetails(modelId, completion.usage),
|
|
194
|
-
});
|
|
195
|
-
genObs?.end();
|
|
196
|
-
// Guard: track token usage (stats only, no enforcement)
|
|
197
|
-
guard.recordTokens(completion.usage?.prompt_tokens ?? 0, completion.usage?.completion_tokens ?? 0);
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
const errorMessage = toErrorMessage(err);
|
|
201
|
-
genObs?.update({ level: 'ERROR', statusMessage: errorMessage, output: { error: errorMessage } });
|
|
202
|
-
genObs?.end();
|
|
203
|
-
throw err;
|
|
204
|
-
}
|
|
205
|
-
const message = completion.choices[0]?.message;
|
|
206
|
-
if (!message) {
|
|
207
|
-
return {
|
|
208
|
-
response: '[No response from model]',
|
|
209
|
-
toolCalls: toolLog,
|
|
210
|
-
usage: {
|
|
211
|
-
prompt_tokens: completion.usage?.prompt_tokens ?? 0,
|
|
212
|
-
completion_tokens: completion.usage?.completion_tokens ?? 0,
|
|
213
|
-
total_tokens: completion.usage?.total_tokens ?? 0,
|
|
214
|
-
},
|
|
215
|
-
cost: toCostDetails(modelId, completion.usage),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
// No tool calls — return the text response
|
|
219
|
-
if (completion.choices[0]?.finish_reason !== 'tool_calls' || !message.tool_calls?.length) {
|
|
220
|
-
let content = message.content || '';
|
|
221
|
-
// Strip <think>...</think> reasoning blocks (e.g. MiniMax M2.x)
|
|
222
|
-
content = content.replace(/<think>[\s\S]*?<\/think>\s*/g, '').trim();
|
|
223
|
-
if (!content && toolLog.length > 0) {
|
|
224
|
-
content = `[Completed with ${toolLog.length} tool calls, no text response]`;
|
|
225
|
-
}
|
|
226
|
-
return {
|
|
227
|
-
response: content,
|
|
228
|
-
toolCalls: toolLog,
|
|
229
|
-
usage: {
|
|
230
|
-
prompt_tokens: completion.usage?.prompt_tokens ?? 0,
|
|
231
|
-
completion_tokens: completion.usage?.completion_tokens ?? 0,
|
|
232
|
-
total_tokens: completion.usage?.total_tokens ?? 0,
|
|
233
|
-
},
|
|
234
|
-
cost: toCostDetails(modelId, completion.usage),
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
// Append assistant message with tool_calls to conversation.
|
|
238
|
-
// Kimi requires reasoning_content when thinking mode is enabled.
|
|
239
|
-
const assistantToolCallMessage = {
|
|
240
|
-
role: 'assistant',
|
|
241
|
-
content: message.content ?? null,
|
|
242
|
-
tool_calls: message.tool_calls,
|
|
243
|
-
};
|
|
244
|
-
const rawReasoning = message.reasoning_content
|
|
245
|
-
?? message.additional_kwargs?.reasoning_content
|
|
246
|
-
?? message.reasoning?.content;
|
|
247
|
-
if (rawReasoning !== undefined && rawReasoning !== null) {
|
|
248
|
-
assistantToolCallMessage.reasoning_content = Array.isArray(rawReasoning)
|
|
249
|
-
? rawReasoning.join('\n')
|
|
250
|
-
: String(rawReasoning);
|
|
251
|
-
}
|
|
252
|
-
else if (requiresReasoningContent) {
|
|
253
|
-
// Some Kimi responses omit reasoning_content despite thinking mode.
|
|
254
|
-
// Send a placeholder to satisfy strict tool-call replay validation.
|
|
255
|
-
assistantToolCallMessage.reasoning_content = ' ';
|
|
256
|
-
}
|
|
257
|
-
apiMessages.push(assistantToolCallMessage);
|
|
258
|
-
// Execute each tool call
|
|
259
|
-
for (const toolCall of message.tool_calls) {
|
|
260
|
-
const fnName = toolCall.function.name;
|
|
261
|
-
if (fnName.startsWith('$')) {
|
|
262
|
-
const unsupported = `Provider-native tool "${fnName}" is not supported in this runtime.`;
|
|
263
|
-
console.warn(`[agent:openai-tools] ${unsupported}`);
|
|
264
|
-
apiMessages.push({
|
|
265
|
-
role: 'tool',
|
|
266
|
-
tool_call_id: toolCall.id,
|
|
267
|
-
content: unsupported,
|
|
268
|
-
});
|
|
269
|
-
toolLog.push(`${fnName} [SKIPPED: provider-native tool unsupported]`);
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
let args;
|
|
273
|
-
try {
|
|
274
|
-
args = JSON.parse(toolCall.function.arguments || '{}');
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
args = {};
|
|
278
|
-
}
|
|
279
|
-
const inputStr = JSON.stringify(args).slice(0, 200);
|
|
280
|
-
console.log(`[agent:openai-tools] -> ${fnName}(${inputStr})`);
|
|
281
|
-
// Guard: spin detection
|
|
282
|
-
const guardResult = guard.recordCall(fnName, args);
|
|
283
|
-
if (guardResult.warning)
|
|
284
|
-
console.warn(`[agent:openai-tools:guard] ${guardResult.warning}`);
|
|
285
|
-
if (guardResult.blocked) {
|
|
286
|
-
apiMessages.push({
|
|
287
|
-
role: 'tool',
|
|
288
|
-
tool_call_id: toolCall.id,
|
|
289
|
-
content: guardResult.warning || 'Blocked: repeated identical call',
|
|
290
|
-
});
|
|
291
|
-
toolLog.push(`${fnName} [BLOCKED: spin detected]`);
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
const { isLangfuseEnabled } = await import('../langfuse.js');
|
|
295
|
-
const toolObs = isLangfuseEnabled()
|
|
296
|
-
? startObservation(`tool:${fnName}`, { input: args, metadata: { app: LANGFUSE_APP_NAME, tool: fnName } }, { asType: 'tool' })
|
|
297
|
-
: null;
|
|
298
|
-
const toolStart = Date.now();
|
|
299
|
-
try {
|
|
300
|
-
const result = await executeTool(fnName, args, toolConfig, toolContext) || '';
|
|
301
|
-
const truncatedResult = truncateToolResult(result);
|
|
302
|
-
const resultPreview = result.slice(0, 200) + (result.length > 200 ? '...' : '');
|
|
303
|
-
console.log(`[agent:openai-tools] <- ${resultPreview}`);
|
|
304
|
-
toolLog.push(`${fnName}(${inputStr}) → ${resultPreview}`);
|
|
305
|
-
toolObs?.update({ output: result });
|
|
306
|
-
toolObs?.end();
|
|
307
|
-
if (toolContext?.auditTraceId) {
|
|
308
|
-
addEvent(toolContext.auditTraceId, {
|
|
309
|
-
type: 'tool_use',
|
|
310
|
-
summary: `${fnName}(${inputStr})`,
|
|
311
|
-
durationMs: Date.now() - toolStart,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
// Add tool result in OpenAI format
|
|
315
|
-
apiMessages.push({
|
|
316
|
-
role: 'tool',
|
|
317
|
-
tool_call_id: toolCall.id,
|
|
318
|
-
content: truncatedResult,
|
|
319
|
-
});
|
|
320
|
-
// Guard: no-progress detection
|
|
321
|
-
const progressResult = guard.recordResult(result);
|
|
322
|
-
if (progressResult.nudge) {
|
|
323
|
-
console.warn(`[agent:openai-tools:guard] ${progressResult.nudge}`);
|
|
324
|
-
apiMessages[apiMessages.length - 1].content += `\n\n[System: ${progressResult.nudge}]`;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
catch (err) {
|
|
328
|
-
const errorMessage = toErrorMessage(err);
|
|
329
|
-
toolObs?.update({ level: 'ERROR', statusMessage: errorMessage, output: { error: errorMessage } });
|
|
330
|
-
toolObs?.end();
|
|
331
|
-
if (toolContext?.auditTraceId) {
|
|
332
|
-
addEvent(toolContext.auditTraceId, {
|
|
333
|
-
type: 'tool_error',
|
|
334
|
-
summary: `${fnName} error: ${errorMessage.slice(0, 150)}`,
|
|
335
|
-
durationMs: Date.now() - toolStart,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
throw err;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
console.warn(`[agent:openai-tools] Max iterations (${maxIterations}) reached`);
|
|
343
|
-
return {
|
|
344
|
-
response: '[Tool use loop reached maximum iterations]',
|
|
345
|
-
toolCalls: toolLog,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
/** Build UsageDetails from OpenAI usage response */
|
|
349
|
-
function buildOpenAIUsageDetails(usage) {
|
|
350
|
-
return {
|
|
351
|
-
prompt_tokens: usage?.prompt_tokens ?? 0,
|
|
352
|
-
completion_tokens: usage?.completion_tokens ?? 0,
|
|
353
|
-
total_tokens: usage?.total_tokens ?? 0,
|
|
354
|
-
};
|
|
355
|
-
}
|
package/dist/sandbox/bridge.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export declare function sandboxBash(containerName: string, command: string, cwd?: string, timeout?: number): Promise<string>;
|
|
2
|
-
export declare function sandboxReadFile(containerName: string, path: string): Promise<string>;
|
|
3
|
-
export declare function sandboxWriteFile(containerName: string, path: string, content: string): Promise<string>;
|
|
4
|
-
export declare function sandboxListDir(containerName: string, path: string): Promise<string>;
|
|
5
|
-
export declare function sandboxGlob(containerName: string, base: string, pattern: string): Promise<string>;
|
package/dist/sandbox/bridge.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { execInContainer } from './runtime.js';
|
|
2
|
-
const MAX_BASH_OUTPUT = 50 * 1024; // 50KB
|
|
3
|
-
const MAX_READ_OUTPUT = 100 * 1024; // 100KB
|
|
4
|
-
function truncate(s, maxBytes) {
|
|
5
|
-
if (Buffer.byteLength(s) <= maxBytes)
|
|
6
|
-
return s;
|
|
7
|
-
const truncated = Buffer.from(s).subarray(0, maxBytes).toString('utf-8');
|
|
8
|
-
return truncated + '\n... (output truncated)';
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Shell-escape a string for use inside sh -c '...'
|
|
12
|
-
* NOTE: execInContainer already wraps in sh -c, so callers pass
|
|
13
|
-
* args as individual tokens. The runtime joins them with spaces
|
|
14
|
-
* and does basic single-quote escaping. For bash commands we pass
|
|
15
|
-
* the whole command as a single arg string.
|
|
16
|
-
*/
|
|
17
|
-
function shellEscape(s) {
|
|
18
|
-
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
19
|
-
}
|
|
20
|
-
export async function sandboxBash(containerName, command, cwd, timeout) {
|
|
21
|
-
// Pass the full command as a single string so sh -c runs it verbatim
|
|
22
|
-
const shellCmd = cwd
|
|
23
|
-
? `cd ${shellEscape(cwd)} && ${command}`
|
|
24
|
-
: command;
|
|
25
|
-
const result = await execInContainer(containerName, [shellCmd], { timeout: timeout ?? 30_000 });
|
|
26
|
-
let output = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
27
|
-
output = truncate(output, MAX_BASH_OUTPUT);
|
|
28
|
-
if (result.exitCode !== 0) {
|
|
29
|
-
output += `\n[exit code: ${result.exitCode}]`;
|
|
30
|
-
}
|
|
31
|
-
return output || '(no output)';
|
|
32
|
-
}
|
|
33
|
-
export async function sandboxReadFile(containerName, path) {
|
|
34
|
-
// Pass as a single command string to avoid double-escaping
|
|
35
|
-
const result = await execInContainer(containerName, [`cat -- ${shellEscape(path)}`]);
|
|
36
|
-
if (result.exitCode !== 0) {
|
|
37
|
-
throw new Error(`Failed to read ${path}: ${result.stderr}`);
|
|
38
|
-
}
|
|
39
|
-
return truncate(result.stdout, MAX_READ_OUTPUT);
|
|
40
|
-
}
|
|
41
|
-
export async function sandboxWriteFile(containerName, path, content) {
|
|
42
|
-
// mkdir -p for parent dir, then write via stdin
|
|
43
|
-
const result = await execInContainer(containerName, [`mkdir -p $(dirname ${shellEscape(path)}) && cat > ${shellEscape(path)}`], { stdin: content });
|
|
44
|
-
if (result.exitCode !== 0) {
|
|
45
|
-
throw new Error(`Failed to write ${path}: ${result.stderr}`);
|
|
46
|
-
}
|
|
47
|
-
const bytes = Buffer.byteLength(content);
|
|
48
|
-
return `Written: ${path} (${bytes} bytes)`;
|
|
49
|
-
}
|
|
50
|
-
export async function sandboxListDir(containerName, path) {
|
|
51
|
-
const result = await execInContainer(containerName, [`ls -la ${shellEscape(path)}`]);
|
|
52
|
-
if (result.exitCode !== 0) {
|
|
53
|
-
throw new Error(`Failed to list ${path}: ${result.stderr}`);
|
|
54
|
-
}
|
|
55
|
-
return result.stdout;
|
|
56
|
-
}
|
|
57
|
-
export async function sandboxGlob(containerName, base, pattern) {
|
|
58
|
-
const result = await execInContainer(containerName, [`find ${shellEscape(base)} -name ${shellEscape(pattern)} -maxdepth 10 -type f`]);
|
|
59
|
-
if (result.exitCode !== 0) {
|
|
60
|
-
throw new Error(`Failed to glob ${base}/${pattern}: ${result.stderr}`);
|
|
61
|
-
}
|
|
62
|
-
return result.stdout;
|
|
63
|
-
}
|
package/dist/sandbox/index.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { createContainer, execInContainer, removeContainer, isContainerRunning, cleanupOrphans, setRuntime, getRuntime, resetRuntime, probeRuntime } from './runtime.js';
|
|
2
|
-
export type { ContainerOpts, ExecOpts, ExecResult } from './runtime.js';
|
|
3
|
-
export { ensureContainer, releaseContainer, pruneIdle, releaseAll, SANDBOX_DEFAULTS } from './manager.js';
|
|
4
|
-
export { sandboxBash, sandboxReadFile, sandboxWriteFile, sandboxListDir, sandboxGlob } from './bridge.js';
|
|
5
|
-
export { validateMountPaths, isBlockedPath, translatePath } from './mount-security.js';
|
package/dist/sandbox/index.js
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export { createContainer, execInContainer, removeContainer, isContainerRunning, cleanupOrphans, setRuntime, getRuntime, resetRuntime, probeRuntime } from './runtime.js';
|
|
2
|
-
export { ensureContainer, releaseContainer, pruneIdle, releaseAll, SANDBOX_DEFAULTS } from './manager.js';
|
|
3
|
-
export { sandboxBash, sandboxReadFile, sandboxWriteFile, sandboxListDir, sandboxGlob } from './bridge.js';
|
|
4
|
-
export { validateMountPaths, isBlockedPath, translatePath } from './mount-security.js';
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { SandboxConfig } from '../types.js';
|
|
2
|
-
export declare const SANDBOX_DEFAULTS: SandboxConfig;
|
|
3
|
-
export declare function ensureContainer(sessionId: string, config: SandboxConfig, allowedPaths: string[]): Promise<string>;
|
|
4
|
-
export declare function releaseContainer(sessionId: string): Promise<void>;
|
|
5
|
-
export declare function pruneIdle(maxIdleMs: number): Promise<number>;
|
|
6
|
-
export declare function releaseAll(): Promise<void>;
|
|
7
|
-
export declare function resetForTesting(): void;
|
package/dist/sandbox/manager.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { createContainer, removeContainer, isContainerRunning } from './runtime.js';
|
|
2
|
-
import { validateMountPaths } from './mount-security.js';
|
|
3
|
-
export const SANDBOX_DEFAULTS = {
|
|
4
|
-
enabled: false,
|
|
5
|
-
image: 'skimpyclaw-sandbox',
|
|
6
|
-
cpus: 2,
|
|
7
|
-
memory: '2G',
|
|
8
|
-
network: 'bridge',
|
|
9
|
-
idleTimeoutMs: 3_600_000,
|
|
10
|
-
};
|
|
11
|
-
const activeContainers = new Map();
|
|
12
|
-
export async function ensureContainer(sessionId, config, allowedPaths) {
|
|
13
|
-
const name = `skimpyclaw-sbx-${sessionId}`;
|
|
14
|
-
const existing = activeContainers.get(sessionId);
|
|
15
|
-
if (existing) {
|
|
16
|
-
const running = await isContainerRunning(existing.name);
|
|
17
|
-
if (running) {
|
|
18
|
-
existing.lastUsed = Date.now();
|
|
19
|
-
return existing.name;
|
|
20
|
-
}
|
|
21
|
-
// Container died — remove from map, recreate
|
|
22
|
-
activeContainers.delete(sessionId);
|
|
23
|
-
}
|
|
24
|
-
// Process restarts clear in-memory state; adopt or clean an existing named
|
|
25
|
-
// container so a duplicate-name create does not fail.
|
|
26
|
-
const alreadyRunning = await isContainerRunning(name);
|
|
27
|
-
if (alreadyRunning) {
|
|
28
|
-
activeContainers.set(sessionId, {
|
|
29
|
-
name,
|
|
30
|
-
sessionId,
|
|
31
|
-
lastUsed: Date.now(),
|
|
32
|
-
});
|
|
33
|
-
return name;
|
|
34
|
-
}
|
|
35
|
-
// Best effort cleanup for stopped or half-created containers with same name.
|
|
36
|
-
await removeContainer(name);
|
|
37
|
-
const mounts = validateMountPaths(allowedPaths);
|
|
38
|
-
const uid = process.getuid?.() ?? 501;
|
|
39
|
-
const gid = process.getgid?.() ?? 20;
|
|
40
|
-
const merged = { ...SANDBOX_DEFAULTS, ...config };
|
|
41
|
-
// Expand ${VAR} references in env values from process.env
|
|
42
|
-
let resolvedEnv;
|
|
43
|
-
if (config.env) {
|
|
44
|
-
resolvedEnv = {};
|
|
45
|
-
for (const [key, val] of Object.entries(config.env)) {
|
|
46
|
-
resolvedEnv[key] = val.replace(/\$\{(\w+)\}/g, (_match, name) => {
|
|
47
|
-
return process.env[name] ?? '';
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
const opts = {
|
|
52
|
-
image: merged.image,
|
|
53
|
-
cpus: merged.cpus,
|
|
54
|
-
memory: merged.memory,
|
|
55
|
-
network: merged.network,
|
|
56
|
-
mounts: mounts.map((m) => ({
|
|
57
|
-
host: m.host,
|
|
58
|
-
container: m.container,
|
|
59
|
-
readOnly: m.readOnly,
|
|
60
|
-
})),
|
|
61
|
-
env: resolvedEnv,
|
|
62
|
-
user: `${uid}:${gid}`,
|
|
63
|
-
};
|
|
64
|
-
await createContainer(name, opts);
|
|
65
|
-
activeContainers.set(sessionId, {
|
|
66
|
-
name,
|
|
67
|
-
sessionId,
|
|
68
|
-
lastUsed: Date.now(),
|
|
69
|
-
});
|
|
70
|
-
return name;
|
|
71
|
-
}
|
|
72
|
-
export async function releaseContainer(sessionId) {
|
|
73
|
-
const entry = activeContainers.get(sessionId);
|
|
74
|
-
if (!entry)
|
|
75
|
-
return;
|
|
76
|
-
activeContainers.delete(sessionId);
|
|
77
|
-
await removeContainer(entry.name);
|
|
78
|
-
}
|
|
79
|
-
export async function pruneIdle(maxIdleMs) {
|
|
80
|
-
const now = Date.now();
|
|
81
|
-
let pruned = 0;
|
|
82
|
-
for (const [sessionId, entry] of activeContainers) {
|
|
83
|
-
if (now - entry.lastUsed > maxIdleMs) {
|
|
84
|
-
activeContainers.delete(sessionId);
|
|
85
|
-
await removeContainer(entry.name);
|
|
86
|
-
pruned++;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return pruned;
|
|
90
|
-
}
|
|
91
|
-
export async function releaseAll() {
|
|
92
|
-
const entries = Array.from(activeContainers.values());
|
|
93
|
-
activeContainers.clear();
|
|
94
|
-
for (const entry of entries) {
|
|
95
|
-
await removeContainer(entry.name);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
export function resetForTesting() {
|
|
99
|
-
activeContainers.clear();
|
|
100
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export interface MountSpec {
|
|
2
|
-
host: string;
|
|
3
|
-
container: string;
|
|
4
|
-
readOnly: boolean;
|
|
5
|
-
}
|
|
6
|
-
export declare function isBlockedPath(resolvedPath: string): boolean;
|
|
7
|
-
export declare function validateMountPaths(allowedPaths: string[]): MountSpec[];
|
|
8
|
-
/**
|
|
9
|
-
* Translate a host path to its container equivalent using mount specs.
|
|
10
|
-
* Returns the original path if no mount matches (will likely fail inside container).
|
|
11
|
-
*/
|
|
12
|
-
export declare function translatePath(hostPath: string, mounts: MountSpec[]): string;
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { realpathSync, existsSync } from 'fs';
|
|
2
|
-
import { resolve, basename } from 'path';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
const BLOCKED_PATTERNS = [
|
|
5
|
-
'.ssh',
|
|
6
|
-
'.gnupg',
|
|
7
|
-
'.gpg',
|
|
8
|
-
'.aws',
|
|
9
|
-
'.azure',
|
|
10
|
-
'.gcloud',
|
|
11
|
-
'credentials',
|
|
12
|
-
'.env',
|
|
13
|
-
'.npmrc',
|
|
14
|
-
'.pypirc',
|
|
15
|
-
'.docker/config.json',
|
|
16
|
-
'.kube',
|
|
17
|
-
];
|
|
18
|
-
export function isBlockedPath(resolvedPath) {
|
|
19
|
-
const segments = resolvedPath.split('/');
|
|
20
|
-
for (const segment of segments) {
|
|
21
|
-
if (BLOCKED_PATTERNS.includes(segment)) {
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
// Also check for compound patterns like .docker/config.json
|
|
26
|
-
for (const pattern of BLOCKED_PATTERNS) {
|
|
27
|
-
if (pattern.includes('/') && resolvedPath.includes(pattern)) {
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
export function validateMountPaths(allowedPaths) {
|
|
34
|
-
const home = homedir();
|
|
35
|
-
const mounts = [];
|
|
36
|
-
const usedBaseNames = new Map();
|
|
37
|
-
for (const rawPath of allowedPaths) {
|
|
38
|
-
const expanded = rawPath.startsWith('~')
|
|
39
|
-
? resolve(home, rawPath.slice(2))
|
|
40
|
-
: resolve(rawPath);
|
|
41
|
-
let resolved;
|
|
42
|
-
try {
|
|
43
|
-
if (!existsSync(expanded)) {
|
|
44
|
-
continue; // Skip missing paths
|
|
45
|
-
}
|
|
46
|
-
resolved = realpathSync(expanded);
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
continue; // Skip paths that can't be resolved
|
|
50
|
-
}
|
|
51
|
-
if (isBlockedPath(resolved)) {
|
|
52
|
-
throw new Error(`Blocked path: ${resolved} matches security exclusion pattern`);
|
|
53
|
-
}
|
|
54
|
-
let base = basename(resolved);
|
|
55
|
-
const count = usedBaseNames.get(base) ?? 0;
|
|
56
|
-
usedBaseNames.set(base, count + 1);
|
|
57
|
-
if (count > 0) {
|
|
58
|
-
base = `${base}_${count}`;
|
|
59
|
-
}
|
|
60
|
-
mounts.push({
|
|
61
|
-
host: resolved,
|
|
62
|
-
container: `/workspace/${base}`,
|
|
63
|
-
readOnly: false,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
// Always add ~/.skimpyclaw at /workspace/config
|
|
67
|
-
const skimpyclawDir = resolve(home, '.skimpyclaw');
|
|
68
|
-
if (existsSync(skimpyclawDir)) {
|
|
69
|
-
const resolved = realpathSync(skimpyclawDir);
|
|
70
|
-
// Only add if not already included
|
|
71
|
-
if (!mounts.some((m) => m.host === resolved)) {
|
|
72
|
-
mounts.push({
|
|
73
|
-
host: resolved,
|
|
74
|
-
container: '/workspace/config',
|
|
75
|
-
readOnly: false,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return mounts;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Translate a host path to its container equivalent using mount specs.
|
|
83
|
-
* Returns the original path if no mount matches (will likely fail inside container).
|
|
84
|
-
*/
|
|
85
|
-
export function translatePath(hostPath, mounts) {
|
|
86
|
-
// Expand ~ to home directory (~ is a shell feature, not handled by resolve())
|
|
87
|
-
const expanded = hostPath.startsWith('~/')
|
|
88
|
-
? homedir() + hostPath.slice(1)
|
|
89
|
-
: hostPath;
|
|
90
|
-
const candidates = getPathCandidates(expanded);
|
|
91
|
-
// Sort by host path length descending so we match the most specific mount first
|
|
92
|
-
const sorted = [...mounts].sort((a, b) => b.host.length - a.host.length);
|
|
93
|
-
for (const candidate of candidates) {
|
|
94
|
-
for (const mount of sorted) {
|
|
95
|
-
if (candidate === mount.host || candidate.startsWith(mount.host + '/')) {
|
|
96
|
-
const relative = candidate.slice(mount.host.length);
|
|
97
|
-
return mount.container + relative;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return hostPath;
|
|
102
|
-
}
|
|
103
|
-
function getPathCandidates(hostPath) {
|
|
104
|
-
const set = new Set();
|
|
105
|
-
const resolved = resolve(hostPath);
|
|
106
|
-
set.add(resolved);
|
|
107
|
-
try {
|
|
108
|
-
set.add(realpathSync(resolved));
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
// Path may not exist yet (e.g. write targets); keep resolved form.
|
|
112
|
-
}
|
|
113
|
-
for (const p of Array.from(set)) {
|
|
114
|
-
if (p.startsWith('/Users/')) {
|
|
115
|
-
set.add(`/System/Volumes/Data${p}`);
|
|
116
|
-
}
|
|
117
|
-
else if (p.startsWith('/System/Volumes/Data/Users/')) {
|
|
118
|
-
set.add(p.replace('/System/Volumes/Data', ''));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return Array.from(set);
|
|
122
|
-
}
|