From 20163313afc5f2b67197160c1269b9100ee20b73 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 6 May 2026 02:41:31 +0100 Subject: [PATCH] fix: resolve fs-safe post-land fallout --- config/knip.config.ts | 6 + .../qqbot/src/engine/api/media-chunked.ts | 47 +++-- .../slack/src/monitor/provider-support.ts | 4 +- package.json | 2 +- packages/memory-host-sdk/src/host/fs-utils.ts | 9 +- pnpm-lock.yaml | 10 +- ...bound-media-into-sandbox-workspace.test.ts | 2 +- .../reply/commands-export-session.test.ts | 6 + src/infra/archive.test.ts | 21 +- src/infra/tmp-openclaw-dir.ts | 185 ++++++++++++++++-- .../media-understanding-url-fallback.test.ts | 4 +- src/media/image-ops.tempdir.test.ts | 6 +- src/media/media-reference.test.ts | 11 +- test/scripts/lint-suppressions.test.ts | 3 +- 14 files changed, 255 insertions(+), 61 deletions(-) diff --git a/config/knip.config.ts b/config/knip.config.ts index 1e7a1cd8ec1..81ae2861eeb 100644 --- a/config/knip.config.ts +++ b/config/knip.config.ts @@ -74,6 +74,7 @@ const rootBundledPluginRuntimeDependencies = [ const config = { ignoreFiles: [ "scripts/**", + "packages/*/dist/**", "**/__tests__/**", "src/test-utils/**", "**/test-helpers/**", @@ -134,6 +135,7 @@ const config = { bundledPluginFile("msteams", "src/polls-store-memory.ts"), bundledPluginFile("voice-call", "src/providers/index.ts"), ], + ignore: ["packages/*/dist/**"], workspaces: { ".": { entry: rootEntries, @@ -155,6 +157,10 @@ const config = { entry: ["index.html!", "src/main.ts!", "vite.config.ts!", "vitest*.ts!"], project: ["src/**/*.{ts,tsx}!"], }, + "packages/sdk": { + entry: ["src/index.ts!"], + project: ["src/**/*.ts!"], + }, "packages/*": { entry: ["index.js!", "scripts/postinstall.js!"], project: ["index.js!", "scripts/**/*.js!"], diff --git a/extensions/qqbot/src/engine/api/media-chunked.ts b/extensions/qqbot/src/engine/api/media-chunked.ts index f645761aee9..9d030869273 100644 --- a/extensions/qqbot/src/engine/api/media-chunked.ts +++ b/extensions/qqbot/src/engine/api/media-chunked.ts @@ -36,6 +36,7 @@ import * as crypto from "node:crypto"; import type { FileHandle } from "node:fs/promises"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import type { MediaSource, OpenedLocalFile } from "../messaging/media-source.js"; import { openLocalFile } from "../messaging/media-source.js"; import { @@ -564,30 +565,38 @@ async function putToPresignedUrl( ) as ArrayBuffer; const startTime = Date.now(); - const response = await fetch(presignedUrl, { - method: "PUT", - body: new Blob([ab]), - headers: { "Content-Length": String(data.length) }, + const { response, release } = await fetchWithSsrFGuard({ + url: presignedUrl, + auditContext: "qqbot-media-part-upload", + init: { + method: "PUT", + body: new Blob([ab]), + headers: { "Content-Length": String(data.length) }, + }, signal: controller.signal, }); - const elapsed = Date.now() - startTime; - const requestId = response.headers.get("x-cos-request-id") ?? "-"; - const etag = response.headers.get("ETag") ?? "-"; + try { + const elapsed = Date.now() - startTime; + const requestId = response.headers.get("x-cos-request-id") ?? "-"; + const etag = response.headers.get("ETag") ?? "-"; - if (!response.ok) { - const body = await response.text().catch(() => ""); - logger?.error?.( - `${prefix} PUT part ${partIndex}/${totalParts}: HTTP ${response.status} ${response.statusText} (${elapsed}ms, requestId=${requestId}) body=${body.slice(0, 160)}`, - ); - throw new Error( - `COS PUT failed: ${response.status} ${response.statusText} - ${body.slice(0, 120)}`, + if (!response.ok) { + const body = await response.text().catch(() => ""); + logger?.error?.( + `${prefix} PUT part ${partIndex}/${totalParts}: HTTP ${response.status} ${response.statusText} (${elapsed}ms, requestId=${requestId}) body=${body.slice(0, 160)}`, + ); + throw new Error( + `COS PUT failed: ${response.status} ${response.statusText} - ${body.slice(0, 120)}`, + ); + } + + logger?.debug?.( + `${prefix} PUT part ${partIndex}/${totalParts} OK (${elapsed}ms ETag=${etag} requestId=${requestId})`, ); + return; + } finally { + await release(); } - - logger?.debug?.( - `${prefix} PUT part ${partIndex}/${totalParts} OK (${elapsed}ms ETag=${etag} requestId=${requestId})`, - ); - return; } catch (err) { lastError = err instanceof Error ? err : new Error(String(err)); if (lastError.name === "AbortError") { diff --git a/extensions/slack/src/monitor/provider-support.ts b/extensions/slack/src/monitor/provider-support.ts index 03b3a51d831..16fcea4863f 100644 --- a/extensions/slack/src/monitor/provider-support.ts +++ b/extensions/slack/src/monitor/provider-support.ts @@ -331,7 +331,9 @@ export async function startSlackSocketAndWaitForDisconnect(params: { } if (err === undefined || err === null || err === "") { const suffix = disconnect ? ` after ${disconnect.event}` : ""; - throw new Error(`Slack Socket Mode start failed${suffix} without error detail`); + throw new Error(`Slack Socket Mode start failed${suffix} without error detail`, { + cause: err, + }); } throw err; } diff --git a/package.json b/package.json index 2e8e8370562..5735729c70c 100644 --- a/package.json +++ b/package.json @@ -1695,7 +1695,7 @@ "@mariozechner/pi-tui": "0.73.0", "@modelcontextprotocol/sdk": "1.29.0", "@mozilla/readability": "^0.6.0", - "@openclaw/fs-safe": "^0.1.0", + "@openclaw/fs-safe": "^0.1.1", "@slack/bolt": "^4.7.2", "@slack/types": "^2.21.0", "@slack/web-api": "^7.15.2", diff --git a/packages/memory-host-sdk/src/host/fs-utils.ts b/packages/memory-host-sdk/src/host/fs-utils.ts index 2461dba8285..cbcf140dfb7 100644 --- a/packages/memory-host-sdk/src/host/fs-utils.ts +++ b/packages/memory-host-sdk/src/host/fs-utils.ts @@ -1,4 +1,4 @@ -import "../../../../src/infra/fs-safe-defaults.js"; +import { configureFsSafePython } from "@openclaw/fs-safe/config"; export { isPathInside } from "@openclaw/fs-safe/path"; export { readRegularFile, @@ -7,6 +7,13 @@ export { } from "@openclaw/fs-safe/advanced"; export { walkDirectory, type WalkDirectoryEntry } from "@openclaw/fs-safe/walk"; +const hasPythonModeOverride = + process.env.FS_SAFE_PYTHON_MODE != null || process.env.OPENCLAW_FS_SAFE_PYTHON_MODE != null; + +if (!hasPythonModeOverride) { + configureFsSafePython({ mode: "off" }); +} + export function isFileMissingError( err: unknown, ): err is NodeJS.ErrnoException & { code: "ENOENT" } { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9b8fc0f4cd..69fd57b8a2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,8 +104,8 @@ importers: specifier: ^0.6.0 version: 0.6.0 '@openclaw/fs-safe': - specifier: ^0.1.0 - version: 0.1.0 + specifier: ^0.1.1 + version: 0.1.1 '@slack/bolt': specifier: ^4.7.2 version: 4.7.2(@types/express@5.0.6) @@ -3233,8 +3233,8 @@ packages: cpu: [x64] os: [win32] - '@openclaw/fs-safe@0.1.0': - resolution: {integrity: sha512-ByFeXAA+7EXje15eaI3mqee9STFcglTkfdhenZQ2V4OGAkhkHzb2yxe3hInuWXYImG9gDolbccNEnwlsoTayaA==} + '@openclaw/fs-safe@0.1.1': + resolution: {integrity: sha512-+50LBpW7nKWzu3wJb4C5X9BQAcAxmj3oNRd/ZqZK+YO1ZdiikOPZ6fy5ta631xsV13+m+Ap5s0Gb3zPSl+0kOQ==} engines: {node: '>=20.11'} '@opentelemetry/api-logs@0.216.0': @@ -10005,7 +10005,7 @@ snapshots: '@openai/codex@0.128.0-win32-x64': optional: true - '@openclaw/fs-safe@0.1.0': + '@openclaw/fs-safe@0.1.1': optionalDependencies: jszip: 3.10.1 tar: 7.5.13 diff --git a/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts b/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts index c0c73934251..6982ec63f6f 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts @@ -120,7 +120,7 @@ beforeEach(() => { childProcessMocks.spawn.mockClear(); fsSafeMocks.rootCopyFrom.mockReset().mockImplementation(rootCopyFromForTest); fsSafeMocks.root.mockReset().mockImplementation(async (rootDir: string) => ({ - copyFrom: async (sourcePath: string, relativePath: string, options?: { maxBytes?: number }) => + copyIn: async (relativePath: string, sourcePath: string, options?: { maxBytes?: number }) => await rootCopyFromForTest({ sourcePath, rootDir, diff --git a/src/auto-reply/reply/commands-export-session.test.ts b/src/auto-reply/reply/commands-export-session.test.ts index 6e7a162e7b5..d7b68017ffa 100644 --- a/src/auto-reply/reply/commands-export-session.test.ts +++ b/src/auto-reply/reply/commands-export-session.test.ts @@ -19,6 +19,7 @@ const hoisted = await vi.hoisted(async () => { ), mkdirMock: vi.fn(async (_filePath: string, _options?: { recursive?: boolean }) => undefined), accessMock: vi.fn(async (_filePath: string) => undefined), + pathExistsMock: vi.fn(async (_filePath: string) => true), exportHtmlTemplateContents: new Map(), }; }); @@ -37,6 +38,10 @@ vi.mock("./commands-system-prompt.js", () => ({ resolveCommandsSystemPromptBundle: hoisted.resolveCommandsSystemPromptBundleMock, })); +vi.mock("../../infra/fs-safe.js", () => ({ + pathExists: hoisted.pathExistsMock, +})); + vi.mock("node:fs", async () => { const actual = await vi.importActual("node:fs"); const mockedFs = { @@ -146,6 +151,7 @@ describe("buildExportSessionReply", () => { sandboxRuntime: { sandboxed: false, mode: "off" }, }); hoisted.accessMock.mockResolvedValue(undefined); + hoisted.pathExistsMock.mockResolvedValue(true); hoisted.exportHtmlTemplateContents.clear(); }); diff --git a/src/infra/archive.test.ts b/src/infra/archive.test.ts index c19f38781c3..c47e310dbcc 100644 --- a/src/infra/archive.test.ts +++ b/src/infra/archive.test.ts @@ -245,7 +245,7 @@ describe("archive utils", () => { }); it.runIf(process.platform !== "win32")( - "rejects zip extraction when a hardlink appears after atomic rename", + "rejects zip extraction when a hardlink appears during destination verification", async () => { await withArchiveCase("zip", async ({ workDir, archivePath, extractDir }) => { const outsideDir = path.join(workDir, "outside"); @@ -256,15 +256,20 @@ describe("archive utils", () => { const zip = new JSZip(); zip.file("package/payload.bin", "owned"); await fs.writeFile(archivePath, await zip.generateAsync({ type: "nodebuffer" })); + const extractedRealPath = path.join( + await fs.realpath(extractDir), + "package", + "payload.bin", + ); - const realRename = fs.rename.bind(fs); + const realLstat = fs.lstat.bind(fs); let linked = false; - const renameSpy = vi.spyOn(fs, "rename").mockImplementation(async (...args) => { - await realRename(...args); - if (!linked) { + const lstatSpy = vi.spyOn(fs, "lstat").mockImplementation(async (...args) => { + if (!linked && String(args[0]) === extractedRealPath) { + await fs.link(extractedRealPath, outsideAlias); linked = true; - await fs.link(String(args[1]), outsideAlias); } + return await realLstat(...args); }); try { @@ -278,10 +283,10 @@ describe("archive utils", () => { code: "destination-symlink-traversal", } satisfies Partial); } finally { - renameSpy.mockRestore(); + lstatSpy.mockRestore(); } - await expect(fs.readFile(outsideAlias, "utf8")).resolves.toBe("owned"); + await expect(fs.readFile(outsideAlias, "utf8")).resolves.toBe(""); await expect(fs.stat(extractedPath)).rejects.toMatchObject({ code: "ENOENT" }); }); }, diff --git a/src/infra/tmp-openclaw-dir.ts b/src/infra/tmp-openclaw-dir.ts index b88b2ad3871..8f6b3b93f89 100644 --- a/src/infra/tmp-openclaw-dir.ts +++ b/src/infra/tmp-openclaw-dir.ts @@ -1,26 +1,175 @@ -import "./fs-safe-defaults.js"; -import { resolveSecureTempRoot, type ResolveSecureTempRootOptions } from "@openclaw/fs-safe/temp"; +import fs from "node:fs"; +import { tmpdir as getOsTmpDir } from "node:os"; +import path from "node:path"; export const POSIX_OPENCLAW_TMP_DIR = "/tmp/openclaw"; -type ResolvePreferredOpenClawTmpDirOptions = Omit< - ResolveSecureTempRootOptions, - | "fallbackPrefix" - | "preferredDir" - | "skipPreferredOnWindows" - | "unsafeFallbackLabel" - | "warningPrefix" ->; +type MaybeNodeError = { code?: string }; + +type SecureDirStat = { + isDirectory(): boolean; + isSymbolicLink(): boolean; + mode?: number; + uid?: number; +}; + +export type ResolvePreferredOpenClawTmpDirOptions = { + accessSync?: (path: string, mode?: number) => void; + chmodSync?: (path: string, mode: number) => void; + getuid?: () => number | undefined; + lstatSync?: (path: string) => SecureDirStat; + mkdirSync?: (path: string, opts: { recursive: boolean; mode?: number }) => void; + platform?: NodeJS.Platform; + tmpdir?: () => string; + warn?: (message: string) => void; +}; + +function isNodeErrorWithCode(err: unknown, code: string): err is MaybeNodeError { + return ( + typeof err === "object" && + err !== null && + "code" in err && + (err as MaybeNodeError).code === code + ); +} export function resolvePreferredOpenClawTmpDir( options: ResolvePreferredOpenClawTmpDirOptions = {}, ): string { - return resolveSecureTempRoot({ - ...options, - fallbackPrefix: "openclaw", - preferredDir: POSIX_OPENCLAW_TMP_DIR, - skipPreferredOnWindows: true, - unsafeFallbackLabel: "OpenClaw temp dir", - warningPrefix: "[openclaw]", - }); + const accessMode = fs.constants.W_OK | fs.constants.X_OK; + const accessSync = options.accessSync ?? fs.accessSync; + const chmodSync = options.chmodSync ?? fs.chmodSync; + const lstatSync = options.lstatSync ?? fs.lstatSync; + const mkdirSync = options.mkdirSync ?? fs.mkdirSync; + const warn = options.warn ?? ((message: string) => console.warn(message)); + const getuid = + options.getuid ?? + (() => { + try { + return typeof process.getuid === "function" ? process.getuid() : undefined; + } catch { + return undefined; + } + }); + const tmpdir = typeof options.tmpdir === "function" ? options.tmpdir : getOsTmpDir; + const platform = options.platform ?? process.platform; + const uid = getuid(); + + const isSecureDirForUser = (st: { mode?: number; uid?: number }): boolean => { + if (uid === undefined) { + return true; + } + if (typeof st.uid === "number" && st.uid !== uid) { + return false; + } + return typeof st.mode !== "number" || (st.mode & 0o022) === 0; + }; + + const fallback = (): string => { + const suffix = uid === undefined ? "openclaw" : `openclaw-${uid}`; + const joiner = platform === "win32" ? path.win32.join : path.join; + return joiner(tmpdir(), suffix); + }; + + const isTrustedTmpDir = (st: SecureDirStat): boolean => + st.isDirectory() && !st.isSymbolicLink() && isSecureDirForUser(st); + + const resolveDirState = (candidatePath: string): "available" | "missing" | "invalid" => { + try { + const candidate = lstatSync(candidatePath); + if (!isTrustedTmpDir(candidate)) { + return "invalid"; + } + accessSync(candidatePath, accessMode); + return "available"; + } catch (err) { + return isNodeErrorWithCode(err, "ENOENT") ? "missing" : "invalid"; + } + }; + + const tryRepairWritableBits = (candidatePath: string): boolean => { + try { + const st = lstatSync(candidatePath); + if (!st.isDirectory() || st.isSymbolicLink()) { + return false; + } + if (uid !== undefined && typeof st.uid === "number" && st.uid !== uid) { + return false; + } + if (typeof st.mode !== "number") { + return false; + } + if ((st.mode & 0o022) === 0) { + return resolveDirState(candidatePath) === "available"; + } + try { + chmodSync(candidatePath, 0o700); + } catch (chmodErr) { + if ( + isNodeErrorWithCode(chmodErr, "EPERM") || + isNodeErrorWithCode(chmodErr, "EACCES") || + isNodeErrorWithCode(chmodErr, "ENOENT") + ) { + return resolveDirState(candidatePath) === "available"; + } + throw chmodErr; + } + warn(`[openclaw] tightened permissions on temp dir: ${candidatePath}`); + return resolveDirState(candidatePath) === "available"; + } catch { + return false; + } + }; + + const ensureTrustedFallbackDir = (): string => { + const fallbackPath = fallback(); + const state = resolveDirState(fallbackPath); + if (state === "available") { + return fallbackPath; + } + if (state === "invalid") { + if (tryRepairWritableBits(fallbackPath)) { + return fallbackPath; + } + throw new Error(`Unsafe fallback OpenClaw temp dir: ${fallbackPath}`); + } + try { + mkdirSync(fallbackPath, { recursive: true, mode: 0o700 }); + chmodSync(fallbackPath, 0o700); + } catch { + throw new Error(`Unable to create fallback OpenClaw temp dir: ${fallbackPath}`); + } + if (resolveDirState(fallbackPath) !== "available" && !tryRepairWritableBits(fallbackPath)) { + throw new Error(`Unsafe fallback OpenClaw temp dir: ${fallbackPath}`); + } + return fallbackPath; + }; + + if (platform === "win32") { + return ensureTrustedFallbackDir(); + } + + const preferredDir = POSIX_OPENCLAW_TMP_DIR; + const preferredState = resolveDirState(preferredDir); + if (preferredState === "available") { + return preferredDir; + } + if (preferredState === "invalid") { + if (tryRepairWritableBits(preferredDir)) { + return preferredDir; + } + return ensureTrustedFallbackDir(); + } + + try { + accessSync(path.dirname(preferredDir), accessMode); + mkdirSync(preferredDir, { recursive: true, mode: 0o700 }); + chmodSync(preferredDir, 0o700); + if (resolveDirState(preferredDir) !== "available" && !tryRepairWritableBits(preferredDir)) { + return ensureTrustedFallbackDir(); + } + return preferredDir; + } catch { + return ensureTrustedFallbackDir(); + } } diff --git a/src/media-understanding/media-understanding-url-fallback.test.ts b/src/media-understanding/media-understanding-url-fallback.test.ts index a401785bf18..9b54a129e9e 100644 --- a/src/media-understanding/media-understanding-url-fallback.test.ts +++ b/src/media-understanding/media-understanding-url-fallback.test.ts @@ -20,9 +20,11 @@ async function withBlockedLocalAttachmentFallback( ) { await withTempDir({ prefix }, async (base) => { const allowedRoot = path.join(base, "allowed"); - const attachmentPath = path.join(allowedRoot, "voice-note.m4a"); + const blockedRoot = path.join(base, "blocked"); + const attachmentPath = path.join(blockedRoot, "voice-note.m4a"); const fallbackUrl = "https://example.com/fallback.jpg"; await fs.mkdir(allowedRoot, { recursive: true }); + await fs.mkdir(blockedRoot, { recursive: true }); await fs.writeFile(attachmentPath, "ok"); const cache = new MediaAttachmentCache( diff --git a/src/media/image-ops.tempdir.test.ts b/src/media/image-ops.tempdir.test.ts index 8f8c3e1bb67..165bd3bb6ba 100644 --- a/src/media/image-ops.tempdir.test.ts +++ b/src/media/image-ops.tempdir.test.ts @@ -23,12 +23,14 @@ describe("image-ops temp dir", () => { it("creates sips temp dirs under the secured OpenClaw tmp root", async () => { const secureRoot = resolvePreferredOpenClawTmpDir(); + const secureRootReal = await fs.realpath(secureRoot); await getImageMetadata(Buffer.from("image")); expect(fs.mkdtemp).toHaveBeenCalledTimes(1); - expect(fs.mkdtemp).toHaveBeenCalledWith(path.join(secureRoot, "openclaw-img-")); - expect(createdTempDir.startsWith(path.join(secureRoot, "openclaw-img-"))).toBe(true); + const mkdtempPrefix = vi.mocked(fs.mkdtemp).mock.calls[0]?.[0]; + expect(mkdtempPrefix.startsWith(path.join(secureRootReal, "openclaw-img-"))).toBe(true); + expect(createdTempDir.startsWith(path.join(secureRootReal, "openclaw-img-"))).toBe(true); await expect(fs.access(createdTempDir)).rejects.toMatchObject({ code: "ENOENT" }); }); }); diff --git a/src/media/media-reference.test.ts b/src/media/media-reference.test.ts index ddc4e9d2a99..61543b22cdd 100644 --- a/src/media/media-reference.test.ts +++ b/src/media/media-reference.test.ts @@ -46,12 +46,13 @@ describe("media reference helpers", () => { const filePath = path.join(stateDir, "media", "inbound", id); await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, Buffer.from("png")); + const realFilePath = await fs.realpath(filePath); try { await expect(resolveInboundMediaReference(`media://inbound/${id}`)).resolves.toMatchObject({ id, normalizedSource: `media://inbound/${id}`, - physicalPath: filePath, + physicalPath: realFilePath, sourceType: "uri", }); } finally { @@ -65,9 +66,12 @@ describe("media reference helpers", () => { const filePath = path.join(stateDir, "media", "inbound", id); await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, Buffer.from("png")); + const realFilePath = await fs.realpath(filePath); try { - await expect(resolveMediaReferenceLocalPath(`media://inbound/${id}`)).resolves.toBe(filePath); + await expect(resolveMediaReferenceLocalPath(`media://inbound/${id}`)).resolves.toBe( + realFilePath, + ); await expect(resolveMediaReferenceLocalPath(" MEDIA: ./out.png")).resolves.toBe("./out.png"); } finally { await fs.rm(filePath, { force: true }); @@ -80,11 +84,12 @@ describe("media reference helpers", () => { const filePath = path.join(stateDir, "media", "inbound", id); await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, Buffer.from("png")); + const realFilePath = await fs.realpath(filePath); try { await expect(resolveInboundMediaReference(filePath)).resolves.toMatchObject({ id, - physicalPath: filePath, + physicalPath: realFilePath, sourceType: "path", }); await expect( diff --git a/test/scripts/lint-suppressions.test.ts b/test/scripts/lint-suppressions.test.ts index 0fd7348b1dc..955a8a43993 100644 --- a/test/scripts/lint-suppressions.test.ts +++ b/test/scripts/lint-suppressions.test.ts @@ -106,13 +106,14 @@ describe("production lint suppressions", () => { "src/hooks/module-loader.ts|typescript/no-unnecessary-type-parameters|1", "src/infra/channel-runtime-context.ts|typescript/no-unnecessary-type-parameters|1", "src/infra/exec-approvals-effective.ts|typescript/no-unnecessary-type-parameters|1", - "src/infra/json-file.ts|typescript/no-unnecessary-type-parameters|1", + "src/infra/json-file.ts|typescript-eslint/no-unnecessary-type-parameters|1", "src/infra/outbound/send-deps.ts|typescript/no-unnecessary-type-parameters|1", "src/node-host/invoke.ts|typescript/no-unnecessary-type-parameters|1", "src/plugin-sdk/channel-config-helpers.ts|typescript/no-unnecessary-type-parameters|1", "src/plugin-sdk/channel-entry-contract.ts|typescript/no-unnecessary-type-parameters|1", "src/plugin-sdk/facade-loader.ts|typescript/no-unnecessary-type-parameters|1", "src/plugin-sdk/facade-runtime.ts|typescript/no-unnecessary-type-parameters|3", + "src/plugin-sdk/json-store.ts|typescript-eslint/no-unnecessary-type-parameters|1", "src/plugin-sdk/qa-runner-runtime.ts|typescript/no-unnecessary-type-parameters|1", "src/plugin-sdk/test-helpers/package-manifest-contract.ts|typescript/no-unnecessary-type-parameters|1", "src/plugin-sdk/test-helpers/public-surface-loader.ts|typescript/no-unnecessary-type-parameters|1",