From a5851741cfd502496437cfb8ffc831b133769246 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 28 Aug 2026 21:33:10 -0700 Subject: [PATCH] test(tools): type the path-safety harnesses instead of erasing with any These two harnesses are the template every service copied when the path hardening sweep began, and both open with `type AnyTool = ToolConfig` plus an `as any` at the `url(...)` call. CLAUDE.md forbids `any` outright, and because they are the template the violation propagated into ten in-flight PRs before review flagged it. Fixing the source stops the next copy inheriting it. The obvious repair does not compile. `ToolConfig` takes its param type in the contravariant position of `request.url`, so no concrete member of the barrel's union is assignable to a widened `ToolConfig, ...>`, and `filter`'s type-predicate overload intersects rather than replaces -- filtering the union directly leaves the mismatch standing. Seeding the enumeration as `Object.values(...)` makes the existing `isVercelTool` / `isDaytonaTool` predicate the single narrowing point, which is the `unknown`-plus-type-guard form the guidelines actually ask for, and removes the need for a cast at the call site. Note these files have no type coverage in CI from either direction: `apps/sim/tsconfig.json` excludes `**/*.test.ts`, and vitest does not typecheck. Verified with a temporary tsconfig lifting the exclusion, confirming via `--listFiles` that both files were genuinely in the program -- an empty program also reports zero errors. That config was not committed. Behaviour is unchanged: 1129 tests pass, identical to before. --- .../sim/tools/daytona/sandbox_path_safety.test.ts | 15 +++++++++++---- .../tools/vercel/edge_config_path_safety.test.ts | 15 +++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/apps/sim/tools/daytona/sandbox_path_safety.test.ts b/apps/sim/tools/daytona/sandbox_path_safety.test.ts index 0baf7d7d714..ca92eea3c61 100644 --- a/apps/sim/tools/daytona/sandbox_path_safety.test.ts +++ b/apps/sim/tools/daytona/sandbox_path_safety.test.ts @@ -16,7 +16,7 @@ import { describe, expect, it } from 'vitest' import * as daytonaTools from '@/tools/daytona/index' import { daytonaToolboxUrl, encodeSandboxId } from '@/tools/daytona/utils' -import type { ToolConfig } from '@/tools/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' const TOOLBOX_PREFIX = '/toolbox/' const API_PREFIX = '/api/sandbox/' @@ -48,7 +48,14 @@ const LEGITIMATE_IDS = [ const SAFE_ID = 'SAFEID' -type AnyTool = ToolConfig +/** + * The slice of a tool this harness reads. `ToolConfig`'s param type sits in the + * contravariant position of `request.url`, so no concrete member of the barrel's + * union is assignable to a widened `ToolConfig, …>`. The + * barrel is therefore seeded as `unknown` below and narrowed by {@link isDaytonaTool}, + * which is the single point where the type is established. + */ +type AnyTool = ToolConfig, ToolResponse> function isDaytonaTool(value: unknown): value is AnyTool { return ( @@ -82,14 +89,14 @@ function buildPath(tool: AnyTool, sandboxId: string): string { if (typeof url !== 'function') { throw new Error(`${tool.id} does not build its URL from params`) } - return new URL(url(buildParams(tool, sandboxId) as any)).pathname + return new URL(url(buildParams(tool, sandboxId))).pathname } function segmentsOf(pathname: string): string[] { return pathname.split('/') } -const SANDBOX_SCOPED_TOOLS = Object.values(daytonaTools) +const SANDBOX_SCOPED_TOOLS = Object.values(daytonaTools) .filter(isDaytonaTool) .filter((tool) => typeof tool.request?.url === 'function') .filter((tool) => { diff --git a/apps/sim/tools/vercel/edge_config_path_safety.test.ts b/apps/sim/tools/vercel/edge_config_path_safety.test.ts index 44580ee1de3..0b0f6115c37 100644 --- a/apps/sim/tools/vercel/edge_config_path_safety.test.ts +++ b/apps/sim/tools/vercel/edge_config_path_safety.test.ts @@ -21,7 +21,7 @@ * output, because string matching is exactly what let this through. */ import { describe, expect, it } from 'vitest' -import type { ToolConfig } from '@/tools/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' import { vercelDeleteEdgeConfigTool } from '@/tools/vercel/delete_edge_config' import { vercelGetEdgeConfigTool } from '@/tools/vercel/get_edge_config' import { vercelGetEdgeConfigItemsTool } from '@/tools/vercel/get_edge_config_items' @@ -64,7 +64,14 @@ const LEGITIMATE_IDS = [ const SAFE_ID = 'SAFEID' -type AnyTool = ToolConfig +/** + * The slice of a tool this harness reads. `ToolConfig`'s param type sits in the + * contravariant position of `request.url`, so no concrete member of the barrel's + * union is assignable to a widened `ToolConfig, …>`. The + * barrel is therefore seeded as `unknown` below and narrowed by {@link isVercelTool}, + * which is the single point where the type is established. + */ +type AnyTool = ToolConfig, ToolResponse> function isVercelTool(value: unknown): value is AnyTool { return ( @@ -102,14 +109,14 @@ function buildPath(tool: AnyTool, value: string): string { if (typeof url !== 'function') { throw new Error(`${tool.id} does not build its URL from params`) } - return new URL(url(buildParams(tool, value) as any)).pathname + return new URL(url(buildParams(tool, value))).pathname } function segmentsOf(pathname: string): string[] { return pathname.split('/') } -const DYNAMIC_PATH_TOOLS = Object.values(vercelTools) +const DYNAMIC_PATH_TOOLS = Object.values(vercelTools) .filter(isVercelTool) .filter((tool) => typeof tool.request?.url === 'function') .filter((tool) => {