@theokit/sdk-tools 0.5.0 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6d65983: `createWebFetchTool` gains a redirect policy + injection seam.
8
+
9
+ - **`maxRedirects?: number`** — caps redirect hops (each SSRF-screened). Default 5 (unchanged). Set `0` to BLOCK ALL redirects (strict no-redirect policy for untrusted, model-chosen URLs).
10
+ - **Distinct `redirect_blocked` error** — a refused redirect now returns `{ ok:false, error:'redirect_blocked' }`, split from `ssrf_blocked` (a blocked private/reserved host). New exported `RedirectBlockedError`; `screenedFetch` throws it on redirect-limit exhaustion (was `SsrfBlockedError("too many redirects")` — a minor, more-precise error refinement).
11
+ - **Injectable `fetchImpl?` / `lookup?`** — drive the tool's redirect + SSRF paths deterministically in tests with no real network/DNS (the seam `screenedFetch` already had, now on the tool surface).
12
+
13
+ Additive + backward-compatible: absent options ⇒ today's behavior; `ssrf_blocked`/`invalid_url`/`timeout`/`too_large` codes + return shape unchanged. Lets a consumer (theocode) replace an app-side SSRF/redirect wrapper with `createWebFetchTool({ maxRedirects: 0 })`.
14
+
3
15
  ## 0.5.0
4
16
 
5
17
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -721,6 +721,15 @@ var SsrfBlockedError = class extends sdk.ConfigurationError {
721
721
  );
722
722
  }
723
723
  };
724
+ var RedirectBlockedError = class extends sdk.ConfigurationError {
725
+ name = "RedirectBlockedError";
726
+ constructor(url, detail) {
727
+ super(
728
+ `Blocked redirect for "${url}"${detail ? ` (${detail})` : ""}: redirect-hop limit exceeded (redirect policy).`,
729
+ { code: "redirect_blocked" }
730
+ );
731
+ }
732
+ };
724
733
  function v4ToInt(ip) {
725
734
  const parts = ip.split(".");
726
735
  return (Number(parts[0]) << 24 | Number(parts[1]) << 16 | Number(parts[2]) << 8 | Number(parts[3])) >>> 0;
@@ -844,7 +853,7 @@ async function screenedFetch(url, options = {}) {
844
853
  if (next === void 0) return res;
845
854
  current = next;
846
855
  }
847
- throw new SsrfBlockedError(url, "too many redirects");
856
+ throw new RedirectBlockedError(url, "too many redirects");
848
857
  }
849
858
  var DEFAULT_BUDGET = 8e3;
850
859
  var DEFAULT_MAX_DEPTH = 4;
@@ -1769,6 +1778,9 @@ var MAX_BODY_BYTES = 1 * 1024 * 1024;
1769
1778
  function createWebFetchTool(opts) {
1770
1779
  const defaultTimeoutMs = opts?.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS4;
1771
1780
  const allowPrivateHosts = opts?.allowPrivateHosts ?? false;
1781
+ const maxRedirects = opts?.maxRedirects;
1782
+ const fetchImpl = opts?.fetchImpl;
1783
+ const lookup = opts?.lookup;
1772
1784
  return sdk.defineTool({
1773
1785
  name: "web_fetch",
1774
1786
  description: "Fetch the contents of a URL via HTTP/HTTPS. Use only for URLs the user provided or that you are confident help with the task; never invent or guess URLs. Rejects non-http(s) URLs and is SSRF-guarded by default (private/loopback/link-local/cloud-metadata hosts are refused with an ssrf_blocked error). The response body is capped at 1 MB. Returns { ok, content, status_code, content_type } or { ok: false, error }.",
@@ -1798,7 +1810,10 @@ function createWebFetchTool(opts) {
1798
1810
  try {
1799
1811
  const response = await screenedFetch(url, {
1800
1812
  signal: controller.signal,
1801
- allowPrivateHosts
1813
+ allowPrivateHosts,
1814
+ ...maxRedirects !== void 0 ? { maxRedirects } : {},
1815
+ ...fetchImpl ? { fetchImpl } : {},
1816
+ ...lookup ? { lookup } : {}
1802
1817
  });
1803
1818
  clearTimeout(timer);
1804
1819
  const contentLength = response.headers.get("content-length");
@@ -1831,6 +1846,9 @@ function createWebFetchTool(opts) {
1831
1846
  });
1832
1847
  } catch (err) {
1833
1848
  clearTimeout(timer);
1849
+ if (err instanceof RedirectBlockedError) {
1850
+ return JSON.stringify({ ok: false, error: "redirect_blocked", url, reason: err.message });
1851
+ }
1834
1852
  if (err instanceof SsrfBlockedError) {
1835
1853
  return JSON.stringify({ ok: false, error: "ssrf_blocked", url, reason: err.message });
1836
1854
  }
@@ -1963,6 +1981,7 @@ async function isBinaryFile(absolutePath) {
1963
1981
 
1964
1982
  exports.CatastrophicCommandError = CatastrophicCommandError;
1965
1983
  exports.DEFAULT_TOOL_GUIDANCE = DEFAULT_TOOL_GUIDANCE;
1984
+ exports.RedirectBlockedError = RedirectBlockedError;
1966
1985
  exports.SsrfBlockedError = SsrfBlockedError;
1967
1986
  exports.buildEnvContext = buildEnvContext;
1968
1987
  exports.buildRepoMap = buildRepoMap;