@oyasmi/pipiclaw 0.5.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +39 -3
  2. package/dist/agent/channel-runner.d.ts +5 -0
  3. package/dist/agent/channel-runner.js +59 -15
  4. package/dist/agent/prompt-builder.js +6 -0
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +2 -1
  7. package/dist/memory/consolidation.js +11 -2
  8. package/dist/memory/session.js +2 -2
  9. package/dist/memory/sidecar-worker.d.ts +1 -0
  10. package/dist/memory/sidecar-worker.js +56 -1
  11. package/dist/paths.d.ts +2 -0
  12. package/dist/paths.js +2 -0
  13. package/dist/runtime/bootstrap.d.ts +2 -1
  14. package/dist/runtime/bootstrap.js +74 -23
  15. package/dist/runtime/delivery.js +56 -5
  16. package/dist/runtime/dingtalk.d.ts +2 -0
  17. package/dist/runtime/dingtalk.js +14 -7
  18. package/dist/runtime/events.d.ts +3 -0
  19. package/dist/runtime/events.js +30 -5
  20. package/dist/security/command-guard.js +4 -0
  21. package/dist/security/config.d.ts +6 -0
  22. package/dist/security/config.js +57 -6
  23. package/dist/security/network.d.ts +28 -0
  24. package/dist/security/network.js +246 -0
  25. package/dist/security/path-guard.js +4 -0
  26. package/dist/security/platform.d.ts +1 -0
  27. package/dist/security/platform.js +3 -0
  28. package/dist/security/types.d.ts +16 -1
  29. package/dist/settings.d.ts +4 -1
  30. package/dist/settings.js +31 -6
  31. package/dist/shared/config-diagnostics.d.ts +7 -0
  32. package/dist/shared/config-diagnostics.js +3 -0
  33. package/dist/subagents/discovery.d.ts +1 -1
  34. package/dist/subagents/discovery.js +1 -1
  35. package/dist/subagents/tool.d.ts +2 -0
  36. package/dist/subagents/tool.js +24 -2
  37. package/dist/tools/config.d.ts +37 -0
  38. package/dist/tools/config.js +170 -0
  39. package/dist/tools/index.d.ts +3 -0
  40. package/dist/tools/index.js +23 -1
  41. package/dist/tools/web-fetch.d.ts +17 -0
  42. package/dist/tools/web-fetch.js +29 -0
  43. package/dist/tools/web-search.d.ts +16 -0
  44. package/dist/tools/web-search.js +29 -0
  45. package/dist/web/client.d.ts +41 -0
  46. package/dist/web/client.js +193 -0
  47. package/dist/web/config.d.ts +19 -0
  48. package/dist/web/config.js +35 -0
  49. package/dist/web/extract.d.ts +7 -0
  50. package/dist/web/extract.js +122 -0
  51. package/dist/web/fetch.d.ts +23 -0
  52. package/dist/web/fetch.js +150 -0
  53. package/dist/web/format.d.ts +21 -0
  54. package/dist/web/format.js +38 -0
  55. package/dist/web/search-providers.d.ts +15 -0
  56. package/dist/web/search-providers.js +199 -0
  57. package/dist/web/search.d.ts +19 -0
  58. package/dist/web/search.js +52 -0
  59. package/package.json +9 -2
