@polos/sdk 0.1.1 → 0.1.2

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 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 {
@@ -212,6 +236,12 @@ var OrchestratorClient = class {
212
236
  this.timeout = config.timeout ?? 3e4;
213
237
  this.maxRetries = config.maxRetries ?? 3;
214
238
  }
239
+ /**
240
+ * Get the API URL.
241
+ */
242
+ getApiUrl() {
243
+ return this.apiUrl;
244
+ }
215
245
  /**
216
246
  * Get the project ID.
217
247
  */
@@ -235,8 +265,8 @@ var OrchestratorClient = class {
235
265
  /**
236
266
  * Make an HTTP request with retry logic.
237
267
  */
238
- async request(method, path, options) {
239
- const url = `${this.apiUrl}${path}`;
268
+ async request(method, path3, options) {
269
+ const url = `${this.apiUrl}${path3}`;
240
270
  const retries = options?.retries ?? this.maxRetries;
241
271
  let lastError;
242
272
  for (let attempt = 0; attempt <= retries; attempt++) {
@@ -288,7 +318,7 @@ var OrchestratorClient = class {
288
318
  }
289
319
  if (attempt < retries) {
290
320
  const delay = Math.min(1e3 * Math.pow(2, attempt), 16e3);
291
- await new Promise((resolve) => setTimeout(resolve, delay));
321
+ await new Promise((resolve8) => setTimeout(resolve8, delay));
292
322
  }
293
323
  }
294
324
  }
@@ -727,7 +757,7 @@ var OrchestratorClient = class {
727
757
  if (execution.status === "completed" || execution.status === "failed" || execution.status === "cancelled") {
728
758
  return execution;
729
759
  }
730
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
760
+ await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
731
761
  }
732
762
  throw new Error(`Execution ${executionId} timed out after ${String(timeout)}ms`);
733
763
  }
@@ -1193,6 +1223,46 @@ function isToolWorkflow(workflow) {
1193
1223
  }
1194
1224
  function defineTool(config, handler) {
1195
1225
  const toolParameters = config.inputSchema ? zodToJsonSchema.zodToJsonSchema(config.inputSchema, { target: "openApi3" }) : { type: "object", properties: {} };
1226
+ const effectiveHandler = config.approval === "always" ? async (ctx, input) => {
1227
+ const approvalId = await ctx.step.uuid("_approval_id");
1228
+ const response = await ctx.step.suspend(
1229
+ `approve_${config.id}_${approvalId}`,
1230
+ {
1231
+ data: {
1232
+ _form: {
1233
+ title: `Approve tool: ${config.id}`,
1234
+ description: `The agent wants to use the "${config.id}" tool.`,
1235
+ fields: [
1236
+ {
1237
+ key: "approved",
1238
+ type: "boolean",
1239
+ label: "Approve this tool call?",
1240
+ required: true,
1241
+ default: false
1242
+ },
1243
+ {
1244
+ key: "feedback",
1245
+ type: "textarea",
1246
+ label: "Feedback for the agent (optional)",
1247
+ description: "If rejecting, tell the agent what to do instead.",
1248
+ required: false
1249
+ }
1250
+ ],
1251
+ context: { tool: config.id, input }
1252
+ },
1253
+ _source: "tool_approval",
1254
+ _tool: config.id
1255
+ }
1256
+ }
1257
+ );
1258
+ if (response.data?.approved !== true) {
1259
+ const feedback = response.data?.feedback;
1260
+ throw new Error(
1261
+ `Tool "${config.id}" was rejected by the user.${feedback ? ` Feedback: ${feedback}` : ""}`
1262
+ );
1263
+ }
1264
+ return handler(ctx, input);
1265
+ } : handler;
1196
1266
  const workflow = defineWorkflow(
1197
1267
  {
1198
1268
  id: config.id,
@@ -1205,7 +1275,8 @@ function defineTool(config, handler) {
1205
1275
  onStart: config.onStart,
1206
1276
  onEnd: config.onEnd
1207
1277
  },
1208
- handler
1278
+ effectiveHandler,
1279
+ config.autoRegister === void 0 ? void 0 : { autoRegister: config.autoRegister }
1209
1280
  );
1210
1281
  const toolWorkflow = Object.assign(workflow, {
1211
1282
  toolDescription: config.description,
@@ -1249,7 +1320,7 @@ function calculateDelay(attempt, options) {
1249
1320
  return Math.round(delay);
1250
1321
  }
1251
1322
  function sleep(ms) {
1252
- return new Promise((resolve) => setTimeout(resolve, ms));
1323
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
1253
1324
  }
1254
1325
  async function retry(fn, options = {}) {
1255
1326
  const opts = { ...DEFAULT_OPTIONS, ...options };
@@ -1438,7 +1509,7 @@ function createStepHelper(options) {
1438
1509
  if (options2.days) totalSeconds += options2.days * 86400;
1439
1510
  if (options2.weeks) totalSeconds += options2.weeks * 604800;
1440
1511
  const totalMs = totalSeconds * 1e3;
1441
- await new Promise((resolve) => setTimeout(resolve, totalMs));
1512
+ await new Promise((resolve8) => setTimeout(resolve8, totalMs));
1442
1513
  store.set(key, { wait_until: new Date(Date.now() + totalMs).toISOString() });
1443
1514
  },
1444
1515
  async waitUntil(key, date) {
@@ -1446,7 +1517,7 @@ function createStepHelper(options) {
1446
1517
  if (cached) return;
1447
1518
  const now = Date.now();
1448
1519
  const waitMs = Math.max(0, date.getTime() - now);
1449
- await new Promise((resolve) => setTimeout(resolve, waitMs));
1520
+ await new Promise((resolve8) => setTimeout(resolve8, waitMs));
1450
1521
  store.set(key, { wait_until: date.toISOString() });
1451
1522
  },
1452
1523
  // eslint-disable-next-line @typescript-eslint/require-await -- stub implementation
@@ -2518,7 +2589,6 @@ async function agentStreamFunction(ctx, payload, agentDef) {
2518
2589
  const allToolResults = [];
2519
2590
  const steps = [];
2520
2591
  let endSteps = false;
2521
- let toolResults;
2522
2592
  let checkedStructuredOutput = false;
2523
2593
  let cachedHistoryMessages = null;
2524
2594
  if (agentDef.conversationHistory > 0 && conversationId) {
@@ -2559,7 +2629,7 @@ async function agentStreamFunction(ctx, payload, agentDef) {
2559
2629
  break;
2560
2630
  }
2561
2631
  }
2562
- const safetyMaxSteps = hasMaxStepsCondition ? null : parseInt(process.env["POLOS_AGENT_MAX_STEPS"] ?? "10", 10);
2632
+ const safetyMaxSteps = hasMaxStepsCondition ? null : parseInt(process.env["POLOS_AGENT_MAX_STEPS"] ?? "20", 10);
2563
2633
  const execCtxForPublish = getExecutionContext();
2564
2634
  const publishEvent = async (topic, eventData) => {
2565
2635
  if (execCtxForPublish?.orchestratorClient) {
@@ -2608,7 +2678,6 @@ async function agentStreamFunction(ctx, payload, agentDef) {
2608
2678
  agent_step: agentStep,
2609
2679
  guardrails,
2610
2680
  guardrail_max_retries: guardrailMaxRetries,
2611
- tool_results: toolResults,
2612
2681
  outputSchema: outputSchemaForLlm
2613
2682
  });
2614
2683
  if (guardrails.length > 0 && streaming && llmResult.content) {
@@ -2637,13 +2706,11 @@ async function agentStreamFunction(ctx, payload, agentDef) {
2637
2706
  temperature: agentConfig.temperature,
2638
2707
  maxTokens: agentConfig.maxOutputTokens,
2639
2708
  agent_step: agentStep,
2640
- tool_results: toolResults,
2641
2709
  outputSchema: outputSchemaForLlm
2642
2710
  },
2643
2711
  publishEvent
2644
2712
  );
2645
2713
  }
2646
- toolResults = void 0;
2647
2714
  if (llmResult.usage) {
2648
2715
  finalInputTokens += llmResult.usage.input_tokens;
2649
2716
  finalOutputTokens += llmResult.usage.output_tokens;
@@ -2656,6 +2723,7 @@ async function agentStreamFunction(ctx, payload, agentDef) {
2656
2723
  `LLM failed to generate output: agent_id=${agentDef.id}, agent_step=${String(agentStep)}`
2657
2724
  );
2658
2725
  }
2726
+ conversationMessages.push(...llmResult.raw_output);
2659
2727
  const batchWorkflows = [];
2660
2728
  const toolCallList = [];
2661
2729
  let toolResultsRecordedList = [];
@@ -2716,7 +2784,7 @@ async function agentStreamFunction(ctx, payload, agentDef) {
2716
2784
  const toolSpec = batchWorkflows[i];
2717
2785
  const toolCallInfo = toolCallList[i];
2718
2786
  const toolName = toolCallInfo.tool_name;
2719
- const toolResult = batchToolResult;
2787
+ const toolResult = batchToolResult.success ? batchToolResult.result : `Error: ${batchToolResult.error ?? "unknown error"}`;
2720
2788
  const toolCallId = toolCallInfo.tool_call_id;
2721
2789
  const toolCallCallId = toolCallInfo.tool_call_call_id;
2722
2790
  if (agentDef.agentHooks.onToolEnd.length > 0) {
@@ -2747,7 +2815,8 @@ async function agentStreamFunction(ctx, payload, agentDef) {
2747
2815
  });
2748
2816
  }
2749
2817
  allToolResults.push(...toolResultsRecordedList);
2750
- toolResults = currentIterationToolResults;
2818
+ const toolResultMessages = convertToolResultsToMessages(currentIterationToolResults);
2819
+ conversationMessages.push(...toolResultMessages);
2751
2820
  }
2752
2821
  steps.push({
2753
2822
  step: agentStep,
@@ -2755,7 +2824,7 @@ async function agentStreamFunction(ctx, payload, agentDef) {
2755
2824
  tool_calls: toolCalls,
2756
2825
  tool_results: toolResultsRecordedList,
2757
2826
  usage: llmResult.usage,
2758
- raw_output: llmResult.raw_output
2827
+ raw_output: conversationMessages
2759
2828
  });
2760
2829
  if (agentDef.agentHooks.onAgentStepEnd.length > 0) {
2761
2830
  const hookResult = await executeHookChain(agentDef.agentHooks.onAgentStepEnd, {
@@ -2772,7 +2841,7 @@ async function agentStreamFunction(ctx, payload, agentDef) {
2772
2841
  throw new Error(hookResult.error ?? "on_agent_step_end hook failed");
2773
2842
  }
2774
2843
  }
2775
- if (toolResults === void 0 || toolResults.length === 0) {
2844
+ if (currentIterationToolResults.length === 0) {
2776
2845
  endSteps = true;
2777
2846
  }
2778
2847
  if (stopConditions.length > 0 && !endSteps) {
@@ -2805,7 +2874,6 @@ async function agentStreamFunction(ctx, payload, agentDef) {
2805
2874
  checkedStructuredOutput = true;
2806
2875
  if (!parseResult.success) {
2807
2876
  endSteps = false;
2808
- conversationMessages = llmResult.raw_output;
2809
2877
  const schemaJson = JSON.stringify(agentDef.outputSchema, null, 2);
2810
2878
  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
2879
 
@@ -2818,9 +2886,6 @@ Please provide ONLY valid JSON that matches the schema, with no additional text
2818
2886
  }
2819
2887
  }
2820
2888
  if (!endSteps) {
2821
- if (!checkedStructuredOutput) {
2822
- conversationMessages = llmResult.raw_output;
2823
- }
2824
2889
  agentStep++;
2825
2890
  }
2826
2891
  }
@@ -3034,7 +3099,7 @@ function defineAgent(config) {
3034
3099
  maxOutputTokens: config.maxOutputTokens
3035
3100
  },
3036
3101
  input,
3037
- streaming: streamingFlag ?? true,
3102
+ streaming: streamingFlag ?? false,
3038
3103
  conversation_id: conversationIdValue
3039
3104
  };
3040
3105
  return agentStreamFunction(ctx, streamPayload, {
@@ -4091,16 +4156,27 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
4091
4156
  if (items.length === 0) return [];
4092
4157
  const existing = checkExistingStep(key);
4093
4158
  if (existing) {
4094
- const raw = handleExistingStep(existing);
4159
+ let raw;
4160
+ try {
4161
+ raw = handleExistingStep(existing);
4162
+ } catch {
4163
+ raw = existing.outputs;
4164
+ }
4095
4165
  if (Array.isArray(raw)) {
4096
4166
  return raw.map((item) => {
4097
- if (item && typeof item === "object" && "result" in item) {
4098
- return item["result"];
4167
+ if (item && typeof item === "object" && "success" in item) {
4168
+ const rec = item;
4169
+ return {
4170
+ workflowId: typeof rec["workflow_id"] === "string" ? rec["workflow_id"] : "",
4171
+ success: rec["success"] === true,
4172
+ result: rec["result"] ?? null,
4173
+ error: typeof rec["error"] === "string" ? rec["error"] : null
4174
+ };
4099
4175
  }
4100
- return item;
4176
+ return { workflowId: "", success: true, result: item, error: null };
4101
4177
  });
4102
4178
  }
4103
- return raw;
4179
+ return raw.map((r) => ({ workflowId: "", success: true, result: r, error: null }));
4104
4180
  }
4105
4181
  const workflows = items.map((item) => {
4106
4182
  const workflowId = typeof item.workflow === "string" ? item.workflow : item.workflow.id;
@@ -4163,7 +4239,7 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
4163
4239
  });
4164
4240
  const waitThreshold = Number(process.env["POLOS_WAIT_THRESHOLD_SECONDS"] ?? "10");
4165
4241
  if (totalSeconds <= waitThreshold) {
4166
- await new Promise((resolve) => setTimeout(resolve, totalSeconds * 1e3));
4242
+ await new Promise((resolve8) => setTimeout(resolve8, totalSeconds * 1e3));
4167
4243
  await saveStepOutput(key, { wait_until: waitUntil.toISOString() });
4168
4244
  return;
4169
4245
  }
@@ -4199,7 +4275,7 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
4199
4275
  });
4200
4276
  const waitThreshold = Number(process.env["POLOS_WAIT_THRESHOLD_SECONDS"] ?? "10");
4201
4277
  if (waitSeconds <= waitThreshold) {
4202
- await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1e3));
4278
+ await new Promise((resolve8) => setTimeout(resolve8, waitSeconds * 1e3));
4203
4279
  await saveStepOutput(key, { wait_until: date.toISOString() });
4204
4280
  return;
4205
4281
  }
@@ -4273,12 +4349,14 @@ function createOrchestratorStepHelper(orchestratorClient, cachedSteps, execCtx,
4273
4349
  return handleExistingStep(existing);
4274
4350
  }
4275
4351
  const topic = `workflow/${execCtx.rootWorkflowId}/${execCtx.rootExecutionId}`;
4352
+ const approvalUrl = `${orchestratorClient.getApiUrl()}/approve/${execCtx.rootExecutionId}/${key}`;
4353
+ 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
4354
  await orchestratorClient.publishEvent({
4277
4355
  topic,
4278
4356
  events: [
4279
4357
  {
4280
4358
  eventType: `suspend_${key}`,
4281
- data: options?.data
4359
+ data: eventData
4282
4360
  }
4283
4361
  ],
4284
4362
  executionId: execCtx.executionId,
@@ -4941,10 +5019,10 @@ var Worker = class {
4941
5019
  process.on("SIGINT", signalHandler);
4942
5020
  process.on("SIGTERM", signalHandler);
4943
5021
  this.signalHandler = signalHandler;
4944
- await new Promise((resolve) => {
5022
+ await new Promise((resolve8) => {
4945
5023
  const checkState = () => {
4946
5024
  if (this.state === "stopping" || this.state === "stopped") {
4947
- resolve();
5025
+ resolve8();
4948
5026
  } else {
4949
5027
  setTimeout(checkState, 100);
4950
5028
  }
@@ -4983,7 +5061,7 @@ var Worker = class {
4983
5061
  const waitTimeout = 3e4;
4984
5062
  const waitStart = Date.now();
4985
5063
  while (this.activeExecutions.size > 0 && Date.now() - waitStart < waitTimeout) {
4986
- await new Promise((resolve) => setTimeout(resolve, 100));
5064
+ await new Promise((resolve8) => setTimeout(resolve8, 100));
4987
5065
  }
4988
5066
  if (this.activeExecutions.size > 0) {
4989
5067
  logger6.warn(`${String(this.activeExecutions.size)} executions did not complete in time`);
@@ -5539,8 +5617,969 @@ var GuardrailResult2 = {
5539
5617
  error
5540
5618
  })
5541
5619
  };
5620
+ var askUserInputSchema = zod.z.object({
5621
+ question: zod.z.string().describe("The question to ask the user"),
5622
+ title: zod.z.string().optional().describe("Short title for the question (shown as heading)"),
5623
+ fields: zod.z.array(
5624
+ zod.z.object({
5625
+ key: zod.z.string().describe("Unique key for this field"),
5626
+ type: zod.z.enum(["text", "textarea", "number", "boolean", "select"]).describe("Field type"),
5627
+ label: zod.z.string().describe("Label shown to user"),
5628
+ description: zod.z.string().optional().describe("Help text for the field"),
5629
+ required: zod.z.boolean().optional().describe("Whether this field is required"),
5630
+ options: zod.z.array(
5631
+ zod.z.object({
5632
+ label: zod.z.string(),
5633
+ value: zod.z.string()
5634
+ })
5635
+ ).optional().describe("Options for select fields")
5636
+ })
5637
+ ).optional().describe(
5638
+ "Structured form fields for the response. If omitted, shows a single text response field."
5639
+ )
5640
+ });
5641
+ function createAskUserTool() {
5642
+ return defineTool(
5643
+ {
5644
+ id: "ask_user",
5645
+ 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.",
5646
+ inputSchema: askUserInputSchema
5647
+ },
5648
+ async (ctx, input) => {
5649
+ const fields = input.fields ?? [
5650
+ {
5651
+ key: "response",
5652
+ type: "textarea",
5653
+ label: input.question,
5654
+ required: true
5655
+ }
5656
+ ];
5657
+ const askId = await ctx.step.uuid("_ask_user_id");
5658
+ const response = await ctx.step.suspend(`ask_user_${askId}`, {
5659
+ data: {
5660
+ _form: {
5661
+ title: input.title ?? "Agent Question",
5662
+ description: input.question,
5663
+ fields
5664
+ },
5665
+ _source: "ask_user",
5666
+ _tool: "ask_user"
5667
+ }
5668
+ });
5669
+ return response?.["data"] ?? {};
5670
+ }
5671
+ );
5672
+ }
5673
+ var webSearchInputSchema = zod.z.object({
5674
+ query: zod.z.string().describe("The search query"),
5675
+ maxResults: zod.z.number().optional().describe("Maximum number of results to return"),
5676
+ topic: zod.z.enum(["general", "news"]).optional().describe("Topic filter: general web search or news")
5677
+ });
5678
+ function createTavilySearchFn(config) {
5679
+ return async (query, options) => {
5680
+ const apiKey = config.apiKey ?? process.env["TAVILY_API_KEY"];
5681
+ if (!apiKey) {
5682
+ throw new Error(
5683
+ "Tavily API key is required. Provide it via the apiKey option or set the TAVILY_API_KEY environment variable."
5684
+ );
5685
+ }
5686
+ const baseUrl = (config.baseUrl ?? "https://api.tavily.com").replace(/\/+$/, "");
5687
+ const body = {
5688
+ query,
5689
+ max_results: options.maxResults,
5690
+ search_depth: config.searchDepth ?? "basic",
5691
+ include_answer: config.includeAnswer ?? true,
5692
+ include_raw_content: config.includeRawContent ?? false,
5693
+ topic: options.topic
5694
+ };
5695
+ const response = await fetch(`${baseUrl}/search`, {
5696
+ method: "POST",
5697
+ headers: {
5698
+ "Content-Type": "application/json",
5699
+ Authorization: `Bearer ${apiKey}`
5700
+ },
5701
+ body: JSON.stringify(body)
5702
+ });
5703
+ if (!response.ok) {
5704
+ let errorMessage;
5705
+ try {
5706
+ const errorBody = await response.json();
5707
+ errorMessage = typeof errorBody["detail"] === "string" ? errorBody["detail"] : JSON.stringify(errorBody);
5708
+ } catch {
5709
+ errorMessage = await response.text();
5710
+ }
5711
+ throw new Error(`Tavily API error (${String(response.status)}): ${errorMessage}`);
5712
+ }
5713
+ const data = await response.json();
5714
+ return {
5715
+ query: data.query,
5716
+ answer: data.answer,
5717
+ results: data.results.map((r) => ({
5718
+ title: r.title,
5719
+ url: r.url,
5720
+ content: r.content,
5721
+ score: r.score,
5722
+ publishedDate: r.published_date
5723
+ }))
5724
+ };
5725
+ };
5726
+ }
5727
+ function createWebSearchTool(config) {
5728
+ const toolId = config?.toolId ?? "web_search";
5729
+ const defaultMaxResults = config?.maxResults ?? 5;
5730
+ const defaultTopic = config?.topic ?? "general";
5731
+ const searchFn = config?.search ?? createTavilySearchFn(config ?? {});
5732
+ return defineTool(
5733
+ {
5734
+ id: toolId,
5735
+ description: "Search the web for current information. Returns a list of relevant results with titles, URLs, and content snippets.",
5736
+ inputSchema: webSearchInputSchema,
5737
+ approval: config?.approval
5738
+ },
5739
+ async (ctx, input) => {
5740
+ const options = {
5741
+ maxResults: input.maxResults ?? defaultMaxResults,
5742
+ topic: input.topic ?? defaultTopic
5743
+ };
5744
+ const result = await ctx.step.run("web_search", () => searchFn(input.query, options), {
5745
+ input: { query: input.query, options }
5746
+ });
5747
+ return result;
5748
+ }
5749
+ );
5750
+ }
5751
+
5752
+ // src/execution/output.ts
5753
+ var DEFAULT_MAX_CHARS = 1e5;
5754
+ var HEAD_RATIO = 0.2;
5755
+ function truncateOutput(output, maxChars) {
5756
+ const max = maxChars ?? DEFAULT_MAX_CHARS;
5757
+ if (output.length <= max) {
5758
+ return { text: output, truncated: false };
5759
+ }
5760
+ const headSize = Math.floor(max * HEAD_RATIO);
5761
+ const tailSize = max - headSize;
5762
+ const omitted = output.length - headSize - tailSize;
5763
+ const head = output.slice(0, headSize);
5764
+ const tail = output.slice(-tailSize);
5765
+ const text = `${head}
5766
+
5767
+ --- truncated ${String(omitted)} characters ---
5768
+
5769
+ ${tail}`;
5770
+ return { text, truncated: true };
5771
+ }
5772
+ function isBinary(buffer) {
5773
+ const checkLength = Math.min(buffer.length, 8192);
5774
+ for (let i = 0; i < checkLength; i++) {
5775
+ if (buffer[i] === 0) {
5776
+ return true;
5777
+ }
5778
+ }
5779
+ return false;
5780
+ }
5781
+ function parseGrepOutput(output) {
5782
+ if (!output.trim()) {
5783
+ return [];
5784
+ }
5785
+ const matches = [];
5786
+ const lines = output.split("\n");
5787
+ for (const line of lines) {
5788
+ if (!line) continue;
5789
+ const match = /^(.+?):(\d+):(.*)$/.exec(line);
5790
+ if (match?.[1] && match[2] && match[3] !== void 0) {
5791
+ matches.push({ path: match[1], line: parseInt(match[2], 10), text: match[3] });
5792
+ }
5793
+ }
5794
+ return matches;
5795
+ }
5796
+ function stripAnsi(text) {
5797
+ return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
5798
+ }
5799
+
5800
+ // src/execution/docker.ts
5801
+ var DEFAULT_CONTAINER_WORKDIR = "/workspace";
5802
+ var DEFAULT_TIMEOUT_SECONDS = 300;
5803
+ var DEFAULT_MAX_OUTPUT_CHARS = 1e5;
5804
+ function spawnCommand(command, args, options) {
5805
+ return new Promise((resolve8, reject) => {
5806
+ const proc = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
5807
+ let stdout = "";
5808
+ let stderr = "";
5809
+ let killed = false;
5810
+ proc.stdout.on("data", (data) => {
5811
+ stdout += data.toString();
5812
+ });
5813
+ proc.stderr.on("data", (data) => {
5814
+ stderr += data.toString();
5815
+ });
5816
+ const timeoutMs = (options?.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3;
5817
+ const timer = setTimeout(() => {
5818
+ killed = true;
5819
+ proc.kill("SIGKILL");
5820
+ }, timeoutMs);
5821
+ proc.on("close", (code) => {
5822
+ clearTimeout(timer);
5823
+ if (killed) {
5824
+ resolve8({
5825
+ exitCode: 137,
5826
+ stdout,
5827
+ stderr: stderr + "\n[Process killed: timeout exceeded]"
5828
+ });
5829
+ } else {
5830
+ resolve8({ exitCode: code ?? 1, stdout, stderr });
5831
+ }
5832
+ });
5833
+ proc.on("error", (err) => {
5834
+ clearTimeout(timer);
5835
+ reject(err);
5836
+ });
5837
+ if (options?.stdin) {
5838
+ proc.stdin.write(options.stdin);
5839
+ }
5840
+ proc.stdin.end();
5841
+ });
5842
+ }
5843
+ var DockerEnvironment = class {
5844
+ type = "docker";
5845
+ containerId = null;
5846
+ containerName;
5847
+ config;
5848
+ containerWorkdir;
5849
+ maxOutputChars;
5850
+ constructor(config, maxOutputChars) {
5851
+ this.config = config;
5852
+ this.containerWorkdir = config.containerWorkdir ?? DEFAULT_CONTAINER_WORKDIR;
5853
+ this.containerName = `polos-sandbox-${crypto.randomUUID().slice(0, 8)}`;
5854
+ this.maxOutputChars = maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
5855
+ }
5856
+ async initialize() {
5857
+ const args = [
5858
+ "run",
5859
+ "-d",
5860
+ "--name",
5861
+ this.containerName,
5862
+ "-v",
5863
+ `${this.config.workspaceDir}:${this.containerWorkdir}:rw`,
5864
+ "-w",
5865
+ this.containerWorkdir
5866
+ ];
5867
+ if (this.config.memory) {
5868
+ args.push("--memory", this.config.memory);
5869
+ }
5870
+ if (this.config.cpus) {
5871
+ args.push("--cpus", this.config.cpus);
5872
+ }
5873
+ args.push("--network", this.config.network ?? "none");
5874
+ if (this.config.env) {
5875
+ for (const [key, value] of Object.entries(this.config.env)) {
5876
+ args.push("-e", `${key}=${value}`);
5877
+ }
5878
+ }
5879
+ args.push(this.config.image, "sleep", "infinity");
5880
+ const result = await spawnCommand("docker", args, { timeout: 60 });
5881
+ if (result.exitCode !== 0) {
5882
+ throw new Error(`Failed to create Docker container: ${result.stderr.trim()}`);
5883
+ }
5884
+ this.containerId = result.stdout.trim().slice(0, 12);
5885
+ if (this.config.setupCommand) {
5886
+ const setupResult = await this.exec(this.config.setupCommand);
5887
+ if (setupResult.exitCode !== 0) {
5888
+ throw new Error(
5889
+ `Setup command failed (exit ${String(setupResult.exitCode)}): ${setupResult.stderr.trim()}`
5890
+ );
5891
+ }
5892
+ }
5893
+ }
5894
+ async exec(command, opts) {
5895
+ this.assertInitialized();
5896
+ const args = ["exec", "-i"];
5897
+ const cwd = opts?.cwd ?? this.containerWorkdir;
5898
+ args.push("-w", cwd);
5899
+ if (opts?.env) {
5900
+ for (const [key, value] of Object.entries(opts.env)) {
5901
+ args.push("-e", `${key}=${value}`);
5902
+ }
5903
+ }
5904
+ args.push(this.containerName, "sh", "-c", command);
5905
+ const timeout = opts?.timeout ?? DEFAULT_TIMEOUT_SECONDS;
5906
+ const start = Date.now();
5907
+ const result = await spawnCommand("docker", args, {
5908
+ timeout,
5909
+ stdin: opts?.stdin
5910
+ });
5911
+ const durationMs = Date.now() - start;
5912
+ const { text: stdout, truncated: stdoutTruncated } = truncateOutput(
5913
+ stripAnsi(result.stdout),
5914
+ this.maxOutputChars
5915
+ );
5916
+ const { text: stderr } = truncateOutput(stripAnsi(result.stderr), this.maxOutputChars);
5917
+ return {
5918
+ exitCode: result.exitCode,
5919
+ stdout,
5920
+ stderr,
5921
+ durationMs,
5922
+ truncated: stdoutTruncated
5923
+ };
5924
+ }
5925
+ async readFile(filePath) {
5926
+ const hostPath = this.toHostPath(filePath);
5927
+ const buffer = await fs2__namespace.readFile(hostPath);
5928
+ if (isBinary(buffer)) {
5929
+ throw new Error(`Cannot read binary file: ${filePath}`);
5930
+ }
5931
+ return buffer.toString("utf-8");
5932
+ }
5933
+ async writeFile(filePath, content) {
5934
+ const hostPath = this.toHostPath(filePath);
5935
+ await fs2__namespace.mkdir(path__namespace.dirname(hostPath), { recursive: true });
5936
+ await fs2__namespace.writeFile(hostPath, content, "utf-8");
5937
+ }
5938
+ async fileExists(filePath) {
5939
+ const hostPath = this.toHostPath(filePath);
5940
+ try {
5941
+ await fs2__namespace.access(hostPath);
5942
+ return true;
5943
+ } catch {
5944
+ return false;
5945
+ }
5946
+ }
5947
+ async glob(pattern, opts) {
5948
+ const cwd = opts?.cwd ?? this.containerWorkdir;
5949
+ let command = `find ${cwd} -type f -name '${pattern}'`;
5950
+ if (opts?.ignore) {
5951
+ for (const ignore of opts.ignore) {
5952
+ command += ` ! -path '${ignore}'`;
5953
+ }
5954
+ }
5955
+ command += " 2>/dev/null | sort | head -1000";
5956
+ const result = await this.exec(command);
5957
+ if (!result.stdout.trim()) return [];
5958
+ return result.stdout.trim().split("\n").filter(Boolean);
5959
+ }
5960
+ async grep(pattern, opts) {
5961
+ const cwd = opts?.cwd ?? this.containerWorkdir;
5962
+ const maxResults = opts?.maxResults ?? 100;
5963
+ let command = "grep -rn";
5964
+ if (opts?.contextLines !== void 0) {
5965
+ command += ` -C ${String(opts.contextLines)}`;
5966
+ }
5967
+ if (opts?.include) {
5968
+ for (const inc of opts.include) {
5969
+ command += ` --include='${inc}'`;
5970
+ }
5971
+ }
5972
+ const escapedPattern = pattern.replace(/'/g, "'\\''");
5973
+ command += ` -- '${escapedPattern}' ${cwd}`;
5974
+ command += ` 2>/dev/null | head -${String(maxResults)}`;
5975
+ const result = await this.exec(command);
5976
+ return parseGrepOutput(result.stdout);
5977
+ }
5978
+ async destroy() {
5979
+ if (!this.containerId) return;
5980
+ try {
5981
+ await spawnCommand("docker", ["rm", "-f", this.containerName], { timeout: 30 });
5982
+ } finally {
5983
+ this.containerId = null;
5984
+ }
5985
+ }
5986
+ getCwd() {
5987
+ return this.containerWorkdir;
5988
+ }
5989
+ getInfo() {
5990
+ return {
5991
+ type: "docker",
5992
+ cwd: this.containerWorkdir,
5993
+ sandboxId: this.containerId ?? void 0
5994
+ };
5995
+ }
5996
+ /**
5997
+ * Translate a container path to the corresponding host filesystem path.
5998
+ * Validates the path stays within the workspace to prevent traversal.
5999
+ */
6000
+ toHostPath(containerPath) {
6001
+ const resolved = path__namespace.posix.resolve(this.containerWorkdir, containerPath);
6002
+ if (!resolved.startsWith(this.containerWorkdir)) {
6003
+ throw new Error(`Path traversal detected: "${containerPath}" resolves outside workspace`);
6004
+ }
6005
+ const relative2 = path__namespace.posix.relative(this.containerWorkdir, resolved);
6006
+ return path__namespace.join(this.config.workspaceDir, relative2);
6007
+ }
6008
+ /**
6009
+ * Translate a host filesystem path to the corresponding container path.
6010
+ */
6011
+ toContainerPath(hostPath) {
6012
+ const resolved = path__namespace.resolve(hostPath);
6013
+ if (!resolved.startsWith(this.config.workspaceDir)) {
6014
+ throw new Error(
6015
+ `Path outside workspace: "${hostPath}" is not within "${this.config.workspaceDir}"`
6016
+ );
6017
+ }
6018
+ const relative2 = path__namespace.relative(this.config.workspaceDir, resolved);
6019
+ return path__namespace.posix.join(this.containerWorkdir, relative2);
6020
+ }
6021
+ assertInitialized() {
6022
+ if (!this.containerId) {
6023
+ throw new Error("Docker environment not initialized. Call initialize() first.");
6024
+ }
6025
+ }
6026
+ };
6027
+ var DEFAULT_TIMEOUT_SECONDS2 = 300;
6028
+ var DEFAULT_MAX_OUTPUT_CHARS2 = 1e5;
6029
+ function spawnLocal(command, options) {
6030
+ return new Promise((resolve8, reject) => {
6031
+ const proc = child_process.spawn("sh", ["-c", command], {
6032
+ cwd: options.cwd,
6033
+ env: options.env ? { ...process.env, ...options.env } : void 0,
6034
+ stdio: ["pipe", "pipe", "pipe"]
6035
+ });
6036
+ let stdout = "";
6037
+ let stderr = "";
6038
+ let killed = false;
6039
+ proc.stdout.on("data", (data) => {
6040
+ stdout += data.toString();
6041
+ });
6042
+ proc.stderr.on("data", (data) => {
6043
+ stderr += data.toString();
6044
+ });
6045
+ const timeoutMs = (options.timeout ?? DEFAULT_TIMEOUT_SECONDS2) * 1e3;
6046
+ const timer = setTimeout(() => {
6047
+ killed = true;
6048
+ proc.kill("SIGKILL");
6049
+ }, timeoutMs);
6050
+ proc.on("close", (code) => {
6051
+ clearTimeout(timer);
6052
+ if (killed) {
6053
+ resolve8({
6054
+ exitCode: 137,
6055
+ stdout,
6056
+ stderr: stderr + "\n[Process killed: timeout exceeded]"
6057
+ });
6058
+ } else {
6059
+ resolve8({ exitCode: code ?? 1, stdout, stderr });
6060
+ }
6061
+ });
6062
+ proc.on("error", (err) => {
6063
+ clearTimeout(timer);
6064
+ reject(err);
6065
+ });
6066
+ if (options.stdin) {
6067
+ proc.stdin.write(options.stdin);
6068
+ }
6069
+ proc.stdin.end();
6070
+ });
6071
+ }
6072
+ var LocalEnvironment = class {
6073
+ type = "local";
6074
+ config;
6075
+ cwd;
6076
+ maxOutputChars;
6077
+ constructor(config, maxOutputChars) {
6078
+ this.config = config ?? {};
6079
+ this.cwd = path__namespace.resolve(config?.cwd ?? process.cwd());
6080
+ this.maxOutputChars = maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS2;
6081
+ }
6082
+ async initialize() {
6083
+ try {
6084
+ const stat2 = await fs2__namespace.stat(this.cwd);
6085
+ if (!stat2.isDirectory()) {
6086
+ throw new Error(`Working directory is not a directory: ${this.cwd}`);
6087
+ }
6088
+ } catch (err) {
6089
+ if (err.code === "ENOENT") {
6090
+ throw new Error(`Working directory does not exist: ${this.cwd}`);
6091
+ }
6092
+ throw err;
6093
+ }
6094
+ }
6095
+ async exec(command, opts) {
6096
+ const cwd = opts?.cwd ? this.resolvePath(opts.cwd) : this.cwd;
6097
+ const timeout = opts?.timeout ?? DEFAULT_TIMEOUT_SECONDS2;
6098
+ const start = Date.now();
6099
+ const result = await spawnLocal(command, {
6100
+ cwd,
6101
+ env: opts?.env,
6102
+ timeout,
6103
+ stdin: opts?.stdin
6104
+ });
6105
+ const durationMs = Date.now() - start;
6106
+ const { text: stdout, truncated: stdoutTruncated } = truncateOutput(
6107
+ stripAnsi(result.stdout),
6108
+ this.maxOutputChars
6109
+ );
6110
+ const { text: stderr } = truncateOutput(stripAnsi(result.stderr), this.maxOutputChars);
6111
+ return {
6112
+ exitCode: result.exitCode,
6113
+ stdout,
6114
+ stderr,
6115
+ durationMs,
6116
+ truncated: stdoutTruncated
6117
+ };
6118
+ }
6119
+ async readFile(filePath) {
6120
+ const resolved = this.resolvePath(filePath);
6121
+ await this.assertNotSymlink(resolved);
6122
+ const buffer = await fs2__namespace.readFile(resolved);
6123
+ if (isBinary(buffer)) {
6124
+ throw new Error(`Cannot read binary file: ${filePath}`);
6125
+ }
6126
+ return buffer.toString("utf-8");
6127
+ }
6128
+ async writeFile(filePath, content) {
6129
+ const resolved = this.resolvePath(filePath);
6130
+ this.assertPathSafe(resolved);
6131
+ const parentDir = path__namespace.dirname(resolved);
6132
+ await fs2__namespace.mkdir(parentDir, { recursive: true });
6133
+ await fs2__namespace.writeFile(resolved, content, "utf-8");
6134
+ }
6135
+ async fileExists(filePath) {
6136
+ const resolved = this.resolvePath(filePath);
6137
+ try {
6138
+ await fs2__namespace.access(resolved);
6139
+ return true;
6140
+ } catch {
6141
+ return false;
6142
+ }
6143
+ }
6144
+ async glob(pattern, opts) {
6145
+ const cwd = opts?.cwd ? this.resolvePath(opts.cwd) : this.cwd;
6146
+ let command = `find ${cwd} -type f -name '${pattern}'`;
6147
+ if (opts?.ignore) {
6148
+ for (const ignore of opts.ignore) {
6149
+ command += ` ! -path '${ignore}'`;
6150
+ }
6151
+ }
6152
+ command += " 2>/dev/null | sort | head -1000";
6153
+ const result = await this.exec(command);
6154
+ if (!result.stdout.trim()) return [];
6155
+ return result.stdout.trim().split("\n").filter(Boolean);
6156
+ }
6157
+ async grep(pattern, opts) {
6158
+ const cwd = opts?.cwd ? this.resolvePath(opts.cwd) : this.cwd;
6159
+ const maxResults = opts?.maxResults ?? 100;
6160
+ let command = "grep -rn";
6161
+ if (opts?.contextLines !== void 0) {
6162
+ command += ` -C ${String(opts.contextLines)}`;
6163
+ }
6164
+ if (opts?.include) {
6165
+ for (const inc of opts.include) {
6166
+ command += ` --include='${inc}'`;
6167
+ }
6168
+ }
6169
+ const escapedPattern = pattern.replace(/'/g, "'\\''");
6170
+ command += ` -- '${escapedPattern}' ${cwd}`;
6171
+ command += ` 2>/dev/null | head -${String(maxResults)}`;
6172
+ const result = await this.exec(command);
6173
+ return parseGrepOutput(result.stdout);
6174
+ }
6175
+ async destroy() {
6176
+ }
6177
+ getCwd() {
6178
+ return this.cwd;
6179
+ }
6180
+ getInfo() {
6181
+ return {
6182
+ type: "local",
6183
+ cwd: this.cwd
6184
+ };
6185
+ }
6186
+ /**
6187
+ * Resolve a path relative to the working directory.
6188
+ */
6189
+ resolvePath(p) {
6190
+ return path__namespace.resolve(this.cwd, p);
6191
+ }
6192
+ /**
6193
+ * Assert that a resolved path stays within the path restriction.
6194
+ * No-op when path restriction is not configured.
6195
+ */
6196
+ assertPathSafe(resolvedPath) {
6197
+ if (!this.config.pathRestriction) return;
6198
+ const restriction = path__namespace.resolve(this.config.pathRestriction);
6199
+ if (resolvedPath !== restriction && !resolvedPath.startsWith(restriction + "/")) {
6200
+ throw new Error(`Path traversal detected: "${resolvedPath}" is outside of "${restriction}"`);
6201
+ }
6202
+ }
6203
+ /**
6204
+ * Assert that a path is not a symbolic link.
6205
+ * Only enforced when path restriction is configured.
6206
+ */
6207
+ async assertNotSymlink(resolvedPath) {
6208
+ if (!this.config.pathRestriction) return;
6209
+ try {
6210
+ const stat2 = await fs2__namespace.lstat(resolvedPath);
6211
+ if (stat2.isSymbolicLink()) {
6212
+ throw new Error(
6213
+ `Symbolic link detected: "${resolvedPath}". Symlinks are blocked when pathRestriction is set.`
6214
+ );
6215
+ }
6216
+ } catch (err) {
6217
+ if (err.code === "ENOENT") return;
6218
+ throw err;
6219
+ }
6220
+ }
6221
+ };
6222
+ function matchGlob(text, pattern) {
6223
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
6224
+ const regexStr = `^${escaped.replace(/\*/g, ".*")}$`;
6225
+ return new RegExp(regexStr).test(text);
6226
+ }
6227
+ function evaluateAllowlist(command, patterns) {
6228
+ const trimmed = command.trim();
6229
+ return patterns.some((pattern) => matchGlob(trimmed, pattern));
6230
+ }
6231
+ function isWithinRestriction(resolvedPath, restriction) {
6232
+ const base = path.resolve(restriction);
6233
+ return resolvedPath === base || resolvedPath.startsWith(base + "/");
6234
+ }
6235
+ function assertSafePath(filePath, restriction) {
6236
+ const base = path.resolve(restriction);
6237
+ const resolved = path.resolve(base, filePath);
6238
+ if (!isWithinRestriction(resolved, base)) {
6239
+ throw new Error(`Path traversal detected: "${filePath}" resolves outside of "${restriction}"`);
6240
+ }
6241
+ }
6242
+
6243
+ // src/execution/tools/exec.ts
6244
+ async function requestApproval(ctx, command, env) {
6245
+ const envInfo = env.getInfo();
6246
+ const approvalId = await ctx.step.uuid("_approval_id");
6247
+ const response = await ctx.step.suspend(
6248
+ `approve_exec_${approvalId}`,
6249
+ {
6250
+ data: {
6251
+ _form: {
6252
+ title: "Approve command execution",
6253
+ description: `The agent wants to run a shell command in the ${envInfo.type} environment.`,
6254
+ fields: [
6255
+ {
6256
+ key: "approved",
6257
+ type: "boolean",
6258
+ label: "Approve this command?",
6259
+ required: true,
6260
+ default: false
6261
+ },
6262
+ {
6263
+ key: "allow_always",
6264
+ type: "boolean",
6265
+ label: "Always allow this command in the future?",
6266
+ required: false,
6267
+ default: false
6268
+ },
6269
+ {
6270
+ key: "feedback",
6271
+ type: "textarea",
6272
+ label: "Feedback for the agent (optional)",
6273
+ description: "If rejecting, tell the agent what to do instead.",
6274
+ required: false
6275
+ }
6276
+ ],
6277
+ context: {
6278
+ command,
6279
+ cwd: env.getCwd(),
6280
+ environment: envInfo.type
6281
+ }
6282
+ },
6283
+ _source: "exec_security",
6284
+ _tool: "exec"
6285
+ }
6286
+ }
6287
+ );
6288
+ const feedback = response.data?.feedback;
6289
+ return {
6290
+ approved: response.data?.approved === true,
6291
+ ...feedback ? { feedback } : {}
6292
+ };
6293
+ }
6294
+ function rejectedResult(command, feedback) {
6295
+ let stderr = `Command rejected by user: ${command}`;
6296
+ if (feedback) {
6297
+ stderr += `
6298
+ User feedback: ${feedback}`;
6299
+ }
6300
+ return {
6301
+ exitCode: -1,
6302
+ stdout: "",
6303
+ stderr,
6304
+ durationMs: 0,
6305
+ truncated: false
6306
+ };
6307
+ }
6308
+ function createExecTool(getEnv, config) {
6309
+ return defineTool(
6310
+ {
6311
+ id: "exec",
6312
+ 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.",
6313
+ inputSchema: zod.z.object({
6314
+ command: zod.z.string().describe("The shell command to execute"),
6315
+ cwd: zod.z.string().optional().describe("Working directory for the command"),
6316
+ env: zod.z.record(zod.z.string()).optional().describe("Environment variables to set"),
6317
+ timeout: zod.z.number().optional().describe("Timeout in seconds (default: 300)")
6318
+ })
6319
+ },
6320
+ async (ctx, input) => {
6321
+ const env = await getEnv();
6322
+ if (config?.security === "approval-always") {
6323
+ const result = await requestApproval(ctx, input.command, env);
6324
+ if (!result.approved) return rejectedResult(input.command, result.feedback);
6325
+ } else if (config?.security === "allowlist") {
6326
+ if (!evaluateAllowlist(input.command, config.allowlist ?? [])) {
6327
+ const result = await requestApproval(ctx, input.command, env);
6328
+ if (!result.approved) return rejectedResult(input.command, result.feedback);
6329
+ }
6330
+ }
6331
+ return env.exec(input.command, {
6332
+ cwd: input.cwd,
6333
+ env: input.env,
6334
+ timeout: input.timeout ?? config?.timeout
6335
+ });
6336
+ }
6337
+ );
6338
+ }
6339
+ function isPathAllowed(resolvedPath, restriction) {
6340
+ return isWithinRestriction(resolvedPath, path.resolve(restriction));
6341
+ }
6342
+ async function requirePathApproval(ctx, toolName, targetPath, restriction) {
6343
+ const approvalId = await ctx.step.uuid("_approval_id");
6344
+ const response = await ctx.step.suspend(
6345
+ `approve_${toolName}_${approvalId}`,
6346
+ {
6347
+ data: {
6348
+ _form: {
6349
+ title: `${toolName}: access outside workspace`,
6350
+ description: `The agent wants to ${toolName} a path outside the workspace.`,
6351
+ fields: [
6352
+ {
6353
+ key: "approved",
6354
+ type: "boolean",
6355
+ label: "Allow this operation?",
6356
+ required: true,
6357
+ default: false
6358
+ },
6359
+ {
6360
+ key: "feedback",
6361
+ type: "textarea",
6362
+ label: "Feedback for the agent (optional)",
6363
+ description: "If rejecting, tell the agent what to do instead.",
6364
+ required: false
6365
+ }
6366
+ ],
6367
+ context: {
6368
+ tool: toolName,
6369
+ path: targetPath,
6370
+ restriction
6371
+ }
6372
+ },
6373
+ _source: "path_approval",
6374
+ _tool: toolName
6375
+ }
6376
+ }
6377
+ );
6378
+ if (response.data?.approved !== true) {
6379
+ const feedback = response.data?.feedback;
6380
+ throw new Error(
6381
+ `Access to "${targetPath}" was rejected by the user.${feedback ? ` Feedback: ${feedback}` : ""}`
6382
+ );
6383
+ }
6384
+ }
6385
+
6386
+ // src/execution/tools/read.ts
6387
+ function createReadTool(getEnv, pathConfig) {
6388
+ return defineTool(
6389
+ {
6390
+ id: "read",
6391
+ 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).",
6392
+ inputSchema: zod.z.object({
6393
+ path: zod.z.string().describe("Path to the file to read"),
6394
+ offset: zod.z.number().optional().describe("Line offset to start reading from (0-based)"),
6395
+ limit: zod.z.number().optional().describe("Maximum number of lines to return")
6396
+ })
6397
+ },
6398
+ async (ctx, input) => {
6399
+ const env = await getEnv();
6400
+ if (pathConfig?.pathRestriction) {
6401
+ const resolved = path.resolve(env.getCwd(), input.path);
6402
+ if (!isPathAllowed(resolved, pathConfig.pathRestriction)) {
6403
+ await requirePathApproval(ctx, "read", resolved, pathConfig.pathRestriction);
6404
+ }
6405
+ }
6406
+ let content = await env.readFile(input.path);
6407
+ if (input.offset !== void 0 || input.limit !== void 0) {
6408
+ const lines = content.split("\n");
6409
+ const start = input.offset ?? 0;
6410
+ const end = input.limit !== void 0 ? start + input.limit : lines.length;
6411
+ content = lines.slice(start, end).join("\n");
6412
+ }
6413
+ return { content, path: input.path };
6414
+ }
6415
+ );
6416
+ }
6417
+ function createWriteTool(getEnv, approval) {
6418
+ return defineTool(
6419
+ {
6420
+ id: "write",
6421
+ 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.",
6422
+ inputSchema: zod.z.object({
6423
+ path: zod.z.string().describe("Path to the file to write"),
6424
+ content: zod.z.string().describe("Content to write to the file")
6425
+ }),
6426
+ approval
6427
+ },
6428
+ async (_ctx, input) => {
6429
+ const env = await getEnv();
6430
+ await env.writeFile(input.path, input.content);
6431
+ return { success: true, path: input.path };
6432
+ }
6433
+ );
6434
+ }
6435
+ function createEditTool(getEnv, approval) {
6436
+ return defineTool(
6437
+ {
6438
+ id: "edit",
6439
+ 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.",
6440
+ inputSchema: zod.z.object({
6441
+ path: zod.z.string().describe("Path to the file to edit"),
6442
+ old_text: zod.z.string().describe("Exact text to find and replace"),
6443
+ new_text: zod.z.string().describe("Text to replace the old_text with")
6444
+ }),
6445
+ approval
6446
+ },
6447
+ async (_ctx, input) => {
6448
+ const env = await getEnv();
6449
+ const content = await env.readFile(input.path);
6450
+ if (!content.includes(input.old_text)) {
6451
+ throw new Error(
6452
+ `old_text not found in ${input.path}. Make sure the text matches exactly, including whitespace and indentation.`
6453
+ );
6454
+ }
6455
+ const newContent = content.replace(input.old_text, input.new_text);
6456
+ await env.writeFile(input.path, newContent);
6457
+ return { success: true, path: input.path };
6458
+ }
6459
+ );
6460
+ }
6461
+ function createGlobTool(getEnv, pathConfig) {
6462
+ return defineTool(
6463
+ {
6464
+ id: "glob",
6465
+ description: "Find files matching a glob pattern. Returns a list of file paths. Use this to discover files in the project structure.",
6466
+ inputSchema: zod.z.object({
6467
+ pattern: zod.z.string().describe('Glob pattern to match (e.g., "*.ts", "src/**/*.js")'),
6468
+ cwd: zod.z.string().optional().describe("Directory to search in"),
6469
+ ignore: zod.z.array(zod.z.string()).optional().describe("Patterns to exclude from results")
6470
+ })
6471
+ },
6472
+ async (ctx, input) => {
6473
+ const env = await getEnv();
6474
+ if (pathConfig?.pathRestriction && input.cwd) {
6475
+ const resolved = path.resolve(env.getCwd(), input.cwd);
6476
+ if (!isPathAllowed(resolved, pathConfig.pathRestriction)) {
6477
+ await requirePathApproval(ctx, "glob", resolved, pathConfig.pathRestriction);
6478
+ }
6479
+ }
6480
+ const files = await env.glob(input.pattern, {
6481
+ cwd: input.cwd,
6482
+ ignore: input.ignore
6483
+ });
6484
+ return { files };
6485
+ }
6486
+ );
6487
+ }
6488
+ function createGrepTool(getEnv, pathConfig) {
6489
+ return defineTool(
6490
+ {
6491
+ id: "grep",
6492
+ 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.",
6493
+ inputSchema: zod.z.object({
6494
+ pattern: zod.z.string().describe("Search pattern (regex supported)"),
6495
+ cwd: zod.z.string().optional().describe("Directory to search in"),
6496
+ include: zod.z.array(zod.z.string()).optional().describe('File patterns to include (e.g., ["*.ts", "*.js"])'),
6497
+ maxResults: zod.z.number().optional().describe("Maximum number of matches to return (default: 100)"),
6498
+ contextLines: zod.z.number().optional().describe("Number of context lines around each match")
6499
+ })
6500
+ },
6501
+ async (ctx, input) => {
6502
+ const env = await getEnv();
6503
+ if (pathConfig?.pathRestriction && input.cwd) {
6504
+ const resolved = path.resolve(env.getCwd(), input.cwd);
6505
+ if (!isPathAllowed(resolved, pathConfig.pathRestriction)) {
6506
+ await requirePathApproval(ctx, "grep", resolved, pathConfig.pathRestriction);
6507
+ }
6508
+ }
6509
+ const matches = await env.grep(input.pattern, {
6510
+ cwd: input.cwd,
6511
+ include: input.include,
6512
+ maxResults: input.maxResults,
6513
+ contextLines: input.contextLines
6514
+ });
6515
+ return { matches };
6516
+ }
6517
+ );
6518
+ }
6519
+
6520
+ // src/execution/sandbox-tools.ts
6521
+ function createEnvironment(config) {
6522
+ const envType = config?.env ?? "docker";
6523
+ switch (envType) {
6524
+ case "docker": {
6525
+ const dockerConfig = config?.docker ?? {
6526
+ image: "node:20-slim",
6527
+ workspaceDir: process.cwd()
6528
+ };
6529
+ return new DockerEnvironment(dockerConfig, config?.exec?.maxOutputChars);
6530
+ }
6531
+ case "e2b":
6532
+ throw new Error("E2B environment is not yet implemented.");
6533
+ case "local":
6534
+ return new LocalEnvironment(config?.local, config?.exec?.maxOutputChars);
6535
+ default:
6536
+ throw new Error(`Unknown environment type: ${String(envType)}`);
6537
+ }
6538
+ }
6539
+ function sandboxTools(config) {
6540
+ let env = null;
6541
+ let envPromise = null;
6542
+ async function getEnv() {
6543
+ if (env) return env;
6544
+ if (envPromise) return envPromise;
6545
+ envPromise = (async () => {
6546
+ const created = createEnvironment(config);
6547
+ await created.initialize();
6548
+ env = created;
6549
+ return env;
6550
+ })();
6551
+ return envPromise;
6552
+ }
6553
+ const envType = config?.env ?? "docker";
6554
+ if (envType === "e2b") {
6555
+ throw new Error("E2B environment is not yet implemented.");
6556
+ }
6557
+ const effectiveExecConfig = envType === "local" && !config?.exec?.security ? { ...config?.exec, security: "approval-always" } : config?.exec;
6558
+ const fileApproval = config?.fileApproval ?? (envType === "local" ? "always" : void 0);
6559
+ const pathConfig = config?.local?.pathRestriction ? { pathRestriction: config.local.pathRestriction } : void 0;
6560
+ const include = new Set(
6561
+ config?.tools ?? ["exec", "read", "write", "edit", "glob", "grep"]
6562
+ );
6563
+ const tools = [];
6564
+ if (include.has("exec")) tools.push(createExecTool(getEnv, effectiveExecConfig));
6565
+ if (include.has("read")) tools.push(createReadTool(getEnv, pathConfig));
6566
+ if (include.has("write")) tools.push(createWriteTool(getEnv, fileApproval));
6567
+ if (include.has("edit")) tools.push(createEditTool(getEnv, fileApproval));
6568
+ if (include.has("glob")) tools.push(createGlobTool(getEnv, pathConfig));
6569
+ if (include.has("grep")) tools.push(createGrepTool(getEnv, pathConfig));
6570
+ const result = tools;
6571
+ result.cleanup = async () => {
6572
+ if (env) {
6573
+ await env.destroy();
6574
+ env = null;
6575
+ envPromise = null;
6576
+ }
6577
+ };
6578
+ return result;
6579
+ }
5542
6580
 
5543
6581
  exports.AgentRunConfig = AgentRunConfig;
6582
+ exports.DockerEnvironment = DockerEnvironment;
5544
6583
  exports.DuplicateWorkflowError = DuplicateWorkflowError;
5545
6584
  exports.ExecutionHandle = ExecutionHandle;
5546
6585
  exports.GuardrailError = GuardrailError;
@@ -5562,6 +6601,7 @@ exports.Worker = Worker;
5562
6601
  exports.WorkerServer = WorkerServer;
5563
6602
  exports.WorkflowNotFoundError = WorkflowNotFoundError;
5564
6603
  exports.agentStreamFunction = agentStreamFunction;
6604
+ exports.assertSafePath = assertSafePath;
5565
6605
  exports.batchAgentInvoke = batchAgentInvoke;
5566
6606
  exports.batchInvoke = batchInvoke;
5567
6607
  exports.composeGuardrails = composeGuardrails;
@@ -5574,15 +6614,24 @@ exports.convertToolResultsToMessages = convertToolResultsToMessages;
5574
6614
  exports.convertToolsToVercel = convertToolsToVercel;
5575
6615
  exports.convertVercelToolCallToPython = convertVercelToolCallToPython;
5576
6616
  exports.convertVercelUsageToPython = convertVercelUsageToPython;
6617
+ exports.createAskUserTool = createAskUserTool;
6618
+ exports.createEditTool = createEditTool;
6619
+ exports.createExecTool = createExecTool;
6620
+ exports.createGlobTool = createGlobTool;
6621
+ exports.createGrepTool = createGrepTool;
5577
6622
  exports.createLogger = createLogger;
6623
+ exports.createReadTool = createReadTool;
5578
6624
  exports.createStepHelper = createStepHelper;
5579
6625
  exports.createStepStore = createStepStore;
6626
+ exports.createWebSearchTool = createWebSearchTool;
5580
6627
  exports.createWorkflowRegistry = createWorkflowRegistry;
6628
+ exports.createWriteTool = createWriteTool;
5581
6629
  exports.defineAgent = defineAgent;
5582
6630
  exports.defineGuardrail = defineGuardrail;
5583
6631
  exports.defineHook = defineHook;
5584
6632
  exports.defineTool = defineTool;
5585
6633
  exports.defineWorkflow = defineWorkflow;
6634
+ exports.evaluateAllowlist = evaluateAllowlist;
5586
6635
  exports.executeGuardrailChain = executeGuardrailChain;
5587
6636
  exports.executeGuardrailsOrThrow = executeGuardrailsOrThrow;
5588
6637
  exports.executeHookChain = executeHookChain;
@@ -5597,6 +6646,7 @@ exports.hasText = hasText;
5597
6646
  exports.initializeOtel = initializeOtel;
5598
6647
  exports.initializeState = initializeState;
5599
6648
  exports.isAgentWorkflow = isAgentWorkflow;
6649
+ exports.isBinary = isBinary;
5600
6650
  exports.isGuardrail = isGuardrail;
5601
6651
  exports.isHook = isHook;
5602
6652
  exports.isOtelAvailable = isOtelAvailable;
@@ -5610,10 +6660,14 @@ exports.normalizeGuardrail = normalizeGuardrail;
5610
6660
  exports.normalizeGuardrails = normalizeGuardrails;
5611
6661
  exports.normalizeHook = normalizeHook;
5612
6662
  exports.normalizeHooks = normalizeHooks;
6663
+ exports.parseGrepOutput = parseGrepOutput;
5613
6664
  exports.retry = retry;
6665
+ exports.sandboxTools = sandboxTools;
5614
6666
  exports.serializeFinalState = serializeFinalState;
5615
6667
  exports.sleep = sleep;
5616
6668
  exports.stopCondition = stopCondition;
6669
+ exports.stripAnsi = stripAnsi;
6670
+ exports.truncateOutput = truncateOutput;
5617
6671
  exports.validateState = validateState;
5618
6672
  //# sourceMappingURL=index.cjs.map
5619
6673
  //# sourceMappingURL=index.cjs.map