@tractorscorch/clank 1.4.0 → 1.4.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [1.4.1] — 2026-03-23
10
+
11
+ ### Security
12
+ - **Config get redaction** — `config get` action now redacts sensitive keys (apiKey, token, botToken) before returning to LLM context
13
+ - **Config set protection** — config tool now blocks prototype pollution (`__proto__`, `constructor`, `prototype`)
14
+ - **Rate limit streaming path** — `handleInboundMessageStreaming` now enforced (was bypassing rate limiter)
15
+ - **SSRF private IPs** — web_fetch now blocks RFC 1918 ranges (10.x, 192.168.x, 172.16-31.x) and IPv4-mapped IPv6
16
+ - **STT workspace containment** — speech_to_text tool now uses guardPath() to prevent reading files outside workspace
17
+
18
+ ### Audit Result
19
+ - 0 dependency vulnerabilities
20
+ - 14 PASS, 1 WARN (bash blocklist is defense-in-depth), 0 FAIL
21
+ - Grade: A
22
+
23
+ ---
24
+
9
25
  ## [1.4.0] — 2026-03-23
10
26
 
11
27
  ### Added
package/dist/index.js CHANGED
@@ -1276,6 +1276,10 @@ var init_registry = __esm({
1276
1276
  });
1277
1277
 
1278
1278
  // src/tools/path-guard.ts
1279
+ var path_guard_exports = {};
1280
+ __export(path_guard_exports, {
1281
+ guardPath: () => guardPath
1282
+ });
1279
1283
  import { resolve, isAbsolute, normalize, relative } from "path";