@@ -0,0 +1,246 @@
1
+ import { lookup } from "node:dns/promises";
2
+ import { isIP } from "node:net";
3
+ export class NetworkGuardError extends Error {
4
+ constructor(options) {
5
+ super(options.message);
6
+ this.name = "NetworkGuardError";
7
+ this.url = options.url;
8
+ this.stage = options.stage;
9
+ this.category = options.category;
10
+ this.resolvedHost = options.resolvedHost;
11
+ this.resolvedAddress = options.resolvedAddress;
12
+ }
13
+ }
14
+ const BLOCKED_HOSTS = new Set(["localhost", "metadata.google.internal", "metadata", "169.254.169.254"]);
15
+ const PRIVATE_IPV4_CIDRS = [
16
+ "0.0.0.0/8",
17
+ "10.0.0.0/8",
18
+ "100.64.0.0/10",
19
+ "127.0.0.0/8",
20
+ "169.254.0.0/16",
21
+ "172.16.0.0/12",
22
+ "192.168.0.0/16",
23
+ "198.18.0.0/15",
24
+ ];
25
+ const PRIVATE_IPV6_CIDRS = ["::1/128", "::/128", "fc00::/7", "fe80::/10"];
26
+ function normalizeHost(host) {
27
+ return host.trim().replace(/\.$/, "").toLowerCase();
28
+ }
29
+ function parseIpv4(ip) {
30
+ const parts = ip.split(".");
31
+ if (parts.length !== 4) {
32
+ return null;
33
+ }
34
+ let value = 0;
35
+ for (const part of parts) {
36
+ if (!/^\d+$/.test(part)) {
37
+ return null;
38
+ }
39
+ const octet = Number.parseInt(part, 10);
40
+ if (octet < 0 || octet > 255) {
41
+ return null;
42
+ }
43
+ value = (value << 8) | octet;
44
+ }
45
+ return value >>> 0;
46
+ }
47
+ function expandIpv6(ip) {
48
+ const normalized = ip.toLowerCase();
49
+ const hasEmbeddedIpv4 = normalized.includes(".");
50
+ let working = normalized;
51
+ if (hasEmbeddedIpv4) {
52
+ const lastColon = working.lastIndexOf(":");
53
+ if (lastColon === -1) {
54
+ return null;
55
+ }
56
+ const ipv4 = parseIpv4(working.slice(lastColon + 1));
57
+ if (ipv4 === null) {
58
+ return null;
59
+ }
60
+ const high = ((ipv4 >>> 16) & 0xffff).toString(16);
61
+ const low = (ipv4 & 0xffff).toString(16);
62
+ working = `${working.slice(0, lastColon)}:${high}:${low}`;
63
+ }
64
+ const pieces = working.split("::");
65
+ if (pieces.length > 2) {
66
+ return null;
67
+ }
68
+ const left = pieces[0] ? pieces[0].split(":").filter(Boolean) : [];
69
+ const right = pieces[1] ? pieces[1].split(":").filter(Boolean) : [];
70
+ if (left.length + right.length > 8) {
71
+ return null;
72
+ }
73
+ const fill = new Array(8 - left.length - right.length).fill("0");
74
+ const groups = pieces.length === 2 ? [...left, ...fill, ...right] : left;
75
+ return groups.length === 8 ? groups : null;
76
+ }
77
+ function parseIpv6(ip) {
78
+ const groups = expandIpv6(ip);
79
+ if (!groups) {
80
+ return null;
81
+ }
82
+ let value = 0n;
83
+ for (const group of groups) {
84
+ if (!/^[0-9a-f]{1,4}$/i.test(group)) {
85
+ return null;
86
+ }
87
+ value = (value << 16n) | BigInt(Number.parseInt(group, 16));
88
+ }
89
+ return value;
90
+ }
91
+ function ipInCidr(ip, cidr) {
92
+ const [network, prefixText] = cidr.split("/");
93
+ const prefix = Number.parseInt(prefixText ?? "", 10);
94
+ if (!Number.isFinite(prefix)) {
95
+ return false;
96
+ }
97
+ const version = isIP(ip);
98
+ if (version === 4) {
99
+ const ipValue = parseIpv4(ip);
100
+ const networkValue = parseIpv4(network);
101
+ if (ipValue === null || networkValue === null || prefix < 0 || prefix > 32) {
102
+ return false;
103
+ }
104
+ const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
105
+ return (ipValue & mask) === (networkValue & mask);
106
+ }
107
+ if (version === 6) {
108
+ const ipValue = parseIpv6(ip);
109
+ const networkValue = parseIpv6(network);
110
+ if (ipValue === null || networkValue === null || prefix < 0 || prefix > 128) {
111
+ return false;
112
+ }
113
+ const shift = 128 - prefix;
114
+ if (shift === 128) {
115
+ return true;
116
+ }
117
+ return ipValue >> BigInt(shift) === networkValue >> BigInt(shift);
118
+ }
119
+ return false;
120
+ }
121
+ function matchesAllowedHost(hostname, allowedHosts) {
122
+ const normalized = normalizeHost(hostname);
123
+ return allowedHosts.some((candidate) => normalizeHost(candidate) === normalized);
124
+ }
125
+ function matchesAllowedCidr(address, allowedCidrs) {
126
+ return allowedCidrs.some((cidr) => ipInCidr(address, cidr.trim()));
127
+ }
128
+ function isBlockedHost(hostname) {
129
+ const normalized = normalizeHost(hostname);
130
+ return normalized.endsWith(".localhost") || BLOCKED_HOSTS.has(normalized);
131
+ }
132
+ function isPrivateAddress(address) {
133
+ const version = isIP(address);
134
+ if (version === 4) {
135
+ return PRIVATE_IPV4_CIDRS.some((cidr) => ipInCidr(address, cidr));
136
+ }
137
+ if (version === 6) {
138
+ return PRIVATE_IPV6_CIDRS.some((cidr) => ipInCidr(address, cidr));
139
+ }
140
+ return false;
141
+ }
142
+ async function validateUrlTarget(rawUrl, context, stage) {
143
+ const url = (() => {
144
+ try {
145
+ return new URL(rawUrl);
146
+ }
147
+ catch {
148
+ throw new NetworkGuardError({
149
+ url: rawUrl,
150
+ stage,
151
+ category: "invalid-url",
152
+ message: `Invalid URL: ${rawUrl}`,
153
+ });
154
+ }
155
+ })();
156
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
157
+ throw new NetworkGuardError({
158
+ url: rawUrl,
159
+ stage,
160
+ category: "unsupported-scheme",
161
+ message: `Only http/https URLs are allowed, got ${url.protocol || "unknown"}`,
162
+ });
163
+ }
164
+ const hostname = normalizeHost(url.hostname);
165
+ if (!hostname) {
166
+ throw new NetworkGuardError({
167
+ url: rawUrl,
168
+ stage,
169
+ category: "missing-host",
170
+ message: `URL is missing a hostname: ${rawUrl}`,
171
+ });
172
+ }
173
+ const { networkGuard } = context.config;
174
+ if (!networkGuard.enabled) {
175
+ return { url: url.toString(), hostname };
176
+ }
177
+ if (matchesAllowedHost(hostname, networkGuard.allowedHosts)) {
178
+ return { url: url.toString(), hostname };
179
+ }
180
+ if (isBlockedHost(hostname)) {
181
+ throw new NetworkGuardError({
182
+ url: rawUrl,
183
+ stage,
184
+ category: "blocked-host",
185
+ message: `Blocked host: ${hostname}`,
186
+ resolvedHost: hostname,
187
+ });
188
+ }
189
+ if (isIP(hostname)) {
190
+ if (!matchesAllowedCidr(hostname, networkGuard.allowedCidrs) && isPrivateAddress(hostname)) {
191
+ throw new NetworkGuardError({
192
+ url: rawUrl,
193
+ stage,
194
+ category: "private-address",
195
+ message: `Blocked private network address: ${hostname}`,
196
+ resolvedHost: hostname,
197
+ resolvedAddress: hostname,
198
+ });
199
+ }
200
+ return { url: url.toString(), hostname, resolvedAddress: hostname };
201
+ }
202
+ let records;
203
+ try {
204
+ records = (await lookup(hostname, { all: true, verbatim: true }));
205
+ }
206
+ catch (error) {
207
+ throw new NetworkGuardError({
208
+ url: rawUrl,
209
+ stage,
210
+ category: "dns-failure",
211
+ message: `Failed to resolve host ${hostname}: ${error instanceof Error ? error.message : String(error)}`,
212
+ resolvedHost: hostname,
213
+ });
214
+ }
215
+ if (records.length === 0) {
216
+ throw new NetworkGuardError({
217
+ url: rawUrl,
218
+ stage,
219
+ category: "dns-failure",
220
+ message: `Failed to resolve host ${hostname}`,
221
+ resolvedHost: hostname,
222
+ });
223
+ }
224
+ for (const record of records) {
225
+ if (matchesAllowedCidr(record.address, networkGuard.allowedCidrs)) {
226
+ return { url: url.toString(), hostname, resolvedAddress: record.address };
227
+ }
228
+ if (isPrivateAddress(record.address)) {
229
+ throw new NetworkGuardError({
230
+ url: rawUrl,
231
+ stage,
232
+ category: "private-address",
233
+ message: `Blocked private network address resolved from ${hostname}: ${record.address}`,
234
+ resolvedHost: hostname,
235
+ resolvedAddress: record.address,
236
+ });
237
+ }
238
+ }
239
+ return { url: url.toString(), hostname, resolvedAddress: records[0]?.address };
240
+ }
241
+ export async function validateNetworkTarget(url, context) {
242
+ return validateUrlTarget(url, context, "request");
243
+ }
244
+ export async function validateRedirectTarget(url, context) {
245
+ return validateUrlTarget(url, context, "redirect");
246
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, lstatSync, realpathSync } from "node:fs";
2
2
  import { homedir, tmpdir } from "node:os";
