plugin-agent-orchestrator 1.0.21 → 1.0.23
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/client-v2.d.ts +2 -0
- package/client-v2.js +1 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/214.723affb37c13bf7a.js +10 -0
- package/dist/client-v2/264.0533912e6c5ea2d7.js +10 -0
- package/dist/client-v2/41.1805b2edfaa4afe2.js +10 -0
- package/dist/client-v2/418.5ae055abf141820e.js +10 -0
- package/dist/client-v2/619.d99d3c9e61c99064.js +10 -0
- package/dist/client-v2/70.a15d7fcec7c41768.js +10 -0
- package/dist/client-v2/892.72db4161511c8a16.js +10 -0
- package/dist/client-v2/926.87f660b670d85bcc.js +10 -0
- package/dist/client-v2/index.js +10 -0
- package/dist/externalVersion.js +7 -6
- package/dist/locale/en-US.json +7 -0
- package/dist/locale/vi-VN.json +7 -0
- package/dist/locale/zh-CN.json +27 -0
- package/dist/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.js +63 -0
- package/dist/server/plugin.js +41 -1
- package/dist/server/services/AgentHarness.js +52 -27
- package/dist/server/services/AgentLoopController.js +8 -2
- package/dist/server/services/AgentLoopService.js +1 -1
- package/dist/server/services/AgentRegistryService.js +53 -42
- package/dist/server/services/CircuitBreaker.js +7 -2
- package/dist/server/services/CodeValidator.js +48 -14
- package/dist/server/services/SandboxRunner.js +18 -14
- package/dist/server/skill-hub/plugin.js +44 -17
- package/dist/server/tools/delegate-task.js +7 -2
- package/dist/server/tools/skill-execute.js +33 -2
- package/dist/server/utils/ai-manager.js +51 -0
- package/dist/server/utils/ctx-utils.js +11 -0
- package/dist/server/utils/skill-settings.js +122 -0
- package/package.json +49 -45
- package/src/client/AIEmployeesContext.tsx +51 -14
- package/src/client/AgentRunsTab.tsx +767 -764
- package/src/client/HarnessProfilesTab.tsx +254 -247
- package/src/client/RulesTab.tsx +780 -716
- package/src/client/TracingTab.tsx +1 -0
- package/src/client/plugin.tsx +34 -27
- package/src/client/skill-hub/components/GitSkillImport.tsx +10 -3
- package/src/client/skill-hub/components/SkillMetrics.tsx +157 -124
- package/src/client/skill-hub/index.tsx +58 -51
- package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +132 -99
- package/src/client/skill-hub/tools/registerSkillLoopCards.ts +71 -58
- package/src/client/tools/registerOrchestratorCards.ts +17 -7
- package/src/client-v2/components/AIEmployeeSelect.tsx +47 -0
- package/src/client-v2/components/AIEmployeesContext.tsx +110 -0
- package/src/client-v2/components/AgentRunsTab.tsx +767 -0
- package/src/client-v2/components/HarnessProfilesTab.tsx +254 -0
- package/src/client-v2/components/RulesTab.tsx +782 -0
- package/src/client-v2/components/TracingTab.tsx +432 -0
- package/src/client-v2/hooks/useApiRequest.ts +114 -0
- package/src/client-v2/pages/AgentRunsPage.tsx +13 -0
- package/src/client-v2/pages/ExecutionHistoryPage.tsx +10 -0
- package/src/client-v2/pages/HarnessProfilesPage.tsx +10 -0
- package/src/client-v2/pages/LoopSettingsPage.tsx +10 -0
- package/src/client-v2/pages/RulesPage.tsx +13 -0
- package/src/client-v2/pages/SkillDefinitionsPage.tsx +10 -0
- package/src/client-v2/pages/SkillMetricsPage.tsx +10 -0
- package/src/client-v2/pages/TracingPage.tsx +13 -0
- package/src/client-v2/plugin.tsx +70 -0
- package/src/client-v2/skill-hub/components/ExecutionHistory.tsx +196 -0
- package/src/client-v2/skill-hub/components/FileLinkList.tsx +37 -0
- package/src/client-v2/skill-hub/components/GitSkillImport.tsx +539 -0
- package/src/client-v2/skill-hub/components/LoopSettings.tsx +331 -0
- package/src/client-v2/skill-hub/components/SkillEditor.tsx +453 -0
- package/src/client-v2/skill-hub/components/SkillManager.tsx +174 -0
- package/src/client-v2/skill-hub/components/SkillMetrics.tsx +157 -0
- package/src/client-v2/skill-hub/components/SkillTestPanel.tsx +135 -0
- package/src/client-v2/skill-hub/locale.ts +13 -0
- package/src/client-v2/skill-hub/tools/loopTemplates.ts +52 -0
- package/src/client-v2/skill-hub/utils/jsonFields.ts +41 -0
- package/src/client-v2/utils/jsonFields.ts +41 -0
- package/src/locale/en-US.json +7 -0
- package/src/locale/vi-VN.json +7 -0
- package/src/locale/zh-CN.json +27 -0
- package/src/server/__tests__/agent-registry-service.test.ts +147 -0
- package/src/server/__tests__/code-validator.test.ts +63 -0
- package/src/server/__tests__/skill-execute.test.ts +33 -0
- package/src/server/__tests__/skill-settings.test.ts +63 -0
- package/src/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.ts +39 -0
- package/src/server/plugin.ts +68 -12
- package/src/server/services/AgentHarness.ts +49 -22
- package/src/server/services/AgentLoopController.ts +17 -6
- package/src/server/services/AgentLoopService.ts +1 -1
- package/src/server/services/AgentPlannerService.ts +10 -0
- package/src/server/services/AgentRegistryService.ts +89 -47
- package/src/server/services/CircuitBreaker.ts +10 -0
- package/src/server/services/CodeValidator.ts +237 -159
- package/src/server/services/SandboxRunner.ts +203 -189
- package/src/server/skill-hub/plugin.ts +933 -898
- package/src/server/tools/delegate-task.ts +12 -9
- package/src/server/tools/skill-execute.ts +194 -160
- package/src/server/utils/ai-manager.ts +24 -0
- package/src/server/utils/ctx-utils.ts +14 -0
- package/src/server/utils/skill-settings.ts +116 -0
- package/dist/client/AIEmployeeSelect.d.ts +0 -11
- package/dist/client/AIEmployeesContext.d.ts +0 -30
- package/dist/client/AgentRunsTab.d.ts +0 -2
- package/dist/client/HarnessProfilesTab.d.ts +0 -2
- package/dist/client/OrchestratorSettings.d.ts +0 -3
- package/dist/client/RulesTab.d.ts +0 -2
- package/dist/client/TracingTab.d.ts +0 -2
- package/dist/client/hooks/useRunEventStream.d.ts +0 -22
- package/dist/client/index.d.ts +0 -2
- package/dist/client/plugin.d.ts +0 -6
- package/dist/client/skill-hub/components/ExecutionHistory.d.ts +0 -2
- package/dist/client/skill-hub/components/ExecutionProgress.d.ts +0 -20
- package/dist/client/skill-hub/components/GitSkillImport.d.ts +0 -7
- package/dist/client/skill-hub/components/LoopSettings.d.ts +0 -2
- package/dist/client/skill-hub/components/SkillEditor.d.ts +0 -7
- package/dist/client/skill-hub/components/SkillManager.d.ts +0 -2
- package/dist/client/skill-hub/components/SkillMetrics.d.ts +0 -2
- package/dist/client/skill-hub/components/SkillTestPanel.d.ts +0 -7
- package/dist/client/skill-hub/index.d.ts +0 -11
- package/dist/client/skill-hub/locale.d.ts +0 -3
- package/dist/client/skill-hub/tools/InteractionSchemasProvider.d.ts +0 -6
- package/dist/client/skill-hub/tools/SkillHubCard.d.ts +0 -3
- package/dist/client/skill-hub/tools/loopTemplates.d.ts +0 -22
- package/dist/client/skill-hub/tools/registerSkillLoopCards.d.ts +0 -1
- package/dist/client/skill-hub/utils/jsonFields.d.ts +0 -3
- package/dist/client/tools/PlanApprovalCard.d.ts +0 -3
- package/dist/client/tools/registerOrchestratorCards.d.ts +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/server/collections/agent-execution-spans.d.ts +0 -9
- package/dist/server/collections/agent-harness-profiles.d.ts +0 -2
- package/dist/server/collections/agent-loop-events.d.ts +0 -2
- package/dist/server/collections/agent-loop-runs.d.ts +0 -2
- package/dist/server/collections/agent-loop-steps.d.ts +0 -2
- package/dist/server/collections/orchestrator-config.d.ts +0 -2
- package/dist/server/collections/orchestrator-logs.d.ts +0 -8
- package/dist/server/collections/skill-definitions.d.ts +0 -3
- package/dist/server/collections/skill-executions.d.ts +0 -3
- package/dist/server/collections/skill-loop-configs.d.ts +0 -3
- package/dist/server/collections/skill-worker-configs.d.ts +0 -3
- package/dist/server/migrations/20260423000000-add-progress-fields.d.ts +0 -4
- package/dist/server/migrations/20260425000000-add-interaction-schema.d.ts +0 -4
- package/dist/server/migrations/20260427000000-add-tracing-detail-fields.d.ts +0 -7
- package/dist/server/migrations/20260427000000-change-packages-to-text.d.ts +0 -4
- package/dist/server/migrations/20260427000001-change-other-json-to-text.d.ts +0 -4
- package/dist/server/migrations/20260429000000-add-llm-fields.d.ts +0 -7
- package/dist/server/migrations/20260429000000-fix-inputargs-json-to-text.d.ts +0 -16
- package/dist/server/migrations/20260503000000-add-orchestrator-trace-fields.d.ts +0 -7
- package/dist/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.d.ts +0 -7
- package/dist/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.d.ts +0 -12
- package/dist/server/migrations/20260601000000-add-token-fields.d.ts +0 -7
- package/dist/server/plugin.d.ts +0 -16
- package/dist/server/resources/agent-loop.d.ts +0 -3
- package/dist/server/resources/tracing.d.ts +0 -7
- package/dist/server/services/AgentHarness.d.ts +0 -44
- package/dist/server/services/AgentLoopController.d.ts +0 -218
- package/dist/server/services/AgentLoopRepository.d.ts +0 -20
- package/dist/server/services/AgentLoopService.d.ts +0 -159
- package/dist/server/services/AgentPlanValidator.d.ts +0 -4
- package/dist/server/services/AgentPlannerService.d.ts +0 -8
- package/dist/server/services/AgentRegistryService.d.ts +0 -21
- package/dist/server/services/CircuitBreaker.d.ts +0 -40
- package/dist/server/services/CodeValidator.d.ts +0 -32
- package/dist/server/services/ContextAggregator.d.ts +0 -45
- package/dist/server/services/ExecutionSpanService.d.ts +0 -46
- package/dist/server/services/FileManager.d.ts +0 -28
- package/dist/server/services/RunEventBus.d.ts +0 -9
- package/dist/server/services/SandboxRunner.d.ts +0 -41
- package/dist/server/services/SkillManager.d.ts +0 -6
- package/dist/server/services/SkillRepositoryService.d.ts +0 -22
- package/dist/server/services/TokenTracker.d.ts +0 -62
- package/dist/server/services/WorkerEnvManager.d.ts +0 -26
- package/dist/server/skill-hub/actions/git-import.d.ts +0 -21
- package/dist/server/skill-hub/mcp/McpController.d.ts +0 -15
- package/dist/server/skill-hub/plugin.d.ts +0 -61
- package/dist/server/skill-hub/tasks/SkillExecutionTask.d.ts +0 -16
- package/dist/server/skill-hub/utils/json-fields.d.ts +0 -7
- package/dist/server/tools/agent-loop.d.ts +0 -235
- package/dist/server/tools/delegate-task.d.ts +0 -19
- package/dist/server/tools/external-rag-search.d.ts +0 -42
- package/dist/server/tools/orchestrator-plan.d.ts +0 -205
- package/dist/server/tools/skill-execute.d.ts +0 -36
- package/dist/server/types.d.ts +0 -47
- package/dist/server/utils/ctx-utils.d.ts +0 -30
- package/dist/server/utils/logging.d.ts +0 -6
- /package/{dist/server/index.d.ts → src/client-v2/index.tsx} +0 -0
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
import { toPlain, asObject, normalizeEmployeeUsername } from '../utils/ctx-utils';
|
|
2
2
|
|
|
3
|
+
type OrchestratorConfigRow = {
|
|
4
|
+
leaderUsername?: string;
|
|
5
|
+
subAgentUsername?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type ModelRef = { llmService: string; model: string };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalize an AI employee's `modelSettings` (or a per-rule override) into a
|
|
12
|
+
* flat { llmService, model } the harness can hand to getLLMService().
|
|
13
|
+
*
|
|
14
|
+
* The admin UI (plugin-ai ModelSettings) stores the dedicated model selection
|
|
15
|
+
* as `{ enabled, models: [{ llmService, model }] }` and clears the flat
|
|
16
|
+
* `llmService`/`model` fields. Older records still use the flat shape, and
|
|
17
|
+
* per-rule overrides arrive flat too. Mirror plugin-ai's resolveModel:
|
|
18
|
+
* - if `enabled`, prefer `models[0]`, else fall back to the flat fields
|
|
19
|
+
* - a bare flat { llmService, model } (e.g. a rule override) is also valid
|
|
20
|
+
*/
|
|
21
|
+
function extractModelRef(value: any): ModelRef | undefined {
|
|
22
|
+
if (!value) return undefined;
|
|
23
|
+
|
|
24
|
+
const isValid = (m: any): m is ModelRef => Boolean(m?.llmService && m?.model);
|
|
25
|
+
|
|
26
|
+
// Dedicated model configuration (UI shape).
|
|
27
|
+
if (value.enabled) {
|
|
28
|
+
const models = Array.isArray(value.models) ? value.models : [];
|
|
29
|
+
const first = models.find(isValid);
|
|
30
|
+
if (first) {
|
|
31
|
+
return { llmService: first.llmService, model: first.model };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Flat legacy shape, or a per-rule override.
|
|
36
|
+
if (isValid(value)) {
|
|
37
|
+
return { llmService: value.llmService, model: value.model };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sanitizeToolPart(value: string) {
|
|
44
|
+
return (value || '').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildDelegateToolName(leaderUsername: string, subAgentUsername: string) {
|
|
48
|
+
return `delegate_${sanitizeToolPart(leaderUsername)}_to_${sanitizeToolPart(subAgentUsername)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildDispatchToolName(leaderUsername: string) {
|
|
52
|
+
return `dispatch_subagents_${sanitizeToolPart(leaderUsername)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildLegacyDelegateToolName(subAgentUsername: string) {
|
|
56
|
+
return `delegate_to_${sanitizeToolPart(subAgentUsername)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
3
59
|
export class AgentRegistryService {
|
|
4
60
|
constructor(private readonly plugin: any) {}
|
|
5
61
|
|
|
@@ -59,26 +115,28 @@ export class AgentRegistryService {
|
|
|
59
115
|
throw new Error(`Sub-agent "${subAgentUsername}" was not found.`);
|
|
60
116
|
}
|
|
61
117
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
118
|
+
// 1. Explicit per-rule override (already a flat { llmService, model }).
|
|
119
|
+
const dynamic = extractModelRef(dynamicValues);
|
|
120
|
+
if (dynamic) {
|
|
121
|
+
return dynamic;
|
|
122
|
+
}
|
|
67
123
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
124
|
+
// 2. Sub-agent's own dedicated model configuration.
|
|
125
|
+
const subAgentModel = extractModelRef(subAgent.modelSettings);
|
|
126
|
+
if (subAgentModel) {
|
|
127
|
+
return subAgentModel;
|
|
72
128
|
}
|
|
73
129
|
|
|
74
|
-
|
|
130
|
+
// 3. Inherit from leader.
|
|
131
|
+
if (leaderUsername) {
|
|
75
132
|
const leader = await this.getAIEmployee(leaderUsername);
|
|
76
|
-
|
|
77
|
-
|
|
133
|
+
const leaderModel = extractModelRef(leader?.modelSettings);
|
|
134
|
+
if (leaderModel) {
|
|
135
|
+
return leaderModel;
|
|
78
136
|
}
|
|
79
137
|
}
|
|
80
138
|
|
|
81
|
-
return
|
|
139
|
+
return undefined;
|
|
82
140
|
}
|
|
83
141
|
|
|
84
142
|
/**
|
|
@@ -127,49 +185,33 @@ export class AgentRegistryService {
|
|
|
127
185
|
const configRepo = this.db.getRepository('orchestratorConfig');
|
|
128
186
|
if (!configRepo) return false;
|
|
129
187
|
|
|
188
|
+
const configs: OrchestratorConfigRow[] = await configRepo.find({
|
|
189
|
+
filter: { enabled: true },
|
|
190
|
+
});
|
|
191
|
+
if (!configs || configs.length === 0) return false;
|
|
192
|
+
|
|
130
193
|
// 1. Check if it matches dispatch_subagents_${leader}
|
|
131
194
|
if (toolName.startsWith('dispatch_subagents_')) {
|
|
132
|
-
|
|
133
|
-
const count = await configRepo.count({
|
|
134
|
-
filter: {
|
|
135
|
-
leaderUsername: leader,
|
|
136
|
-
enabled: true,
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
return count > 0;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// 2. Check if it matches delegate_${leader}_to_${subAgent}
|
|
143
|
-
if (toolName.startsWith('delegate_') && toolName.includes('_to_')) {
|
|
144
|
-
const parts = toolName.substring('delegate_'.length).split('_to_');
|
|
145
|
-
if (parts.length === 2) {
|
|
146
|
-
const [leader, subAgent] = parts;
|
|
147
|
-
const count = await configRepo.count({
|
|
148
|
-
filter: {
|
|
149
|
-
leaderUsername: leader,
|
|
150
|
-
subAgentUsername: subAgent,
|
|
151
|
-
enabled: true,
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
if (count > 0) return true;
|
|
155
|
-
}
|
|
195
|
+
return configs.some((config) => buildDispatchToolName(config.leaderUsername || '') === toolName);
|
|
156
196
|
}
|
|
157
197
|
|
|
158
|
-
//
|
|
198
|
+
// 2. Check legacy alias: delegate_to_${subAgent}
|
|
159
199
|
if (toolName.startsWith('delegate_to_')) {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
enabled: true,
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
// Legacy alias is only registered if there is exactly one leader for this subAgent
|
|
168
|
-
if (configs?.length === 1) {
|
|
200
|
+
const matchingConfigs = configs.filter(
|
|
201
|
+
(config) => buildLegacyDelegateToolName(config.subAgentUsername || '') === toolName,
|
|
202
|
+
);
|
|
203
|
+
if (matchingConfigs.length === 1) {
|
|
169
204
|
return true;
|
|
170
205
|
}
|
|
171
206
|
}
|
|
172
207
|
|
|
208
|
+
// 3. Check if it matches delegate_${leader}_to_${subAgent}
|
|
209
|
+
if (toolName.startsWith('delegate_') && toolName.includes('_to_')) {
|
|
210
|
+
return configs.some(
|
|
211
|
+
(config) => buildDelegateToolName(config.leaderUsername || '', config.subAgentUsername || '') === toolName,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
173
215
|
return false;
|
|
174
216
|
} catch {
|
|
175
217
|
return false;
|
|
@@ -100,6 +100,16 @@ export class CircuitBreakerRegistry {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Build the circuit key for a sub-agent. Keying by `${leader}::${target}` keeps
|
|
105
|
+
* one leader's failures from opening the circuit for the same sub-agent under a
|
|
106
|
+
* different leader — their delegations are independent code paths. Falls back to
|
|
107
|
+
* the bare target when no leader is known.
|
|
108
|
+
*/
|
|
109
|
+
export function subAgentCircuitKey(leaderUsername: string | undefined, target: string): string {
|
|
110
|
+
return leaderUsername ? `${leaderUsername}::${target}` : target;
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
// Singleton shared across the plugin
|
|
104
114
|
let globalInstance: CircuitBreakerRegistry | null = null;
|
|
105
115
|
|
|
@@ -1,159 +1,237 @@
|
|
|
1
|
-
interface ForbiddenPattern {
|
|
2
|
-
pattern: RegExp;
|
|
3
|
-
reason: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const DANGEROUS_NODE_PATTERNS: ForbiddenPattern[] = [
|
|
7
|
-
{ pattern: /require\s*\(\s*['"]child_process['"]\s*\)/, reason: 'child_process module not allowed' },
|
|
8
|
-
{ pattern: /require\s*\(\s*['"]cluster['"]\s*\)/, reason: 'cluster module not allowed' },
|
|
9
|
-
{ pattern: /require\s*\(\s*['"]dgram['"]\s*\)/, reason: 'dgram module not allowed' },
|
|
10
|
-
{ pattern: /require\s*\(\s*['"]net['"]\s*\)/, reason: 'net module not allowed' },
|
|
11
|
-
{ pattern: /require\s*\(\s*['"]http['"]\s*\)/, reason: 'http module not allowed' },
|
|
12
|
-
{ pattern: /require\s*\(\s*['"]https['"]\s*\)/, reason: 'https module not allowed' },
|
|
13
|
-
{ pattern: /require\s*\(\s*['"]vm['"]\s*\)/, reason: 'vm module not allowed' },
|
|
14
|
-
{ pattern: /
|
|
15
|
-
{ pattern: /
|
|
16
|
-
{ pattern: /
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{ pattern: /
|
|
21
|
-
{ pattern:
|
|
22
|
-
|
|
23
|
-
{ pattern:
|
|
24
|
-
{ pattern:
|
|
25
|
-
{ pattern:
|
|
26
|
-
|
|
27
|
-
{ pattern:
|
|
28
|
-
|
|
29
|
-
{ pattern:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
'
|
|
48
|
-
|
|
49
|
-
'
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
1
|
+
interface ForbiddenPattern {
|
|
2
|
+
pattern: RegExp;
|
|
3
|
+
reason: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const DANGEROUS_NODE_PATTERNS: ForbiddenPattern[] = [
|
|
7
|
+
{ pattern: /require\s*\(\s*['"]child_process['"]\s*\)/, reason: 'child_process module not allowed' },
|
|
8
|
+
{ pattern: /require\s*\(\s*['"]cluster['"]\s*\)/, reason: 'cluster module not allowed' },
|
|
9
|
+
{ pattern: /require\s*\(\s*['"]dgram['"]\s*\)/, reason: 'dgram module not allowed' },
|
|
10
|
+
{ pattern: /require\s*\(\s*['"]net['"]\s*\)/, reason: 'net module not allowed' },
|
|
11
|
+
{ pattern: /require\s*\(\s*['"]http['"]\s*\)/, reason: 'http module not allowed' },
|
|
12
|
+
{ pattern: /require\s*\(\s*['"]https['"]\s*\)/, reason: 'https module not allowed' },
|
|
13
|
+
{ pattern: /require\s*\(\s*['"]vm['"]\s*\)/, reason: 'vm module not allowed' },
|
|
14
|
+
{ pattern: /require\s*\(\s*['"]worker_threads['"]\s*\)/, reason: 'worker_threads module not allowed' },
|
|
15
|
+
{ pattern: /require\s*\(\s*['"]inspector['"]\s*\)/, reason: 'inspector module not allowed' },
|
|
16
|
+
{ pattern: /require\s*\(\s*['"]v8['"]\s*\)/, reason: 'v8 module not allowed' },
|
|
17
|
+
{ pattern: /process\.exit/, reason: 'process.exit not allowed' },
|
|
18
|
+
{ pattern: /process\.env(?!\s*\.OUTPUT_DIR)/, reason: 'process.env access not allowed (use OUTPUT_DIR only)' },
|
|
19
|
+
{ pattern: /process\.kill/, reason: 'process.kill not allowed' },
|
|
20
|
+
{ pattern: /process\.binding\s*\(/, reason: 'process.binding not allowed' },
|
|
21
|
+
{ pattern: /\bglobalThis\b/, reason: 'globalThis access not allowed' },
|
|
22
|
+
// Indirect eval / dynamic Function construction (bypasses the import allowlist).
|
|
23
|
+
{ pattern: /\beval\s*\(/, reason: 'eval not allowed' },
|
|
24
|
+
{ pattern: /\bnew\s+Function\s*\(/, reason: 'new Function() not allowed' },
|
|
25
|
+
{ pattern: /\bFunction\s*\(/, reason: 'Function() constructor not allowed' },
|
|
26
|
+
// Dynamic import() can pull arbitrary modules at runtime, bypassing require() checks.
|
|
27
|
+
{ pattern: /\bimport\s*\(/, reason: 'dynamic import() not allowed' },
|
|
28
|
+
// require resolved from a non-literal expression (e.g. require(varName)).
|
|
29
|
+
{ pattern: /require\s*\(\s*(?!['"])/, reason: 'require() with a non-literal argument not allowed' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const DANGEROUS_PYTHON_PATTERNS: ForbiddenPattern[] = [
|
|
33
|
+
{ pattern: /import\s+subprocess/, reason: 'subprocess module not allowed' },
|
|
34
|
+
{ pattern: /from\s+subprocess\s+import/, reason: 'subprocess module not allowed' },
|
|
35
|
+
{ pattern: /import\s+shutil/, reason: 'shutil module not allowed' },
|
|
36
|
+
{ pattern: /import\s+socket/, reason: 'socket module not allowed' },
|
|
37
|
+
{ pattern: /import\s+ctypes/, reason: 'ctypes module not allowed' },
|
|
38
|
+
{ pattern: /import\s+pickle/, reason: 'pickle module not allowed' },
|
|
39
|
+
{ pattern: /import\s+marshal/, reason: 'marshal module not allowed' },
|
|
40
|
+
{ pattern: /import\s+importlib/, reason: 'importlib module not allowed' },
|
|
41
|
+
{
|
|
42
|
+
pattern: /from\s+(?:socket|ctypes|pickle|marshal|importlib)\s+import/,
|
|
43
|
+
reason: 'restricted module import not allowed',
|
|
44
|
+
},
|
|
45
|
+
{ pattern: /__import__\s*\(/, reason: '__import__ not allowed' },
|
|
46
|
+
{ pattern: /\bimportlib\b/, reason: 'importlib access not allowed' },
|
|
47
|
+
{ pattern: /os\.system\s*\(/, reason: 'os.system not allowed' },
|
|
48
|
+
{ pattern: /os\.popen\s*\(/, reason: 'os.popen not allowed' },
|
|
49
|
+
{ pattern: /os\.exec\w*\s*\(/, reason: 'os.exec* not allowed' },
|
|
50
|
+
{ pattern: /os\.spawn\w*\s*\(/, reason: 'os.spawn* not allowed' },
|
|
51
|
+
{ pattern: /os\.fork\s*\(/, reason: 'os.fork not allowed' },
|
|
52
|
+
// getattr/setattr can reconstruct blocked names like getattr(os,'sys'+'tem').
|
|
53
|
+
{ pattern: /\bgetattr\s*\(/, reason: 'getattr not allowed (can bypass name checks)' },
|
|
54
|
+
{ pattern: /\bsetattr\s*\(/, reason: 'setattr not allowed' },
|
|
55
|
+
{ pattern: /\b__builtins__\b/, reason: '__builtins__ access not allowed' },
|
|
56
|
+
{ pattern: /\beval\s*\(/, reason: 'eval not allowed' },
|
|
57
|
+
{ pattern: /\bexec\s*\(/, reason: 'exec not allowed' },
|
|
58
|
+
{ pattern: /\bcompile\s*\(/, reason: 'compile not allowed' },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/** Built-in Node.js modules that are always allowed in sandbox code */
|
|
62
|
+
const NODE_BUILTINS = [
|
|
63
|
+
'fs',
|
|
64
|
+
'path',
|
|
65
|
+
'os',
|
|
66
|
+
'crypto',
|
|
67
|
+
'util',
|
|
68
|
+
'stream',
|
|
69
|
+
'buffer',
|
|
70
|
+
'querystring',
|
|
71
|
+
'url',
|
|
72
|
+
'assert',
|
|
73
|
+
'events',
|
|
74
|
+
'string_decoder',
|
|
75
|
+
'zlib',
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/** Built-in Python modules that are always allowed in sandbox code */
|
|
79
|
+
const PYTHON_BUILTINS = [
|
|
80
|
+
'os',
|
|
81
|
+
'sys',
|
|
82
|
+
'json',
|
|
83
|
+
'math',
|
|
84
|
+
'datetime',
|
|
85
|
+
'collections',
|
|
86
|
+
'itertools',
|
|
87
|
+
'functools',
|
|
88
|
+
'pathlib',
|
|
89
|
+
'typing',
|
|
90
|
+
'io',
|
|
91
|
+
'csv',
|
|
92
|
+
're',
|
|
93
|
+
'string',
|
|
94
|
+
'textwrap',
|
|
95
|
+
'decimal',
|
|
96
|
+
'fractions',
|
|
97
|
+
'random',
|
|
98
|
+
'statistics',
|
|
99
|
+
'copy',
|
|
100
|
+
'enum',
|
|
101
|
+
'dataclasses',
|
|
102
|
+
'abc',
|
|
103
|
+
'contextlib',
|
|
104
|
+
'operator',
|
|
105
|
+
'time',
|
|
106
|
+
'calendar',
|
|
107
|
+
'locale',
|
|
108
|
+
'struct',
|
|
109
|
+
'hashlib',
|
|
110
|
+
'base64',
|
|
111
|
+
'binascii',
|
|
112
|
+
'codecs',
|
|
113
|
+
'unicodedata',
|
|
114
|
+
'pprint',
|
|
115
|
+
'warnings',
|
|
116
|
+
'traceback',
|
|
117
|
+
'logging',
|
|
118
|
+
'unittest',
|
|
119
|
+
'argparse',
|
|
120
|
+
'ast',
|
|
121
|
+
'tempfile',
|
|
122
|
+
'xml',
|
|
123
|
+
'zipfile',
|
|
124
|
+
// Pre-installed local packages (trusted, bundled with plugin)
|
|
125
|
+
'svg_to_pptx',
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Maps PyPI package names to their Python import names
|
|
130
|
+
* (only where they differ from the package name).
|
|
131
|
+
*/
|
|
132
|
+
const PYTHON_IMPORT_NAME_MAP: Record<string, string> = {
|
|
133
|
+
'python-docx': 'docx',
|
|
134
|
+
'python-pptx': 'pptx',
|
|
135
|
+
Pillow: 'PIL',
|
|
136
|
+
pyyaml: 'yaml',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export class CodeValidator {
|
|
140
|
+
/**
|
|
141
|
+
* Check code against forbidden patterns (dangerous modules/functions).
|
|
142
|
+
* @throws CodeValidationError if a forbidden pattern is found.
|
|
143
|
+
*/
|
|
144
|
+
validate(code: string, language: 'node' | 'python'): void {
|
|
145
|
+
const patterns = language === 'node' ? DANGEROUS_NODE_PATTERNS : DANGEROUS_PYTHON_PATTERNS;
|
|
146
|
+
|
|
147
|
+
for (const { pattern, reason } of patterns) {
|
|
148
|
+
if (pattern.test(code)) {
|
|
149
|
+
throw new CodeValidationError(reason, pattern.source);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Validate that code only imports packages in the allowlist (builtins +
|
|
156
|
+
* whitelist). Called after the basic forbidden pattern check.
|
|
157
|
+
*
|
|
158
|
+
* An empty whitelist does NOT skip validation: only built-in modules are
|
|
159
|
+
* then allowed. This closes an exfiltration hole — stdlib modules such as
|
|
160
|
+
* Python `urllib`/`socket`/`ftplib` need no install, so skipping the check
|
|
161
|
+
* when the env was not initialized would let a skill open outbound
|
|
162
|
+
* connections. Set SKILL_HUB_ALLOW_ANY_IMPORT=true to restore the old
|
|
163
|
+
* skip-when-empty behaviour for legacy deployments.
|
|
164
|
+
*
|
|
165
|
+
* @param code - The code to validate
|
|
166
|
+
* @param language - 'node' or 'python'
|
|
167
|
+
* @param whitelist - Allowed package names (from skillWorkerConfigs.packageWhitelist)
|
|
168
|
+
*/
|
|
169
|
+
validateImports(code: string, language: 'node' | 'python', whitelist: string[]): void {
|
|
170
|
+
if (!whitelist?.length && process.env.SKILL_HUB_ALLOW_ANY_IMPORT === 'true') {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const list = whitelist || [];
|
|
174
|
+
|
|
175
|
+
if (language === 'node') {
|
|
176
|
+
this.validateNodeImports(code, list);
|
|
177
|
+
} else if (language === 'python') {
|
|
178
|
+
this.validatePythonImports(code, list);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check Node.js require() calls against the whitelist.
|
|
184
|
+
* Built-in modules (fs, path, etc.) are always allowed.
|
|
185
|
+
*/
|
|
186
|
+
private validateNodeImports(code: string, whitelist: string[]): void {
|
|
187
|
+
// Match require statements with string literal arguments (non-relative paths)
|
|
188
|
+
const requires = [...code.matchAll(/require\s*\(\s*['"]([^'"./][^'"]*)['"]\s*\)/g)];
|
|
189
|
+
|
|
190
|
+
for (const match of requires) {
|
|
191
|
+
const raw = match[1];
|
|
192
|
+
// Handle scoped packages (e.g. '@org/lib') and subpaths (e.g. 'lib/utils')
|
|
193
|
+
const pkgName = raw.startsWith('@') ? raw.split('/').slice(0, 2).join('/') : raw.split('/')[0];
|
|
194
|
+
|
|
195
|
+
if (NODE_BUILTINS.includes(pkgName)) continue;
|
|
196
|
+
if (whitelist.includes(pkgName)) continue;
|
|
197
|
+
|
|
198
|
+
throw new CodeValidationError(
|
|
199
|
+
`Package "${pkgName}" is not in the allowed whitelist. Allowed: ${whitelist.join(', ')}`,
|
|
200
|
+
`require('${pkgName}')`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Check Python import/from statements against the whitelist.
|
|
207
|
+
* Built-in modules (os, sys, json, etc.) are always allowed.
|
|
208
|
+
* Handles PyPI→import name mapping (e.g., python-docx → docx, Pillow → PIL).
|
|
209
|
+
*/
|
|
210
|
+
private validatePythonImports(code: string, whitelist: string[]): void {
|
|
211
|
+
// Match: import pkg, from pkg import ..., import pkg.sub
|
|
212
|
+
const imports = [...code.matchAll(/(?:^|\n)\s*(?:import|from)\s+([a-zA-Z_][a-zA-Z0-9_]*)/g)];
|
|
213
|
+
|
|
214
|
+
// Build allowed import names from whitelist
|
|
215
|
+
const allowedImports = new Set([...PYTHON_BUILTINS, ...whitelist.map((p) => PYTHON_IMPORT_NAME_MAP[p] || p)]);
|
|
216
|
+
|
|
217
|
+
for (const match of imports) {
|
|
218
|
+
const pkg = match[1];
|
|
219
|
+
if (allowedImports.has(pkg)) continue;
|
|
220
|
+
|
|
221
|
+
throw new CodeValidationError(
|
|
222
|
+
`Module "${pkg}" is not in the allowed whitelist. Allowed: ${[...allowedImports].join(', ')}`,
|
|
223
|
+
`import ${pkg}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export class CodeValidationError extends Error {
|
|
230
|
+
constructor(
|
|
231
|
+
message: string,
|
|
232
|
+
public matchedPattern: string,
|
|
233
|
+
) {
|
|
234
|
+
super(`Code validation failed: ${message}`);
|
|
235
|
+
(this as any).name = 'CodeValidationError';
|
|
236
|
+
}
|
|
237
|
+
}
|