From 1b88ff8d5380d707548a06ef5100c993d209ce8f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 3 Jul 2026 13:54:13 -0400 Subject: [PATCH] fix(core): clean up Effect audit patterns (#35174) --- bun.lock | 29 +++++++++++----- package.json | 2 ++ packages/core/src/database/path.ts | 5 ++- packages/core/src/filesystem/watcher.ts | 3 +- packages/core/src/models-dev.ts | 17 ++++++++-- packages/core/src/plugin/provider/openai.ts | 34 ++++++++++--------- .../core/src/plugin/provider/sap-ai-core.ts | 17 +++++----- packages/core/src/project/copy.ts | 3 +- packages/core/src/ripgrep.ts | 6 ++-- packages/core/src/session/projector.ts | 2 +- .../core/src/session/runner/to-llm-message.ts | 15 ++++---- packages/server/src/handlers/credential.ts | 6 ++-- packages/server/src/handlers/fs.ts | 3 +- packages/server/src/handlers/permission.ts | 12 ++++--- packages/server/src/handlers/pty.ts | 3 +- packages/server/src/handlers/question.ts | 6 ++-- script/ast-grep/no-json-parse-cast.yml | 6 ++++ .../no-nested-effect-service-yield.yml | 11 ++++++ 18 files changed, 118 insertions(+), 62 deletions(-) create mode 100644 script/ast-grep/no-json-parse-cast.yml create mode 100644 script/ast-grep/no-nested-effect-service-yield.yml diff --git a/bun.lock b/bun.lock index 5eca1724f18..9c5bbb15919 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ }, "devDependencies": { "@actions/artifact": "5.0.1", + "@ast-grep/cli": "0.44.0", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", "@typescript/native-preview": "catalog:", @@ -1236,6 +1237,22 @@ "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="], + "@ast-grep/cli": ["@ast-grep/cli@0.44.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.44.0", "@ast-grep/cli-darwin-x64": "0.44.0", "@ast-grep/cli-linux-arm64-gnu": "0.44.0", "@ast-grep/cli-linux-x64-gnu": "0.44.0", "@ast-grep/cli-win32-arm64-msvc": "0.44.0", "@ast-grep/cli-win32-ia32-msvc": "0.44.0", "@ast-grep/cli-win32-x64-msvc": "0.44.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-Jf4PuP7XjzsMa3m9gYxmzV8KyWZc4w1ZzKe/t0+90wWxmSasQJe6AtMkJxHEi98MGgfAF1nWziqjDd0/6EsBjA=="], + + "@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.44.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bF7euu/hF/cYg4510z8110vh60rrqfrBdsfRqVGd6xqNSPENu7CJnTVN/Z4Nk5U1NM8YKzUD+dYx1ySUJ0CUNQ=="], + + "@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.44.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0fI9caQGp1dFcmBATNlVytIRdAeYb91v1D2xjMIi1bSX+l8Uj846JUiaimUGBuBZmyFq+BScoWM4RnprEmZMpQ=="], + + "@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-JB6EUnqEtGGtyg1GqNquld/++1CvaWD7r84IwwhddX1qx0NmDoHyn2mKd8vnQ24Z0RkV3g7y7foMLakELbGtDw=="], + + "@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rNL0LsI682D9EMzfaGVEtZa1xaqTtGb2I+Zk4ZzidX6u+fF7f79wdqyKahKjXzoIrGkuhkoL3gcyLKAtQd9+qg=="], + + "@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.44.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-lqD0MhGQAddh2YoV/brKQ6GVcFLmRiTBwIElutwedaUvRCdasTGukFPYuSWk/iI8Kv19xom6s7l+mGuZ7v+xwQ=="], + + "@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.44.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-ZJrnS+2OkNfwyr6yrN69glP67uybBxDvl9mqZvh1J44vB3OFn9U9c+cVAoZIAo7JD5F4rZNxwyu3gcy4+xuwEA=="], + + "@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.44.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OJEo7f95YYaSuS1byUB7ZctbzxoA7/wCoAol+pt6pvfdW/8Wq+L1qU28glwx7dQ0HgTsnPZbWpXQwmZpCBHhZg=="], + "@astrojs/check": ["@astrojs/check@0.9.6", "", { "dependencies": { "@astrojs/language-server": "^2.16.1", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA=="], "@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="], @@ -3490,7 +3507,7 @@ "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], - "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], @@ -6048,6 +6065,8 @@ "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], "@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], @@ -6114,8 +6133,6 @@ "@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], - "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -6328,8 +6345,6 @@ "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], - "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="], @@ -6360,8 +6375,6 @@ "node-gyp/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="], - "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], @@ -6428,8 +6441,6 @@ "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], - "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], "shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], diff --git a/package.json b/package.json index 226a86ff2c6..0810127669f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", "lint": "oxlint", + "lint:effect-patterns": "ast-grep scan --rule script/ast-grep/no-json-parse-cast.yml packages/core/src packages/server/src packages/protocol/src && ast-grep scan --rule script/ast-grep/no-nested-effect-service-yield.yml packages/core/src packages/server/src packages/protocol/src", "typecheck": "bun turbo typecheck", "upgrade-opentui": "bun run script/upgrade-opentui.ts", "postinstall": "bun run --cwd packages/core fix-node-pty", @@ -95,6 +96,7 @@ }, "devDependencies": { "@actions/artifact": "5.0.1", + "@ast-grep/cli": "0.44.0", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", "@typescript/native-preview": "catalog:", diff --git a/packages/core/src/database/path.ts b/packages/core/src/database/path.ts index 379d5f8aa76..93fd2d5773b 100644 --- a/packages/core/src/database/path.ts +++ b/packages/core/src/database/path.ts @@ -1,5 +1,6 @@ import nodePath from "path" import { customType } from "drizzle-orm/sqlite-core" +import { Schema } from "effect" import { AbsolutePath } from "../schema" function storagePath(input: string) { @@ -74,6 +75,8 @@ export const pathColumn = customType<{ }, }) +const decodeAbsoluteArray = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Array(Schema.String))) + export const absoluteArrayColumn = customType<{ data: AbsolutePath[] driverData: string @@ -86,6 +89,6 @@ export const absoluteArrayColumn = customType<{ return JSON.stringify(input.map(absolute)) }, fromDriver(input) { - return (JSON.parse(input) as string[]).map((item) => AbsolutePath.make(toPlatform(absolute(item)))) + return decodeAbsoluteArray(input).map((item) => AbsolutePath.make(toPlatform(absolute(item)))) }, }) diff --git a/packages/core/src/filesystem/watcher.ts b/packages/core/src/filesystem/watcher.ts index fe7a5b2594c..1efc9d69074 100644 --- a/packages/core/src/filesystem/watcher.ts +++ b/packages/core/src/filesystem/watcher.ts @@ -113,7 +113,8 @@ const layer = Layer.effect( ) } - const config = (yield* (yield* Config.Service).entries()) + const configService = yield* Config.Service + const config = (yield* configService.entries()) .filter((entry): entry is Config.Document => entry.type === "document") .flatMap((item) => item.info.watcher?.ignore ?? []) yield* Effect.forkScoped( diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index cf2991168a0..8a3fb965bfc 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -47,7 +47,7 @@ const Cost = Schema.Struct({ const ReasoningOption = Schema.Union([ Schema.Struct({ type: Schema.Literal("effort"), - values: Schema.Array(Schema.String), + values: Schema.Array(Schema.Union([Schema.String, Schema.Null])), }), Schema.Struct({ type: Schema.Literal("toggle"), @@ -125,6 +125,10 @@ export const Provider = Schema.Struct({ export type Provider = Schema.Schema.Type +const Providers = Schema.Record(Schema.String, Provider) +const decodeProviders = Schema.decodeUnknownEffect(Schema.fromJsonString(Providers)) +const decodeProvidersUnknown = Schema.decodeUnknownEffect(Providers) + export const Event = ModelsDev.Event declare const OPENCODE_MODELS_DEV: Record | undefined @@ -176,6 +180,7 @@ const layer = Layer.effect( }) const loadFromDisk = fs.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).pipe( + Effect.flatMap(decodeProvidersUnknown), Effect.catch((error) => { if ( Flag.OPENCODE_MODELS_PATH === undefined && @@ -186,11 +191,17 @@ const layer = Layer.effect( } return Effect.succeed(undefined) }), - Effect.map((v) => v as Record | undefined), ) const loadSnapshot = Effect.sync(() => typeof OPENCODE_MODELS_DEV === "undefined" ? undefined : OPENCODE_MODELS_DEV, + ).pipe( + Effect.flatMap((snapshot) => + snapshot === undefined ? Effect.succeed(undefined) : decodeProvidersUnknown(snapshot), + ), + Effect.catch((cause) => + Effect.logWarning("bundled models snapshot failed schema decode", { cause }).pipe(Effect.as(undefined)), + ), ) const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () { @@ -221,7 +232,7 @@ const layer = Layer.effect( return yield* fetchAndWrite() }), ) - return JSON.parse(text) as Record + return yield* decodeProviders(text) }).pipe(Effect.withSpan("ModelsDev.populate"), Effect.orDie) const [cachedGet, invalidate] = yield* Effect.cachedInvalidateWithTTL(populate, Duration.infinity) diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts index 82e2319a1b0..e8974195a6c 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -1,7 +1,7 @@ import { createServer } from "node:http" import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { Deferred, Effect, Semaphore, Stream } from "effect" +import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect" import type { Scope } from "effect" import { Credential } from "../../credential" import { EventV2 } from "../../event" @@ -32,11 +32,16 @@ type TokenResponse = { expires_in?: number } -type Claims = { - chatgpt_account_id?: string - organizations?: Array<{ id: string }> - "https://api.openai.com/auth"?: { chatgpt_account_id?: string } -} +const Claims = Schema.fromJsonString( + Schema.Struct({ + chatgpt_account_id: Schema.optional(Schema.String), + organizations: Schema.optional(Schema.Array(Schema.Struct({ id: Schema.String }))), + "https://api.openai.com/auth": Schema.optional( + Schema.Struct({ chatgpt_account_id: Schema.optional(Schema.String) }), + ), + }), +) +const decodeClaims = Schema.decodeUnknownOption(Claims) const browser = { integrationID: Integration.ID.make("openai"), @@ -315,14 +320,11 @@ function extractAccountID(tokens: TokenResponse) { function claim(token: string) { const part = token.split(".")[1] if (!part) return - try { - const claims = JSON.parse(Buffer.from(part, "base64url").toString()) as Claims - return ( - claims.chatgpt_account_id ?? - claims["https://api.openai.com/auth"]?.chatgpt_account_id ?? - claims.organizations?.[0]?.id - ) - } catch { - return - } + const claims = Option.getOrUndefined(decodeClaims(Buffer.from(part, "base64url").toString())) + if (!claims) return + return ( + claims.chatgpt_account_id ?? + claims["https://api.openai.com/auth"]?.chatgpt_account_id ?? + claims.organizations?.[0]?.id + ) } diff --git a/packages/core/src/plugin/provider/sap-ai-core.ts b/packages/core/src/plugin/provider/sap-ai-core.ts index 8c668d8b414..b6c3a540e61 100644 --- a/packages/core/src/plugin/provider/sap-ai-core.ts +++ b/packages/core/src/plugin/provider/sap-ai-core.ts @@ -19,17 +19,18 @@ export const SapAICorePlugin = define({ const installedPath = evt.package.startsWith("file://") ? evt.package : (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint - if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`) + if (!installedPath) return yield* Effect.die(new Error(`Package ${evt.package} has no import entrypoint`)) - const mod = yield* Effect.promise(async () => { - return (await import( - installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href - )) as Record any> - }).pipe(Effect.orDie) + const mod: Record = yield* Effect.promise( + () => import(installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href), + ) const match = Object.keys(mod).find((name) => name.startsWith("create")) - if (!match) throw new Error(`Package ${evt.package} has no provider factory export`) + if (!match) return yield* Effect.die(new Error(`Package ${evt.package} has no provider factory export`)) + const factory = mod[match] + if (typeof factory !== "function") + return yield* Effect.die(new Error(`Package ${evt.package} provider factory export is not callable`)) - evt.sdk = mod[match]( + evt.sdk = factory( serviceKey ? { deploymentId: process.env.AICORE_DEPLOYMENT_ID, resourceGroup: process.env.AICORE_RESOURCE_GROUP } : {}, diff --git a/packages/core/src/project/copy.ts b/packages/core/src/project/copy.ts index 0980ef44f75..9183e26eb9f 100644 --- a/packages/core/src/project/copy.ts +++ b/packages/core/src/project/copy.ts @@ -202,7 +202,8 @@ const layer = Layer.effect( const copyDirectory = yield* canonical(input.directory) const stored = yield* directories.get({ projectID: input.projectID, directory: copyDirectory }) if (!stored?.strategy) return yield* new InvalidDirectoryError({ directory: copyDirectory }) - yield* (yield* getStrategy(StrategyID.make(stored.strategy))).remove({ + const strategy = yield* getStrategy(StrategyID.make(stored.strategy)) + yield* strategy.remove({ directory: copyDirectory, force: input.force, }) diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index ac8ea52d934..7e1056c4d82 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -35,6 +35,7 @@ const RawMatch = Schema.Struct({ ), }), }) +const decodeJsonRecord = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString) type RawMatchData = (typeof RawMatch.Type)["data"] @@ -232,10 +233,7 @@ const layer = Layer.effect( parse: (line) => (Buffer.byteLength(line, "utf8") > MAX_RECORD_BYTES ? Effect.fail(failure(`Ripgrep JSON record exceeded ${MAX_RECORD_BYTES} bytes`)) - : Effect.try({ - try: () => JSON.parse(line) as unknown, - catch: (cause) => failure("Invalid ripgrep JSON output", cause), - }) + : decodeJsonRecord(line).pipe(Effect.mapError((cause) => failure("Invalid ripgrep JSON output", cause))) ).pipe( Effect.flatMap((json) => { if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match") diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index c7145e8dbac..5e2d112229d 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -438,7 +438,7 @@ function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, me const layer = Layer.effectDiscard( Effect.gen(function* () { const events = yield* EventV2.Service - const { db } = yield* Database.Service + const db = (yield* Database.Service).db yield* events.project(SessionV1.Event.Created, (event) => Effect.gen(function* () { const stored = yield* db diff --git a/packages/core/src/session/runner/to-llm-message.ts b/packages/core/src/session/runner/to-llm-message.ts index 8ddd7cce5c3..0d9055e4803 100644 --- a/packages/core/src/session/runner/to-llm-message.ts +++ b/packages/core/src/session/runner/to-llm-message.ts @@ -7,6 +7,7 @@ import { type Model, type ProviderMetadata, } from "@opencode-ai/llm" +import { Option, Schema } from "effect" import { SessionMessage } from "../message" import type { FileAttachment } from "../prompt" @@ -18,14 +19,12 @@ const media = (file: FileAttachment): ContentPart => ({ metadata: file.description === undefined ? undefined : { description: file.description }, }) -const toolInput = (tool: SessionMessage.AssistantTool) => { - if (tool.state.status !== "pending") return tool.state.input - try { - return JSON.parse(tool.state.input) as unknown - } catch { - return tool.state.input - } -} +const decodeToolInput = Schema.decodeUnknownOption(Schema.UnknownFromJsonString) + +const toolInput = (tool: SessionMessage.AssistantTool) => + tool.state.status === "pending" + ? Option.getOrElse(decodeToolInput(tool.state.input), () => tool.state.input) + : tool.state.input const toolCall = (tool: SessionMessage.AssistantTool, providerMetadata: ProviderMetadata | undefined): ContentPart => ToolCallPart.make({ diff --git a/packages/server/src/handlers/credential.ts b/packages/server/src/handlers/credential.ts index 7e138a5d5a2..49e84d49baf 100644 --- a/packages/server/src/handlers/credential.ts +++ b/packages/server/src/handlers/credential.ts @@ -8,14 +8,16 @@ export const CredentialHandler = HttpApiBuilder.group(Api, "server.credential", .handle( "credential.update", Effect.fn(function* (ctx) { - yield* (yield* Integration.Service).connection.update(ctx.params.credentialID, { label: ctx.payload.label }) + const integration = yield* Integration.Service + yield* integration.connection.update(ctx.params.credentialID, { label: ctx.payload.label }) return HttpApiSchema.NoContent.make() }), ) .handle( "credential.remove", Effect.fn(function* (ctx) { - yield* (yield* Integration.Service).connection.remove(ctx.params.credentialID) + const integration = yield* Integration.Service + yield* integration.connection.remove(ctx.params.credentialID) return HttpApiSchema.NoContent.make() }), ), diff --git a/packages/server/src/handlers/fs.ts b/packages/server/src/handlers/fs.ts index c7d1d43bab7..d623f2d7e64 100644 --- a/packages/server/src/handlers/fs.ts +++ b/packages/server/src/handlers/fs.ts @@ -11,7 +11,8 @@ export const FileSystemHandler = HttpApiBuilder.group(Api, "server.fs", (handler return handlers .handleRaw("fs.read", (ctx) => Effect.gen(function* () { - const file = yield* (yield* FileSystem.Service).read({ + const fs = yield* FileSystem.Service + const file = yield* fs.read({ path: RelativePath.make( decodeURIComponent(new URL(ctx.request.url, "http://localhost").pathname.slice(13)), ), diff --git a/packages/server/src/handlers/permission.ts b/packages/server/src/handlers/permission.ts index 0425c149961..cbc49180f64 100644 --- a/packages/server/src/handlers/permission.ts +++ b/packages/server/src/handlers/permission.ts @@ -17,7 +17,8 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", .handle( "permission.request.list", Effect.fn(function* () { - return yield* response((yield* PermissionV2.Service).list()) + const permission = yield* PermissionV2.Service + return yield* response(permission.list()) }), ) .handle( @@ -59,7 +60,8 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", .handle( "session.permission.get", Effect.fn(function* (ctx) { - const request = yield* (yield* PermissionV2.Service).get(ctx.params.requestID) + const permission = yield* PermissionV2.Service + const request = yield* permission.get(ctx.params.requestID) if (!request || request.sessionID !== ctx.params.sessionID) return yield* missingRequest(ctx.params.requestID) return { data: request } }), @@ -80,8 +82,9 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", "permission.saved.list", Effect.fn(function* (ctx) { const location = yield* Location.Service + const saved = yield* PermissionSaved.Service return { - data: yield* (yield* PermissionSaved.Service).list({ + data: yield* saved.list({ projectID: ctx.query.projectID ?? location.project.id, }), } @@ -90,7 +93,8 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", .handle( "permission.saved.remove", Effect.fn(function* (ctx) { - yield* (yield* PermissionSaved.Service).remove(ctx.params.id) + const saved = yield* PermissionSaved.Service + yield* saved.remove(ctx.params.id) return HttpApiSchema.NoContent.make() }), ) diff --git a/packages/server/src/handlers/pty.ts b/packages/server/src/handlers/pty.ts index cda2cf43e9e..e2c5fcc5685 100644 --- a/packages/server/src/handlers/pty.ts +++ b/packages/server/src/handlers/pty.ts @@ -32,7 +32,8 @@ export const PtyHandler = HttpApiBuilder.group(Api, "server.pty", (handlers) => .handle( "pty.list", Effect.fn(function* () { - return yield* response((yield* Pty.Service).list()) + const pty = yield* Pty.Service + return yield* response(pty.list()) }), ) .handle( diff --git a/packages/server/src/handlers/question.ts b/packages/server/src/handlers/question.ts index 954afe0df58..d8011dd2697 100644 --- a/packages/server/src/handlers/question.ts +++ b/packages/server/src/handlers/question.ts @@ -26,13 +26,15 @@ export const QuestionHandler = HttpApiBuilder.group(Api, "server.question", (han .handle( "question.request.list", Effect.fn(function* () { - return yield* response((yield* QuestionV2.Service).list()) + const question = yield* QuestionV2.Service + return yield* response(question.list()) }), ) .handle( "session.question.list", Effect.fn(function* (ctx) { - const requests = yield* (yield* QuestionV2.Service).list() + const question = yield* QuestionV2.Service + const requests = yield* question.list() return { data: requests.filter((request) => request.sessionID === ctx.params.sessionID) } }), ) diff --git a/script/ast-grep/no-json-parse-cast.yml b/script/ast-grep/no-json-parse-cast.yml new file mode 100644 index 00000000000..923528d8809 --- /dev/null +++ b/script/ast-grep/no-json-parse-cast.yml @@ -0,0 +1,6 @@ +id: no-json-parse-cast +language: TypeScript +message: Prefer Effect Schema JSON decoding over JSON.parse casts. +severity: error +rule: + pattern: JSON.parse($INPUT) as $TYPE diff --git a/script/ast-grep/no-nested-effect-service-yield.yml b/script/ast-grep/no-nested-effect-service-yield.yml new file mode 100644 index 00000000000..aaa6107a483 --- /dev/null +++ b/script/ast-grep/no-nested-effect-service-yield.yml @@ -0,0 +1,11 @@ +id: no-nested-effect-service-yield +language: TypeScript +message: Bind Effect services before calling methods instead of nesting service yields. +severity: error +rule: + any: + - pattern: (yield* $SERVICE).$METHOD($$$ARGS) + - pattern: (yield* $SERVICE).$PROPERTY.$METHOD($$$ARGS) +constraints: + SERVICE: + regex: \.Service$