1280
1284
  function guardPath(inputPath, projectRoot, opts) {
1281
1285
  const resolved = isAbsolute(inputPath) ? normalize(inputPath) : normalize(resolve(projectRoot, inputPath));
@@ -2255,6 +2259,12 @@ var init_web_fetch = __esm({
2255
2259
  if (host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "0.0.0.0") {
2256
2260
  return { ok: false, error: "localhost URLs are blocked (SSRF protection)" };
2257
2261
  }
2262
+ if (/^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host)) {
2263
+ return { ok: false, error: "Private network IPs are blocked (SSRF protection)" };
2264
+ }
2265
+ if (host.startsWith("[::ffff:")) {
2266
+ return { ok: false, error: "IPv4-mapped IPv6 addresses are blocked" };
2267
+ }
2258
2268
  if (host === "169.254.169.254" || host === "metadata.google.internal") {
2259
2269
  return { ok: false, error: "Cloud metadata endpoints are blocked" };
2260
2270
  }
@@ -2391,9 +2401,21 @@ var init_config_tool = __esm({
2391
2401
  return `Key not found: ${key}`;
2392
2402
  }
2393
2403
  }
2394
- return typeof current === "object" ? JSON.stringify(current, null, 2) : String(current);
2404
+ if (typeof current === "object") {
2405
+ return JSON.stringify(redactConfig(current), null, 2);
2406
+ }
2407
+ const SENSITIVE = /* @__PURE__ */ new Set(["apikey", "api_key", "apiKey", "token", "bottoken", "botToken", "secret", "password", "pin"]);
2408
+ const lastKey = keys[keys.length - 1];
2409
+ if (SENSITIVE.has(lastKey) && typeof current === "string") {
2410
+ return "[REDACTED]";
2411
+ }
2412
+ return String(current);
2395
2413
  }
2396
2414
  if (action === "set") {
2415
+ const BLOCKED_KEYS = ["__proto__", "constructor", "prototype"];
2416
+ if (keys.some((k) => BLOCKED_KEYS.includes(k))) {
2417
+ return "Error: blocked \u2014 unsafe key";
2418
+ }
2397
2419
  let parsed = args.value;
2398
2420
  try {
2399
2421
  parsed = JSON.parse(args.value);
@@ -4247,14 +4269,17 @@ var init_voice_tool = __esm({
4247
4269
  },
4248
4270
  safetyLevel: "low",
4249
4271
  readOnly: true,
4250
- validate(args) {
4272
+ validate(args, ctx) {
4251
4273
  if (!args.file_path || typeof args.file_path !== "string") return { ok: false, error: "file_path is required" };
4252
4274
  return { ok: true };
4253
4275
  },
4254
- async execute(args) {
4276
+ async execute(args, ctx) {
4255
4277
  const { readFile: readFile13 } = await import("fs/promises");
4256
4278
  const { existsSync: existsSync12 } = await import("fs");
4257
- const filePath = args.file_path;
4279
+ const { guardPath: guardPath2 } = await Promise.resolve().then(() => (init_path_guard(), path_guard_exports));
4280
+ const guard = guardPath2(args.file_path, ctx.projectRoot, { allowExternal: ctx.allowExternal });
4281
+ if (!guard.ok) return guard.error;
4282
+ const filePath = guard.path;
4258
4283
  if (!existsSync12(filePath)) return `Error: File not found: ${filePath}`;
4259
4284
  const config = await loadConfig();
4260
4285
  const engine = new STTEngine(config);
@@ -5948,6 +5973,13 @@ var init_server = __esm({
5948
5973
  * Used by channel adapters for real-time streaming (e.g., Telegram message editing).
5949
5974
  */
5950
5975
  async handleInboundMessageStreaming(context, text, callbacks) {
5976
+ const rlKey = deriveSessionKey(context);
5977
+ if (this.isRateLimited(rlKey)) {
5978
+ throw new Error("Rate limited \u2014 too many messages. Wait a moment.");
5979
+ }
5980
+ return this._handleInboundMessageStreamingInner(context, text, callbacks);
5981
+ }
5982
+ async _handleInboundMessageStreamingInner(context, text, callbacks) {
5951
5983
  const agentId = resolveRoute(
5952
5984
  context,
5953
5985
  [],
@@ -6021,7 +6053,7 @@ var init_server = __esm({
6021
6053
  res.writeHead(200, { "Content-Type": "application/json" });
6022
6054
  res.end(JSON.stringify({
6023
6055
  status: "ok",
6024
- version: "1.4.0",
6056
+ version: "1.4.1",
6025
6057
  uptime: process.uptime(),
6026
6058
  clients: this.clients.size,
6027
6059
  agents: this.engines.size
@@ -6129,7 +6161,7 @@ var init_server = __esm({
6129
6161
  const hello = {
6130
6162
  type: "hello",
6131
6163
  protocol: PROTOCOL_VERSION,
6132
- version: "1.4.0",
6164
+ version: "1.4.1",
6133
6165
  agents: this.config.agents.list.map((a) => ({
6134
6166
  id: a.id,
6135
6167
  name: a.name || a.id,
@@ -7511,7 +7543,7 @@ async function runTui(opts) {
7511
7543
  ws.on("open", () => {
7512
7544
  ws.send(JSON.stringify({
7513
7545
  type: "connect",
7514
- params: { auth: { token }, mode: "tui", version: "1.4.0" }
7546
+ params: { auth: { token }, mode: "tui", version: "1.4.1" }
7515
7547
  }));
7516
7548
  });
7517
7549
  ws.on("message", (data) => {
@@ -7940,7 +7972,7 @@ import { fileURLToPath as fileURLToPath5 } from "url";
7940
7972
  import { dirname as dirname5, join as join19 } from "path";
7941
7973
  var __filename3 = fileURLToPath5(import.meta.url);
7942
7974
  var __dirname3 = dirname5(__filename3);
7943
- var version = "1.4.0";
7975
+ var version = "1.4.1";
7944
7976
  try {
7945
7977
  const pkg = JSON.parse(readFileSync(join19(__dirname3, "..", "package.json"), "utf-8"));
7946
7978
  version = pkg.version;