diff --git a/.oxlintrc.json b/.oxlintrc.json index 7ccf77f27224..eb266a5854d9 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,6 +1,7 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": ["unicorn", "typescript", "oxc"], + "jsPlugins": ["./scripts/oxlint-type-evidence.mjs"], "categories": { "correctness": "error", "perf": "error", @@ -66,6 +67,8 @@ "oxc/no-accumulating-spread": "error", "oxc/no-async-endpoint-handlers": "error", "oxc/no-map-spread": "error", + "openclaw-type-evidence/no-unknown-type-aliases": "error", + "openclaw-type-evidence/no-widen-then-assert": "error", "promise/no-callback-in-promise": "error", "promise/no-multiple-resolved": "error", "promise/no-promise-in-callback": "error", @@ -213,6 +216,7 @@ // Intentional negative-test corpora contain parser and lint violations by contract. ".agents/skills/autoreview/tests/fixtures/**", "test/fixtures/oxlint-boundary-guards/**", + "test/fixtures/oxlint-type-evidence/**", "**/a2ui.bundle.js", "extensions/diffs/assets/viewer-runtime.js", "extensions/diffs-language-pack/assets/viewer-runtime.js", @@ -274,6 +278,12 @@ "typescript/no-explicit-any": "off" } }, + { + "files": ["**/*.d.ts"], + "rules": { + "openclaw-type-evidence/no-unknown-type-aliases": "off" + } + }, { "files": [ "src/**/*.{js,ts,mts,cts}", diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 6b6721901b75..22e1b0f2763d 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -35,3 +35,34 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## anti-slop + +OpenClaw's type-evidence Oxlint rules were adapted from anti-slop. + +- Upstream: https://github.com/dmmulroy/anti-slop +- Source revision: abaeb63b29e63062f778771d5447bd2e9c3c680f +- License: MIT +- Copyright: Copyright (c) 2026 Dillon Mulroy + +MIT License + +Copyright (c) 2026 Dillon Mulroy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config/knip.all-exports.config.ts b/config/knip.all-exports.config.ts index 2be5e34811f7..030be23eb446 100644 --- a/config/knip.all-exports.config.ts +++ b/config/knip.all-exports.config.ts @@ -69,6 +69,7 @@ const ROOT_TEST_ENTRY_GLOBS = [ "test/fixtures/ts-topology/basic/**/*.{js,mjs,cjs,ts,mts,cts}!", // The focused Oxlint test invokes these deliberate violations by path. "test/fixtures/oxlint-boundary-guards/*.ts!", + "test/fixtures/oxlint-type-evidence/*.ts!", ] as const; const workspaces = Object.fromEntries( diff --git a/config/knip.config.ts b/config/knip.config.ts index 2ff3d4a53e15..dc7341bc6cc1 100644 --- a/config/knip.config.ts +++ b/config/knip.config.ts @@ -81,6 +81,8 @@ const repositoryScriptEntries = [ "scripts/openclaw-release-clawhub-runtime-state.ts!", // Oxlint loads this JS plugin by path from config/oxlint/boundary-guards.json. "scripts/oxlint-boundary-guards.mjs!", + // Oxlint loads this JS plugin by path from the root and focused lint configs. + "scripts/oxlint-type-evidence.mjs!", "scripts/plugin-prerelease-liveish-matrix.mts!", // Generates the checked-in native protocol models from core descriptor metadata. "scripts/protocol-gen.ts!", diff --git a/config/knip.scripts-exports.config.ts b/config/knip.scripts-exports.config.ts index b0cc86fb0d45..6fa9568958ac 100644 --- a/config/knip.scripts-exports.config.ts +++ b/config/knip.scripts-exports.config.ts @@ -58,6 +58,7 @@ const config = { ], // Oxlint consumes this required default export through a JSON config path. "scripts/oxlint-boundary-guards.mjs": ["exports"], + "scripts/oxlint-type-evidence.mjs": ["exports"], // Wrangler consumes the Worker default export and instantiates the Durable // Object class by name from wrangler.jsonc; Knip cannot resolve either. "scripts/cloudflare/src/index.ts": ["exports"], diff --git a/config/oxlint/type-evidence.json b/config/oxlint/type-evidence.json new file mode 100644 index 000000000000..ff8e6c0fecfb --- /dev/null +++ b/config/oxlint/type-evidence.json @@ -0,0 +1,10 @@ +{ + "$schema": "../../node_modules/oxlint/configuration_schema.json", + "plugins": [], + "jsPlugins": ["../../scripts/oxlint-type-evidence.mjs"], + "categories": { "correctness": "off" }, + "rules": { + "openclaw-type-evidence/no-unknown-type-aliases": "error", + "openclaw-type-evidence/no-widen-then-assert": "error" + } +} diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json index cef1865d17e2..6d45d378f741 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json @@ -1 +1 @@ -{"contentHash":"e2a5ce75757e7a275312c91ce95bdb4f13091a3c613dd4c66fdda0b2e4d3bf83","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} +{"contentHash":"f95093ed8782337b995c648a13f460ecddba40b11431d1eba6a9781804a0e99d","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json index be3be85887a4..12d62af6d7b3 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json @@ -1 +1 @@ -{"contentHash":"0e4494129d83b508c82788383f8dfead578976ed474be2cfbec557aa6caf88e7","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} +{"contentHash":"afcb925bae1788d479e61faf6da9b1f4255d133cd42f387fc31555890fc3cfc1","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-core.json b/docs/.generated/plugin-sdk-api-baseline/channel-core.json index f09a6a6b79c0..f68b4184b7ea 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-core.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-core.json @@ -1 +1 @@ -{"contentHash":"8643a3553553855a519e825fcaa483024ecd3538675c5a2d474af841fb5070d7","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} +{"contentHash":"9cac147d4e5170745d298b867ca8c6ffb0513b940cf1d26a62b46a6cceb10852","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json index 75f1fa2425cb..07a582bc0a0d 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json @@ -1 +1 @@ -{"contentHash":"13ec7276e7c37ae4e7589e67302c2bf531674f2c439b70d4a8a44a4ee136f685","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} +{"contentHash":"4cff7cdf2e9a2818661157b37c6d14dd020f527e00b90fe4452c3aa8281ef16e","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json index cef050dd62be..4f1f448b2d08 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json @@ -1 +1 @@ -{"contentHash":"ed0a5bf19bbe6e9641c8de12ea94105865850343c319a58953a2dce21e6d3f49","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"} +{"contentHash":"b4d9b4733b97ce9fa39e03b83b5c4c7b169e2992a2f1f75dcf052cc4c3c294c5","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-message.json b/docs/.generated/plugin-sdk-api-baseline/channel-message.json index 5c94495737f6..840a2a41cb33 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-message.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-message.json @@ -1 +1 @@ -{"contentHash":"feb6187469b8f0e88c0445ba025cc557c94871db55f39075eb63044d8ba48cd7","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} +{"contentHash":"d832c4bc18c7995c6971bbabf9cd742a01656278e8b51ab4d63cd7322f545a88","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json index 825f8710f7d7..4e2abc519f6e 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json @@ -1 +1 @@ -{"contentHash":"97d39817d24eaf5b2808960770447188daa0b2959514efbd44db007797e40e3b","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} +{"contentHash":"8211226bd9a2b20b486cf7581e65f116c30063b3971a359cade2921a6a54d6f8","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json b/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json index d9f559e8fb26..939403278744 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json @@ -1 +1 @@ -{"contentHash":"0d9fc3f9ef06aaa9995c5a670d62b99cfce25f0b2c42152aa33214f9dec78b19","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing"} +{"contentHash":"0186f09bddb33bf9dbe6c2874d3b0e4704d8ce8d055abc694a1fe93f3e5cb980","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json index 3ef00e4dac54..8cc45723e492 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json @@ -1 +1 @@ -{"contentHash":"e824840c22ccade58891ad2952a6aea4aafa8c163adb4383e9a5aa8b47cce44b","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} +{"contentHash":"c89bfe63fcec8e912673caa0995ed2b0de7a624c385f22c24c061239cbbe12bb","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} diff --git a/docs/.generated/plugin-sdk-api-baseline/core.json b/docs/.generated/plugin-sdk-api-baseline/core.json index f4b0c317ba1b..98a720fa596d 100644 --- a/docs/.generated/plugin-sdk-api-baseline/core.json +++ b/docs/.generated/plugin-sdk-api-baseline/core.json @@ -1 +1 @@ -{"contentHash":"08bba8059fb44659737936c0ef4c163cfff29872db3088b72d2fff82f4c4ac85","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} +{"contentHash":"ece426ee0e06d143d79b9a761743e26564cff5562d6bff95c3467da7387f0518","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/discord.json b/docs/.generated/plugin-sdk-api-baseline/discord.json index 8e24587c0713..22ec48b1d752 100644 --- a/docs/.generated/plugin-sdk-api-baseline/discord.json +++ b/docs/.generated/plugin-sdk-api-baseline/discord.json @@ -1 +1 @@ -{"contentHash":"db6ac3119377920f375bdb1cd01082815022f2ad49de3dc5d7ac258a6a864ca6","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} +{"contentHash":"75e88b7733d802d1119709f0a88ab2e2a28d9ec95c42e3872bd3a065b8f9a663","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} diff --git a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json index e5f37fac7973..4b382af955f3 100644 --- a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json +++ b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json @@ -1 +1 @@ -{"contentHash":"7bb8bb13f101a7018aea253debd9c3611635541b8ad2d847e827300b9f94991f","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} +{"contentHash":"d75dd7f78180cebeebd788eec6d738178e0e9a89fef7904ff3807ec35d690cc5","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} diff --git a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json index 1620befd327d..345d2590c9ef 100644 --- a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json @@ -1 +1 @@ -{"contentHash":"e5c950c85caf1fe7edf4d672f95afc074e926e300981d96707ed904304e001b4","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} +{"contentHash":"d12d25f6cc1177632afe16e110dab1a12f65db41382d3507b99c5eba88fa55b4","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-command-runtime.json b/docs/.generated/plugin-sdk-api-baseline/plugin-command-runtime.json index 1cbc4e1402bd..453c03afa0ed 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-command-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-command-runtime.json @@ -1 +1 @@ -{"contentHash":"5bb2b4560b715a99bb635510b031b4350b9af16ca4c9247c6d28b54753ef1b7a","entrypoint":"plugin-command-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime"} +{"contentHash":"d25a630bd5eec03aadda39b73e4b6b8ef1ba636b2c98afb5e3a52d07f82448bb","entrypoint":"plugin-command-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json index 077b526765f1..e7de3af2f2ba 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json @@ -1 +1 @@ -{"contentHash":"f6867c932f2632409f933d8e158d01f303679950b4e6e3a01ba28b0ea8b4d7da","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} +{"contentHash":"ba3e6b379ababd8130f7e600b6860ce07f7121dbc104712a97805c2f3da3e510","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json index 41fcb539bf51..84bf6049242c 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json @@ -1 +1 @@ -{"contentHash":"53a44cf7b1b8866054e905dd7a34623e75dbf4be6da5b58e7d47fd638e2a92f5","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} +{"contentHash":"e72b069106dbe98a76e548f1af7e3af244f381e2d6618dd069320a44e966608d","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json index 824af6e0846e..986a6ceb804a 100644 --- a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json @@ -1 +1 @@ -{"contentHash":"03aeade34faf6a1c6bfee3e88f684ccb4abe657ea8f3475c96162698baeeb314","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} +{"contentHash":"aa2a15aa436ec9cb463a2521ded8cfd92225b441649445a0b1f2deb5f9f7a034","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/runtime-store.json b/docs/.generated/plugin-sdk-api-baseline/runtime-store.json index d7a05cfce8fd..6227637a8032 100644 --- a/docs/.generated/plugin-sdk-api-baseline/runtime-store.json +++ b/docs/.generated/plugin-sdk-api-baseline/runtime-store.json @@ -1 +1 @@ -{"contentHash":"b965caa74f02ea0fa0b4b5a57bfacd0699df991452b5aa95f21237858930508f","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store"} +{"contentHash":"10169d7b8325ccc0de4d24d3d17aeefc689b9508347b2a8698e74d90e54857ae","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store"} diff --git a/docs/.generated/plugin-sdk-api-baseline/session-catalog.json b/docs/.generated/plugin-sdk-api-baseline/session-catalog.json index 6135612f8dec..00a20ac81eeb 100644 --- a/docs/.generated/plugin-sdk-api-baseline/session-catalog.json +++ b/docs/.generated/plugin-sdk-api-baseline/session-catalog.json @@ -1 +1 @@ -{"contentHash":"1c76ac2e6a40a56ae0dbf5f6e2b5d911883f8e11cc7c0f435f6cce0c185a60c2","entrypoint":"session-catalog","importSpecifier":"openclaw/plugin-sdk/session-catalog"} +{"contentHash":"016903fae0d5b0858eca5310dd81a38005d2de05343f9e058971a8ca4eaf25e4","entrypoint":"session-catalog","importSpecifier":"openclaw/plugin-sdk/session-catalog"} diff --git a/docs/.generated/plugin-sdk-api-baseline/session-store-runtime.json b/docs/.generated/plugin-sdk-api-baseline/session-store-runtime.json index 6bfd1cab6435..3f45a52573d6 100644 --- a/docs/.generated/plugin-sdk-api-baseline/session-store-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/session-store-runtime.json @@ -1 +1 @@ -{"contentHash":"96150841a6e550228fcf0310e0aaf56ebd75b53ad9c5eb2d0557bbb8a5b67290","entrypoint":"session-store-runtime","importSpecifier":"openclaw/plugin-sdk/session-store-runtime"} +{"contentHash":"42b029dd356f0bb03525f66bb7eb62dd3a2d02aaf72abafd47967d431c625c9b","entrypoint":"session-store-runtime","importSpecifier":"openclaw/plugin-sdk/session-store-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json index 750aa90a0ebb..2e9395036fc4 100644 --- a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json +++ b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json @@ -1 +1 @@ -{"contentHash":"83795fac5e17a90840255de8446aaf23b988a14d19bc76a327b0b74ef5f9ee8a","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} +{"contentHash":"be52565eb108d7328f563882b5bcc26e03496bb3b5ef88879976f03f41b1a3bf","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} diff --git a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json index 50c7b7d50551..821516547756 100644 --- a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json +++ b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json @@ -1 +1 @@ -{"contentHash":"9511576a6c8d6631cbe81dc7aca200f670feaf7fc03d6b3308066b5caa3ab50c","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} +{"contentHash":"4a7e8f3791e029e4ecb8aaa9750190dde04678c5164e151ec2643d677f2627c9","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} diff --git a/extensions/browser/src/browser/pw-tools-core.annotate.test.ts b/extensions/browser/src/browser/pw-tools-core.annotate.test.ts index 9d7a60e1774b..0b648acbfd1e 100644 --- a/extensions/browser/src/browser/pw-tools-core.annotate.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.annotate.test.ts @@ -8,14 +8,12 @@ import { installPwToolsCoreTestHooks(); const mod = await import("./pw-tools-core.interactions.js"); -type EvaluateArg = unknown; - function evaluateMockReturning(view: { x: number; y: number; width?: number; height?: number }) { // Caller reads { x, y, width, height } in one evaluate; default to a normal // desktop viewport so refs near the top stay in-viewport unless a test puts // them out of range explicitly. const result = { width: 1280, height: 720, ...view }; - return vi.fn(async (arg: EvaluateArg) => { + return vi.fn(async (arg: unknown) => { if (typeof arg === "function") { return result; } diff --git a/extensions/memory-core/src/dreaming-phases.ts b/extensions/memory-core/src/dreaming-phases.ts index 63230363dd50..fe4ab0874a3e 100644 --- a/extensions/memory-core/src/dreaming-phases.ts +++ b/extensions/memory-core/src/dreaming-phases.ts @@ -12,7 +12,7 @@ import { resolveMemoryLightDreamingConfig, resolveMemoryRemDreamingConfig, } from "openclaw/plugin-sdk/memory-core-host-status"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime"; import { appendFailedDreamingEvent } from "./dreaming-events.js"; import { @@ -67,7 +67,6 @@ import { } from "./short-term-promotion.js"; type Logger = Pick; -type DreamingHostConfig = unknown; type DreamingPhaseStorageConfig = { timezone?: string; storage: { mode: "inline" | "separate" | "both"; separateReports: boolean }; @@ -557,7 +556,7 @@ function isCheckpointSessionTranscriptPath(absolutePath: string): boolean { } function resolveSessionAgentsForWorkspace(params: { - cfg: DreamingHostConfig; + cfg: OpenClawConfig; workspaceDir: string; primaryWorkspaceDir?: string; }): string[] { @@ -566,13 +565,10 @@ function resolveSessionAgentsForWorkspace(params: { return []; } const target = normalizeMemoryCoreWorkspaceKey(workspaceDir); - const workspaces = resolveMemoryDreamingWorkspaces( - cfg as Parameters[0], - { - primaryWorkspaceDir, - primaryAgentId: "main", - }, - ); + const workspaces = resolveMemoryDreamingWorkspaces(cfg, { + primaryWorkspaceDir, + primaryAgentId: "main", + }); const match = workspaces.find( (entry) => normalizeMemoryCoreWorkspaceKey(entry.workspaceDir) === target, ); @@ -584,7 +580,7 @@ function resolveSessionAgentsForWorkspace(params: { async function collectSessionIngestionBatches(params: { workspaceDir: string; - cfg?: DreamingHostConfig; + cfg?: OpenClawConfig; primaryWorkspaceDir?: string; lookbackDays: number; nowMs: number; @@ -719,7 +715,7 @@ async function collectSessionIngestionBatches(params: { async function ingestSessionTranscriptSignals(params: { workspaceDir: string; - cfg?: DreamingHostConfig; + cfg?: OpenClawConfig; primaryWorkspaceDir?: string; lookbackDays: number; nowMs: number; @@ -1286,7 +1282,7 @@ export function previewRemDreaming(params: { async function runLightDreaming(params: { agentId?: string; workspaceDir: string; - cfg?: DreamingHostConfig; + cfg?: OpenClawConfig; primaryWorkspaceDir?: string; config: LightDreamingConfig; logger: Logger; @@ -1386,7 +1382,7 @@ async function runLightDreaming(params: { async function runRemDreaming(params: { agentId?: string; workspaceDir: string; - cfg?: DreamingHostConfig; + cfg?: OpenClawConfig; primaryWorkspaceDir?: string; config: RemDreamingConfig; logger: Logger; @@ -1504,7 +1500,7 @@ export async function runDreamingSweepPhases(params: { agentId?: string; workspaceDir: string; pluginConfig?: Record; - cfg?: DreamingHostConfig; + cfg?: OpenClawConfig; logger: Logger; subagent?: DreamNarrativeRequest["subagent"]; detachNarratives?: boolean; @@ -1524,7 +1520,7 @@ export async function runDreamingSweepPhases(params: { const light = resolveMemoryLightDreamingConfig({ pluginConfig: params.pluginConfig, - cfg: params.cfg as Parameters[0]["cfg"], + cfg: params.cfg, }); if (light.enabled && light.limit > 0) { try { @@ -1555,7 +1551,7 @@ export async function runDreamingSweepPhases(params: { const rem = resolveMemoryRemDreamingConfig({ pluginConfig: params.pluginConfig, - cfg: params.cfg as Parameters[0]["cfg"], + cfg: params.cfg, }); if (rem.enabled && rem.limit > 0) { try { diff --git a/extensions/searxng/src/searxng-search-provider.test.ts b/extensions/searxng/src/searxng-search-provider.test.ts index 36b3c7c156a9..c08c6c8c8809 100644 --- a/extensions/searxng/src/searxng-search-provider.test.ts +++ b/extensions/searxng/src/searxng-search-provider.test.ts @@ -195,11 +195,11 @@ describe("searxng web search provider", () => { it("persists base URL to plugin config via setConfiguredCredentialValue", () => { const provider = createSearxngWebSearchProvider(); - const config = {} as Record; const setConfiguredCredentialValue = provider.setConfiguredCredentialValue; if (!setConfiguredCredentialValue) { throw new Error("Expected SearXNG provider setConfiguredCredentialValue"); } + const config: Parameters[0] = {}; setConfiguredCredentialValue(config, "http://search.local:9000"); diff --git a/scripts/oxlint-type-evidence.mjs b/scripts/oxlint-type-evidence.mjs new file mode 100644 index 000000000000..c4e9b8a8276d --- /dev/null +++ b/scripts/oxlint-type-evidence.mjs @@ -0,0 +1,441 @@ +// Adapted from dmmulroy/anti-slop at abaeb63b29e63062f778771d5447bd2e9c3c680f. +// See THIRD_PARTY_NOTICES.md for the upstream MIT license. + +const functionBoundaryTypes = new Set([ + "ArrowFunctionExpression", + "FunctionDeclaration", + "FunctionExpression", + "TSDeclareFunction", + "TSEmptyBodyFunctionExpression", +]); + +function unwrapExpressionParentheses(expression) { + let current = expression; + while (current.type === "ParenthesizedExpression") { + current = current.expression; + } + return current; +} + +function unwrapTypeParentheses(type) { + let current = type; + while (current.type === "TSParenthesizedType") { + current = current.typeAnnotation; + } + return current; +} + +function typeReferenceName(type) { + return type.typeName.type === "Identifier" ? type.typeName.name : null; +} + +function isUnknownOrAnyType(type) { + const unwrapped = unwrapTypeParentheses(type); + return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword"; +} + +function isBroadRecordKeyType(type) { + const unwrapped = unwrapTypeParentheses(type); + if ( + unwrapped.type === "TSStringKeyword" || + unwrapped.type === "TSNumberKeyword" || + unwrapped.type === "TSSymbolKeyword" + ) { + return true; + } + if (unwrapped.type === "TSUnionType") { + return unwrapped.types.every(isBroadRecordKeyType); + } + return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey"; +} + +function isBroadRecordType(type) { + const unwrapped = unwrapTypeParentheses(type); + + if (unwrapped.type === "TSTypeReference") { + if (typeReferenceName(unwrapped) === "Readonly") { + const [inner] = unwrapped.typeArguments?.params ?? []; + return inner !== undefined && isBroadRecordType(inner); + } + + if (typeReferenceName(unwrapped) !== "Record") { + return false; + } + const parameters = unwrapped.typeArguments?.params ?? []; + return ( + parameters.length === 2 && + parameters[0] !== undefined && + parameters[1] !== undefined && + isBroadRecordKeyType(parameters[0]) && + isUnknownOrAnyType(parameters[1]) + ); + } + + if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) { + return false; + } + const [member] = unwrapped.members; + const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : []; + return ( + member?.type === "TSIndexSignature" && + member.parameters.length === 1 && + parameter !== undefined && + isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) && + isUnknownOrAnyType(member.typeAnnotation.typeAnnotation) + ); +} + +function broadTypeKind(type) { + const unwrapped = unwrapTypeParentheses(type); + if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") { + return "top"; + } + if (unwrapped.type === "TSObjectKeyword") { + return "object"; + } + return isBroadRecordType(unwrapped) ? "record" : null; +} + +function assertedExpression(node) { + return unwrapExpressionParentheses(node.expression); +} + +function assertionFromExpression(expression) { + const unwrapped = unwrapExpressionParentheses(expression); + return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion" + ? unwrapped + : null; +} + +function normalizedTypeText(sourceText, type) { + return sourceText.slice(type.start, type.end).replaceAll(/\s+/gu, ""); +} + +function typesHaveSameSyntax(sourceText, left, right) { + return ( + left !== null && + normalizedTypeText(sourceText, unwrapTypeParentheses(left)) === + normalizedTypeText(sourceText, unwrapTypeParentheses(right)) + ); +} + +function isDefinitelyObjectType(type) { + const unwrapped = unwrapTypeParentheses(type); + switch (unwrapped.type) { + case "TSArrayType": + case "TSConstructorType": + case "TSFunctionType": + case "TSMappedType": + case "TSObjectKeyword": + case "TSTupleType": + return true; + case "TSTypeLiteral": + return unwrapped.members.length > 0; + case "TSIntersectionType": + return unwrapped.types.every(isDefinitelyObjectType); + case "TSTypeOperator": + return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation); + default: + return false; + } +} + +function isDefinitelyNarrowerRecordType(type) { + const unwrapped = unwrapTypeParentheses(type); + if (unwrapped.type === "TSTypeLiteral") { + return unwrapped.members.some((member) => member.type !== "TSIndexSignature"); + } + + if (unwrapped.type !== "TSTypeReference") { + return false; + } + if (typeReferenceName(unwrapped) === "Readonly") { + const [inner] = unwrapped.typeArguments?.params ?? []; + return inner !== undefined && isDefinitelyNarrowerRecordType(inner); + } + if (typeReferenceName(unwrapped) !== "Record") { + return false; + } + + const parameters = unwrapped.typeArguments?.params ?? []; + return ( + parameters.length === 2 && parameters[1] !== undefined && !isUnknownOrAnyType(parameters[1]) + ); +} + +function functionBoundary(node) { + let current = node.parent; + while (current !== null && current.type !== "Program") { + if (functionBoundaryTypes.has(current.type)) { + return current; + } + current = current.parent; + } + return null; +} + +function resolvedVariableForIdentifier(scopes, identifier) { + for (const scope of scopes) { + const reference = scope.references.find( + (candidate) => + candidate.identifier.start === identifier.start && + candidate.identifier.end === identifier.end, + ); + if (reference !== undefined) { + return reference.resolved; + } + } + return null; +} + +function variableDeclarator(variable) { + for (const definition of variable.defs) { + if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") { + return definition.node; + } + } + return null; +} + +function knownValueEvidence(expression, scopes, boundary, visitedVariables) { + const unwrapped = unwrapExpressionParentheses(expression); + + if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") { + if (broadTypeKind(unwrapped.typeAnnotation) !== null) { + return null; + } + return { type: unwrapped.typeAnnotation }; + } + + if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") { + return { type: null }; + } + + if ( + unwrapped.type === "ArrayExpression" || + unwrapped.type === "ArrowFunctionExpression" || + unwrapped.type === "ClassExpression" || + unwrapped.type === "FunctionExpression" || + unwrapped.type === "NewExpression" || + unwrapped.type === "ObjectExpression" + ) { + return { type: null }; + } + + if (unwrapped.type !== "Identifier") { + return null; + } + const variable = resolvedVariableForIdentifier(scopes, unwrapped); + if (variable === null || visitedVariables.has(variable)) { + return null; + } + + const annotatedIdentifier = variable.identifiers.find( + (identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== undefined, + ); + const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation; + if (annotation !== undefined && annotatedIdentifier !== undefined) { + if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) { + return null; + } + return { type: annotation }; + } + + const declarator = variableDeclarator(variable); + if ( + declarator === null || + declarator.parent.type !== "VariableDeclaration" || + declarator.parent.kind !== "const" || + declarator.init === null || + variable.references.some((reference) => reference.isWrite() && !reference.init) || + functionBoundary(declarator) !== boundary + ) { + return null; + } + + return knownValueEvidence( + declarator.init, + scopes, + boundary, + new Set([...visitedVariables, variable]), + ); +} + +function widenedBinding(variable, scopes) { + const declarator = variableDeclarator(variable); + if ( + declarator === null || + declarator.parent.type !== "VariableDeclaration" || + declarator.parent.kind !== "const" || + declarator.id.type !== "Identifier" || + declarator.init === null || + variable.references.some((reference) => reference.isWrite() && !reference.init) + ) { + return null; + } + + const boundary = functionBoundary(declarator); + const declaredType = declarator.id.typeAnnotation?.typeAnnotation; + const initializerAssertion = assertionFromExpression(declarator.init); + const initializerBroadKind = + initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation); + const declaredBroadKind = declaredType === undefined ? null : broadTypeKind(declaredType); + const broadKind = declaredBroadKind ?? initializerBroadKind; + if (broadKind === null) { + return null; + } + + const originalExpression = + initializerAssertion !== null && initializerBroadKind !== null + ? assertedExpression(initializerAssertion) + : declarator.init; + const evidence = knownValueEvidence(originalExpression, scopes, boundary, new Set([variable])); + return evidence === null ? null : { broadKind, evidence, declaredAt: declarator.end, boundary }; +} + +function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) { + if (broadTypeKind(assertedType) !== null) { + return false; + } + if (broadKind === "top") { + return true; + } + if (typesHaveSameSyntax(sourceText, evidence.type, assertedType)) { + return true; + } + if (broadKind === "object") { + return isDefinitelyObjectType(assertedType); + } + return isDefinitelyNarrowerRecordType(assertedType); +} + +const noUnknownTypeAliasesRule = { + meta: { + type: "problem", + docs: { + description: + "Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary.", + }, + messages: { + unknownAlias: + "Type alias `{{alias}}` only renames `unknown`. Keep `unknown` explicit at the boundary or replace it with the parsed owner type.", + }, + }, + create(context) { + const aliases = new Map(); + + const resolvesToUnknown = (type, visited = new Set()) => { + if (type.type === "TSUnknownKeyword") { + return true; + } + if (type.type === "TSParenthesizedType") { + return resolvesToUnknown(type.typeAnnotation, visited); + } + const name = + type.type === "TSTypeReference" && + type.typeName.type === "Identifier" && + (type.typeArguments === null || + type.typeArguments === undefined || + type.typeArguments.params.length === 0) + ? type.typeName.name + : null; + if (name === null || visited.has(name)) { + return false; + } + const alias = aliases.get(name); + if ( + alias === undefined || + (alias.typeParameters !== null && alias.typeParameters !== undefined) + ) { + return false; + } + const nextVisited = new Set(visited); + nextVisited.add(name); + return resolvesToUnknown(alias.typeAnnotation, nextVisited); + }; + + return { + Program(node) { + for (const statement of node.body) { + const declaration = + statement.type === "ExportNamedDeclaration" ? statement.declaration : statement; + if (declaration?.type === "TSTypeAliasDeclaration") { + aliases.set(declaration.id.name, declaration); + } + } + for (const alias of aliases.values()) { + if (!resolvesToUnknown(alias.typeAnnotation, new Set([alias.id.name]))) { + continue; + } + context.report({ + node: alias.id, + messageId: "unknownAlias", + data: { alias: alias.id.name }, + }); + } + }, + }; + }, +}; + +const noWidenThenAssertRule = { + meta: { + type: "problem", + docs: { + description: + "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type.", + }, + messages: { + widenThenAssert: + 'Binding "{{name}}" erases established type evidence by widening the value, then reconstructs that evidence with a type assertion. Preserve the precise type end-to-end; if the input is genuinely unknown, parse it once at the boundary instead.', + }, + }, + create(context) { + const scopes = context.sourceCode.scopeManager.scopes; + + const checkAssertion = (node) => { + const expression = assertedExpression(node); + if (expression.type !== "Identifier") { + return; + } + + const variable = resolvedVariableForIdentifier(scopes, expression); + if (variable === null) { + return; + } + const widened = widenedBinding(variable, scopes); + if ( + widened === null || + node.start <= widened.declaredAt || + functionBoundary(node) !== widened.boundary || + !assertionIsNarrower( + context.sourceCode.text, + widened.broadKind, + widened.evidence, + node.typeAnnotation, + ) + ) { + return; + } + + context.report({ + node, + messageId: "widenThenAssert", + data: { name: expression.name }, + }); + }; + + return { + TSAsExpression: checkAssertion, + TSTypeAssertion: checkAssertion, + }; + }, +}; + +export default { + meta: { name: "openclaw-type-evidence" }, + rules: { + "no-unknown-type-aliases": noUnknownTypeAliasesRule, + "no-widen-then-assert": noWidenThenAssertRule, + }, +}; diff --git a/src/agents/embedded-agent-runner/run/attempt-tool-call-replay-sanitization.ts b/src/agents/embedded-agent-runner/run/attempt-tool-call-replay-sanitization.ts index b5204813827f..55eb281c0782 100644 --- a/src/agents/embedded-agent-runner/run/attempt-tool-call-replay-sanitization.ts +++ b/src/agents/embedded-agent-runner/run/attempt-tool-call-replay-sanitization.ts @@ -512,9 +512,9 @@ export function wrapStreamFnSanitizeMalformedToolCalls( } } const nextContext = { - ...(context as unknown as Record), - messages: nextMessages, - } as unknown; - return baseFn(model, nextContext as typeof context, options); + ...context, + messages: nextMessages as typeof context.messages, + }; + return baseFn(model, nextContext, options); }; } diff --git a/src/agents/runtime-plan/build.ts b/src/agents/runtime-plan/build.ts index f118734c0226..bbe399ca25c7 100644 --- a/src/agents/runtime-plan/build.ts +++ b/src/agents/runtime-plan/build.ts @@ -1,4 +1,3 @@ -import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; /** * Builds prepared runtime plans consumed by embedded agent runs. A plan * centralizes provider hooks, auth, tool schema policy, transcript policy, @@ -46,10 +45,6 @@ function formatResolvedRef(params: { provider: string; modelId: string }): strin return `${params.provider}/${params.modelId}`; } -function asOpenClawConfig(value: unknown): OpenClawConfig | undefined { - return asOptionalRecord(value) as OpenClawConfig | undefined; -} - function asProviderRuntimeModel( value: BuildAgentRuntimePlanParams["model"], ): ProviderRuntimeModel | undefined { @@ -62,7 +57,7 @@ type RuntimePlanMetadataParams = BuildAgentRuntimeDeliveryPlanParams & { function resolveCompatibleMetadataSnapshot( params: RuntimePlanMetadataParams, - config: OpenClawConfig | undefined = asOpenClawConfig(params.config), + config: OpenClawConfig | undefined = params.config, ): PluginMetadataSnapshot | undefined { const metadataSnapshot = params.metadataSnapshot as PluginMetadataSnapshot | undefined; return metadataSnapshot && @@ -96,7 +91,7 @@ function resolvePreparedProviderRuntimeHandle( ...resolveProviderRuntimePluginHandle({ provider: params.provider, modelId: params.modelId, - config: asOpenClawConfig(params.config), + config: params.config, workspaceDir: params.workspaceDir, env: process.env, ...(compatibleMetadataSnapshot ? { pluginMetadataSnapshot: compatibleMetadataSnapshot } : {}), @@ -110,7 +105,7 @@ function resolvePreparedProviderRuntimeHandle( export function buildAgentRuntimeDeliveryPlan( params: BuildAgentRuntimeDeliveryPlanParams, ): AgentRuntimeDeliveryPlan { - const config = asOpenClawConfig(params.config); + const config = params.config; const providerRuntimeHandle = resolvePreparedProviderRuntimeHandle(params); return { isSilentPayload(payload): boolean { @@ -151,7 +146,7 @@ function buildAgentRuntimeOutcomePlan(): AgentRuntimeOutcomePlan { /** Build the complete runtime plan for an embedded agent attempt. */ export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): AgentRuntimePlan { - const config = asOpenClawConfig(params.config); + const config = params.config; const model = asProviderRuntimeModel(params.model); const modelApi = params.modelApi ?? params.model?.api ?? undefined; const transport = params.resolvedTransport; @@ -282,7 +277,7 @@ export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): Agen runtimeHandle: providerRuntimeHandleForPlugins, context: { ...context, - config: asOpenClawConfig(context.config), + config: context.config, }, }); }, @@ -294,7 +289,7 @@ export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): Agen runtimeHandle: providerRuntimeHandleForPlugins, context: { ...context, - config: asOpenClawConfig(context.config), + config: context.config, }, }); }, diff --git a/src/agents/runtime-plan/types.ts b/src/agents/runtime-plan/types.ts index 4ea49c0b85df..f8a5432c885c 100644 --- a/src/agents/runtime-plan/types.ts +++ b/src/agents/runtime-plan/types.ts @@ -4,6 +4,7 @@ * observability decisions shared across embedded-agent hot paths. */ import type { TSchema } from "typebox"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { ModelPickerAction } from "../../interactive/payload.js"; import type { ModelApi, @@ -51,9 +52,6 @@ type AgentRuntimeFailoverReason = | "unclassified" | "unknown"; -/** Provider/runtime config object passed through plugin boundaries. */ -type AgentRuntimeConfig = unknown; - /** Provider model descriptor consumed by runtime-plan hooks. */ type AgentRuntimeModel = { id?: string; @@ -91,7 +89,7 @@ type AgentRuntimeTextTransforms = { type AgentRuntimeProviderHandle = { provider: string; modelId?: string | null; - config?: AgentRuntimeConfig; + config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; applyAutoEnable?: boolean; @@ -341,7 +339,7 @@ type AgentRuntimeSystemPromptContribution = { /** Context passed when resolving provider system prompt contributions. */ type AgentRuntimeSystemPromptContributionContext = { - config?: AgentRuntimeConfig; + config?: OpenClawConfig; agentDir?: string; workspaceDir?: string; provider: string; @@ -566,7 +564,7 @@ export type AgentRuntimePlan = { /** Inputs needed to build delivery-only runtime decisions. */ export type BuildAgentRuntimeDeliveryPlanParams = { - config?: AgentRuntimeConfig; + config?: OpenClawConfig; workspaceDir?: string; agentDir?: string; provider: string; @@ -576,7 +574,7 @@ export type BuildAgentRuntimeDeliveryPlanParams = { /** Inputs needed to build the full prepared runtime plan. */ export type BuildAgentRuntimePlanParams = { - config?: AgentRuntimeConfig; + config?: OpenClawConfig; workspaceDir?: string; agentDir?: string; provider: string; diff --git a/src/agents/sessions/extensions/types.ts b/src/agents/sessions/extensions/types.ts index 461195fa6881..deecf38869ec 100644 --- a/src/agents/sessions/extensions/types.ts +++ b/src/agents/sessions/extensions/types.ts @@ -1086,8 +1086,6 @@ export interface ContextEventResult { messages?: AgentMessage[]; } -type BeforeProviderRequestEventResult = unknown; - export interface ToolCallEventResult { /** Block tool execution. To modify arguments, mutate `event.input` in place instead. */ block?: boolean; @@ -1225,7 +1223,7 @@ export interface ExtensionAPI { on(event: "context", handler: ExtensionHandler): void; on( event: "before_provider_request", - handler: ExtensionHandler, + handler: ExtensionHandler, ): void; on(event: "after_provider_response", handler: ExtensionHandler): void; on( diff --git a/src/agents/sessions/session-manager-codec.ts b/src/agents/sessions/session-manager-codec.ts index 8232deda70d6..594de932278f 100644 --- a/src/agents/sessions/session-manager-codec.ts +++ b/src/agents/sessions/session-manager-codec.ts @@ -1,4 +1,5 @@ import { isRecord } from "@openclaw/normalization-core/record-coerce"; +import type { TranscriptEvent } from "../../config/sessions/session-accessor.js"; import { selectSessionTranscriptLeafControlledPath } from "../../config/sessions/transcript-tree.js"; import { CURRENT_SESSION_VERSION } from "../../config/sessions/version.js"; import { logWarn } from "../../logger.js"; @@ -388,13 +389,13 @@ export function parseOpaqueLeafEntry(record: unknown): }; } -export function partitionSessionFileEntries(entries: readonly FileEntry[]): { +export function partitionSessionFileEntries(entries: readonly TranscriptEvent[]): { fileEntries: FileEntry[]; - opaqueEntries: Array<{ index: number; record: unknown }>; + opaqueEntries: Array<{ index: number; record: TranscriptEvent }>; fileEntriesByOriginalIndex: Array; } { const fileEntries: FileEntry[] = []; - const opaqueEntries: Array<{ index: number; record: unknown }> = []; + const opaqueEntries: Array<{ index: number; record: TranscriptEvent }> = []; const fileEntriesByOriginalIndex: Array = []; const header = entries.find( (entry) => isRecord(entry) && entry.type === "session" && typeof entry.id === "string", diff --git a/src/agents/sessions/session-manager-core.ts b/src/agents/sessions/session-manager-core.ts index f27f0906d932..98ae3677ce56 100644 --- a/src/agents/sessions/session-manager-core.ts +++ b/src/agents/sessions/session-manager-core.ts @@ -1,7 +1,9 @@ +import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { loadTranscriptEventsSync, replaceTranscriptEventsSync, type SessionTranscriptRuntimeTarget, + type TranscriptEvent, } from "../../config/sessions/session-accessor.js"; import { isSessionTranscriptSideAppendEntry } from "../../config/sessions/transcript-tree.js"; import { CURRENT_SESSION_VERSION } from "../../config/sessions/version.js"; @@ -58,19 +60,19 @@ export class SessionManagerCore { } setSessionTarget(target: SessionManagerPersistenceTarget): void { - const entries = loadTranscriptEventsSync(target) as FileEntry[]; + const entries = loadTranscriptEventsSync(target); const header = entries.find( - (entry) => typeof entry === "object" && entry !== null && entry.type === "session", + (entry): entry is Record => isRecord(entry) && entry.type === "session", ); this.setLoadedSessionTarget(target, entries); - if (header?.cwd) { + if (typeof header?.cwd === "string") { this.cwd = header.cwd; } } protected setLoadedSessionTarget( target: SessionManagerPersistenceTarget | undefined, - entries: FileEntry[], + entries: TranscriptEvent[], ): void { const partitioned = partitionSessionFileEntries(entries); // Only a physically empty transcript may initialize lazily. Opaque persisted rows still need @@ -441,13 +443,17 @@ export class SessionManagerCore { protected getPersistedFileEntries( leafAppendParentId: string | null = this.appendParentId, leafAppendMode?: "side", - ): unknown[] { + ): TranscriptEvent[] { this.clampOpaqueFileEntryIndexes(); - const entries: unknown[] = []; + const entries: TranscriptEvent[] = []; let opaqueIndex = 0; for (let index = 0; index <= this.fileEntries.length; index += 1) { while (this.opaqueFileEntries[opaqueIndex]?.index === index) { - entries.push(this.opaqueFileEntries[opaqueIndex]?.record); + const opaqueEntry = this.opaqueFileEntries[opaqueIndex]; + if (!opaqueEntry) { + break; + } + entries.push(opaqueEntry.record); opaqueIndex += 1; } const entry = this.fileEntries[index]; @@ -456,7 +462,11 @@ export class SessionManagerCore { } } while (opaqueIndex < this.opaqueFileEntries.length) { - entries.push(this.opaqueFileEntries[opaqueIndex]?.record); + const opaqueEntry = this.opaqueFileEntries[opaqueIndex]; + if (!opaqueEntry) { + break; + } + entries.push(opaqueEntry.record); opaqueIndex += 1; } @@ -498,7 +508,7 @@ export class SessionManagerCore { return entries; } - getPersistedEntries(): unknown[] { + getPersistedEntries(): TranscriptEvent[] { return this.getPersistedFileEntries(); } diff --git a/src/agents/sessions/session-manager-persistence.ts b/src/agents/sessions/session-manager-persistence.ts index c5e2ed1fe9d2..3b964f488274 100644 --- a/src/agents/sessions/session-manager-persistence.ts +++ b/src/agents/sessions/session-manager-persistence.ts @@ -180,7 +180,7 @@ export class SessionManagerPersistence extends SessionManagerCore { this.persistenceHeaderPending = false; } const leafEntry = parseOpaqueLeafEntry(entry); - if (leafEntry) { + if (leafEntry && isRecord(entry)) { requireTranscriptEventAppend( appendTranscriptEventSync(scope, entry), `Session transcript leaf control was not persisted: ${leafEntry.id}`, diff --git a/src/agents/sessions/session-manager-types.ts b/src/agents/sessions/session-manager-types.ts index 4f1a8231ca3c..24aab1beb13e 100644 --- a/src/agents/sessions/session-manager-types.ts +++ b/src/agents/sessions/session-manager-types.ts @@ -1,3 +1,4 @@ +import type { TranscriptEvent } from "../../config/sessions/session-accessor.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { ImageContent, TextContent } from "../../llm/types.js"; import type { AgentMessage } from "../runtime/index.js"; @@ -133,7 +134,7 @@ export interface SessionContext { export type PreservedOpaqueFileEntry = { index: number; - record: unknown; + record: TranscriptEvent; }; export type SessionLeafControl = { diff --git a/src/commands/doctor-session-transcript-headers.test.ts b/src/commands/doctor-session-transcript-headers.test.ts index 032518e49b68..af39b9cdb0cd 100644 --- a/src/commands/doctor-session-transcript-headers.test.ts +++ b/src/commands/doctor-session-transcript-headers.test.ts @@ -4,6 +4,7 @@ import { SessionManager } from "../agents/sessions/session-manager.js"; import { loadTranscriptEventsSync, replaceTranscriptEventsSync, + type TranscriptEvent, upsertSessionEntryCore, } from "../config/sessions/session-accessor.js"; import { readTranscriptStorageRows } from "../config/sessions/session-accessor.sqlite-read.js"; @@ -67,7 +68,7 @@ describe("doctor SQLite session transcript header repair", () => { }); async function seedHeaderlessTranscript( - events: readonly unknown[], + events: readonly TranscriptEvent[], options: { spawnedCwd?: string } = { spawnedCwd: SPAWNED_CWD }, ): Promise { await upsertSessionEntryCore(scope, { diff --git a/src/config/sessions/session-accessor.sqlite-contract.ts b/src/config/sessions/session-accessor.sqlite-contract.ts index 30207a8b0a84..e1734abb0d5a 100644 --- a/src/config/sessions/session-accessor.sqlite-contract.ts +++ b/src/config/sessions/session-accessor.sqlite-contract.ts @@ -14,6 +14,7 @@ import type { SessionLifecycleArtifactCleanupResult, SessionLifecycleStoreTarget, } from "./session-accessor.lifecycle-types.js"; +import type { TranscriptEvent } from "./session-accessor.types.js"; import type { ResolvedSessionMaintenanceConfig } from "./store-maintenance.js"; import type { TranscriptEntryAnchor } from "./transcript-entry-anchor.js"; import type { SessionEntry } from "./types.js"; @@ -77,8 +78,6 @@ export type SessionTranscriptInstance = SessionEntrySummary & { updatedAtMs: number; }; -export type TranscriptEvent = unknown; - export type TranscriptEventAppendOptions = { appendIntent?: "active-branch"; }; @@ -119,6 +118,7 @@ export type { SessionTranscriptRawDeltaResult, SessionTranscriptVisibleMessageDeltaLimits, SessionTranscriptVisibleMessageDeltaResult, + TranscriptEvent, } from "./session-accessor.types.js"; export type TranscriptMessageAppendOptions = { diff --git a/src/config/sessions/session-accessor.types.ts b/src/config/sessions/session-accessor.types.ts index 4066dc27bd88..db514160a427 100644 --- a/src/config/sessions/session-accessor.types.ts +++ b/src/config/sessions/session-accessor.types.ts @@ -193,8 +193,8 @@ export type ExactSessionEntry = { entry: SessionEntry; }; -/** Raw transcript record for non-message events; message records use appendTranscriptMessage. */ -export type TranscriptEvent = unknown; +/** JSON event persisted in a transcript; message records use appendTranscriptMessage. */ +export type TranscriptEvent = object | string | number | boolean | null; export type SessionTranscriptStats = { eventCount: number; diff --git a/src/config/sessions/session-reset-boundary-event.ts b/src/config/sessions/session-reset-boundary-event.ts index 3aeffbc61ee9..00d444eef40c 100644 --- a/src/config/sessions/session-reset-boundary-event.ts +++ b/src/config/sessions/session-reset-boundary-event.ts @@ -1,5 +1,6 @@ import { randomUUID } from "node:crypto"; import path from "node:path"; +import type { TranscriptEvent } from "./session-accessor.types.js"; import { DEFAULT_REPLAY_MAX_MESSAGES, replayableTranscriptRole, @@ -21,7 +22,7 @@ type SessionResetBoundaryEvent = { export type SessionResetBoundaryPlan = { event: SessionResetBoundaryEvent; - seedEvents: unknown[]; + seedEvents: object[]; }; function recordId(record: unknown): string | undefined { @@ -154,7 +155,7 @@ async function readLegacyTranscriptEvents(sessionFile: string | undefined): Prom } export async function buildSessionResetBoundaryPlan(params: { - events: readonly unknown[]; + events: readonly TranscriptEvent[]; legacySessionFile?: string; reason: SessionResetBoundaryReason; }): Promise { @@ -169,7 +170,7 @@ export async function buildSessionResetBoundaryPlan(params: { ? [] : await readLegacyTranscriptEvents(params.legacySessionFile); const seedEvents = legacyEvents.filter( - (event) => + (event): event is object => event !== null && typeof event === "object" && !Array.isArray(event) && diff --git a/src/gateway/session-transcript-readers.markers.test.ts b/src/gateway/session-transcript-readers.markers.test.ts index 289273bfa268..58fb1bb551a3 100644 --- a/src/gateway/session-transcript-readers.markers.test.ts +++ b/src/gateway/session-transcript-readers.markers.test.ts @@ -1,7 +1,10 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js"; -import { replaceTranscriptEvents } from "../config/sessions/session-accessor.js"; +import { + replaceTranscriptEvents, + type TranscriptEvent, +} from "../config/sessions/session-accessor.js"; import { closeOpenClawAgentDatabasesForTest } from "../state/openclaw-agent-db.js"; import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; import { captureEnv, setTestEnvValue } from "../test-utils/env.js"; @@ -37,7 +40,7 @@ describe("session transcript reader marker projection", () => { async function writeTranscript( sessionId: string, - events: unknown[], + events: TranscriptEvent[], ): Promise { const scope = { agentId: "main", diff --git a/src/gateway/session-transcript-readers.test.ts b/src/gateway/session-transcript-readers.test.ts index caf5c2d37fe7..79fa6249d406 100644 --- a/src/gateway/session-transcript-readers.test.ts +++ b/src/gateway/session-transcript-readers.test.ts @@ -7,6 +7,7 @@ import * as sessionAccessor from "../config/sessions/session-accessor.js"; import { persistSessionTranscriptTurn, replaceTranscriptEvents, + type TranscriptEvent, upsertSessionEntryCore, } from "../config/sessions/session-accessor.js"; import { waitForSessionTranscriptIndexReconcile } from "../config/sessions/session-transcript-reconcile.js"; @@ -65,7 +66,7 @@ describe("session transcript reader facade", () => { async function writeTranscript( sessionId: string, - events: unknown[], + events: TranscriptEvent[], ): Promise { const scope = { agentId: "main", diff --git a/src/infra/state-migrations.media-persistence.ts b/src/infra/state-migrations.media-persistence.ts index 2bc833c380e2..374d241859e6 100644 --- a/src/infra/state-migrations.media-persistence.ts +++ b/src/infra/state-migrations.media-persistence.ts @@ -127,8 +127,9 @@ function assertEventIdentitiesUnchanged( if (before.length !== after.length) { throw new Error(`${owner} event count changed during media migration`); } - for (let index = 0; index < before.length; index += 1) { - if (eventIdentity(before[index]) !== eventIdentity(after[index])) { + for (const [index, beforeEvent] of before.entries()) { + const afterEvent = after[index]; + if (afterEvent === undefined || eventIdentity(beforeEvent) !== eventIdentity(afterEvent)) { throw new Error(`${owner} event identity changed at index ${index}`); } } diff --git a/src/plugin-sdk/channel-entry-contract.ts b/src/plugin-sdk/channel-entry-contract.ts index 20d0bbe8fb50..b7487f105bcf 100644 --- a/src/plugin-sdk/channel-entry-contract.ts +++ b/src/plugin-sdk/channel-entry-contract.ts @@ -38,7 +38,7 @@ export type { BundledEntryModuleLoadOptions, } from "./channel-entry-contract.types.js"; -type BundledChannelRuntime = unknown; +type BundledChannelRuntime = OpenClawPluginApi["runtime"]; type ChannelEntryConfigSchema = TPlugin extends ChannelPlugin diff --git a/src/plugin-sdk/session-transcript-lock-runtime.ts b/src/plugin-sdk/session-transcript-lock-runtime.ts index 60b7fa8dc71c..85871331a5ad 100644 --- a/src/plugin-sdk/session-transcript-lock-runtime.ts +++ b/src/plugin-sdk/session-transcript-lock-runtime.ts @@ -3,6 +3,7 @@ import { resolveSessionTranscriptRuntimeTarget, withTranscriptWriteLock, type SessionTranscriptWriteLockAccessorContext, + type TranscriptEvent, type TranscriptMessageAppendOptions, type TranscriptMessageAppendResult, type TranscriptUpdatePayload, @@ -31,7 +32,7 @@ export type InternalSessionTranscriptWriteLockContext = { options: Omit, "config">, ) => Promise | undefined>; publishUpdate: (update?: TranscriptUpdatePayload) => Promise; - readEvents: () => Promise; + readEvents: () => Promise; target: InternalSessionTranscriptTarget; }; diff --git a/src/plugin-sdk/session-transcript-runtime.ts b/src/plugin-sdk/session-transcript-runtime.ts index 90c145739064..bb2b1cab9785 100644 --- a/src/plugin-sdk/session-transcript-runtime.ts +++ b/src/plugin-sdk/session-transcript-runtime.ts @@ -20,6 +20,7 @@ import { type SessionTranscriptRawDeltaLimits, type SessionTranscriptRawDeltaResult, type SessionTranscriptVisibleMessageDeltaLimits, + type TranscriptEvent as SessionTranscriptEvent, } from "../config/sessions/session-accessor.js"; import { resolveMirroredTranscriptText } from "../config/sessions/transcript-mirror.js"; import { @@ -67,10 +68,9 @@ export type { SessionTranscriptMemoryHitKey, SessionTranscriptMemoryHitKeyParams, SessionTranscriptReadParams, + SessionTranscriptEvent, }; -export type SessionTranscriptEvent = unknown; - export type SessionTranscriptTargetParams = SessionTranscriptReadParams; /** Scoped target and bounds for one raw generation-aware transcript page. */ diff --git a/test/fixtures/oxlint-type-evidence/no-unknown-type-aliases-violation.ts b/test/fixtures/oxlint-type-evidence/no-unknown-type-aliases-violation.ts new file mode 100644 index 000000000000..3e34c74bb924 --- /dev/null +++ b/test/fixtures/oxlint-type-evidence/no-unknown-type-aliases-violation.ts @@ -0,0 +1,7 @@ +type DirectUnknown = unknown; +type ParenthesizedUnknown = unknown; +type IndirectUnknown = DirectUnknown; + +type GenericAlias = T; +type ConcreteAlias = GenericAlias; +type UsefulBoundary = { value: unknown }; diff --git a/test/fixtures/oxlint-type-evidence/no-widen-then-assert-violation.ts b/test/fixtures/oxlint-type-evidence/no-widen-then-assert-violation.ts new file mode 100644 index 000000000000..19b2f9362d3d --- /dev/null +++ b/test/fixtures/oxlint-type-evidence/no-widen-then-assert-violation.ts @@ -0,0 +1,13 @@ +type User = { id: string }; + +const user: unknown = { id: "user-1" }; +const assertedUser = user as User; + +const original = { id: "user-2" }; +const erased = original as unknown; +const restoredUser = erased as User; + +const preserved = { id: "user-3" } satisfies User; +const preservedUser: User = preserved; + +export { assertedUser, preservedUser, restoredUser }; diff --git a/test/fixtures/oxlint-type-evidence/valid.ts b/test/fixtures/oxlint-type-evidence/valid.ts new file mode 100644 index 000000000000..799ee90b0a41 --- /dev/null +++ b/test/fixtures/oxlint-type-evidence/valid.ts @@ -0,0 +1,14 @@ +type User = { id: string }; +type UsefulBoundary = { value: unknown }; + +const preserved = { id: "user-1" } satisfies User; +const preservedUser: User = preserved; + +function parseBoundary(value: unknown): User | undefined { + if (typeof value !== "object" || value === null || !("id" in value)) { + return undefined; + } + return typeof value.id === "string" ? { id: value.id } : undefined; +} + +export { parseBoundary, preservedUser, type UsefulBoundary }; diff --git a/test/scripts/oxlint-config.test.ts b/test/scripts/oxlint-config.test.ts index fd89bc9778a2..ab75b52ef1dc 100644 --- a/test/scripts/oxlint-config.test.ts +++ b/test/scripts/oxlint-config.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; type OxlintConfig = { ignorePatterns?: string[]; + jsPlugins?: string[]; overrides?: Array<{ excludeFiles?: string[]; files?: string[]; @@ -147,6 +148,7 @@ describe("oxlint config", () => { "docs/_layouts/", ".agents/skills/autoreview/tests/fixtures/**", "test/fixtures/oxlint-boundary-guards/**", + "test/fixtures/oxlint-type-evidence/**", "**/a2ui.bundle.js", "extensions/diffs/assets/viewer-runtime.js", "extensions/diffs-language-pack/assets/viewer-runtime.js", @@ -291,6 +293,18 @@ describe("oxlint config", () => { ]); }); + it("enables the repository type-evidence plugin", () => { + const config = readJson(".oxlintrc.json") as OxlintConfig; + + expect(config.jsPlugins).toContain("./scripts/oxlint-type-evidence.mjs"); + expect(config.rules?.["openclaw-type-evidence/no-unknown-type-aliases"]).toBe("error"); + expect(config.rules?.["openclaw-type-evidence/no-widen-then-assert"]).toBe("error"); + expect(config.overrides).toContainEqual({ + files: ["**/*.d.ts"], + rules: { "openclaw-type-evidence/no-unknown-type-aliases": "off" }, + }); + }); + it("enables clean zero-baseline lint rules", () => { const config = readJson(".oxlintrc.json") as OxlintConfig; diff --git a/test/scripts/oxlint-type-evidence.test.ts b/test/scripts/oxlint-type-evidence.test.ts new file mode 100644 index 000000000000..aeeddf3791ee --- /dev/null +++ b/test/scripts/oxlint-type-evidence.test.ts @@ -0,0 +1,48 @@ +import { spawnSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; + +const FIXTURES = "test/fixtures/oxlint-type-evidence"; +const cases = [ + { + rule: "openclaw-type-evidence/no-unknown-type-aliases", + violation: `${FIXTURES}/no-unknown-type-aliases-violation.ts`, + violations: 3, + }, + { + rule: "openclaw-type-evidence/no-widen-then-assert", + violation: `${FIXTURES}/no-widen-then-assert-violation.ts`, + violations: 2, + }, +]; + +function runRule(target: string) { + return spawnSync( + process.execPath, + [ + "scripts/run-oxlint.mjs", + "--openclaw-focused-config", + "--config", + "config/oxlint/type-evidence.json", + target, + ], + { encoding: "utf8" }, + ); +} + +describe("oxlint type-evidence rules", () => { + it.each(cases)("reports the intended $rule flows", (testCase) => { + const violation = runRule(testCase.violation); + const output = `${violation.stdout}${violation.stderr}`; + expect(violation.status).toBe(1); + expect(output.split(`${testCase.rule.replace("/", "(")})`)).toHaveLength( + testCase.violations + 1, + ); + }); + + it("allows explicit unknown boundaries and preserved type evidence", () => { + const result = runRule(`${FIXTURES}/valid.ts`); + + expect(result.status).toBe(0); + expect(`${result.stdout}${result.stderr}`).not.toContain("openclaw-type-evidence"); + }); +});