@polos/sdk 0.1.1 → 0.1.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/dist/index.cjs +1184 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +568 -5
- package/dist/index.d.ts +568 -5
- package/dist/index.js +1150 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -8,10 +8,34 @@ var Fastify = require('fastify');
|
|
|
8
8
|
var api = require('@opentelemetry/api');
|
|
9
9
|
var sdkTraceBase = require('@opentelemetry/sdk-trace-base');
|
|
10
10
|
var contextAsyncHooks = require('@opentelemetry/context-async-hooks');
|
|
11
|
+
var zod = require('zod');
|
|
12
|
+
var child_process = require('child_process');
|
|
13
|
+
var fs2 = require('fs/promises');
|
|
14
|
+
var path = require('path');
|
|
11
15
|
|
|
12
16
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
17
|
|
|
18
|
+
function _interopNamespace(e) {
|
|
19
|
+
if (e && e.__esModule) return e;
|
|
20
|
+
var n = Object.create(null);
|
|
21
|
+
if (e) {
|
|
22
|
+
Object.keys(e).forEach(function (k) {
|
|
23
|
+
if (k !== 'default') {
|
|
24
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
25
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
get: function () { return e[k]; }
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
n.default = e;
|
|
33
|
+
return Object.freeze(n);
|
|
34
|
+
}
|
|
35
|
+
|
|
14
36
|
var Fastify__default = /*#__PURE__*/_interopDefault(Fastify);
|
|
37
|
+
var fs2__namespace = /*#__PURE__*/_interopNamespace(fs2);
|
|
38
|
+
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
15
39
|
|
|
16
40
|
// src/core/registry.ts
|
|
17
41
|
var WorkflowNotFoundError = class extends Error {
|
|
@@ -32,9 +56,6 @@ function createWorkflowRegistry() {
|
|
|
32
56
|
const workflows = /* @__PURE__ */ new Map();
|
|
33
57
|
return {
|
|
34
58
|
register(workflow) {
|
|
35
|
-
if (workflows.has(workflow.id)) {
|
|
36
|
-
throw new DuplicateWorkflowError(workflow.id);
|
|
37
|
-
}
|
|
38
59
|
workflows.set(workflow.id, workflow);
|
|
39
60
|
},
|
|
40
61
|
get(workflowId) {
|
|
@@ -212,6 +233,12 @@ var OrchestratorClient = class {
|
|
|
212
233
|
this.timeout = config.timeout ?? 3e4;
|
|
213
234
|
this.maxRetries = config.maxRetries ?? 3;
|
|
214
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Get the API URL.
|
|
238
|
+
*/
|
|
239
|
+
getApiUrl() {
|
|
240
|
+
return this.apiUrl;
|
|
241
|
+
}
|
|
215
242
|
/**
|
|
216
243
|
* Get the project ID.
|
|
217
244
|
*/
|
|
@@ -235,8 +262,8 @@ var OrchestratorClient = class {
|
|
|
235
262
|
/**
|
|
236
263
|
* Make an HTTP request with retry logic.
|
|
237
264
|
*/
|
|
238
|
-
async request(method,
|
|
239
|
-
const url = `${this.apiUrl}${
|
|
265
|
+
async request(method, path3, options) {
|
|
266
|
+
const url = `${this.apiUrl}${path3}`;
|
|
240
267
|
const retries = options?.retries ?? this.maxRetries;
|
|
241
268
|
let lastError;
|
|
242
269
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
@@ -288,7 +315,7 @@ var OrchestratorClient = class {
|
|
|
288
315
|
}
|
|
289
316
|
if (attempt < retries) {
|
|
290
317
|
const delay = Math.min(1e3 * Math.pow(2, attempt), 16e3);
|
|
291
|
-
await new Promise((
|
|
318
|
+
await new Promise((resolve8) => setTimeout(resolve8, delay));
|
|
292
319
|
}
|
|
293
320
|
}
|
|
294
321
|
}
|
|
@@ -727,7 +754,7 @@ var OrchestratorClient = class {
|
|
|
727
754
|
if (execution.status === "completed" || execution.status === "failed" || execution.status === "cancelled") {
|
|
728
755
|
return execution;
|
|
729
756
|
}
|
|
730
|
-
await new Promise((
|
|
757
|
+
await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
|
|
731
758
|
}
|
|
732
759
|
throw new Error(`Execution ${executionId} timed out after ${String(timeout)}ms`);
|
|
733
760
|
}
|
|
@@ -1193,6 +1220,46 @@ function isToolWorkflow(workflow) {
|
|
|
1193
1220
|
}
|
|
1194
1221
|
function defineTool(config, handler) {
|
|
1195
1222
|
const toolParameters = config.inputSchema ? zodToJsonSchema.zodToJsonSchema(config.inputSchema, { target: "openApi3" }) : { type: "object", properties: {} };
|
|
1223
|
+
const effectiveHandler = config.approval === "always" ? async (ctx, input) => {
|
|
1224
|
+
const approvalId = await ctx.step.uuid("_approval_id");
|
|
1225
|
+
const response = await ctx.step.suspend(
|
|
1226
|
+
`approve_${config.id}_${approvalId}`,
|
|
1227
|
+
{
|
|
1228
|
+
data: {
|
|
1229
|
+
_form: {
|
|
1230
|
+
title: `Approve tool: ${config.id}`,
|
|
1231
|
+
description: `The agent wants to use the "${config.id}" tool.`,
|
|
1232
|
+
fields: [
|
|
1233
|
+
{
|
|
1234
|
+
key: "approved",
|
|
1235
|
+
type: "boolean",
|
|
1236
|
+
label: "Approve this tool call?",
|
|
1237
|
+
required: true,
|
|
1238
|
+
default: false
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
key: "feedback",
|
|
1242
|
+
type: "textarea",
|
|
1243
|
+
label: "Feedback for the agent (optional)",
|
|
1244
|
+
description: "If rejecting, tell the agent what to do instead.",
|
|
1245
|
+
required: false
|
|
1246
|
+
}
|
|
1247
|
+
],
|
|
1248
|
+
context: { tool: config.id, input }
|
|
1249
|
+
},
|
|
1250
|
+
_source: "tool_approval",
|
|
1251
|
+
_tool: config.id
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
);
|
|
1255
|
+
if (response.data?.approved !== true) {
|
|
1256
|
+
const feedback = response.data?.feedback;
|
|
1257
|
+
throw new Error(
|
|
1258
|
+
`Tool "${config.id}" was rejected by the user.${feedback ? ` Feedback: ${feedback}` : ""}`
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
return handler(ctx, input);
|
|
1262
|
+
} : handler;
|
|
1196
1263
|
const workflow = defineWorkflow(
|
|
1197
1264
|
{
|
|
1198
1265
|
id: config.id,
|
|
@@ -1205,7 +1272,8 @@ function defineTool(config, handler) {
|
|
|
1205
1272
|
onStart: config.onStart,
|
|
1206
1273
|
onEnd: config.onEnd
|
|
1207
1274
|
},
|
|
1208
|
-
|
|
1275
|
+
effectiveHandler,
|
|
1276
|
+
config.autoRegister === void 0 ? void 0 : { autoRegister: config.autoRegister }
|
|
1209
1277
|
);
|
|
1210
1278
|
const toolWorkflow = Object.assign(workflow, {
|
|
1211
1279
|
toolDescription: config.description,
|
|
@@ -1249,7 +1317,7 @@ function calculateDelay(attempt, options) {
|
|
|
1249
1317
|
return Math.round(delay);
|
|
1250
1318
|
}
|
|
1251
1319
|
function sleep(ms) {
|
|
1252
|
-
return new Promise((
|
|
1320
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
1253
1321
|
}
|
|
1254
1322
|
async function retry(fn, options = {}) {
|
|
1255
1323
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
@@ -1438,7 +1506,7 @@ function createStepHelper(options) {
|
|
|
1438
1506
|
if (options2.days) totalSeconds += options2.days * 86400;
|
|
1439
1507
|
if (options2.weeks) totalSeconds += options2.weeks * 604800;
|
|
1440
1508
|
const totalMs = totalSeconds * 1e3;
|
|
1441
|
-
await new Promise((
|
|
1509
|
+
await new Promise((resolve8) => setTimeout(resolve8, totalMs));
|
|
1442
1510
|
store.set(key, { wait_until: new Date(Date.now() + totalMs).toISOString() });
|
|
1443
1511
|
},
|
|
1444
1512
|
async waitUntil(key, date) {
|
|
@@ -1446,7 +1514,7 @@ function createStepHelper(options) {
|
|
|
1446
1514
|
if (cached) return;
|
|
1447
1515
|
const now = Date.now();
|
|
1448
1516
|
const waitMs = Math.max(0, date.getTime() - now);
|
|
1449
|
-
await new Promise((
|
|
1517
|
+
await new Promise((resolve8) => setTimeout(resolve8, waitMs));
|
|
1450
1518
|
store.set(key, { wait_until: date.toISOString() });
|
|
1451
1519
|
},
|
|
1452
1520
|
// eslint-disable-next-line @typescript-eslint/require-await -- stub implementation
|
|
@@ -1739,11 +1807,18 @@ function convertMiddlewareToolCallToPython(tc) {
|
|
|
1739
1807
|
function convertVercelUsageToPython(usage) {
|
|
1740
1808
|
const input = usage.inputTokens ?? 0;
|
|
1741
1809
|
const output = usage.outputTokens ?? 0;
|
|
1742
|
-
|
|
1810
|
+
const result = {
|
|
1743
1811
|
input_tokens: input,
|
|
1744
1812
|
output_tokens: output,
|
|
1745
1813
|
total_tokens: usage.totalTokens ?? input + output
|
|
1746
1814
|
};
|
|
1815
|
+
if (usage.inputTokenDetails?.cacheReadTokens != null) {
|
|
1816
|
+
result.cache_read_input_tokens = usage.inputTokenDetails.cacheReadTokens;
|
|
1817
|
+
}
|
|
1818
|
+
if (usage.inputTokenDetails?.cacheWriteTokens != null) {
|
|
1819
|
+
result.cache_creation_input_tokens = usage.inputTokenDetails.cacheWriteTokens;
|
|
1820
|
+
}
|
|
1821
|
+
return result;
|
|
1747
1822
|
}
|
|
1748
1823
|
function convertFinishReason(reason) {
|
|
1749
1824
|
if (!reason) return null;
|
|
@@ -1758,6 +1833,41 @@ function convertFinishReason(reason) {
|
|
|
1758
1833
|
}
|
|
1759
1834
|
|
|
1760
1835
|
// src/llm/llm.ts
|
|
1836
|
+
var ANTHROPIC_CACHE_BREAKPOINT = {
|
|
1837
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
1838
|
+
};
|
|
1839
|
+
function isAnthropicModel(model) {
|
|
1840
|
+
return getModelProvider(model).startsWith("anthropic");
|
|
1841
|
+
}
|
|
1842
|
+
function applyAnthropicCacheControl(args, model) {
|
|
1843
|
+
if (!isAnthropicModel(model)) return;
|
|
1844
|
+
if (typeof args["system"] === "string") {
|
|
1845
|
+
args["system"] = {
|
|
1846
|
+
role: "system",
|
|
1847
|
+
content: args["system"],
|
|
1848
|
+
providerOptions: ANTHROPIC_CACHE_BREAKPOINT
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
const tools = args["tools"];
|
|
1852
|
+
if (tools) {
|
|
1853
|
+
const toolNames = Object.keys(tools);
|
|
1854
|
+
if (toolNames.length > 0) {
|
|
1855
|
+
const lastToolName = toolNames[toolNames.length - 1];
|
|
1856
|
+
tools[lastToolName] = {
|
|
1857
|
+
...tools[lastToolName],
|
|
1858
|
+
providerOptions: ANTHROPIC_CACHE_BREAKPOINT
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
const messages = args["messages"];
|
|
1863
|
+
if (messages && messages.length > 0) {
|
|
1864
|
+
const lastMsg = messages[messages.length - 1];
|
|
1865
|
+
messages[messages.length - 1] = {
|
|
1866
|
+
...lastMsg,
|
|
1867
|
+
providerOptions: ANTHROPIC_CACHE_BREAKPOINT
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1761
1871
|
function buildGenerateArgs(model, messages, options) {
|
|
1762
1872
|
const args = { model, messages };
|
|
1763
1873
|
const tools = convertToolsToVercel(options.tools);
|
|
@@ -1769,6 +1879,7 @@ function buildGenerateArgs(model, messages, options) {
|
|
|
1769
1879
|
if (options.outputSchema) {
|
|
1770
1880
|
args["experimental_output"] = ai.Output.object({ schema: options.outputSchema });
|
|
1771
1881
|
}
|
|
1882
|
+
applyAnthropicCacheControl(args, model);
|
|
1772
1883
|
return args;
|
|
1773
1884
|
}
|
|
1774
1885
|
var LLM = class {
|
|
@@ -2514,11 +2625,12 @@ async function agentStreamFunction(ctx, payload, agentDef) {
|
|
|
2514
2625
|
let finalInputTokens = 0;
|
|
2515
2626
|
let finalOutputTokens = 0;
|
|
2516
2627
|
let finalTotalTokens = 0;
|
|
2628
|
+
let finalCacheReadInputTokens = 0;
|
|
2629
|
+
let finalCacheCreationInputTokens = 0;
|
|
2517
2630
|
let lastLlmResultContent = null;
|
|
2518
2631
|
const allToolResults = [];
|
|
2519
2632
|
const steps = [];
|
|
2520
2633
|
let endSteps = false;
|
|
2521
|
-
let toolResults;
|
|
2522
2634
|
let checkedStructuredOutput = false;
|
|
2523
2635
|
let cachedHistoryMessages = null;
|
|
2524
2636
|
if (agentDef.conversationHistory > 0 && conversationId) {
|
|
@@ -2559,7 +2671,7 @@ async function agentStreamFunction(ctx, payload, agentDef) {
|
|
|
2559
2671
|
break;
|
|
2560
2672
|
}
|
|
2561
2673
|
}
|
|
2562
|
-
const safetyMaxSteps = hasMaxStepsCondition ? null : parseInt(process.env["POLOS_AGENT_MAX_STEPS"] ?? "
|
|
2674
|
+
const safetyMaxSteps = hasMaxStepsCondition ? null : parseInt(process.env["POLOS_AGENT_MAX_STEPS"] ?? "20", 10);
|
|
2563
2675
|
const execCtxForPublish = getExecutionContext();
|
|
2564
2676
|
const publishEvent = async (topic, eventData) => {
|
|
2565
2677
|
if (execCtxForPublish?.orchestratorClient) {
|
|
@@ -2608,7 +2720,6 @@ async function agentStreamFunction(ctx, payload, agentDef) {
|
|
|
2608
2720
|
agent_step: agentStep,
|
|
2609
2721
|
guardrails,
|
|
2610
2722
|
guardrail_max_retries: guardrailMaxRetries,
|
|
2611
|
-
tool_results: toolResults,
|
|
2612
2723
|
outputSchema: outputSchemaForLlm
|
|
2613
2724
|
});
|
|
2614
2725
|
if (guardrails.length > 0 && streaming && llmResult.content) {
|
|
@@ -2637,17 +2748,21 @@ async function agentStreamFunction(ctx, payload, agentDef) {
|
|
|
2637
2748
|
temperature: agentConfig.temperature,
|
|
2638
2749
|
maxTokens: agentConfig.maxOutputTokens,
|
|
2639
2750
|
agent_step: agentStep,
|
|
2640
|
-
tool_results: toolResults,
|
|
2641
2751
|
outputSchema: outputSchemaForLlm
|
|
2642
2752
|
},
|
|
2643
2753
|
publishEvent
|
|
2644
2754
|
);
|
|
2645
2755
|
}
|
|
2646
|
-
toolResults = void 0;
|
|
2647
2756
|
if (llmResult.usage) {
|
|
2648
2757
|
finalInputTokens += llmResult.usage.input_tokens;
|
|
2649
2758
|
finalOutputTokens += llmResult.usage.output_tokens;
|
|
2650
2759
|
finalTotalTokens += llmResult.usage.total_tokens;
|
|
2760
|
+
if (llmResult.usage.cache_read_input_tokens) {
|
|
2761
|
+
finalCacheReadInputTokens += llmResult.usage.cache_read_input_tokens;
|
|
2762
|
+
}
|
|
2763
|
+
if (llmResult.usage.cache_creation_input_tokens) {
|
|
2764
|
+
finalCacheCreationInputTokens += llmResult.usage.cache_creation_input_tokens;
|
|
2765
|
+
}
|
|
2651
2766
|
}
|
|
2652
2767
|
lastLlmResultContent = llmResult.content;
|
|
2653
2768
|
const toolCalls = llmResult.tool_calls ?? [];
|
|
@@ -2656,6 +2771,7 @@ async function agentStreamFunction(ctx, payload, agentDef) {
|
|
|
2656
2771
|
`LLM failed to generate output: agent_id=${agentDef.id}, agent_step=${String(agentStep)}`
|
|
2657
2772
|
);
|
|
2658
2773
|
}
|
|
2774
|
+
conversationMessages.push(...llmResult.raw_output);
|
|
2659
2775
|
const batchWorkflows = [];
|
|
2660
2776
|
const toolCallList = [];
|
|
2661
2777
|
let toolResultsRecordedList = [];
|
|
@@ -2716,7 +2832,7 @@ async function agentStreamFunction(ctx, payload, agentDef) {
|
|
|
2716
2832
|
const toolSpec = batchWorkflows[i];
|
|
2717
2833
|
const toolCallInfo = toolCallList[i];
|
|
2718
2834
|
const toolName = toolCallInfo.tool_name;
|
|
2719
|
-
const toolResult = batchToolResult
|
|
2835
|
+
const toolResult = batchToolResult.success ? batchToolResult.result : `Error: ${batchToolResult.error ?? "unknown error"}`;
|
|
2720
2836
|
const toolCallId = toolCallInfo.tool_call_id;
|
|
2721
2837
|
const toolCallCallId = toolCallInfo.tool_call_call_id;
|
|
2722
2838
|
if (agentDef.agentHooks.onToolEnd.length > 0) {
|
|
@@ -2747,7 +2863,8 @@ async function agentStreamFunction(ctx, payload, agentDef) {
|
|
|
2747
2863
|
});
|
|
2748
2864
|
}
|
|
2749
2865
|
allToolResults.push(...toolResultsRecordedList);
|
|
2750
|
-
|
|
2866
|
+
const toolResultMessages = convertToolResultsToMessages(currentIterationToolResults);
|
|
2867
|
+
conversationMessages.push(...toolResultMessages);
|
|
2751
2868
|
}
|
|
2752
2869
|
steps.push({
|
|
2753
2870
|
step: agentStep,
|
|
@@ -2755,7 +2872,7 @@ async function agentStreamFunction(ctx, payload, agentDef) {
|
|
|
2755
2872
|
tool_calls: toolCalls,
|
|
2756
2873
|
tool_results: toolResultsRecordedList,
|
|
2757
2874
|
usage: llmResult.usage,
|
|
2758
|
-
raw_output:
|
|
2875
|
+
raw_output: conversationMessages
|
|
2759
2876
|
});
|
|
2760
2877
|
if (agentDef.agentHooks.onAgentStepEnd.length > 0) {
|
|
2761
2878
|
const hookResult = await executeHookChain(agentDef.agentHooks.onAgentStepEnd, {
|
|
@@ -2772,7 +2889,7 @@ async function agentStreamFunction(ctx, payload, agentDef) {
|
|
|
2772
2889
|
throw new Error(hookResult.error ?? "on_agent_step_end hook failed");
|
|
2773
2890
|
}
|
|
2774
2891
|
}
|
|
2775
|
-
if (
|
|
2892
|
+
if (currentIterationToolResults.length === 0) {
|
|
2776
2893
|
endSteps = true;
|
|
2777
2894
|
}
|
|
2778
2895
|
if (stopConditions.length > 0 && !endSteps) {
|
|
@@ -2805,7 +2922,6 @@ async function agentStreamFunction(ctx, payload, agentDef) {
|
|
|
2805
2922
|
checkedStructuredOutput = true;
|
|
2806
2923
|
if (!parseResult.success) {
|
|
2807
2924
|
endSteps = false;
|
|
2808
|
-
conversationMessages = llmResult.raw_output;
|
|
2809
2925
|
const schemaJson = JSON.stringify(agentDef.outputSchema, null, 2);
|
|
2810
2926
|
const fixPrompt = `The previous response was not valid JSON matching the required schema. Please reformat your response to be valid JSON that strictly conforms to this schema:
|
|
2811
2927
|
|
|
@@ -2818,9 +2934,6 @@ Please provide ONLY valid JSON that matches the schema, with no additional text
|
|
|
2818
2934
|
}
|
|
2819
2935
|
}
|
|
2820
2936
|
if (!endSteps) {
|
|
2821
|
-
if (!checkedStructuredOutput) {
|
|
2822
|
-
conversationMessages = llmResult.raw_output;
|
|
2823
|
-
}
|
|
2824
2937
|
agentStep++;
|
|
2825
2938
|
}
|
|
2826
2939
|
}
|
|
@@ -2873,7 +2986,11 @@ Please provide ONLY valid JSON that matches the schema, with no additional text
|
|
|
2873
2986
|
usage: {
|
|
2874
2987
|
input_tokens: finalInputTokens,
|
|
2875
2988
|
output_tokens: finalOutputTokens,
|
|
2876
|
-
total_tokens: finalTotalTokens
|
|
2989
|
+
total_tokens: finalTotalTokens,
|
|
2990
|
+
...finalCacheReadInputTokens > 0 && { cache_read_input_tokens: finalCacheReadInputTokens },
|
|
2991
|
+
...finalCacheCreationInputTokens > 0 && {
|
|
2992
|
+
cache_creation_input_tokens: finalCacheCreationInputTokens
|
|
2993
|
+
}
|
|
2877
2994
|
}
|
|
2878
2995
|
};
|
|
2879
2996
|
}
|
|
@@ -3034,7 +3151,7 @@ function defineAgent(config) {
|
|
|
3034
3151
|
maxOutputTokens: config.maxOutputTokens
|
|
3035
3152
|
},
|
|
3036
3153
|
input,
|
|
3037
|
-
streaming: streamingFlag ??
|
|
3154
|
+
streaming: streamingFlag ?? config.streamToWorkflow ?? false,
|
|
3038
3155
|
conversation_id: conversationIdValue
|
|
3039
3156
|
};
|
|
3040
3157
|
return agentStreamFunction(ctx, streamPayload, {
|
|
@@ -4091,16 +4208,27 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
|
|
|
4091
4208
|
if (items.length === 0) return [];
|
|
4092
4209
|
const existing = checkExistingStep(key);
|
|
4093
4210
|
if (existing) {
|
|
4094
|
-
|
|
4211
|
+
let raw;
|
|
4212
|
+
try {
|
|
4213
|
+
raw = handleExistingStep(existing);
|
|
4214
|
+
} catch {
|
|
4215
|
+
raw = existing.outputs;
|
|
4216
|
+
}
|
|
4095
4217
|
if (Array.isArray(raw)) {
|
|
4096
4218
|
return raw.map((item) => {
|
|
4097
|
-
if (item && typeof item === "object" && "
|
|
4098
|
-
|
|
4219
|
+
if (item && typeof item === "object" && "success" in item) {
|
|
4220
|
+
const rec = item;
|
|
4221
|
+
return {
|
|
4222
|
+
workflowId: typeof rec["workflow_id"] === "string" ? rec["workflow_id"] : "",
|
|
4223
|
+
success: rec["success"] === true,
|
|
4224
|
+
result: rec["result"] ?? null,
|
|
4225
|
+
error: typeof rec["error"] === "string" ? rec["error"] : null
|
|
4226
|
+
};
|
|
4099
4227
|
}
|
|
4100
|
-
return item;
|
|
4228
|
+
return { workflowId: "", success: true, result: item, error: null };
|
|
4101
4229
|
});
|
|
4102
4230
|
}
|
|
4103
|
-
return raw;
|
|
4231
|
+
return raw.map((r) => ({ workflowId: "", success: true, result: r, error: null }));
|
|
4104
4232
|
}
|
|
4105
4233
|
const workflows = items.map((item) => {
|
|
4106
4234
|
const workflowId = typeof item.workflow === "string" ? item.workflow : item.workflow.id;
|
|
@@ -4163,7 +4291,7 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
|
|
|
4163
4291
|
});
|
|
4164
4292
|
const waitThreshold = Number(process.env["POLOS_WAIT_THRESHOLD_SECONDS"] ?? "10");
|
|
4165
4293
|
if (totalSeconds <= waitThreshold) {
|
|
4166
|
-
await new Promise((
|
|
4294
|
+
await new Promise((resolve8) => setTimeout(resolve8, totalSeconds * 1e3));
|
|
4167
4295
|
await saveStepOutput(key, { wait_until: waitUntil.toISOString() });
|
|
4168
4296
|
return;
|
|
4169
4297
|
}
|
|
@@ -4199,7 +4327,7 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
|
|
|
4199
4327
|
});
|
|
4200
4328
|
const waitThreshold = Number(process.env["POLOS_WAIT_THRESHOLD_SECONDS"] ?? "10");
|
|
4201
4329
|
if (waitSeconds <= waitThreshold) {
|
|
4202
|
-
await new Promise((
|
|
4330
|
+
await new Promise((resolve8) => setTimeout(resolve8, waitSeconds * 1e3));
|
|
4203
4331
|
await saveStepOutput(key, { wait_until: date.toISOString() });
|
|
4204
4332
|
return;
|
|
4205
4333
|
}
|
|
@@ -4273,12 +4401,14 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
|
|
|
4273
4401
|
return handleExistingStep(existing);
|
|
4274
4402
|
}
|
|
4275
4403
|
const topic = `workflow/${execCtx.rootWorkflowId}/${execCtx.rootExecutionId}`;
|
|
4404
|
+
const approvalUrl = `${orchestratorClient.getApiUrl()}/approve/${execCtx.rootExecutionId}/${key}`;
|
|
4405
|
+
const eventData = options?.data != null && typeof options.data === "object" && !Array.isArray(options.data) ? { ...options.data, _approval_url: approvalUrl } : { _original: options?.data, _approval_url: approvalUrl };
|
|
4276
4406
|
await orchestratorClient.publishEvent({
|
|
4277
4407
|
topic,
|
|
4278
4408
|
events: [
|
|
4279
4409
|
{
|
|
4280
4410
|
eventType: `suspend_${key}`,
|
|
4281
|
-
data:
|
|
4411
|
+
data: eventData
|
|
4282
4412
|
}
|
|
4283
4413
|
],
|
|
4284
4414
|
executionId: execCtx.executionId,
|
|
@@ -4941,10 +5071,10 @@ var Worker = class {
|
|
|
4941
5071
|
process.on("SIGINT", signalHandler);
|
|
4942
5072
|
process.on("SIGTERM", signalHandler);
|
|
4943
5073
|
this.signalHandler = signalHandler;
|
|
4944
|
-
await new Promise((
|
|
5074
|
+
await new Promise((resolve8) => {
|
|
4945
5075
|
const checkState = () => {
|
|
4946
5076
|
if (this.state === "stopping" || this.state === "stopped") {
|
|
4947
|
-
|
|
5077
|
+
resolve8();
|
|
4948
5078
|
} else {
|
|
4949
5079
|
setTimeout(checkState, 100);
|
|
4950
5080
|
}
|
|
@@ -4983,7 +5113,7 @@ var Worker = class {
|
|
|
4983
5113
|
const waitTimeout = 3e4;
|
|
4984
5114
|
const waitStart = Date.now();
|
|
4985
5115
|
while (this.activeExecutions.size > 0 && Date.now() - waitStart < waitTimeout) {
|
|
4986
|
-
await new Promise((
|
|
5116
|
+
await new Promise((resolve8) => setTimeout(resolve8, 100));
|
|
4987
5117
|
}
|
|
4988
5118
|
if (this.activeExecutions.size > 0) {
|
|
4989
5119
|
logger6.warn(`${String(this.activeExecutions.size)} executions did not complete in time`);
|
|
@@ -5539,8 +5669,1009 @@ var GuardrailResult2 = {
|
|
|
5539
5669
|
error
|
|
5540
5670
|
})
|
|
5541
5671
|
};
|
|
5672
|
+
var askUserInputSchema = zod.z.object({
|
|
5673
|
+
question: zod.z.string().describe("The question to ask the user"),
|
|
5674
|
+
title: zod.z.string().optional().describe("Short title for the question (shown as heading)"),
|
|
5675
|
+
fields: zod.z.array(
|
|
5676
|
+
zod.z.object({
|
|
5677
|
+
key: zod.z.string().describe("Unique key for this field"),
|
|
5678
|
+
type: zod.z.enum(["text", "textarea", "number", "boolean", "select"]).describe("Field type"),
|
|
5679
|
+
label: zod.z.string().describe("Label shown to user"),
|
|
5680
|
+
description: zod.z.string().optional().describe("Help text for the field"),
|
|
5681
|
+
required: zod.z.boolean().optional().describe("Whether this field is required"),
|
|
5682
|
+
options: zod.z.array(
|
|
5683
|
+
zod.z.object({
|
|
5684
|
+
label: zod.z.string(),
|
|
5685
|
+
value: zod.z.string()
|
|
5686
|
+
})
|
|
5687
|
+
).optional().describe("Options for select fields")
|
|
5688
|
+
})
|
|
5689
|
+
).optional().describe(
|
|
5690
|
+
"Structured form fields for the response. If omitted, shows a single text response field."
|
|
5691
|
+
)
|
|
5692
|
+
});
|
|
5693
|
+
function createAskUserTool() {
|
|
5694
|
+
return defineTool(
|
|
5695
|
+
{
|
|
5696
|
+
id: "ask_user",
|
|
5697
|
+
description: "Ask the user a question and wait for their response. Use this when you need clarification, a decision, or any input from the user. You can define structured fields (text, select, boolean, etc.) for specific response formats, or omit fields for a free-text response.",
|
|
5698
|
+
inputSchema: askUserInputSchema
|
|
5699
|
+
},
|
|
5700
|
+
async (ctx, input) => {
|
|
5701
|
+
const fields = input.fields ?? [
|
|
5702
|
+
{
|
|
5703
|
+
key: "response",
|
|
5704
|
+
type: "textarea",
|
|
5705
|
+
label: input.question,
|
|
5706
|
+
required: true
|
|
5707
|
+
}
|
|
5708
|
+
];
|
|
5709
|
+
const askId = await ctx.step.uuid("_ask_user_id");
|
|
5710
|
+
const response = await ctx.step.suspend(`ask_user_${askId}`, {
|
|
5711
|
+
data: {
|
|
5712
|
+
_form: {
|
|
5713
|
+
title: input.title ?? "Agent Question",
|
|
5714
|
+
description: input.question,
|
|
5715
|
+
fields
|
|
5716
|
+
},
|
|
5717
|
+
_source: "ask_user",
|
|
5718
|
+
_tool: "ask_user"
|
|
5719
|
+
}
|
|
5720
|
+
});
|
|
5721
|
+
return response?.["data"] ?? {};
|
|
5722
|
+
}
|
|
5723
|
+
);
|
|
5724
|
+
}
|
|
5725
|
+
var webSearchInputSchema = zod.z.object({
|
|
5726
|
+
query: zod.z.string().describe("The search query"),
|
|
5727
|
+
maxResults: zod.z.number().optional().describe("Maximum number of results to return"),
|
|
5728
|
+
topic: zod.z.enum(["general", "news"]).optional().describe("Topic filter: general web search or news")
|
|
5729
|
+
});
|
|
5730
|
+
function createTavilySearchFn(config) {
|
|
5731
|
+
return async (query, options) => {
|
|
5732
|
+
const apiKey = config.apiKey ?? process.env["TAVILY_API_KEY"];
|
|
5733
|
+
if (!apiKey) {
|
|
5734
|
+
throw new Error(
|
|
5735
|
+
"Tavily API key is required. Provide it via the apiKey option or set the TAVILY_API_KEY environment variable."
|
|
5736
|
+
);
|
|
5737
|
+
}
|
|
5738
|
+
const baseUrl = (config.baseUrl ?? "https://api.tavily.com").replace(/\/+$/, "");
|
|
5739
|
+
const body = {
|
|
5740
|
+
query,
|
|
5741
|
+
max_results: options.maxResults,
|
|
5742
|
+
search_depth: config.searchDepth ?? "basic",
|
|
5743
|
+
include_answer: config.includeAnswer ?? true,
|
|
5744
|
+
include_raw_content: config.includeRawContent ?? false,
|
|
5745
|
+
topic: options.topic
|
|
5746
|
+
};
|
|
5747
|
+
const response = await fetch(`${baseUrl}/search`, {
|
|
5748
|
+
method: "POST",
|
|
5749
|
+
headers: {
|
|
5750
|
+
"Content-Type": "application/json",
|
|
5751
|
+
Authorization: `Bearer ${apiKey}`
|
|
5752
|
+
},
|
|
5753
|
+
body: JSON.stringify(body)
|
|
5754
|
+
});
|
|
5755
|
+
if (!response.ok) {
|
|
5756
|
+
let errorMessage;
|
|
5757
|
+
try {
|
|
5758
|
+
const errorBody = await response.json();
|
|
5759
|
+
errorMessage = typeof errorBody["detail"] === "string" ? errorBody["detail"] : JSON.stringify(errorBody);
|
|
5760
|
+
} catch {
|
|
5761
|
+
errorMessage = await response.text();
|
|
5762
|
+
}
|
|
5763
|
+
throw new Error(`Tavily API error (${String(response.status)}): ${errorMessage}`);
|
|
5764
|
+
}
|
|
5765
|
+
const data = await response.json();
|
|
5766
|
+
return {
|
|
5767
|
+
query: data.query,
|
|
5768
|
+
answer: data.answer,
|
|
5769
|
+
results: data.results.map((r) => ({
|
|
5770
|
+
title: r.title,
|
|
5771
|
+
url: r.url,
|
|
5772
|
+
content: r.content,
|
|
5773
|
+
score: r.score,
|
|
5774
|
+
publishedDate: r.published_date
|
|
5775
|
+
}))
|
|
5776
|
+
};
|
|
5777
|
+
};
|
|
5778
|
+
}
|
|
5779
|
+
function createWebSearchTool(config) {
|
|
5780
|
+
const toolId = config?.toolId ?? "web_search";
|
|
5781
|
+
const defaultMaxResults = config?.maxResults ?? 5;
|
|
5782
|
+
const defaultTopic = config?.topic ?? "general";
|
|
5783
|
+
const searchFn = config?.search ?? createTavilySearchFn(config ?? {});
|
|
5784
|
+
return defineTool(
|
|
5785
|
+
{
|
|
5786
|
+
id: toolId,
|
|
5787
|
+
description: "Search the web for current information. Returns a list of relevant results with titles, URLs, and content snippets.",
|
|
5788
|
+
inputSchema: webSearchInputSchema,
|
|
5789
|
+
approval: config?.approval
|
|
5790
|
+
},
|
|
5791
|
+
async (ctx, input) => {
|
|
5792
|
+
const options = {
|
|
5793
|
+
maxResults: input.maxResults ?? defaultMaxResults,
|
|
5794
|
+
topic: input.topic ?? defaultTopic
|
|
5795
|
+
};
|
|
5796
|
+
const result = await ctx.step.run("web_search", () => searchFn(input.query, options), {
|
|
5797
|
+
input: { query: input.query, options }
|
|
5798
|
+
});
|
|
5799
|
+
return result;
|
|
5800
|
+
}
|
|
5801
|
+
);
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
// src/execution/output.ts
|
|
5805
|
+
var DEFAULT_MAX_CHARS = 1e5;
|
|
5806
|
+
var HEAD_RATIO = 0.2;
|
|
5807
|
+
function truncateOutput(output, maxChars) {
|
|
5808
|
+
const max = maxChars ?? DEFAULT_MAX_CHARS;
|
|
5809
|
+
if (output.length <= max) {
|
|
5810
|
+
return { text: output, truncated: false };
|
|
5811
|
+
}
|
|
5812
|
+
const headSize = Math.floor(max * HEAD_RATIO);
|
|
5813
|
+
const tailSize = max - headSize;
|
|
5814
|
+
const omitted = output.length - headSize - tailSize;
|
|
5815
|
+
const head = output.slice(0, headSize);
|
|
5816
|
+
const tail = output.slice(-tailSize);
|
|
5817
|
+
const text = `${head}
|
|
5818
|
+
|
|
5819
|
+
--- truncated ${String(omitted)} characters ---
|
|
5820
|
+
|
|
5821
|
+
${tail}`;
|
|
5822
|
+
return { text, truncated: true };
|
|
5823
|
+
}
|
|
5824
|
+
function isBinary(buffer) {
|
|
5825
|
+
const checkLength = Math.min(buffer.length, 8192);
|
|
5826
|
+
for (let i = 0; i < checkLength; i++) {
|
|
5827
|
+
if (buffer[i] === 0) {
|
|
5828
|
+
return true;
|
|
5829
|
+
}
|
|
5830
|
+
}
|
|
5831
|
+
return false;
|
|
5832
|
+
}
|
|
5833
|
+
function parseGrepOutput(output) {
|
|
5834
|
+
if (!output.trim()) {
|
|
5835
|
+
return [];
|
|
5836
|
+
}
|
|
5837
|
+
const matches = [];
|
|
5838
|
+
const lines = output.split("\n");
|
|
5839
|
+
for (const line of lines) {
|
|
5840
|
+
if (!line) continue;
|
|
5841
|
+
const match = /^(.+?):(\d+):(.*)$/.exec(line);
|
|
5842
|
+
if (match?.[1] && match[2] && match[3] !== void 0) {
|
|
5843
|
+
matches.push({ path: match[1], line: parseInt(match[2], 10), text: match[3] });
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
return matches;
|
|
5847
|
+
}
|
|
5848
|
+
function stripAnsi(text) {
|
|
5849
|
+
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
5850
|
+
}
|
|
5851
|
+
|
|
5852
|
+
// src/execution/docker.ts
|
|
5853
|
+
var DEFAULT_CONTAINER_WORKDIR = "/workspace";
|
|
5854
|
+
var DEFAULT_TIMEOUT_SECONDS = 300;
|
|
5855
|
+
var DEFAULT_MAX_OUTPUT_CHARS = 1e5;
|
|
5856
|
+
function spawnCommand(command, args, options) {
|
|
5857
|
+
return new Promise((resolve8, reject) => {
|
|
5858
|
+
let settled = false;
|
|
5859
|
+
const settle = (fn) => {
|
|
5860
|
+
if (!settled) {
|
|
5861
|
+
settled = true;
|
|
5862
|
+
fn();
|
|
5863
|
+
}
|
|
5864
|
+
};
|
|
5865
|
+
const proc = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
5866
|
+
let stdout = "";
|
|
5867
|
+
let stderr = "";
|
|
5868
|
+
let killed = false;
|
|
5869
|
+
proc.stdout.on("data", (data) => {
|
|
5870
|
+
stdout += data.toString();
|
|
5871
|
+
});
|
|
5872
|
+
proc.stderr.on("data", (data) => {
|
|
5873
|
+
stderr += data.toString();
|
|
5874
|
+
});
|
|
5875
|
+
proc.stdin.on("error", () => {
|
|
5876
|
+
});
|
|
5877
|
+
proc.stdout.on("error", () => {
|
|
5878
|
+
});
|
|
5879
|
+
proc.stderr.on("error", () => {
|
|
5880
|
+
});
|
|
5881
|
+
const timeoutMs = (options?.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3;
|
|
5882
|
+
const timer = setTimeout(() => {
|
|
5883
|
+
killed = true;
|
|
5884
|
+
proc.kill("SIGKILL");
|
|
5885
|
+
}, timeoutMs);
|
|
5886
|
+
proc.on("close", (code) => {
|
|
5887
|
+
clearTimeout(timer);
|
|
5888
|
+
settle(() => {
|
|
5889
|
+
if (killed) {
|
|
5890
|
+
resolve8({
|
|
5891
|
+
exitCode: 137,
|
|
5892
|
+
stdout,
|
|
5893
|
+
stderr: stderr + "\n[Process killed: timeout exceeded]"
|
|
5894
|
+
});
|
|
5895
|
+
} else {
|
|
5896
|
+
resolve8({ exitCode: code ?? 1, stdout, stderr });
|
|
5897
|
+
}
|
|
5898
|
+
});
|
|
5899
|
+
});
|
|
5900
|
+
proc.on("error", (err) => {
|
|
5901
|
+
clearTimeout(timer);
|
|
5902
|
+
settle(() => {
|
|
5903
|
+
reject(err);
|
|
5904
|
+
});
|
|
5905
|
+
});
|
|
5906
|
+
if (options?.stdin) {
|
|
5907
|
+
proc.stdin.write(options.stdin, () => {
|
|
5908
|
+
proc.stdin.end();
|
|
5909
|
+
});
|
|
5910
|
+
} else {
|
|
5911
|
+
proc.stdin.end();
|
|
5912
|
+
}
|
|
5913
|
+
});
|
|
5914
|
+
}
|
|
5915
|
+
var DockerEnvironment = class {
|
|
5916
|
+
type = "docker";
|
|
5917
|
+
containerId = null;
|
|
5918
|
+
containerName;
|
|
5919
|
+
config;
|
|
5920
|
+
containerWorkdir;
|
|
5921
|
+
maxOutputChars;
|
|
5922
|
+
constructor(config, maxOutputChars) {
|
|
5923
|
+
this.config = config;
|
|
5924
|
+
this.containerWorkdir = config.containerWorkdir ?? DEFAULT_CONTAINER_WORKDIR;
|
|
5925
|
+
this.containerName = `polos-sandbox-${crypto.randomUUID().slice(0, 8)}`;
|
|
5926
|
+
this.maxOutputChars = maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
5927
|
+
}
|
|
5928
|
+
async initialize() {
|
|
5929
|
+
const args = [
|
|
5930
|
+
"run",
|
|
5931
|
+
"-d",
|
|
5932
|
+
"--name",
|
|
5933
|
+
this.containerName,
|
|
5934
|
+
"-v",
|
|
5935
|
+
`${this.config.workspaceDir}:${this.containerWorkdir}:rw`,
|
|
5936
|
+
"-w",
|
|
5937
|
+
this.containerWorkdir
|
|
5938
|
+
];
|
|
5939
|
+
if (this.config.memory) {
|
|
5940
|
+
args.push("--memory", this.config.memory);
|
|
5941
|
+
}
|
|
5942
|
+
if (this.config.cpus) {
|
|
5943
|
+
args.push("--cpus", this.config.cpus);
|
|
5944
|
+
}
|
|
5945
|
+
args.push("--network", this.config.network ?? "none");
|
|
5946
|
+
if (this.config.env) {
|
|
5947
|
+
for (const [key, value] of Object.entries(this.config.env)) {
|
|
5948
|
+
args.push("-e", `${key}=${value}`);
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
args.push(this.config.image, "sleep", "infinity");
|
|
5952
|
+
const result = await spawnCommand("docker", args, { timeout: 60 });
|
|
5953
|
+
if (result.exitCode !== 0) {
|
|
5954
|
+
throw new Error(`Failed to create Docker container: ${result.stderr.trim()}`);
|
|
5955
|
+
}
|
|
5956
|
+
this.containerId = result.stdout.trim().slice(0, 12);
|
|
5957
|
+
if (this.config.setupCommand) {
|
|
5958
|
+
const setupResult = await this.exec(this.config.setupCommand);
|
|
5959
|
+
if (setupResult.exitCode !== 0) {
|
|
5960
|
+
throw new Error(
|
|
5961
|
+
`Setup command failed (exit ${String(setupResult.exitCode)}): ${setupResult.stderr.trim()}`
|
|
5962
|
+
);
|
|
5963
|
+
}
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5966
|
+
async exec(command, opts) {
|
|
5967
|
+
this.assertInitialized();
|
|
5968
|
+
const args = opts?.stdin ? ["exec", "-i"] : ["exec"];
|
|
5969
|
+
const cwd = opts?.cwd ?? this.containerWorkdir;
|
|
5970
|
+
args.push("-w", cwd);
|
|
5971
|
+
if (opts?.env) {
|
|
5972
|
+
for (const [key, value] of Object.entries(opts.env)) {
|
|
5973
|
+
args.push("-e", `${key}=${value}`);
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
args.push(this.containerName, "sh", "-c", command);
|
|
5977
|
+
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
5978
|
+
const start = Date.now();
|
|
5979
|
+
const result = await spawnCommand("docker", args, {
|
|
5980
|
+
timeout,
|
|
5981
|
+
stdin: opts?.stdin
|
|
5982
|
+
});
|
|
5983
|
+
const durationMs = Date.now() - start;
|
|
5984
|
+
const { text: stdout, truncated: stdoutTruncated } = truncateOutput(
|
|
5985
|
+
stripAnsi(result.stdout),
|
|
5986
|
+
this.maxOutputChars
|
|
5987
|
+
);
|
|
5988
|
+
const { text: stderr } = truncateOutput(stripAnsi(result.stderr), this.maxOutputChars);
|
|
5989
|
+
return {
|
|
5990
|
+
exitCode: result.exitCode,
|
|
5991
|
+
stdout,
|
|
5992
|
+
stderr,
|
|
5993
|
+
durationMs,
|
|
5994
|
+
truncated: stdoutTruncated
|
|
5995
|
+
};
|
|
5996
|
+
}
|
|
5997
|
+
async readFile(filePath) {
|
|
5998
|
+
const hostPath = this.toHostPath(filePath);
|
|
5999
|
+
const buffer = await fs2__namespace.readFile(hostPath);
|
|
6000
|
+
if (isBinary(buffer)) {
|
|
6001
|
+
throw new Error(`Cannot read binary file: ${filePath}`);
|
|
6002
|
+
}
|
|
6003
|
+
return buffer.toString("utf-8");
|
|
6004
|
+
}
|
|
6005
|
+
async writeFile(filePath, content) {
|
|
6006
|
+
const hostPath = this.toHostPath(filePath);
|
|
6007
|
+
await fs2__namespace.mkdir(path__namespace.dirname(hostPath), { recursive: true });
|
|
6008
|
+
await fs2__namespace.writeFile(hostPath, content, "utf-8");
|
|
6009
|
+
}
|
|
6010
|
+
async fileExists(filePath) {
|
|
6011
|
+
const hostPath = this.toHostPath(filePath);
|
|
6012
|
+
try {
|
|
6013
|
+
await fs2__namespace.access(hostPath);
|
|
6014
|
+
return true;
|
|
6015
|
+
} catch {
|
|
6016
|
+
return false;
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6019
|
+
async glob(pattern, opts) {
|
|
6020
|
+
const cwd = opts?.cwd ?? this.containerWorkdir;
|
|
6021
|
+
let command = `find ${cwd} -type f -name '${pattern}'`;
|
|
6022
|
+
if (opts?.ignore) {
|
|
6023
|
+
for (const ignore of opts.ignore) {
|
|
6024
|
+
command += ` ! -path '${ignore}'`;
|
|
6025
|
+
}
|
|
6026
|
+
}
|
|
6027
|
+
command += " 2>/dev/null | sort | head -1000";
|
|
6028
|
+
const result = await this.exec(command);
|
|
6029
|
+
if (!result.stdout.trim()) return [];
|
|
6030
|
+
return result.stdout.trim().split("\n").filter(Boolean);
|
|
6031
|
+
}
|
|
6032
|
+
async grep(pattern, opts) {
|
|
6033
|
+
const cwd = opts?.cwd ?? this.containerWorkdir;
|
|
6034
|
+
const maxResults = opts?.maxResults ?? 100;
|
|
6035
|
+
let command = "grep -rn";
|
|
6036
|
+
if (opts?.contextLines !== void 0) {
|
|
6037
|
+
command += ` -C ${String(opts.contextLines)}`;
|
|
6038
|
+
}
|
|
6039
|
+
if (opts?.include) {
|
|
6040
|
+
for (const inc of opts.include) {
|
|
6041
|
+
command += ` --include='${inc}'`;
|
|
6042
|
+
}
|
|
6043
|
+
}
|
|
6044
|
+
const escapedPattern = pattern.replace(/'/g, "'\\''");
|
|
6045
|
+
command += ` -- '${escapedPattern}' ${cwd}`;
|
|
6046
|
+
command += ` 2>/dev/null | head -${String(maxResults)}`;
|
|
6047
|
+
const result = await this.exec(command);
|
|
6048
|
+
return parseGrepOutput(result.stdout);
|
|
6049
|
+
}
|
|
6050
|
+
async destroy() {
|
|
6051
|
+
if (!this.containerId) return;
|
|
6052
|
+
try {
|
|
6053
|
+
await spawnCommand("docker", ["rm", "-f", this.containerName], { timeout: 30 });
|
|
6054
|
+
} finally {
|
|
6055
|
+
this.containerId = null;
|
|
6056
|
+
}
|
|
6057
|
+
}
|
|
6058
|
+
getCwd() {
|
|
6059
|
+
return this.containerWorkdir;
|
|
6060
|
+
}
|
|
6061
|
+
getInfo() {
|
|
6062
|
+
return {
|
|
6063
|
+
type: "docker",
|
|
6064
|
+
cwd: this.containerWorkdir,
|
|
6065
|
+
sandboxId: this.containerId ?? void 0
|
|
6066
|
+
};
|
|
6067
|
+
}
|
|
6068
|
+
/**
|
|
6069
|
+
* Translate a container path to the corresponding host filesystem path.
|
|
6070
|
+
* Validates the path stays within the workspace to prevent traversal.
|
|
6071
|
+
*/
|
|
6072
|
+
toHostPath(containerPath) {
|
|
6073
|
+
const resolved = path__namespace.posix.resolve(this.containerWorkdir, containerPath);
|
|
6074
|
+
if (!resolved.startsWith(this.containerWorkdir)) {
|
|
6075
|
+
throw new Error(`Path traversal detected: "${containerPath}" resolves outside workspace`);
|
|
6076
|
+
}
|
|
6077
|
+
const relative2 = path__namespace.posix.relative(this.containerWorkdir, resolved);
|
|
6078
|
+
return path__namespace.join(this.config.workspaceDir, relative2);
|
|
6079
|
+
}
|
|
6080
|
+
/**
|
|
6081
|
+
* Translate a host filesystem path to the corresponding container path.
|
|
6082
|
+
*/
|
|
6083
|
+
toContainerPath(hostPath) {
|
|
6084
|
+
const resolved = path__namespace.resolve(hostPath);
|
|
6085
|
+
if (!resolved.startsWith(this.config.workspaceDir)) {
|
|
6086
|
+
throw new Error(
|
|
6087
|
+
`Path outside workspace: "${hostPath}" is not within "${this.config.workspaceDir}"`
|
|
6088
|
+
);
|
|
6089
|
+
}
|
|
6090
|
+
const relative2 = path__namespace.relative(this.config.workspaceDir, resolved);
|
|
6091
|
+
return path__namespace.posix.join(this.containerWorkdir, relative2);
|
|
6092
|
+
}
|
|
6093
|
+
assertInitialized() {
|
|
6094
|
+
if (!this.containerId) {
|
|
6095
|
+
throw new Error("Docker environment not initialized. Call initialize() first.");
|
|
6096
|
+
}
|
|
6097
|
+
}
|
|
6098
|
+
};
|
|
6099
|
+
var DEFAULT_TIMEOUT_SECONDS2 = 300;
|
|
6100
|
+
var DEFAULT_MAX_OUTPUT_CHARS2 = 1e5;
|
|
6101
|
+
function spawnLocal(command, options) {
|
|
6102
|
+
return new Promise((resolve8, reject) => {
|
|
6103
|
+
let settled = false;
|
|
6104
|
+
const settle = (fn) => {
|
|
6105
|
+
if (!settled) {
|
|
6106
|
+
settled = true;
|
|
6107
|
+
fn();
|
|
6108
|
+
}
|
|
6109
|
+
};
|
|
6110
|
+
const proc = child_process.spawn("sh", ["-c", command], {
|
|
6111
|
+
cwd: options.cwd,
|
|
6112
|
+
env: options.env ? { ...process.env, ...options.env } : void 0,
|
|
6113
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
6114
|
+
});
|
|
6115
|
+
let stdout = "";
|
|
6116
|
+
let stderr = "";
|
|
6117
|
+
let killed = false;
|
|
6118
|
+
proc.stdout.on("data", (data) => {
|
|
6119
|
+
stdout += data.toString();
|
|
6120
|
+
});
|
|
6121
|
+
proc.stderr.on("data", (data) => {
|
|
6122
|
+
stderr += data.toString();
|
|
6123
|
+
});
|
|
6124
|
+
proc.stdin.on("error", () => {
|
|
6125
|
+
});
|
|
6126
|
+
proc.stdout.on("error", () => {
|
|
6127
|
+
});
|
|
6128
|
+
proc.stderr.on("error", () => {
|
|
6129
|
+
});
|
|
6130
|
+
const timeoutMs = (options.timeout ?? DEFAULT_TIMEOUT_SECONDS2) * 1e3;
|
|
6131
|
+
const timer = setTimeout(() => {
|
|
6132
|
+
killed = true;
|
|
6133
|
+
proc.kill("SIGKILL");
|
|
6134
|
+
}, timeoutMs);
|
|
6135
|
+
proc.on("close", (code) => {
|
|
6136
|
+
clearTimeout(timer);
|
|
6137
|
+
settle(() => {
|
|
6138
|
+
if (killed) {
|
|
6139
|
+
resolve8({
|
|
6140
|
+
exitCode: 137,
|
|
6141
|
+
stdout,
|
|
6142
|
+
stderr: stderr + "\n[Process killed: timeout exceeded]"
|
|
6143
|
+
});
|
|
6144
|
+
} else {
|
|
6145
|
+
resolve8({ exitCode: code ?? 1, stdout, stderr });
|
|
6146
|
+
}
|
|
6147
|
+
});
|
|
6148
|
+
});
|
|
6149
|
+
proc.on("error", (err) => {
|
|
6150
|
+
clearTimeout(timer);
|
|
6151
|
+
settle(() => {
|
|
6152
|
+
reject(err);
|
|
6153
|
+
});
|
|
6154
|
+
});
|
|
6155
|
+
if (options.stdin) {
|
|
6156
|
+
proc.stdin.write(options.stdin, () => {
|
|
6157
|
+
proc.stdin.end();
|
|
6158
|
+
});
|
|
6159
|
+
} else {
|
|
6160
|
+
proc.stdin.end();
|
|
6161
|
+
}
|
|
6162
|
+
});
|
|
6163
|
+
}
|
|
6164
|
+
var LocalEnvironment = class {
|
|
6165
|
+
type = "local";
|
|
6166
|
+
config;
|
|
6167
|
+
cwd;
|
|
6168
|
+
maxOutputChars;
|
|
6169
|
+
constructor(config, maxOutputChars) {
|
|
6170
|
+
this.config = config ?? {};
|
|
6171
|
+
this.cwd = path__namespace.resolve(config?.cwd ?? process.cwd());
|
|
6172
|
+
this.maxOutputChars = maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS2;
|
|
6173
|
+
}
|
|
6174
|
+
async initialize() {
|
|
6175
|
+
try {
|
|
6176
|
+
const stat2 = await fs2__namespace.stat(this.cwd);
|
|
6177
|
+
if (!stat2.isDirectory()) {
|
|
6178
|
+
throw new Error(`Working directory is not a directory: ${this.cwd}`);
|
|
6179
|
+
}
|
|
6180
|
+
} catch (err) {
|
|
6181
|
+
if (err.code === "ENOENT") {
|
|
6182
|
+
throw new Error(`Working directory does not exist: ${this.cwd}`);
|
|
6183
|
+
}
|
|
6184
|
+
throw err;
|
|
6185
|
+
}
|
|
6186
|
+
}
|
|
6187
|
+
async exec(command, opts) {
|
|
6188
|
+
const cwd = opts?.cwd ? this.resolvePath(opts.cwd) : this.cwd;
|
|
6189
|
+
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT_SECONDS2;
|
|
6190
|
+
const start = Date.now();
|
|
6191
|
+
const result = await spawnLocal(command, {
|
|
6192
|
+
cwd,
|
|
6193
|
+
env: opts?.env,
|
|
6194
|
+
timeout,
|
|
6195
|
+
stdin: opts?.stdin
|
|
6196
|
+
});
|
|
6197
|
+
const durationMs = Date.now() - start;
|
|
6198
|
+
const { text: stdout, truncated: stdoutTruncated } = truncateOutput(
|
|
6199
|
+
stripAnsi(result.stdout),
|
|
6200
|
+
this.maxOutputChars
|
|
6201
|
+
);
|
|
6202
|
+
const { text: stderr } = truncateOutput(stripAnsi(result.stderr), this.maxOutputChars);
|
|
6203
|
+
return {
|
|
6204
|
+
exitCode: result.exitCode,
|
|
6205
|
+
stdout,
|
|
6206
|
+
stderr,
|
|
6207
|
+
durationMs,
|
|
6208
|
+
truncated: stdoutTruncated
|
|
6209
|
+
};
|
|
6210
|
+
}
|
|
6211
|
+
async readFile(filePath) {
|
|
6212
|
+
const resolved = this.resolvePath(filePath);
|
|
6213
|
+
await this.assertNotSymlink(resolved);
|
|
6214
|
+
const buffer = await fs2__namespace.readFile(resolved);
|
|
6215
|
+
if (isBinary(buffer)) {
|
|
6216
|
+
throw new Error(`Cannot read binary file: ${filePath}`);
|
|
6217
|
+
}
|
|
6218
|
+
return buffer.toString("utf-8");
|
|
6219
|
+
}
|
|
6220
|
+
async writeFile(filePath, content) {
|
|
6221
|
+
const resolved = this.resolvePath(filePath);
|
|
6222
|
+
this.assertPathSafe(resolved);
|
|
6223
|
+
const parentDir = path__namespace.dirname(resolved);
|
|
6224
|
+
await fs2__namespace.mkdir(parentDir, { recursive: true });
|
|
6225
|
+
await fs2__namespace.writeFile(resolved, content, "utf-8");
|
|
6226
|
+
}
|
|
6227
|
+
async fileExists(filePath) {
|
|
6228
|
+
const resolved = this.resolvePath(filePath);
|
|
6229
|
+
try {
|
|
6230
|
+
await fs2__namespace.access(resolved);
|
|
6231
|
+
return true;
|
|
6232
|
+
} catch {
|
|
6233
|
+
return false;
|
|
6234
|
+
}
|
|
6235
|
+
}
|
|
6236
|
+
async glob(pattern, opts) {
|
|
6237
|
+
const cwd = opts?.cwd ? this.resolvePath(opts.cwd) : this.cwd;
|
|
6238
|
+
let command = `find ${cwd} -type f -name '${pattern}'`;
|
|
6239
|
+
if (opts?.ignore) {
|
|
6240
|
+
for (const ignore of opts.ignore) {
|
|
6241
|
+
command += ` ! -path '${ignore}'`;
|
|
6242
|
+
}
|
|
6243
|
+
}
|
|
6244
|
+
command += " 2>/dev/null | sort | head -1000";
|
|
6245
|
+
const result = await this.exec(command);
|
|
6246
|
+
if (!result.stdout.trim()) return [];
|
|
6247
|
+
return result.stdout.trim().split("\n").filter(Boolean);
|
|
6248
|
+
}
|
|
6249
|
+
async grep(pattern, opts) {
|
|
6250
|
+
const cwd = opts?.cwd ? this.resolvePath(opts.cwd) : this.cwd;
|
|
6251
|
+
const maxResults = opts?.maxResults ?? 100;
|
|
6252
|
+
let command = "grep -rn";
|
|
6253
|
+
if (opts?.contextLines !== void 0) {
|
|
6254
|
+
command += ` -C ${String(opts.contextLines)}`;
|
|
6255
|
+
}
|
|
6256
|
+
if (opts?.include) {
|
|
6257
|
+
for (const inc of opts.include) {
|
|
6258
|
+
command += ` --include='${inc}'`;
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
const escapedPattern = pattern.replace(/'/g, "'\\''");
|
|
6262
|
+
command += ` -- '${escapedPattern}' ${cwd}`;
|
|
6263
|
+
command += ` 2>/dev/null | head -${String(maxResults)}`;
|
|
6264
|
+
const result = await this.exec(command);
|
|
6265
|
+
return parseGrepOutput(result.stdout);
|
|
6266
|
+
}
|
|
6267
|
+
async destroy() {
|
|
6268
|
+
}
|
|
6269
|
+
getCwd() {
|
|
6270
|
+
return this.cwd;
|
|
6271
|
+
}
|
|
6272
|
+
getInfo() {
|
|
6273
|
+
return {
|
|
6274
|
+
type: "local",
|
|
6275
|
+
cwd: this.cwd
|
|
6276
|
+
};
|
|
6277
|
+
}
|
|
6278
|
+
/**
|
|
6279
|
+
* Resolve a path relative to the working directory.
|
|
6280
|
+
*/
|
|
6281
|
+
resolvePath(p) {
|
|
6282
|
+
return path__namespace.resolve(this.cwd, p);
|
|
6283
|
+
}
|
|
6284
|
+
/**
|
|
6285
|
+
* Assert that a resolved path stays within the path restriction.
|
|
6286
|
+
* No-op when path restriction is not configured.
|
|
6287
|
+
*/
|
|
6288
|
+
assertPathSafe(resolvedPath) {
|
|
6289
|
+
if (!this.config.pathRestriction) return;
|
|
6290
|
+
const restriction = path__namespace.resolve(this.config.pathRestriction);
|
|
6291
|
+
if (resolvedPath !== restriction && !resolvedPath.startsWith(restriction + "/")) {
|
|
6292
|
+
throw new Error(`Path traversal detected: "${resolvedPath}" is outside of "${restriction}"`);
|
|
6293
|
+
}
|
|
6294
|
+
}
|
|
6295
|
+
/**
|
|
6296
|
+
* Assert that a path is not a symbolic link.
|
|
6297
|
+
* Only enforced when path restriction is configured.
|
|
6298
|
+
*/
|
|
6299
|
+
async assertNotSymlink(resolvedPath) {
|
|
6300
|
+
if (!this.config.pathRestriction) return;
|
|
6301
|
+
try {
|
|
6302
|
+
const stat2 = await fs2__namespace.lstat(resolvedPath);
|
|
6303
|
+
if (stat2.isSymbolicLink()) {
|
|
6304
|
+
throw new Error(
|
|
6305
|
+
`Symbolic link detected: "${resolvedPath}". Symlinks are blocked when pathRestriction is set.`
|
|
6306
|
+
);
|
|
6307
|
+
}
|
|
6308
|
+
} catch (err) {
|
|
6309
|
+
if (err.code === "ENOENT") return;
|
|
6310
|
+
throw err;
|
|
6311
|
+
}
|
|
6312
|
+
}
|
|
6313
|
+
};
|
|
6314
|
+
function matchGlob(text, pattern) {
|
|
6315
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
6316
|
+
const regexStr = `^${escaped.replace(/\*/g, ".*")}$`;
|
|
6317
|
+
return new RegExp(regexStr).test(text);
|
|
6318
|
+
}
|
|
6319
|
+
function evaluateAllowlist(command, patterns) {
|
|
6320
|
+
const trimmed = command.trim();
|
|
6321
|
+
return patterns.some((pattern) => matchGlob(trimmed, pattern));
|
|
6322
|
+
}
|
|
6323
|
+
function isWithinRestriction(resolvedPath, restriction) {
|
|
6324
|
+
const base = path.resolve(restriction);
|
|
6325
|
+
return resolvedPath === base || resolvedPath.startsWith(base + "/");
|
|
6326
|
+
}
|
|
6327
|
+
function assertSafePath(filePath, restriction) {
|
|
6328
|
+
const base = path.resolve(restriction);
|
|
6329
|
+
const resolved = path.resolve(base, filePath);
|
|
6330
|
+
if (!isWithinRestriction(resolved, base)) {
|
|
6331
|
+
throw new Error(`Path traversal detected: "${filePath}" resolves outside of "${restriction}"`);
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
|
|
6335
|
+
// src/execution/tools/exec.ts
|
|
6336
|
+
async function requestApproval(ctx, command, env) {
|
|
6337
|
+
const envInfo = env.getInfo();
|
|
6338
|
+
const approvalId = await ctx.step.uuid("_approval_id");
|
|
6339
|
+
const response = await ctx.step.suspend(
|
|
6340
|
+
`approve_exec_${approvalId}`,
|
|
6341
|
+
{
|
|
6342
|
+
data: {
|
|
6343
|
+
_form: {
|
|
6344
|
+
title: "Approve command execution",
|
|
6345
|
+
description: `The agent wants to run a shell command in the ${envInfo.type} environment.`,
|
|
6346
|
+
fields: [
|
|
6347
|
+
{
|
|
6348
|
+
key: "approved",
|
|
6349
|
+
type: "boolean",
|
|
6350
|
+
label: "Approve this command?",
|
|
6351
|
+
required: true,
|
|
6352
|
+
default: false
|
|
6353
|
+
},
|
|
6354
|
+
{
|
|
6355
|
+
key: "allow_always",
|
|
6356
|
+
type: "boolean",
|
|
6357
|
+
label: "Always allow this command in the future?",
|
|
6358
|
+
required: false,
|
|
6359
|
+
default: false
|
|
6360
|
+
},
|
|
6361
|
+
{
|
|
6362
|
+
key: "feedback",
|
|
6363
|
+
type: "textarea",
|
|
6364
|
+
label: "Feedback for the agent (optional)",
|
|
6365
|
+
description: "If rejecting, tell the agent what to do instead.",
|
|
6366
|
+
required: false
|
|
6367
|
+
}
|
|
6368
|
+
],
|
|
6369
|
+
context: {
|
|
6370
|
+
command,
|
|
6371
|
+
cwd: env.getCwd(),
|
|
6372
|
+
environment: envInfo.type
|
|
6373
|
+
}
|
|
6374
|
+
},
|
|
6375
|
+
_source: "exec_security",
|
|
6376
|
+
_tool: "exec"
|
|
6377
|
+
}
|
|
6378
|
+
}
|
|
6379
|
+
);
|
|
6380
|
+
const feedback = response.data?.feedback;
|
|
6381
|
+
return {
|
|
6382
|
+
approved: response.data?.approved === true,
|
|
6383
|
+
...feedback ? { feedback } : {}
|
|
6384
|
+
};
|
|
6385
|
+
}
|
|
6386
|
+
function rejectedResult(command, feedback) {
|
|
6387
|
+
let stderr = `Command rejected by user: ${command}`;
|
|
6388
|
+
if (feedback) {
|
|
6389
|
+
stderr += `
|
|
6390
|
+
User feedback: ${feedback}`;
|
|
6391
|
+
}
|
|
6392
|
+
return {
|
|
6393
|
+
exitCode: -1,
|
|
6394
|
+
stdout: "",
|
|
6395
|
+
stderr,
|
|
6396
|
+
durationMs: 0,
|
|
6397
|
+
truncated: false
|
|
6398
|
+
};
|
|
6399
|
+
}
|
|
6400
|
+
function createExecTool(getEnv, config) {
|
|
6401
|
+
return defineTool(
|
|
6402
|
+
{
|
|
6403
|
+
id: "exec",
|
|
6404
|
+
description: "Execute a shell command in the sandbox environment. Returns stdout, stderr, and exit code. Use this for running builds, tests, installing packages, or any shell operation.",
|
|
6405
|
+
inputSchema: zod.z.object({
|
|
6406
|
+
command: zod.z.string().describe("The shell command to execute"),
|
|
6407
|
+
cwd: zod.z.string().optional().describe("Working directory for the command"),
|
|
6408
|
+
env: zod.z.record(zod.z.string()).optional().describe("Environment variables to set"),
|
|
6409
|
+
timeout: zod.z.number().optional().describe("Timeout in seconds (default: 300)")
|
|
6410
|
+
})
|
|
6411
|
+
},
|
|
6412
|
+
async (ctx, input) => {
|
|
6413
|
+
const env = await getEnv();
|
|
6414
|
+
if (config?.security === "approval-always") {
|
|
6415
|
+
const result = await requestApproval(ctx, input.command, env);
|
|
6416
|
+
if (!result.approved) return rejectedResult(input.command, result.feedback);
|
|
6417
|
+
} else if (config?.security === "allowlist") {
|
|
6418
|
+
if (!evaluateAllowlist(input.command, config.allowlist ?? [])) {
|
|
6419
|
+
const result = await requestApproval(ctx, input.command, env);
|
|
6420
|
+
if (!result.approved) return rejectedResult(input.command, result.feedback);
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
6423
|
+
return env.exec(input.command, {
|
|
6424
|
+
cwd: input.cwd,
|
|
6425
|
+
env: input.env,
|
|
6426
|
+
timeout: input.timeout ?? config?.timeout
|
|
6427
|
+
});
|
|
6428
|
+
}
|
|
6429
|
+
);
|
|
6430
|
+
}
|
|
6431
|
+
function isPathAllowed(resolvedPath, restriction) {
|
|
6432
|
+
return isWithinRestriction(resolvedPath, path.resolve(restriction));
|
|
6433
|
+
}
|
|
6434
|
+
async function requirePathApproval(ctx, toolName, targetPath, restriction) {
|
|
6435
|
+
const approvalId = await ctx.step.uuid("_approval_id");
|
|
6436
|
+
const response = await ctx.step.suspend(
|
|
6437
|
+
`approve_${toolName}_${approvalId}`,
|
|
6438
|
+
{
|
|
6439
|
+
data: {
|
|
6440
|
+
_form: {
|
|
6441
|
+
title: `${toolName}: access outside workspace`,
|
|
6442
|
+
description: `The agent wants to ${toolName} a path outside the workspace.`,
|
|
6443
|
+
fields: [
|
|
6444
|
+
{
|
|
6445
|
+
key: "approved",
|
|
6446
|
+
type: "boolean",
|
|
6447
|
+
label: "Allow this operation?",
|
|
6448
|
+
required: true,
|
|
6449
|
+
default: false
|
|
6450
|
+
},
|
|
6451
|
+
{
|
|
6452
|
+
key: "feedback",
|
|
6453
|
+
type: "textarea",
|
|
6454
|
+
label: "Feedback for the agent (optional)",
|
|
6455
|
+
description: "If rejecting, tell the agent what to do instead.",
|
|
6456
|
+
required: false
|
|
6457
|
+
}
|
|
6458
|
+
],
|
|
6459
|
+
context: {
|
|
6460
|
+
tool: toolName,
|
|
6461
|
+
path: targetPath,
|
|
6462
|
+
restriction
|
|
6463
|
+
}
|
|
6464
|
+
},
|
|
6465
|
+
_source: "path_approval",
|
|
6466
|
+
_tool: toolName
|
|
6467
|
+
}
|
|
6468
|
+
}
|
|
6469
|
+
);
|
|
6470
|
+
if (response.data?.approved !== true) {
|
|
6471
|
+
const feedback = response.data?.feedback;
|
|
6472
|
+
throw new Error(
|
|
6473
|
+
`Access to "${targetPath}" was rejected by the user.${feedback ? ` Feedback: ${feedback}` : ""}`
|
|
6474
|
+
);
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
|
|
6478
|
+
// src/execution/tools/read.ts
|
|
6479
|
+
function createReadTool(getEnv, pathConfig) {
|
|
6480
|
+
return defineTool(
|
|
6481
|
+
{
|
|
6482
|
+
id: "read",
|
|
6483
|
+
description: "Read the contents of a file. Returns the file content as text. Optionally specify offset (line number to start from, 0-based) and limit (number of lines).",
|
|
6484
|
+
inputSchema: zod.z.object({
|
|
6485
|
+
path: zod.z.string().describe("Path to the file to read"),
|
|
6486
|
+
offset: zod.z.number().optional().describe("Line offset to start reading from (0-based)"),
|
|
6487
|
+
limit: zod.z.number().optional().describe("Maximum number of lines to return")
|
|
6488
|
+
})
|
|
6489
|
+
},
|
|
6490
|
+
async (ctx, input) => {
|
|
6491
|
+
const env = await getEnv();
|
|
6492
|
+
if (pathConfig?.pathRestriction) {
|
|
6493
|
+
const resolved = path.resolve(env.getCwd(), input.path);
|
|
6494
|
+
if (!isPathAllowed(resolved, pathConfig.pathRestriction)) {
|
|
6495
|
+
await requirePathApproval(ctx, "read", resolved, pathConfig.pathRestriction);
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
let content = await env.readFile(input.path);
|
|
6499
|
+
if (input.offset !== void 0 || input.limit !== void 0) {
|
|
6500
|
+
const lines = content.split("\n");
|
|
6501
|
+
const start = input.offset ?? 0;
|
|
6502
|
+
const end = input.limit !== void 0 ? start + input.limit : lines.length;
|
|
6503
|
+
content = lines.slice(start, end).join("\n");
|
|
6504
|
+
}
|
|
6505
|
+
return { content, path: input.path };
|
|
6506
|
+
}
|
|
6507
|
+
);
|
|
6508
|
+
}
|
|
6509
|
+
function createWriteTool(getEnv, approval) {
|
|
6510
|
+
return defineTool(
|
|
6511
|
+
{
|
|
6512
|
+
id: "write",
|
|
6513
|
+
description: "Write content to a file. Creates the file if it does not exist, or overwrites it if it does. Parent directories are created automatically.",
|
|
6514
|
+
inputSchema: zod.z.object({
|
|
6515
|
+
path: zod.z.string().describe("Path to the file to write"),
|
|
6516
|
+
content: zod.z.string().describe("Content to write to the file")
|
|
6517
|
+
}),
|
|
6518
|
+
approval
|
|
6519
|
+
},
|
|
6520
|
+
async (_ctx, input) => {
|
|
6521
|
+
const env = await getEnv();
|
|
6522
|
+
await env.writeFile(input.path, input.content);
|
|
6523
|
+
return { success: true, path: input.path };
|
|
6524
|
+
}
|
|
6525
|
+
);
|
|
6526
|
+
}
|
|
6527
|
+
function createEditTool(getEnv, approval) {
|
|
6528
|
+
return defineTool(
|
|
6529
|
+
{
|
|
6530
|
+
id: "edit",
|
|
6531
|
+
description: "Edit a file by replacing an exact string match. The old_text must match exactly (including whitespace and indentation). Use this for precise code modifications.",
|
|
6532
|
+
inputSchema: zod.z.object({
|
|
6533
|
+
path: zod.z.string().describe("Path to the file to edit"),
|
|
6534
|
+
old_text: zod.z.string().describe("Exact text to find and replace"),
|
|
6535
|
+
new_text: zod.z.string().describe("Text to replace the old_text with")
|
|
6536
|
+
}),
|
|
6537
|
+
approval
|
|
6538
|
+
},
|
|
6539
|
+
async (_ctx, input) => {
|
|
6540
|
+
const env = await getEnv();
|
|
6541
|
+
const content = await env.readFile(input.path);
|
|
6542
|
+
if (!content.includes(input.old_text)) {
|
|
6543
|
+
throw new Error(
|
|
6544
|
+
`old_text not found in ${input.path}. Make sure the text matches exactly, including whitespace and indentation.`
|
|
6545
|
+
);
|
|
6546
|
+
}
|
|
6547
|
+
const newContent = content.replace(input.old_text, input.new_text);
|
|
6548
|
+
await env.writeFile(input.path, newContent);
|
|
6549
|
+
return { success: true, path: input.path };
|
|
6550
|
+
}
|
|
6551
|
+
);
|
|
6552
|
+
}
|
|
6553
|
+
function createGlobTool(getEnv, pathConfig) {
|
|
6554
|
+
return defineTool(
|
|
6555
|
+
{
|
|
6556
|
+
id: "glob",
|
|
6557
|
+
description: "Find files matching a glob pattern. Returns a list of file paths. Use this to discover files in the project structure.",
|
|
6558
|
+
inputSchema: zod.z.object({
|
|
6559
|
+
pattern: zod.z.string().describe('Glob pattern to match (e.g., "*.ts", "src/**/*.js")'),
|
|
6560
|
+
cwd: zod.z.string().optional().describe("Directory to search in"),
|
|
6561
|
+
ignore: zod.z.array(zod.z.string()).optional().describe("Patterns to exclude from results")
|
|
6562
|
+
})
|
|
6563
|
+
},
|
|
6564
|
+
async (ctx, input) => {
|
|
6565
|
+
const env = await getEnv();
|
|
6566
|
+
if (pathConfig?.pathRestriction && input.cwd) {
|
|
6567
|
+
const resolved = path.resolve(env.getCwd(), input.cwd);
|
|
6568
|
+
if (!isPathAllowed(resolved, pathConfig.pathRestriction)) {
|
|
6569
|
+
await requirePathApproval(ctx, "glob", resolved, pathConfig.pathRestriction);
|
|
6570
|
+
}
|
|
6571
|
+
}
|
|
6572
|
+
const files = await env.glob(input.pattern, {
|
|
6573
|
+
cwd: input.cwd,
|
|
6574
|
+
ignore: input.ignore
|
|
6575
|
+
});
|
|
6576
|
+
return { files };
|
|
6577
|
+
}
|
|
6578
|
+
);
|
|
6579
|
+
}
|
|
6580
|
+
function createGrepTool(getEnv, pathConfig) {
|
|
6581
|
+
return defineTool(
|
|
6582
|
+
{
|
|
6583
|
+
id: "grep",
|
|
6584
|
+
description: "Search file contents for a pattern using grep. Returns matching lines with file paths and line numbers. Use this to find code patterns, references, or specific text.",
|
|
6585
|
+
inputSchema: zod.z.object({
|
|
6586
|
+
pattern: zod.z.string().describe("Search pattern (regex supported)"),
|
|
6587
|
+
cwd: zod.z.string().optional().describe("Directory to search in"),
|
|
6588
|
+
include: zod.z.array(zod.z.string()).optional().describe('File patterns to include (e.g., ["*.ts", "*.js"])'),
|
|
6589
|
+
maxResults: zod.z.number().optional().describe("Maximum number of matches to return (default: 100)"),
|
|
6590
|
+
contextLines: zod.z.number().optional().describe("Number of context lines around each match")
|
|
6591
|
+
})
|
|
6592
|
+
},
|
|
6593
|
+
async (ctx, input) => {
|
|
6594
|
+
const env = await getEnv();
|
|
6595
|
+
if (pathConfig?.pathRestriction && input.cwd) {
|
|
6596
|
+
const resolved = path.resolve(env.getCwd(), input.cwd);
|
|
6597
|
+
if (!isPathAllowed(resolved, pathConfig.pathRestriction)) {
|
|
6598
|
+
await requirePathApproval(ctx, "grep", resolved, pathConfig.pathRestriction);
|
|
6599
|
+
}
|
|
6600
|
+
}
|
|
6601
|
+
const matches = await env.grep(input.pattern, {
|
|
6602
|
+
cwd: input.cwd,
|
|
6603
|
+
include: input.include,
|
|
6604
|
+
maxResults: input.maxResults,
|
|
6605
|
+
contextLines: input.contextLines
|
|
6606
|
+
});
|
|
6607
|
+
return { matches };
|
|
6608
|
+
}
|
|
6609
|
+
);
|
|
6610
|
+
}
|
|
6611
|
+
|
|
6612
|
+
// src/execution/sandbox-tools.ts
|
|
6613
|
+
function createEnvironment(config) {
|
|
6614
|
+
const envType = config?.env ?? "docker";
|
|
6615
|
+
switch (envType) {
|
|
6616
|
+
case "docker": {
|
|
6617
|
+
const dockerConfig = config?.docker ?? {
|
|
6618
|
+
image: "node:20-slim",
|
|
6619
|
+
workspaceDir: process.cwd()
|
|
6620
|
+
};
|
|
6621
|
+
return new DockerEnvironment(dockerConfig, config?.exec?.maxOutputChars);
|
|
6622
|
+
}
|
|
6623
|
+
case "e2b":
|
|
6624
|
+
throw new Error("E2B environment is not yet implemented.");
|
|
6625
|
+
case "local":
|
|
6626
|
+
return new LocalEnvironment(config?.local, config?.exec?.maxOutputChars);
|
|
6627
|
+
default:
|
|
6628
|
+
throw new Error(`Unknown environment type: ${String(envType)}`);
|
|
6629
|
+
}
|
|
6630
|
+
}
|
|
6631
|
+
function sandboxTools(config) {
|
|
6632
|
+
let env = null;
|
|
6633
|
+
let envPromise = null;
|
|
6634
|
+
async function getEnv() {
|
|
6635
|
+
if (env) return env;
|
|
6636
|
+
if (envPromise) return envPromise;
|
|
6637
|
+
envPromise = (async () => {
|
|
6638
|
+
const created = createEnvironment(config);
|
|
6639
|
+
await created.initialize();
|
|
6640
|
+
env = created;
|
|
6641
|
+
return env;
|
|
6642
|
+
})();
|
|
6643
|
+
return envPromise;
|
|
6644
|
+
}
|
|
6645
|
+
const envType = config?.env ?? "docker";
|
|
6646
|
+
if (envType === "e2b") {
|
|
6647
|
+
throw new Error("E2B environment is not yet implemented.");
|
|
6648
|
+
}
|
|
6649
|
+
const effectiveExecConfig = envType === "local" && !config?.exec?.security ? { ...config?.exec, security: "approval-always" } : config?.exec;
|
|
6650
|
+
const fileApproval = config?.fileApproval ?? (envType === "local" ? "always" : void 0);
|
|
6651
|
+
const pathConfig = config?.local?.pathRestriction ? { pathRestriction: config.local.pathRestriction } : void 0;
|
|
6652
|
+
const include = new Set(
|
|
6653
|
+
config?.tools ?? ["exec", "read", "write", "edit", "glob", "grep"]
|
|
6654
|
+
);
|
|
6655
|
+
const tools = [];
|
|
6656
|
+
if (include.has("exec")) tools.push(createExecTool(getEnv, effectiveExecConfig));
|
|
6657
|
+
if (include.has("read")) tools.push(createReadTool(getEnv, pathConfig));
|
|
6658
|
+
if (include.has("write")) tools.push(createWriteTool(getEnv, fileApproval));
|
|
6659
|
+
if (include.has("edit")) tools.push(createEditTool(getEnv, fileApproval));
|
|
6660
|
+
if (include.has("glob")) tools.push(createGlobTool(getEnv, pathConfig));
|
|
6661
|
+
if (include.has("grep")) tools.push(createGrepTool(getEnv, pathConfig));
|
|
6662
|
+
const result = tools;
|
|
6663
|
+
result.cleanup = async () => {
|
|
6664
|
+
if (env) {
|
|
6665
|
+
await env.destroy();
|
|
6666
|
+
env = null;
|
|
6667
|
+
envPromise = null;
|
|
6668
|
+
}
|
|
6669
|
+
};
|
|
6670
|
+
return result;
|
|
6671
|
+
}
|
|
5542
6672
|
|
|
5543
6673
|
exports.AgentRunConfig = AgentRunConfig;
|
|
6674
|
+
exports.DockerEnvironment = DockerEnvironment;
|
|
5544
6675
|
exports.DuplicateWorkflowError = DuplicateWorkflowError;
|
|
5545
6676
|
exports.ExecutionHandle = ExecutionHandle;
|
|
5546
6677
|
exports.GuardrailError = GuardrailError;
|
|
@@ -5562,6 +6693,7 @@ exports.Worker = Worker;
|
|
|
5562
6693
|
exports.WorkerServer = WorkerServer;
|
|
5563
6694
|
exports.WorkflowNotFoundError = WorkflowNotFoundError;
|
|
5564
6695
|
exports.agentStreamFunction = agentStreamFunction;
|
|
6696
|
+
exports.assertSafePath = assertSafePath;
|
|
5565
6697
|
exports.batchAgentInvoke = batchAgentInvoke;
|
|
5566
6698
|
exports.batchInvoke = batchInvoke;
|
|
5567
6699
|
exports.composeGuardrails = composeGuardrails;
|
|
@@ -5574,15 +6706,24 @@ exports.convertToolResultsToMessages = convertToolResultsToMessages;
|
|
|
5574
6706
|
exports.convertToolsToVercel = convertToolsToVercel;
|
|
5575
6707
|
exports.convertVercelToolCallToPython = convertVercelToolCallToPython;
|
|
5576
6708
|
exports.convertVercelUsageToPython = convertVercelUsageToPython;
|
|
6709
|
+
exports.createAskUserTool = createAskUserTool;
|
|
6710
|
+
exports.createEditTool = createEditTool;
|
|
6711
|
+
exports.createExecTool = createExecTool;
|
|
6712
|
+
exports.createGlobTool = createGlobTool;
|
|
6713
|
+
exports.createGrepTool = createGrepTool;
|
|
5577
6714
|
exports.createLogger = createLogger;
|
|
6715
|
+
exports.createReadTool = createReadTool;
|
|
5578
6716
|
exports.createStepHelper = createStepHelper;
|
|
5579
6717
|
exports.createStepStore = createStepStore;
|
|
6718
|
+
exports.createWebSearchTool = createWebSearchTool;
|
|
5580
6719
|
exports.createWorkflowRegistry = createWorkflowRegistry;
|
|
6720
|
+
exports.createWriteTool = createWriteTool;
|
|
5581
6721
|
exports.defineAgent = defineAgent;
|
|
5582
6722
|
exports.defineGuardrail = defineGuardrail;
|
|
5583
6723
|
exports.defineHook = defineHook;
|
|
5584
6724
|
exports.defineTool = defineTool;
|
|
5585
6725
|
exports.defineWorkflow = defineWorkflow;
|
|
6726
|
+
exports.evaluateAllowlist = evaluateAllowlist;
|
|
5586
6727
|
exports.executeGuardrailChain = executeGuardrailChain;
|
|
5587
6728
|
exports.executeGuardrailsOrThrow = executeGuardrailsOrThrow;
|
|
5588
6729
|
exports.executeHookChain = executeHookChain;
|
|
@@ -5597,6 +6738,7 @@ exports.hasText = hasText;
|
|
|
5597
6738
|
exports.initializeOtel = initializeOtel;
|
|
5598
6739
|
exports.initializeState = initializeState;
|
|
5599
6740
|
exports.isAgentWorkflow = isAgentWorkflow;
|
|
6741
|
+
exports.isBinary = isBinary;
|
|
5600
6742
|
exports.isGuardrail = isGuardrail;
|
|
5601
6743
|
exports.isHook = isHook;
|
|
5602
6744
|
exports.isOtelAvailable = isOtelAvailable;
|
|
@@ -5610,10 +6752,14 @@ exports.normalizeGuardrail = normalizeGuardrail;
|
|
|
5610
6752
|
exports.normalizeGuardrails = normalizeGuardrails;
|
|
5611
6753
|
exports.normalizeHook = normalizeHook;
|
|
5612
6754
|
exports.normalizeHooks = normalizeHooks;
|
|
6755
|
+
exports.parseGrepOutput = parseGrepOutput;
|
|
5613
6756
|
exports.retry = retry;
|
|
6757
|
+
exports.sandboxTools = sandboxTools;
|
|
5614
6758
|
exports.serializeFinalState = serializeFinalState;
|
|
5615
6759
|
exports.sleep = sleep;
|
|
5616
6760
|
exports.stopCondition = stopCondition;
|
|
6761
|
+
exports.stripAnsi = stripAnsi;
|
|
6762
|
+
exports.truncateOutput = truncateOutput;
|
|
5617
6763
|
exports.validateState = validateState;
|
|
5618
6764
|
//# sourceMappingURL=index.cjs.map
|
|
5619
6765
|
//# sourceMappingURL=index.cjs.map
|