@poncho-ai/harness 0.18.0 → 0.19.1

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.18.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.19.1 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
3
3
  > tsup src/index.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 196.78 KB
11
- ESM ⚡️ Build success in 133ms
10
+ ESM dist/index.js 199.42 KB
11
+ ESM ⚡️ Build success in 125ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 6813ms
14
- DTS dist/index.d.ts 24.01 KB
13
+ DTS ⚡️ Build success in 7505ms
14
+ DTS dist/index.d.ts 24.33 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.19.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`470563b`](https://github.com/cesr/poncho-ai/commit/470563b96bbb5d2c6358a1c89dd3b52beb7799c8) Thanks [@cesr](https://github.com/cesr)! - Fix LocalUploadStore ENOENT on Vercel: use /tmp for uploads on serverless environments instead of the read-only working directory.
8
+
9
+ ## 0.19.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`075b9ac`](https://github.com/cesr/poncho-ai/commit/075b9ac3556847af913bf2b58f030575c3b99852) Thanks [@cesr](https://github.com/cesr)! - Batch tool approvals, fix serverless session persistence and adapter init
14
+ - Batch tool approvals: all approval-requiring tool calls in a single step are now collected and presented together instead of one at a time.
15
+ - Fix messaging adapter route registration: routes are only registered after successful initialization, preventing "Adapter not initialised" errors on Vercel.
16
+ - Add stateless signed-cookie sessions so web UI auth survives serverless cold starts.
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies [[`075b9ac`](https://github.com/cesr/poncho-ai/commit/075b9ac3556847af913bf2b58f030575c3b99852)]:
21
+ - @poncho-ai/sdk@1.4.0
22
+
3
23
  ## 0.18.0
4
24
 
5
25
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -94,6 +94,7 @@ interface Conversation {
94
94
  name: string;
95
95
  input: Record<string, unknown>;
96
96
  }>;
97
+ decision?: "approved" | "denied";
97
98
  }>;
98
99
  ownerId: string;
99
100
  tenantId: string | null;
@@ -268,6 +269,8 @@ type BuiltInToolToggles = {
268
269
  list_directory?: boolean;
269
270
  read_file?: boolean;
270
271
  write_file?: boolean;
272
+ delete_file?: boolean;
273
+ delete_directory?: boolean;
271
274
  };
272
275
  interface MessagingChannelConfig {
273
276
  platform: "slack" | "resend";
@@ -276,6 +279,7 @@ interface MessagingChannelConfig {
276
279
  apiKeyEnv?: string;
277
280
  webhookSecretEnv?: string;
278
281
  fromEnv?: string;
282
+ replyToEnv?: string;
279
283
  allowedSenders?: string[];
280
284
  mode?: "auto-reply" | "tool";
281
285
  allowedRecipients?: string[];
@@ -359,6 +363,8 @@ declare const loadPonchoConfig: (workingDir: string) => Promise<PonchoConfig | u
359
363
 
360
364
  declare const createDefaultTools: (workingDir: string) => ToolDefinition[];
361
365
  declare const createWriteTool: (workingDir: string) => ToolDefinition;
366
+ declare const createDeleteTool: (workingDir: string) => ToolDefinition;
367
+ declare const createDeleteDirectoryTool: (workingDir: string) => ToolDefinition;
362
368
 
363
369
  declare const PONCHO_UPLOAD_SCHEME = "poncho-upload://";
364
370
  interface UploadStore {
@@ -681,4 +687,4 @@ declare class TelemetryEmitter {
681
687
 
682
688
  declare const createSubagentTools: (manager: SubagentManager, getConversationId: () => string | undefined, getOwnerId: () => string) => ToolDefinition[];
683
689
 
684
- export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, type ConversationSummary, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type SubagentManager, type SubagentResult, type SubagentSummary, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
690
+ export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, type ConversationSummary, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type SubagentManager, type SubagentResult, type SubagentSummary, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
package/dist/index.js CHANGED
@@ -378,7 +378,7 @@ var loadPonchoConfig = async (workingDir) => {
378
378
  };
379
379
 
380
380
  // src/default-tools.ts
381
- import { mkdir, readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
381
+ import { mkdir, readdir, readFile as readFile3, rm, unlink, writeFile as writeFile2 } from "fs/promises";
382
382
  import { dirname, resolve as resolve4, sep } from "path";
383
383
  import { defineTool } from "@poncho-ai/sdk";
384
384
  var resolveSafePath = (workingDir, inputPath) => {
@@ -463,6 +463,52 @@ var createWriteTool = (workingDir) => defineTool({
463
463
  return { path, written: true };
464
464
  }
465
465
  });
466
+ var createDeleteTool = (workingDir) => defineTool({
467
+ name: "delete_file",
468
+ description: "Delete a file at a path inside the working directory",
469
+ inputSchema: {
470
+ type: "object",
471
+ properties: {
472
+ path: {
473
+ type: "string",
474
+ description: "File path relative to working directory"
475
+ }
476
+ },
477
+ required: ["path"],
478
+ additionalProperties: false
479
+ },
480
+ handler: async (input) => {
481
+ const path = typeof input.path === "string" ? input.path : "";
482
+ const resolved = resolveSafePath(workingDir, path);
483
+ await unlink(resolved);
484
+ return { path, deleted: true };
485
+ }
486
+ });
487
+ var createDeleteDirectoryTool = (workingDir) => defineTool({
488
+ name: "delete_directory",
489
+ description: "Recursively delete a directory and all its contents inside the working directory",
490
+ inputSchema: {
491
+ type: "object",
492
+ properties: {
493
+ path: {
494
+ type: "string",
495
+ description: "Directory path relative to working directory"
496
+ }
497
+ },
498
+ required: ["path"],
499
+ additionalProperties: false
500
+ },
501
+ handler: async (input) => {
502
+ const path = typeof input.path === "string" ? input.path : "";
503
+ if (!path) throw new Error("Path must not be empty.");
504
+ const resolved = resolveSafePath(workingDir, path);
505
+ if (resolved === resolve4(workingDir)) {
506
+ throw new Error("Cannot delete the working directory root.");
507
+ }
508
+ await rm(resolved, { recursive: true });
509
+ return { path, deleted: true };
510
+ }
511
+ });
466
512
 
467
513
  // src/harness.ts
468
514
  import { randomUUID as randomUUID3 } from "crypto";
@@ -470,8 +516,9 @@ import { getTextContent as getTextContent2 } from "@poncho-ai/sdk";
470
516
 
471
517
  // src/upload-store.ts
472
518
  import { createHash as createHash2 } from "crypto";
473
- import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3, rm } from "fs/promises";
519
+ import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3, rm as rm2 } from "fs/promises";
474
520
  import { createRequire } from "module";
521
+ import { homedir as homedir2 } from "os";
475
522
  import { resolve as resolve5 } from "path";
476
523
  var tryImport = async (mod, workingDir) => {
477
524
  try {
@@ -559,10 +606,15 @@ var MIME_EXT_MAP = {
559
606
  "audio/wav": ".wav"
560
607
  };
561
608
  var mimeToExt = (mediaType) => MIME_EXT_MAP[mediaType] ?? `.${mediaType.split("/").pop() ?? "bin"}`;
609
+ var isServerlessEnv = () => {
610
+ const cwd = process.cwd();
611
+ const home = homedir2();
612
+ return process.env.VERCEL === "1" || process.env.VERCEL_ENV !== void 0 || process.env.VERCEL_URL !== void 0 || process.env.AWS_LAMBDA_FUNCTION_NAME !== void 0 || process.env.AWS_EXECUTION_ENV?.includes("AWS_Lambda") === true || process.env.LAMBDA_TASK_ROOT !== void 0 || process.env.NOW_REGION !== void 0 || cwd.startsWith("/var/task") || home.startsWith("/var/task") || process.env.SERVERLESS === "1";
613
+ };
562
614
  var LocalUploadStore = class {
563
615
  uploadsDir;
564
616
  constructor(workingDir) {
565
- this.uploadsDir = resolve5(workingDir, ".poncho", "uploads");
617
+ this.uploadsDir = isServerlessEnv() ? "/tmp/.poncho/uploads" : resolve5(workingDir, ".poncho", "uploads");
566
618
  }
567
619
  async put(_key, data, mediaType) {
568
620
  const key = deriveUploadKey(data, mediaType);
@@ -577,7 +629,7 @@ var LocalUploadStore = class {
577
629
  }
578
630
  async delete(urlOrKey) {
579
631
  const key = urlOrKey.startsWith(PONCHO_UPLOAD_SCHEME) ? urlOrKey.slice(PONCHO_UPLOAD_SCHEME.length) : urlOrKey;
580
- await rm(resolve5(this.uploadsDir, key), { force: true });
632
+ await rm2(resolve5(this.uploadsDir, key), { force: true });
581
633
  }
582
634
  };
583
635
  var VercelBlobUploadStore = class {
@@ -2908,6 +2960,7 @@ The agent will respond in Slack threads when @mentioned. Each Slack thread maps
2908
2960
  RESEND_API_KEY=re_...
2909
2961
  RESEND_WEBHOOK_SECRET=whsec_...
2910
2962
  RESEND_FROM=Agent <agent@yourdomain.com>
2963
+ RESEND_REPLY_TO=support@yourdomain.com # optional
2911
2964
  \`\`\`
2912
2965
  5. Add to \`poncho.config.js\`:
2913
2966
  \`\`\`javascript
@@ -3080,7 +3133,7 @@ var AgentHarness = class {
3080
3133
  isToolEnabled(name) {
3081
3134
  const access3 = this.resolveToolAccess(name);
3082
3135
  if (access3 === false) return false;
3083
- if (name === "write_file") {
3136
+ if (name === "write_file" || name === "delete_file" || name === "delete_directory") {
3084
3137
  return this.shouldEnableWriteTool();
3085
3138
  }
3086
3139
  return true;
@@ -3123,6 +3176,12 @@ var AgentHarness = class {
3123
3176
  if (this.isToolEnabled("write_file")) {
3124
3177
  this.registerIfMissing(createWriteTool(this.workingDir));
3125
3178
  }
3179
+ if (this.isToolEnabled("delete_file")) {
3180
+ this.registerIfMissing(createDeleteTool(this.workingDir));
3181
+ }
3182
+ if (this.isToolEnabled("delete_directory")) {
3183
+ this.registerIfMissing(createDeleteDirectoryTool(this.workingDir));
3184
+ }
3126
3185
  }
3127
3186
  shouldEnableWriteTool() {
3128
3187
  const override = process.env.PONCHO_FS_WRITE?.toLowerCase();
@@ -4242,6 +4301,7 @@ ${textContent}` };
4242
4301
  const toolResultsForModel = [];
4243
4302
  const richToolResults = [];
4244
4303
  const approvedCalls = [];
4304
+ const approvalNeeded = [];
4245
4305
  for (const call of toolCalls) {
4246
4306
  if (isCancelled()) {
4247
4307
  yield emitCancellation();
@@ -4249,52 +4309,60 @@ ${textContent}` };
4249
4309
  }
4250
4310
  const runtimeToolName = exposedToolNames.get(call.name) ?? call.name;
4251
4311
  yield pushEvent({ type: "tool:started", tool: runtimeToolName, input: call.input });
4252
- const requiresApproval = this.requiresApprovalForToolCall(
4253
- runtimeToolName,
4254
- call.input
4255
- );
4256
- if (requiresApproval) {
4257
- const approvalId = `approval_${randomUUID3()}`;
4258
- yield pushEvent({
4259
- type: "tool:approval:required",
4260
- tool: runtimeToolName,
4261
- input: call.input,
4262
- approvalId
4312
+ if (this.requiresApprovalForToolCall(runtimeToolName, call.input)) {
4313
+ approvalNeeded.push({
4314
+ approvalId: `approval_${randomUUID3()}`,
4315
+ id: call.id,
4316
+ name: runtimeToolName,
4317
+ input: call.input
4263
4318
  });
4264
- const assistantContent2 = JSON.stringify({
4265
- text: fullText,
4266
- tool_calls: toolCalls.map((tc) => ({
4267
- id: tc.id,
4268
- name: exposedToolNames.get(tc.name) ?? tc.name,
4269
- input: tc.input
4270
- }))
4319
+ } else {
4320
+ approvedCalls.push({
4321
+ id: call.id,
4322
+ name: runtimeToolName,
4323
+ input: call.input
4271
4324
  });
4272
- const assistantMsg = {
4273
- role: "assistant",
4274
- content: assistantContent2,
4275
- metadata: { timestamp: now(), id: randomUUID3(), step }
4276
- };
4277
- const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
4325
+ }
4326
+ }
4327
+ if (approvalNeeded.length > 0) {
4328
+ for (const an of approvalNeeded) {
4278
4329
  yield pushEvent({
4279
- type: "tool:approval:checkpoint",
4280
- approvalId,
4281
- tool: runtimeToolName,
4282
- toolCallId: call.id,
4283
- input: call.input,
4284
- checkpointMessages: deltaMessages,
4285
- pendingToolCalls: toolCalls.map((tc) => ({
4286
- id: tc.id,
4287
- name: exposedToolNames.get(tc.name) ?? tc.name,
4288
- input: tc.input
4289
- }))
4330
+ type: "tool:approval:required",
4331
+ tool: an.name,
4332
+ input: an.input,
4333
+ approvalId: an.approvalId
4290
4334
  });
4291
- return;
4292
4335
  }
4293
- approvedCalls.push({
4294
- id: call.id,
4295
- name: runtimeToolName,
4296
- input: call.input
4336
+ const assistantContent2 = JSON.stringify({
4337
+ text: fullText,
4338
+ tool_calls: toolCalls.map((tc) => ({
4339
+ id: tc.id,
4340
+ name: exposedToolNames.get(tc.name) ?? tc.name,
4341
+ input: tc.input
4342
+ }))
4343
+ });
4344
+ const assistantMsg = {
4345
+ role: "assistant",
4346
+ content: assistantContent2,
4347
+ metadata: { timestamp: now(), id: randomUUID3(), step }
4348
+ };
4349
+ const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
4350
+ yield pushEvent({
4351
+ type: "tool:approval:checkpoint",
4352
+ approvals: approvalNeeded.map((an) => ({
4353
+ approvalId: an.approvalId,
4354
+ tool: an.name,
4355
+ toolCallId: an.id,
4356
+ input: an.input
4357
+ })),
4358
+ checkpointMessages: deltaMessages,
4359
+ pendingToolCalls: toolCalls.map((tc) => ({
4360
+ id: tc.id,
4361
+ name: exposedToolNames.get(tc.name) ?? tc.name,
4362
+ input: tc.input
4363
+ }))
4297
4364
  });
4365
+ return;
4298
4366
  }
4299
4367
  const batchStart = now();
4300
4368
  if (isCancelled()) {
@@ -4588,7 +4656,7 @@ var LatitudeCapture = class {
4588
4656
 
4589
4657
  // src/state.ts
4590
4658
  import { randomUUID as randomUUID4 } from "crypto";
4591
- import { mkdir as mkdir4, readFile as readFile7, readdir as readdir4, rename as rename2, rm as rm2, writeFile as writeFile5 } from "fs/promises";
4659
+ import { mkdir as mkdir4, readFile as readFile7, readdir as readdir4, rename as rename2, rm as rm3, writeFile as writeFile5 } from "fs/promises";
4592
4660
  import { dirname as dirname4, resolve as resolve9 } from "path";
4593
4661
  var DEFAULT_OWNER = "local-owner";
4594
4662
  var LOCAL_STATE_FILE = "state.json";
@@ -4963,7 +5031,7 @@ var FileConversationStore = class {
4963
5031
  if (removed) {
4964
5032
  this.writing = this.writing.then(async () => {
4965
5033
  if (existing) {
4966
- await rm2(resolve9(conversationsDir, existing.fileName), { force: true });
5034
+ await rm3(resolve9(conversationsDir, existing.fileName), { force: true });
4967
5035
  }
4968
5036
  await this.writeIndex();
4969
5037
  });
@@ -5711,6 +5779,8 @@ export {
5711
5779
  buildSkillContextWindow,
5712
5780
  createConversationStore,
5713
5781
  createDefaultTools,
5782
+ createDeleteDirectoryTool,
5783
+ createDeleteTool,
5714
5784
  createMemoryStore,
5715
5785
  createMemoryTools,
5716
5786
  createModelProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  "redis": "^5.10.0",
32
32
  "yaml": "^2.4.0",
33
33
  "zod": "^3.22.0",
34
- "@poncho-ai/sdk": "1.3.0"
34
+ "@poncho-ai/sdk": "1.4.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/mustache": "^4.2.6",
package/src/config.ts CHANGED
@@ -39,6 +39,8 @@ export type BuiltInToolToggles = {
39
39
  list_directory?: boolean;
40
40
  read_file?: boolean;
41
41
  write_file?: boolean;
42
+ delete_file?: boolean;
43
+ delete_directory?: boolean;
42
44
  };
43
45
 
44
46
  export interface MessagingChannelConfig {
@@ -50,6 +52,7 @@ export interface MessagingChannelConfig {
50
52
  apiKeyEnv?: string;
51
53
  webhookSecretEnv?: string;
52
54
  fromEnv?: string;
55
+ replyToEnv?: string;
53
56
  allowedSenders?: string[];
54
57
  mode?: "auto-reply" | "tool";
55
58
  allowedRecipients?: string[];
@@ -1,4 +1,4 @@
1
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, readdir, readFile, rm, unlink, writeFile } from "node:fs/promises";
2
2
  import { dirname, resolve, sep } from "node:path";
3
3
  import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
4
4
 
@@ -87,3 +87,53 @@ export const createWriteTool = (workingDir: string): ToolDefinition =>
87
87
  return { path, written: true };
88
88
  },
89
89
  });
90
+
91
+ export const createDeleteTool = (workingDir: string): ToolDefinition =>
92
+ defineTool({
93
+ name: "delete_file",
94
+ description: "Delete a file at a path inside the working directory",
95
+ inputSchema: {
96
+ type: "object",
97
+ properties: {
98
+ path: {
99
+ type: "string",
100
+ description: "File path relative to working directory",
101
+ },
102
+ },
103
+ required: ["path"],
104
+ additionalProperties: false,
105
+ },
106
+ handler: async (input) => {
107
+ const path = typeof input.path === "string" ? input.path : "";
108
+ const resolved = resolveSafePath(workingDir, path);
109
+ await unlink(resolved);
110
+ return { path, deleted: true };
111
+ },
112
+ });
113
+
114
+ export const createDeleteDirectoryTool = (workingDir: string): ToolDefinition =>
115
+ defineTool({
116
+ name: "delete_directory",
117
+ description: "Recursively delete a directory and all its contents inside the working directory",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: {
121
+ path: {
122
+ type: "string",
123
+ description: "Directory path relative to working directory",
124
+ },
125
+ },
126
+ required: ["path"],
127
+ additionalProperties: false,
128
+ },
129
+ handler: async (input) => {
130
+ const path = typeof input.path === "string" ? input.path : "";
131
+ if (!path) throw new Error("Path must not be empty.");
132
+ const resolved = resolveSafePath(workingDir, path);
133
+ if (resolved === resolve(workingDir)) {
134
+ throw new Error("Cannot delete the working directory root.");
135
+ }
136
+ await rm(resolved, { recursive: true });
137
+ return { path, deleted: true };
138
+ },
139
+ });
package/src/harness.ts CHANGED
@@ -15,7 +15,7 @@ import type { UploadStore } from "./upload-store.js";
15
15
  import { PONCHO_UPLOAD_SCHEME, deriveUploadKey } from "./upload-store.js";
16
16
  import { parseAgentFile, renderAgentPrompt, type ParsedAgent, type AgentFrontmatter } from "./agent-parser.js";
17
17
  import { loadPonchoConfig, resolveMemoryConfig, type PonchoConfig, type ToolAccess, type BuiltInToolToggles } from "./config.js";
18
- import { createDefaultTools, createWriteTool } from "./default-tools.js";
18
+ import { createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createWriteTool } from "./default-tools.js";
19
19
  import {
20
20
  createMemoryStore,
21
21
  createMemoryTools,
@@ -324,6 +324,7 @@ The agent will respond in Slack threads when @mentioned. Each Slack thread maps
324
324
  RESEND_API_KEY=re_...
325
325
  RESEND_WEBHOOK_SECRET=whsec_...
326
326
  RESEND_FROM=Agent <agent@yourdomain.com>
327
+ RESEND_REPLY_TO=support@yourdomain.com # optional
327
328
  \`\`\`
328
329
  5. Add to \`poncho.config.js\`:
329
330
  \`\`\`javascript
@@ -526,7 +527,7 @@ export class AgentHarness {
526
527
  private isToolEnabled(name: string): boolean {
527
528
  const access = this.resolveToolAccess(name);
528
529
  if (access === false) return false;
529
- if (name === "write_file") {
530
+ if (name === "write_file" || name === "delete_file" || name === "delete_directory") {
530
531
  return this.shouldEnableWriteTool();
531
532
  }
532
533
  return true;
@@ -574,6 +575,12 @@ export class AgentHarness {
574
575
  if (this.isToolEnabled("write_file")) {
575
576
  this.registerIfMissing(createWriteTool(this.workingDir));
576
577
  }
578
+ if (this.isToolEnabled("delete_file")) {
579
+ this.registerIfMissing(createDeleteTool(this.workingDir));
580
+ }
581
+ if (this.isToolEnabled("delete_directory")) {
582
+ this.registerIfMissing(createDeleteDirectoryTool(this.workingDir));
583
+ }
577
584
  }
578
585
 
579
586
  private shouldEnableWriteTool(): boolean {
@@ -1906,7 +1913,14 @@ ${boundedMainMemory.trim()}`
1906
1913
  name: string;
1907
1914
  input: Record<string, unknown>;
1908
1915
  }> = [];
1916
+ const approvalNeeded: Array<{
1917
+ approvalId: string;
1918
+ id: string;
1919
+ name: string;
1920
+ input: Record<string, unknown>;
1921
+ }> = [];
1909
1922
 
1923
+ // Phase 1: classify all tool calls
1910
1924
  for (const call of toolCalls) {
1911
1925
  if (isCancelled()) {
1912
1926
  yield emitCancellation();
@@ -1914,54 +1928,66 @@ ${boundedMainMemory.trim()}`
1914
1928
  }
1915
1929
  const runtimeToolName = exposedToolNames.get(call.name) ?? call.name;
1916
1930
  yield pushEvent({ type: "tool:started", tool: runtimeToolName, input: call.input });
1917
- const requiresApproval = this.requiresApprovalForToolCall(
1918
- runtimeToolName,
1919
- call.input,
1920
- );
1921
- if (requiresApproval) {
1922
- const approvalId = `approval_${randomUUID()}`;
1923
- yield pushEvent({
1924
- type: "tool:approval:required",
1925
- tool: runtimeToolName,
1931
+ if (this.requiresApprovalForToolCall(runtimeToolName, call.input)) {
1932
+ approvalNeeded.push({
1933
+ approvalId: `approval_${randomUUID()}`,
1934
+ id: call.id,
1935
+ name: runtimeToolName,
1926
1936
  input: call.input,
1927
- approvalId,
1928
1937
  });
1929
-
1930
- const assistantContent = JSON.stringify({
1931
- text: fullText,
1932
- tool_calls: toolCalls.map(tc => ({
1933
- id: tc.id,
1934
- name: exposedToolNames.get(tc.name) ?? tc.name,
1935
- input: tc.input,
1936
- })),
1938
+ } else {
1939
+ approvedCalls.push({
1940
+ id: call.id,
1941
+ name: runtimeToolName,
1942
+ input: call.input,
1937
1943
  });
1938
- const assistantMsg: Message = {
1939
- role: "assistant",
1940
- content: assistantContent,
1941
- metadata: { timestamp: now(), id: randomUUID(), step },
1942
- };
1943
- const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
1944
+ }
1945
+ }
1946
+
1947
+ // Phase 2a: if any tools need approval, emit events for ALL of them and checkpoint
1948
+ if (approvalNeeded.length > 0) {
1949
+ for (const an of approvalNeeded) {
1944
1950
  yield pushEvent({
1945
- type: "tool:approval:checkpoint",
1946
- approvalId,
1947
- tool: runtimeToolName,
1948
- toolCallId: call.id,
1949
- input: call.input,
1950
- checkpointMessages: deltaMessages,
1951
- pendingToolCalls: toolCalls.map(tc => ({
1952
- id: tc.id,
1953
- name: exposedToolNames.get(tc.name) ?? tc.name,
1954
- input: tc.input,
1955
- })),
1951
+ type: "tool:approval:required",
1952
+ tool: an.name,
1953
+ input: an.input,
1954
+ approvalId: an.approvalId,
1956
1955
  });
1957
- return;
1958
1956
  }
1959
- approvedCalls.push({
1960
- id: call.id,
1961
- name: runtimeToolName,
1962
- input: call.input,
1957
+
1958
+ const assistantContent = JSON.stringify({
1959
+ text: fullText,
1960
+ tool_calls: toolCalls.map(tc => ({
1961
+ id: tc.id,
1962
+ name: exposedToolNames.get(tc.name) ?? tc.name,
1963
+ input: tc.input,
1964
+ })),
1965
+ });
1966
+ const assistantMsg: Message = {
1967
+ role: "assistant",
1968
+ content: assistantContent,
1969
+ metadata: { timestamp: now(), id: randomUUID(), step },
1970
+ };
1971
+ const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
1972
+ yield pushEvent({
1973
+ type: "tool:approval:checkpoint",
1974
+ approvals: approvalNeeded.map(an => ({
1975
+ approvalId: an.approvalId,
1976
+ tool: an.name,
1977
+ toolCallId: an.id,
1978
+ input: an.input,
1979
+ })),
1980
+ checkpointMessages: deltaMessages,
1981
+ pendingToolCalls: toolCalls.map(tc => ({
1982
+ id: tc.id,
1983
+ name: exposedToolNames.get(tc.name) ?? tc.name,
1984
+ input: tc.input,
1985
+ })),
1963
1986
  });
1987
+ return;
1964
1988
  }
1989
+
1990
+ // Phase 2b: no approvals needed — execute all auto-approved calls
1965
1991
  const batchStart = now();
1966
1992
  if (isCancelled()) {
1967
1993
  yield emitCancellation();
package/src/state.ts CHANGED
@@ -35,6 +35,7 @@ export interface Conversation {
35
35
  checkpointMessages?: Message[];
36
36
  baseMessageCount?: number;
37
37
  pendingToolCalls?: Array<{ id: string; name: string; input: Record<string, unknown> }>;
38
+ decision?: "approved" | "denied";
38
39
  }>;
39
40
  ownerId: string;
40
41
  tenantId: string | null;
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
3
3
  import { createRequire } from "node:module";
4
+ import { homedir } from "node:os";
4
5
  import { resolve } from "node:path";
5
6
  import type { UploadsConfig } from "./config.js";
6
7
 
@@ -127,11 +128,30 @@ const mimeToExt = (mediaType: string): string =>
127
128
  // Local filesystem implementation
128
129
  // ---------------------------------------------------------------------------
129
130
 
131
+ const isServerlessEnv = (): boolean => {
132
+ const cwd = process.cwd();
133
+ const home = homedir();
134
+ return (
135
+ process.env.VERCEL === "1" ||
136
+ process.env.VERCEL_ENV !== undefined ||
137
+ process.env.VERCEL_URL !== undefined ||
138
+ process.env.AWS_LAMBDA_FUNCTION_NAME !== undefined ||
139
+ process.env.AWS_EXECUTION_ENV?.includes("AWS_Lambda") === true ||
140
+ process.env.LAMBDA_TASK_ROOT !== undefined ||
141
+ process.env.NOW_REGION !== undefined ||
142
+ cwd.startsWith("/var/task") ||
143
+ home.startsWith("/var/task") ||
144
+ process.env.SERVERLESS === "1"
145
+ );
146
+ };
147
+
130
148
  export class LocalUploadStore implements UploadStore {
131
149
  private readonly uploadsDir: string;
132
150
 
133
151
  constructor(workingDir: string) {
134
- this.uploadsDir = resolve(workingDir, ".poncho", "uploads");
152
+ this.uploadsDir = isServerlessEnv()
153
+ ? "/tmp/.poncho/uploads"
154
+ : resolve(workingDir, ".poncho", "uploads");
135
155
  }
136
156
 
137
157
  async put(_key: string, data: Buffer, mediaType: string): Promise<string> {