3
3
  import { basename, dirname, isAbsolute, normalize, resolve } from "node:path";
4
+ import { isWindowsPlatform } from "./platform.js";
4
5
  const PRIVATE_KEY_EXTENSIONS = new Set([".pem", ".key", ".p12", ".pfx"]);
5
6
  const PRIVATE_KEY_NAME_HINTS = /(id_rsa|id_ed25519|private|secret|credentials)/i;
6
7
  const PROC_MEM_PATH = /^\/proc\/\d+\/mem(?:\/|$)/;
@@ -197,6 +198,9 @@ export function guardPath(rawPath, operation, ctx) {
197
198
  if (!ctx.config.enabled) {
198
199
  return { allowed: true, operation, rawPath };
199
200
  }
201
+ if (isWindowsPlatform()) {
202
+ return { allowed: true, operation, rawPath };
203
+ }
200
204
  const homeDir = ctx.homeDir ?? homedir();
201
205
  const effectiveCtx = {
202
206
  ...ctx,
@@ -0,0 +1 @@
1
+ export declare function isWindowsPlatform(): boolean;
@@ -0,0 +1,3 @@
1
+ export function isWindowsPlatform() {
2
+ return process.platform === "win32";
3
+ }
@@ -14,6 +14,12 @@ export interface SecurityConfig {
14
14
  writeDeny: string[];
15
15
  resolveSymlinks: boolean;
16
16
  };
17
+ networkGuard: {
18
+ enabled: boolean;
19
+ allowedCidrs: string[];
20
+ allowedHosts: string[];
21
+ maxRedirects: number;
22
+ };
17
23
  audit: {
18
24
  logBlocked: boolean;
19
25
  logFile?: string;
@@ -63,4 +69,13 @@ export interface BlockedCommandLogEvent extends SecurityLogEventBase {
63
69
  reason?: string;
64
70
  matchedText?: string;
65
71
  }
66
- export type SecurityLogEvent = BlockedPathLogEvent | BlockedCommandLogEvent;
72
+ export interface BlockedNetworkLogEvent extends SecurityLogEventBase {
73
+ type: "network";
74
+ url: string;
75
+ stage: "request" | "redirect";
76
+ resolvedHost?: string;
77
+ resolvedAddress?: string;
78
+ category?: string;
79
+ reason?: string;
80
+ }
81
+ export type SecurityLogEvent = BlockedPathLogEvent | BlockedCommandLogEvent | BlockedNetworkLogEvent;
@@ -7,6 +7,7 @@
7
7
  * This module currently provides only PipiclawSettingsManager.
8
8
  */
9
9
  import type { Transport } from "@mariozechner/pi-ai";
10
+ import type { ConfigDiagnostic } from "./shared/config-diagnostics.js";
10
11
  type PackageSource = string | {
11
12
  source: string;
12
13
  extensions?: string[];
@@ -83,10 +84,13 @@ export interface PipiclawSettings {
83
84
  export declare class PipiclawSettingsManager {
84
85
  private settingsPath;
85
86
  private settings;
87
+ private loadErrors;
86
88
  constructor(baseDir: string);
87
89
  private load;
88
90
  private save;
89
91
  reload(): void;
92
+ drainErrors(): SettingsError[];
93
+ getDiagnostics(): ConfigDiagnostic[];
90
94
  getCompactionSettings(): PipiclawCompactionSettings;
91
95
  getCompactionEnabled(): boolean;
92
96
  setCompactionEnabled(enabled: boolean): void;
@@ -176,6 +180,5 @@ export declare class PipiclawSettingsManager {
176
180
  getProjectSettings(): Settings;
177
181
  applyOverrides(overrides: Partial<Settings>): void;
178
182
  flush(): Promise<void>;
179
- drainErrors(): SettingsError[];
180
183
  }
181
184
  export {};
package/dist/settings.js CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
10
10
  import { dirname, join } from "path";
11
+ import * as log from "./log.js";
11
12
  const DEFAULT_COMPACTION = {
12
13
  enabled: true,
13
14
  reserveTokens: 16384,
@@ -40,18 +41,32 @@ const DEFAULT_SESSION_MEMORY = {
40
41
  */
41
42
  export class PipiclawSettingsManager {
42
43
  constructor(baseDir) {
44
+ this.loadErrors = [];
43
45
  this.settingsPath = join(baseDir, "settings.json");
44
46
  this.settings = this.load();
45
47
  }
46
48
  load() {
49
+ this.loadErrors = [];
47
50
  if (!existsSync(this.settingsPath)) {
48
51
  return {};
49
52
  }
50
53
  try {
51
54
  const content = readFileSync(this.settingsPath, "utf-8");
52
- return JSON.parse(content);
55
+ const parsed = JSON.parse(content);
56
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
57
+ this.loadErrors.push({
58
+ scope: "global",
59
+ error: new Error(`Expected a JSON object in ${this.settingsPath}`),
60
+ });
61
+ return {};
62
+ }
63
+ return parsed;
53
64
  }
54
- catch {
65
+ catch (error) {
66
+ this.loadErrors.push({
67
+ scope: "global",
68
+ error: error instanceof Error ? error : new Error(String(error)),
69
+ });
55
70
  return {};
56
71
  }
57
72
  }
@@ -64,12 +79,25 @@ export class PipiclawSettingsManager {
64
79
  writeFileSync(this.settingsPath, JSON.stringify(this.settings, null, 2), "utf-8");
65
80
  }
66
81
  catch (error) {
67
- console.error(`Warning: Could not save settings file: ${error}`);
82
+ log.logWarning(`Could not save settings file`, `${this.settingsPath}\n${String(error)}`);
68
83
  }
69
84
  }
70
85
  reload() {
71
86
  this.settings = this.load();
72
87
  }
88
+ drainErrors() {
89
+ const errors = this.loadErrors;
90
+ this.loadErrors = [];
91
+ return errors;
92
+ }
93
+ getDiagnostics() {
94
+ return this.loadErrors.map(({ error }) => ({
95
+ source: "settings",
96
+ path: this.settingsPath,
97
+ severity: "error",
98
+ message: error.message,
99
+ }));
100
+ }
73
101
  getCompactionSettings() {
74
102
  return {
75
103
  ...DEFAULT_COMPACTION,
@@ -377,7 +405,4 @@ export class PipiclawSettingsManager {
377
405
  flush() {
378
406
  return Promise.resolve();
379
407
  }
380
- drainErrors() {
381
- return [];
382
- }
383
408
  }
@@ -0,0 +1,7 @@
1
+ export interface ConfigDiagnostic {
2
+ source: "settings" | "tools" | "security";
3
+ path: string;
4
+ severity: "warning" | "error";
5
+ message: string;
6
+ }
7
+ export declare function formatConfigDiagnostic(diagnostic: ConfigDiagnostic): string;
@@ -0,0 +1,3 @@
1
+ export function formatConfigDiagnostic(diagnostic) {
2
+ return `${diagnostic.source}.json: ${diagnostic.message}`;
3
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Api, Model } from "@mariozechner/pi-ai";
2
- declare const ALLOWED_SUB_AGENT_TOOLS: readonly ["read", "bash", "edit", "write"];
2
+ declare const ALLOWED_SUB_AGENT_TOOLS: readonly ["read", "bash", "edit", "write", "web_search", "web_fetch"];
3
3
  declare const ALLOWED_CONTEXT_MODES: readonly ["isolated", "contextual"];
4
4
  declare const ALLOWED_MEMORY_MODES: readonly ["none", "session", "relevant"];
5
5
  export type SubAgentToolName = (typeof ALLOWED_SUB_AGENT_TOOLS)[number];
@@ -3,7 +3,7 @@ import { existsSync, readdirSync, readFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { findExactModelReferenceMatch, formatModelReference } from "../models/utils.js";
5
5
  import { SUB_AGENTS_DIR_NAME } from "../paths.js";
6
- const ALLOWED_SUB_AGENT_TOOLS = ["read", "bash", "edit", "write"];
6
+ const ALLOWED_SUB_AGENT_TOOLS = ["read", "bash", "edit", "write", "web_search", "web_fetch"];
7
7
  const DEFAULT_SUB_AGENT_TOOLS = ["read", "bash"];
8
8
  const DEFAULT_MAX_TURNS = 24;
9
9
  const DEFAULT_MAX_TOOL_CALLS = 48;
@@ -4,6 +4,7 @@ import type { Executor } from "../sandbox.js";
4
4
  import type { SecurityConfig } from "../security/types.js";
5
5
  import type { PipiclawMemoryRecallSettings } from "../settings.js";
6
6
  import type { UsageTotals } from "../shared/types.js";
7
+ import type { PipiclawWebToolsConfig } from "../tools/config.js";
7
8
  import { type ResolvedSubAgentConfig, type SubAgentDiscoveryResult } from "./discovery.js";
8
9
  declare const subagentSchema: import("@sinclair/typebox").TObject<{
9
10
  label: import("@sinclair/typebox").TString;
@@ -44,6 +45,7 @@ export interface SubAgentToolOptions {
44
45
  getSubAgentDiscovery?: () => SubAgentDiscoveryResult;
45
46
  getMemoryRecallSettings?: () => PipiclawMemoryRecallSettings;
46
47
  securityConfig?: SecurityConfig;
48
+ webConfig?: PipiclawWebToolsConfig;
47
49
  runtimeContext: {
48
50
  workspacePath: string;
49
51
  channelId: string;
@@ -11,6 +11,8 @@ import { clipText, extractAssistantText, extractLabelFromArgs, HAN_REGEX } from
11
11
  import { createBashTool } from "../tools/bash.js";
12
12
  import { createEditTool } from "../tools/edit.js";
13
13
  import { createReadTool } from "../tools/read.js";
14
+ import { createWebFetchTool } from "../tools/web-fetch.js";
15
+ import { createWebSearchTool } from "../tools/web-search.js";
14
16
  import { createWriteTool } from "../tools/write.js";
15
17
  import { formatSubAgentList, resolveSubAgentConfig, validateSubAgentTask, } from "./discovery.js";
16
18
  const subagentSchema = Type.Object({
@@ -118,6 +120,25 @@ function createToolSet(executor, bashTimeoutSec, options) {
118
120
  }),
119
121
  ];
120
122
  }
123
+ function createNamedToolSet(executor, bashTimeoutSec, options) {
124
+ const tools = createToolSet(executor, bashTimeoutSec, options);
125
+ const byName = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
126
+ if (options.webConfig && options.webConfig.enable !== false) {
127
+ byName.web_search = createWebSearchTool({
128
+ webConfig: options.webConfig,
129
+ securityConfig: options.securityConfig ?? DEFAULT_SECURITY_CONFIG,
130
+ workspaceDir: options.workspaceDir,
131
+ channelId: options.runtimeContext.channelId,
132
+ });
133
+ byName.web_fetch = createWebFetchTool({
134
+ webConfig: options.webConfig,
135
+ securityConfig: options.securityConfig ?? DEFAULT_SECURITY_CONFIG,
136
+ workspaceDir: options.workspaceDir,
137
+ channelId: options.runtimeContext.channelId,
138
+ });
139
+ }
140
+ return byName;
141
+ }
121
142
  function buildSubAgentTask(task, config, runtimeContext, contextBlocks) {
122
143
  const taskText = task.trim();
123
144
  const lines = [
@@ -288,17 +309,18 @@ export function createSubAgentTool(options) {
288
309
  details: createDetails(config, usage, assistantTurns, toolCalls, Date.now() - startedAt, Boolean(failureReason), failureReason),
289
310
  });
290
311
  };
312
+ const availableTools = Object.values(createNamedToolSet(options.executor, config.bashTimeoutSec, options));
291
313
  const worker = options.createWorker?.({
292
314
  subAgent: config,
293
315
  apiKey,
294
- tools: filterToolsByName(createToolSet(options.executor, config.bashTimeoutSec, options), config.tools),
316
+ tools: filterToolsByName(availableTools, config.tools),
295
317
  }) ??
296
318
  new Agent({
297
319
  initialState: {
298
320
  systemPrompt: config.systemPrompt,
299
321
  model: config.model,
300
322
  thinkingLevel: "off",
301
- tools: filterToolsByName(createToolSet(options.executor, config.bashTimeoutSec, options), config.tools),
323
+ tools: filterToolsByName(availableTools, config.tools),
302
324
  },
303
325
  convertToLlm,
304
326
  getApiKey: async () => apiKey,
@@ -0,0 +1,37 @@
1
+ import type { ConfigDiagnostic } from "../shared/config-diagnostics.js";
2
+ export type WebSearchProvider = "brave" | "tavily" | "jina" | "searxng" | "duckduckgo";
3
+ export interface PipiclawWebSearchConfig {
4
+ provider: WebSearchProvider;
5
+ apiKey: string;
6
+ baseUrl: string;
7
+ maxResults: number;
8
+ timeoutMs: number;
9
+ }
10
+ export interface PipiclawWebFetchConfig {
11
+ maxChars: number;
12
+ timeoutMs: number;
13
+ maxImageBytes: number;
14
+ maxResponseBytes: number;
15
+ preferJina: boolean;
16
+ enableJinaFallback: boolean;
17
+ defaultExtractMode: "markdown" | "text";
18
+ }
19
+ export interface PipiclawWebToolsConfig {
20
+ enable: boolean;
21
+ proxy: string | null;
22
+ search: PipiclawWebSearchConfig;
23
+ fetch: PipiclawWebFetchConfig;
24
+ }
25
+ export interface PipiclawToolsConfig {
26
+ tools: {
27
+ web: PipiclawWebToolsConfig;
28
+ };
29
+ }
30
+ export interface LoadedToolsConfig {
31
+ config: PipiclawToolsConfig;
32
+ diagnostics: ConfigDiagnostic[];
33
+ }
34
+ export declare const DEFAULT_TOOLS_CONFIG: PipiclawToolsConfig;
35
+ export declare function getToolsConfigPath(appHomeDir?: string): string;
36
+ export declare function loadToolsConfigWithDiagnostics(appHomeDir?: string): LoadedToolsConfig;
37
+ export declare function loadToolsConfig(appHomeDir?: string): PipiclawToolsConfig;