Resolve PR #36 review feedback: untrack dist/ and sync with upstream main#1
Conversation
Adds `opencodezen` as a new AI gateway option alongside the existing `vercel`, `openrouter`, and `cloudflare` options. Uses @ai-sdk/openai with baseURL https://opencode.ai/zen/v1 (OpenAI-compatible endpoint). Includes model ID aliasing to map canonical provider/model IDs to Zen's naming convention (e.g. anthropic/claude-haiku-4.5 → claude-haiku-4-5). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Override tar to ^7.5.0 to ensure latest patched version - Fixes CVE-2026-23745, CVE-2026-24842, CVE-2026-26960, CVE-2026-29786, CVE-2026-31802 - Resolves hardlink/symlink path traversal vulnerabilities in node-tar
feat(cua): add CUA mode via OpenAI Responses API
feat: support per-step and per-call AI overrides in runSteps/runUserFlow
fix: patch tar vulnerabilities via npm overrides
Previously the browser_upload_file tool had a schema/implementation mismatch: the schema claimed to accept absolute paths but the implementation always prefixed with uploadBasePath, causing absolute paths like /tmp/file.png to become invalid (./uploads//tmp/file.png). - Extract resolveUploadPath() helper that detects absolute paths (Unix / or Windows C:\) and uses them as-is - Update Zod schema description to document both absolute/relative path support - Remove misleading inline comment - Add unit tests for all path resolution cases Closes bug0inc#45
Add video assertions: record step run and validate via Gemini Files API
…-models feat: add support for openai models
…-run-step Sam/extend callback url to run step
feat: add OpenCode Zen gateway support
…le-path-consistency fix: support both absolute and relative paths in browser_upload_file
Add consensusPolicy: fail-on-disagreement option for assertions
The dist/ build output is already listed in .gitignore and should not be tracked. Per maintainer review feedback on PR bug0inc#36, untrack it.
…85n8 # Conflicts: # README.md # src/__tests__/data-cache.test.ts # src/__tests__/integration/run-steps.test.ts # src/data-cache.ts # src/index.ts # src/redis.ts
There was a problem hiding this comment.
Code Review
This pull request introduces CUA (Computer-Use Agent) mode for visual, coordinate-based browser automation via OpenAI's Responses API, video-based assertions evaluated via Gemini's Files API, support for the OpenCode Zen gateway, and configurable consensus policies for assertion disagreements. It also refactors Redis and Cache initialization to be lazy. The review feedback highlights several critical issues: potential TypeError crashes due to unsafe property access on test?.info() in src/assertion.ts and missing null checks on action in src/cua/actions.ts; a compatibility issue with screencast in src/video.ts on non-Chromium browsers; and configuration-sync bugs in src/redis.ts and src/cache.ts where static initialization flags prevent re-configuration when configure() is called with new parameters.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| test?.info().annotations.push({ | ||
| type: "AI Summary (video analysis)", | ||
| description: videoResult.reasoning, | ||
| }); |
There was a problem hiding this comment.
Unsafe property access test?.info().annotations when test is undefined. If test is not provided, test?.info() will evaluate to undefined, and attempting to access .annotations on it will throw a TypeError. Use optional chaining to safely access the annotations array.
test?.info()?.annotations.push({\n type: \"AI Summary (video analysis)\",\n description: videoResult.reasoning,\n });| let client: Redis | null = null; | ||
| let initialized = false; | ||
|
|
||
| /** | ||
| * Returns a memoized Redis client. Reads `configure({ redis: { url } })` first, | ||
| * then falls back to `process.env.REDIS_URL`. Returns null when neither is set, | ||
| * which disables step caching, {{global.*}} placeholders, and project data. | ||
| * | ||
| * Lazy: the connection is opened on first call so users can call `configure()` | ||
| * before any Redis-dependent code path runs. | ||
| */ | ||
| export function getRedis(): Redis | null { | ||
| if (initialized) return client; | ||
| initialized = true; | ||
|
|
||
| const url = getConfig().redis?.url ?? process.env.REDIS_URL; | ||
| if (!url) { | ||
| logger.warn( | ||
| "Redis URL not set (configure({ redis: { url } }) or REDIS_URL). " + | ||
| "Step caching, global placeholders, and project data are disabled.", | ||
| ); | ||
| return null; | ||
| } | ||
|
|
||
| client = new Redis(url); | ||
| return client; | ||
| } |
There was a problem hiding this comment.
Caching the initialized state prevents re-configuration of the Redis client when configure() is called with a new URL. Instead of a static initialized flag, track the active URL and re-initialize the client if the URL changes.
let client: Redis | null = null;\nlet activeUrl: string | undefined = undefined;\n\n/**\n * Returns a memoized Redis client. Reads `configure({ redis: { url } })` first,\n * then falls back to `process.env.REDIS_URL`. Returns null when neither is set,\n * which disables step caching, {{global.*}} placeholders, and project data.\n *\n * Lazy: the connection is opened on first call so users can call `configure()`\n * before any Redis-dependent code path runs.\n */\nexport function getRedis(): Redis | null {\n const url = getConfig().redis?.url ?? process.env.REDIS_URL;\n \n if (url !== activeUrl) {\n if (client) {\n client.disconnect();\n client = null;\n }\n activeUrl = url;\n if (url) {\n client = new Redis(url);\n } else {\n logger.warn(\n \"Redis URL not set (configure({ redis: { url } }) or REDIS_URL). \" +\n \"Step caching, global placeholders, and project data are disabled.\",\n );\n }\n }\n return client;\n}| let cacheInstance: CacheStore | null = null; | ||
| let initialized = false; | ||
|
|
||
| /** | ||
| * Creates the cache store based on environment variables. | ||
| * Returns a memoized cache store based on the `CACHE_PROVIDER` env var: | ||
| * - "redis" (also the default): uses Redis via `getRedis()`, which honors | ||
| * `configure({ redis: { url } })` and `REDIS_URL`. | ||
| * - "file": uses JSON files on disk at `CACHE_DIR` (defaults to | ||
| * `.passmark-cache`). Handy for committing a warm cache to Git for CI. | ||
| * - "none": disables caching entirely. | ||
| * | ||
| * CACHE_PROVIDER selects the backend: | ||
| * - "redis" (default when REDIS_URL is set): uses Redis via ioredis | ||
| * - "file": uses JSON files on disk at CACHE_DIR (defaults to .passmark-cache) | ||
| * - "none": disables caching entirely | ||
| * Returns null when caching is disabled or no Redis URL is configured, which | ||
| * disables step caching, {{global.*}} placeholders, and project data. | ||
| * | ||
| * For backwards compatibility, if CACHE_PROVIDER is not set: | ||
| * - If REDIS_URL is set → uses Redis | ||
| * - Otherwise → caching is disabled (null) | ||
| * Lazy: resolved on first call so users can call `configure()` beforehand. | ||
| */ | ||
| function createCacheStore(): CacheStore | null { | ||
| export function getCache(): CacheStore | null { | ||
| if (initialized) return cacheInstance; | ||
| initialized = true; | ||
|
|
||
| const provider = process.env.CACHE_PROVIDER?.toLowerCase(); | ||
|
|
||
| if (provider === "none") { | ||
| logger.warn("Cache provider set to 'none'. Caching is disabled."); | ||
| return null; | ||
| logger.warn("CACHE_PROVIDER is 'none'. Caching is disabled."); | ||
| cacheInstance = null; | ||
| return cacheInstance; | ||
| } | ||
|
|
||
| if (provider === "file") { | ||
| const dir = process.env.CACHE_DIR || ".passmark-cache"; | ||
| logger.info(`Using file-based cache at: ${dir}`); | ||
| return new FileStore(dir); | ||
| } | ||
|
|
||
| if (provider === "redis" || (!provider && process.env.REDIS_URL)) { | ||
| if (!process.env.REDIS_URL) { | ||
| logger.warn("CACHE_PROVIDER is 'redis' but REDIS_URL is not set. Caching is disabled."); | ||
| return null; | ||
| } | ||
| logger.info("Using Redis cache."); | ||
| return new RedisStore(process.env.REDIS_URL); | ||
| cacheInstance = new FileStore(dir); | ||
| return cacheInstance; | ||
| } | ||
|
|
||
| if (provider) { | ||
| if (provider && provider !== "redis") { | ||
| logger.warn(`Unknown CACHE_PROVIDER '${provider}'. Caching is disabled.`); | ||
| return null; | ||
| cacheInstance = null; | ||
| return cacheInstance; | ||
| } | ||
|
|
||
| // No CACHE_PROVIDER and no REDIS_URL | ||
| logger.warn( | ||
| "No cache provider configured. Set CACHE_PROVIDER=redis|file|none or REDIS_URL. " + | ||
| "Step caching, global placeholders, and project data are disabled.", | ||
| ); | ||
| return null; | ||
| // Default ("redis" or unset): use the configured Redis client. getRedis() | ||
| // already warns when no URL is set. | ||
| const redis = getRedis(); | ||
| cacheInstance = redis ? new RedisStore(redis) : null; | ||
| return cacheInstance; | ||
| } |
There was a problem hiding this comment.
Caching the initialized state prevents re-configuration of the Cache store when configure() or env vars are updated. Track the active provider, directory, and redis client to dynamically re-initialize the cache store when configuration changes.
let cacheInstance: CacheStore | null = null;\nlet activeProvider: string | undefined = undefined;\nlet activeDir: string | undefined = undefined;\nlet activeRedis: any = undefined;\n\n/**\n * Returns a memoized cache store based on the `CACHE_PROVIDER` env var:\n * - \"redis\" (also the default): uses Redis via `getRedis()`, which honors\n * `configure({ redis: { url } })` and `REDIS_URL`.\n * - \"file\": uses JSON files on disk at `CACHE_DIR` (defaults to\n * `.passmark-cache`). Handy for committing a warm cache to Git for CI.\n * - \"none\": disables caching entirely.\n *\n * Returns null when caching is disabled or no Redis URL is configured, which\n * disables step caching, {{global.*}} placeholders, and project data.\n *\n * Lazy: resolved on first call so users can call `configure()` beforehand.\n */\nexport function getCache(): CacheStore | null {\n const provider = process.env.CACHE_PROVIDER?.toLowerCase();\n const dir = process.env.CACHE_DIR || \".passmark-cache\";\n const redis = getRedis();\n\n if (\n provider !== activeProvider ||\n (provider === \"file\" && dir !== activeDir) ||\n (provider !== \"none\" && provider !== \"file\" && redis !== activeRedis)\n ) {\n activeProvider = provider;\n activeDir = dir;\n activeRedis = redis;\n\n if (provider === \"none\") {\n logger.warn(\"CACHE_PROVIDER is 'none'. Caching is disabled.\");\n cacheInstance = null;\n } else if (provider === \"file\") {\n logger.info(`Using file-based cache at: ${dir}`);\n cacheInstance = new FileStore(dir);\n } else if (provider && provider !== \"redis\") {\n logger.warn(`Unknown CACHE_PROVIDER '${provider}'. Caching is disabled.`);\n cacheInstance = null;\n } else {\n cacheInstance = redis ? new RedisStore(redis) : null;\n }\n }\n return cacheInstance;\n}| export async function executeAction(page: Page, action: ComputerAction): Promise<void> { | ||
| switch (action.type) { |
There was a problem hiding this comment.
Missing null/undefined check on action in executeAction can cause a TypeError crash if an empty or null action is passed. Add a defensive guard at the beginning of the function.
export async function executeAction(page: Page, action: ComputerAction): Promise<void> {\n if (!action) return;\n switch (action.type) {| await resolvePage(this.page).screencast.start({ | ||
| path: this.filePath, | ||
| size: { width: VIDEO_DEFAULT_WIDTH, height: VIDEO_DEFAULT_HEIGHT }, | ||
| }); |
There was a problem hiding this comment.
Unsafe access to screencast on Playwright's Page object without checking if it is supported. screencast is an internal, undocumented Chromium-only API. If the tests are run on Firefox or WebKit, this will throw a TypeError. Add a check to verify screencast is supported before starting the recording.
const pageInstance = resolvePage(this.page) as any;\n if (!pageInstance.screencast) {\n throw new Error(\"Screencast is not supported on this browser/platform (Chromium only)\");\n }\n await pageInstance.screencast.start({\n path: this.filePath,\n size: { width: VIDEO_DEFAULT_WIDTH, height: VIDEO_DEFAULT_HEIGHT },\n });
What
Addresses the two requests from maintainer @panda-sandeep on bug0inc/passmark#36 (the "Pluggable cache with redis as fallback" PR, whose head is
sparklayer-io:cache-plugin):dist/was force-committed despite being listed in.gitignore. This branch removes it from tracking (git rm -r --cached dist); it stays ignored.cache-pluginhad diverged significantly frombug0inc/passmark:main. This branch merges current upstreammainand resolves all conflicts.How the merge conflicts were resolved
Upstream had meanwhile refactored
src/redis.tsfrom a module-level singleton into a lazy, config-awaregetRedis()(honouringconfigure({ redis: { url } })thenREDIS_URL). The cache-plugin feature had instead replacedredis.tswith an eagerly-constructed pluggablecachestore.Rather than pick one design, the resolution keeps upstream's richer
redis.tsand reworkssrc/cache.tsinto a pluggable layer that wraps it:getCache()is now lazy + memoized (mirrorsgetRedis()), soconfigure()still works before first use.CACHE_PROVIDERselects the backend:redis(default),file(JSON on disk atCACHE_DIR, handy for committing a warm cache to Git for CI — the original use case), ornone.redisbackend delegates togetRedis(), so the upstreamconfigure({ redis })API andREDIS_URLfallback are preserved.data-cache.tsandindex.tsnow callgetCache(); tests mock../cache'sgetCache.Also fixed a duplicate
createOpenAIimport insrc/models.tsintroduced by the auto-merge, and documentedCACHE_PROVIDER/CACHE_DIRin.env.exampleand the README.Verification
pnpm run build(tsc) — passespnpm test— 164 passedpnpm run lint— 0 errors (pre-existinganywarnings only)Note on branches
This PR targets
cache-pluginso the diff shows exactly the dist removal + upstream sync. To actually update bug0inc#36, these commits need to land on thecache-pluginbranch (e.g. merge this PR, or I can push directly tocache-pluginif you'd prefer — just say the word).https://claude.ai/code/session_01WvniLbWm19ZpfXxKRyW6Kv
Generated by Claude Code