Skip to content

fix(opencode): sync runtime safety hotfixes#504

Merged
Astro-Han merged 16 commits intodevfrom
codex/i477-runtime-safety-plan
May 8, 2026
Merged

fix(opencode): sync runtime safety hotfixes#504
Astro-Han merged 16 commits intodevfrom
codex/i477-runtime-safety-plan

Conversation

@Astro-Han
Copy link
Copy Markdown
Owner

@Astro-Han Astro-Han commented May 8, 2026

Summary

  • Syncs PR 3 runtime/session/tool safety hotfixes from upstream target 014dbd34c4f5612d9a037b3641a8244b213a8a30 in five atomic commits.
  • Hardens tool permissions, subagent cancellation, compaction replay, bounded VCS/resource handling, retry classification, malformed unicode sanitization, and model replay serialization.
  • Keeps #24992 codesearch removal split out because the local surface includes generated OpenAPI/SDK and broad app/UI references.

Why

#477 is tracking upstream sync after opencode 17701628bd. PR #482 and #487 already landed the provider/model and auth/client slices. This PR implements the next consolidated runtime/session/tool slice while keeping desktop, HttpApi/listener, generated SDK/OpenAPI, dependency, and package-manifest churn out of scope.

Included upstream anchors:

#21114 read unsupported image guard
#23290 child session external_directory permission inheritance
#24576 LSP workspace symbol forwarding, already absorbed locally
#24553 bash parser cleanup, already covered by local bash parser shape
#24861 tree-sitter bash tree cleanup
#24864 withTimeout rejected-promise timer cleanup
#24898 compaction fork tail remap
#25019 MCP remote URL validation
#25123 external skills flag semantics
#25226 opencode-owned tmp directory preapproval
#25241 omit empty text in media-only tool results
#25354 npm cache detection fallback, local equivalent added
#25431 read offset 0 semantics
#25452 Tool.execute spans for MCP/plugin tools
#25581 bounded git/VCS patch capture
#25723 worktree createFromInfo async boot
#25787 malformed patch boundary, adapted to local VCS/session-diff surface
#25798 subtask cancellation propagation
#25851 compaction summary/tail ordering
#25888 retry server_is_overloaded
#25934 malformed surrogate sanitization
#21370 Anthropic signed reasoning replay separator
#26037 formatter stdout/stderr ignored to avoid hangs
#26276 Bedrock/Anthropic replay filtering and Bedrock PDF tool-result media routing

Split/defer decisions:

#24992 split to PR 3a: local codesearch still spans runtime, app/UI/i18n, generated SDK, and OpenAPI surfaces, so it would violate this PR's boundary.
#24576 already absorbed: local LSP tool already forwards query and has coverage.
#24553 already compatible: local bash parser shape differs, but #24861 covers the cleanup behavior that applies here.
#25354 adapted locally: existing npm cache detection was stronger, with a missing fallback added for bare cache directories.
#25787 adapted locally: upstream server VCS path does not map directly to PawWork's current VCS shape; this PR adds local malformed-patch protection at the persisted session-diff boundary.

Related Issue

Refs #477

Human Review Status

Pending. A human should make the final merge decision after reviewing the final diff and verification evidence.

Review Focus

  • Runtime/session replay behavior in message-v2.ts and provider/transform.ts, especially Anthropic signed reasoning, Bedrock empty text behavior, Bedrock PDF extraction, and surrogate sanitization.
  • Permission boundary for opencode-owned temp directory preapproval in agent/bash flows.
  • Bounded VCS patch behavior and the local #25787 adaptation in session-diff.ts.
  • Confirm #24992 remains intentionally split out rather than partly deleted here.

Risk Notes

Behavioral runtime change. Main risks are provider payload shape, permission scope, and VCS diff truncation. This PR intentionally avoids desktop, HttpApi/listener, generated SDK/OpenAPI, dependency, lockfile, package manifest, and release/workflow changes.

How To Verify

Tool/read/MCP/tool-registry/bash/agent/LSP tests: 126 passed
Skill discovery tests: 15 passed
Effect runner/session prompt/subagent tests: 87 passed
Session pagination/compaction/message-v2/retry/provider transform tests: 341 passed
Git/VCS/worktree/format tests: 51 passed
Core npm tests: 4 passed
UI session-diff tests: 3 passed
packages/opencode typecheck: passed (rerun after Claude skill default fix)
packages/core typecheck: passed
packages/ui typecheck: passed
git diff --check: passed
Final boundary grep: passed, no desktop, HttpApi listener, generated SDK/OpenAPI, package.json, bun.lock, .github, or app non-i18n changes

Screenshots or Recordings

Not applicable. No visible UI workflow change; the only UI-package change is defensive malformed persisted patch parsing covered by session-diff.test.ts.

Checklist

  • Human review status is stated above as pending, approved, or not required
  • I linked the related issue, or stated why there is no issue
  • This PR has type, primary area, and priority labels, or I requested maintainer labeling
  • I described the review focus and any meaningful risks
  • I listed the relevant verification steps and the key result for each
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope
  • I manually checked visible UI or copy changes when needed, with screenshots or recordings
  • I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes
  • I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant
  • I reviewed the final diff for unrelated changes and suspicious dependency changes
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English

Summary by CodeRabbit

  • New Features

    • Exposed secure app tmp directory; tmp is created with tight permissions.
    • Git: bounded patch generation, truncation-aware results, and untracked-file stats.
    • Surrogate sanitization for outgoing message content.
  • Bug Fixes

    • External-directory permission checks now include and correctly resolve the project tmp path (symlink-aware).
    • NPM install entrypoint resolution improved for scoped packages.
    • Media/message filtering and attachment handling fixed.
  • Improvements

    • Tool execution tracing, safer timeout cleanup, and async boot error logging.
    • Read tool accepts offset=0 as the first line.

@Astro-Han Astro-Han added bug Something isn't working P1 High priority upstream Tracked upstream or vendor behavior harness Model harness, prompts, tool descriptions, and session mechanics labels May 8, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack

Warning

Rate limit exceeded

@Astro-Han has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 38 minutes and 58 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9185c070-596e-4e66-b9fc-4c69a5e3bdbd

📥 Commits

Reviewing files that changed from the base of the PR and between c0f6d06 and cd26f52.

📒 Files selected for processing (1)
  • packages/opencode/src/tool/external-directory.ts
📝 Walkthrough

Walkthrough

Adds a secure application tmp path (Global.Path.tmp), implements byte-limited git patch APIs and cumulative patch budgeting, sanitizes lone UTF‑16 surrogates in outgoing messages, tightens media/compaction message logic, refactors tool/agent execution and effect-driven cancellation, switches external-directory permission checks to use resolved realpaths, and expands tests across these areas.

Changes

Infrastructure and Global Configuration

Layer / File(s) Summary
Global Path Configuration
packages/core/src/global.ts
Adds tmp path under app state, ensures private directory creation, extends Interface and Service.of(...) to expose tmp: string.
Flag and Skill Loading
packages/core/src/flag/flag.ts, packages/opencode/src/skill/index.ts
OPENCODE_DISABLE_EXTERNAL_SKILLS now depends only on its env var; skill loading uses agents-only external dir constant and externalDirs array when enabled.
NPM Package Handling & Tests
packages/core/src/npm.ts, packages/core/test/npm.test.ts
Introduces packageName helper for scoped packages; Npm.add fallback resolves entrypoints in node_modules/<name> if arborist first node absent; adds npmLayer test.

Git and VCS Diff System

Layer / File(s) Summary
Git Patch API
packages/opencode/src/git/index.ts
Adds Git.Patch/PatchOptions types, extends Result with truncation flags, adds Options.maxOutputBytes, implements patch* and statUntracked, and updates run to track per-stream truncation.
VCS Patch Batching and Limits
packages/opencode/src/project/vcs.ts
Introduces PatchBatch and boundedPatch enforcing per-patch and cumulative byte caps; refactors unstaged/staged/branchHead to use git.patch* with context and byte limits; simplifies exported layer dependencies.

Provider Message Handling

Layer / File(s) Summary
Message Sanitization
packages/opencode/src/provider/transform.ts
Adds sanitizeSurrogates(content) and integrates surrogate sanitization into normalizeMessages() for system/user/assistant and tool-result content; updates reasoning emptiness logic to respect provider metadata.
Message Attachment and Media Logic
packages/opencode/src/session/message-v2.ts
Generalizes synthetic media prompt, tightens Bedrock media support to image/*, omits empty text parts, extracts unsupported media into user messages, inserts space for Anthropic signed reasoning, and expands filterCompacted logic.

Tool and Agent Execution

Layer / File(s) Summary
Tool Execution Tracing
packages/opencode/src/tool/registry.ts, packages/opencode/src/session/prompt.ts
Wraps plugin and MCP tool execution in Effect.withSpan("Tool.execute") with tool/session/message attributes; moves ctx.ask permission check into traced MCP execution.
Agent Tool Cancellation
packages/opencode/src/tool/agent.ts
Changes AgentPromptOps.cancel to return Effect.Effect<void>; refactors abort listener management via Effect.acquireUseRelease and uses effect-driven cancellation with EffectBridge; filters inherited permissions to external_directory and deny rules.
Bash & Read Tools
packages/opencode/src/tool/bash.ts, packages/opencode/src/tool/bash.txt, packages/opencode/src/tool/read.ts
Bash tool returns Tree, manages parse tree lifecycle with acquireRelease and deletes tree in scoped block, uses Global.Path.tmp for ${tmp}, and maps resolved paths through external permission resolver; Read tool adds SUPPORTED_IMAGE_MIMES, allows offset: 0 logically (defaults to 1 for slicing), and validates mime-based image classification.

Session and Permission Management

Layer / File(s) Summary
External-directory & Realpath
packages/opencode/src/tool/external-directory.ts, packages/opencode/src/agent/agent.ts
Adds resolveExternalPathForPermission() to compute tolerant realpath resolution; assertExternalDirectoryEffect uses resolved path for containment checks and adds realpath metadata; agent init auto-allows tmp glob and Truncate.GLOB unless explicitly denied.
Session Forking and Compaction
packages/opencode/src/session/session.ts, packages/opencode/src/session/message-v2.ts
Session.fork persists cloned parts via updatePart(p) and remaps compaction tail_start_id using idMap; filterCompacted can reorder/slice when tail/compaction/summary markers match specific relationships.

Additional Updates

Layer / File(s) Summary
MCP URL Validation
packages/opencode/src/mcp/index.ts
Adds remoteURL helper to validate/parse MCP URLs; returns failed status on invalid URLs and reuses parsed URL when constructing transports.
Format IO Handling
packages/opencode/src/format/index.ts
Configures ChildProcess.make to ignore stdin/stdout/stderr for formatter invocations.
Provider Error Handling
packages/opencode/src/provider/error.ts
Recognizes server_is_overloaded in parseStreamError mapping.
Timeout Cleanup
packages/opencode/src/util/timeout.ts
Moves timer cleanup into finally so timers are cleared on both resolution and rejection.
Worktree Boot Error Logging
packages/opencode/src/worktree/index.ts
Forks boot effect in scope and catches cause to log bootstrap failures instead of letting failure propagate.
UI Patch Parsing
packages/ui/src/components/session-diff.ts
Wraps parsePatch in try/catch to preserve original patch on parse errors and ensures before/after strings include trailing newline when non-empty.
Test Infrastructure
packages/core/test/*, packages/opencode/test/*, packages/ui/test/*
Broad test additions and updates covering surrogate sanitization, message filtering/media, git patch truncation/statUntracked, MCP invalid URL handling, external-directory symlink cases, session fork/compaction remapping, tool cancellation, Read tool offset and media tests, timeout cleanup, and UI malformed patch cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

"I found a tmp under state and hopped,
Replaced lone surrogates, tidied each crop,
Forked parts remapped with a new little pop,
Effects now trace where cancellations stop,
Tests clap their paws — code’s snug in its shop." 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(opencode): sync runtime safety hotfixes' directly summarizes the main change—syncing upstream safety hotfixes into the opencode package—using clear, concise phrasing with proper Conventional Commits format.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all required template sections: a detailed Summary explaining the scope and intent, a Why section referencing issue #477 and prior PRs, Related Issue link, Human Review Status, Review Focus with specific areas, Risk Notes with behavioral context, How To Verify with test results, and a completed Checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/i477-runtime-safety-plan

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a global temporary directory, refactors VCS diffing to use native git patches with size limits, and enhances media handling in tool results, specifically addressing Bedrock's PDF limitations. It also implements surrogate sanitization for text content and refines subagent permission inheritance and cancellation logic. A critical issue was identified in the message compaction logic where the reordering of messages could lead to unintended data loss for messages preceding the tail index.

Comment thread packages/opencode/src/session/message-v2.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (7)
packages/opencode/test/skill/skill.test.ts (1)

253-290: ⚡ Quick win

Add a complementary flag-off test for Claude skill exclusion.

This new default-path assertion is good, but please add a sibling case that sets OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=true and verifies .claude/skills is excluded while .agents/skills still loads. It will protect the new decoupled-flag behavior from regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/test/skill/skill.test.ts` around lines 253 - 290, Add a
sibling test that creates the same temp layout (.agents/skills/agent-skill and
.claude/skills/claude-skill) but sets OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=true
before invoking Skill.all(); assert that the agent-skill is present and
claude-skill is not. Use the same tmpdir setup and Instance.provide pattern, set
process.env.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = "true" (and restore/clear it
after the test), call Skill.all(), then expect skills.find(s => s.name ===
"agent-skill") toBeDefined() and expect(skills.find(s => s.name ===
"claude-skill")).toBeUndefined().
packages/ui/src/components/session-diff.ts (1)

30-54: ⚡ Quick win

The undefined-dereference path inside try silently swallows any future TypeError, not just parse failures.

const [patch] = parsePatch(diff.patch) can yield undefined (when parsePatch returns []), and the resulting TypeError on patch.hunks is the mechanism that triggers the catch. This is an implicit error-detection pattern: any future accidental TypeError introduced inside the try block (e.g., a null-dereference in newly added code) would be swallowed identically and silently degrade the diff view to empty content.

Consider guarding explicitly:

♻️ Proposed fix — explicit guard instead of implicit TypeError
     try {
-      const [patch] = parsePatch(diff.patch)
+      const patches = parsePatch(diff.patch)
+      if (!patches.length) return { before: "", after: "", patch: diff.patch }
+      const [patch] = patches
 
       const beforeLines = []
       const afterLines = []
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/session-diff.ts` around lines 30 - 54, The try
block currently relies on a TypeError from accessing patch.hunks to detect parse
failures; instead explicitly guard the parse result: call
parsePatch(diff.patch), check the returned array (e.g., const parsed =
parsePatch(diff.patch); const patch = parsed[0];) and if !patch or !patch.hunks
immediately return the fallback { before: "", after: "", patch: diff.patch } (or
throw a clear parse error), then proceed to build beforeLines/afterLines; keep
the try/catch narrowly around parsePatch if you still want to catch parser
exceptions, and don't swallow unrelated TypeErrors from the rest of the logic.
packages/opencode/src/git/index.ts (2)

475-490: 💤 Low value

statUntracked parsing is fragile to whitespace — minor.

result.text() is not trimmed and parts[1] carries the trailing <TAB><file><LF> after split("\t"). parseInt happens to be tolerant of leading whitespace on parts[0], and the parts[1] numeric prefix is parsed correctly because parseInt stops at the first non-digit (the tab is consumed by the split, but the second-tab separator means parts[1] is just digits). So today this works, but a future formatting change in git's --numstat (or e.g. a leading BOM under odd locales) would silently break it. Consider trimming and matching on a regex like ^(\d+|-)\t(\d+|-)\t for robustness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/git/index.ts` around lines 475 - 490, statUntracked's
parsing of result.text() is fragile to extra whitespace or formatting changes;
update the parser in the statUntracked Effect.fn to trim the output and extract
additions/deletions via a strict regex (e.g. match /^(\d+|-)\t(\d+|-)\t/)
against result.text() instead of relying on split("\t"), then parse the captured
groups into numbers (treat "-" as 0) before returning the Stat-shaped object;
continue to respect result.truncated and ensure Number.isFinite checks remain in
place.

127-143: ⚖️ Poor tradeoff

Stream is fully drained even after maxOutputBytes is hit.

Stream.runFold doesn't short-circuit, and the spawn isn't interrupted, so a 100 MB diff under a 10 MB cap still pays the cost of reading the trailing 90 MB and waiting for git to terminate naturally. Memory is correctly bounded (chunks doesn't grow past the cap), but wall-clock and IO are not.

Reasonable to defer for this PR (caps are 10 MB and patches are usually small), but for adversarial inputs consider:

  • Stream.takeWhile over the byte counter and interrupting the child via handle.kill?.(...) once truncation is observed, or
  • forking a watcher fiber that interrupts the spawn scope when the truncated flag flips.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/git/index.ts` around lines 127 - 143, The current
collect implementation using Stream.runFold continues draining handle.stdout
even after opts.maxOutputBytes is reached; change it to stop consuming and abort
the child process when truncation is hit: replace the runFold usage with a
stream pipeline that tracks bytes (e.g., Stream.mapChunks/Stream.takeWhile or
Stream.scan + Stream.takeWhile) so the stream short-circuits once accumulated
bytes >= opts.maxOutputBytes, and on truncation call handle.kill?.('SIGKILL' or
appropriate) from the interrupt/finalizer (or spawn a watcher fiber that
interrupts the spawn scope) so git is terminated early; keep the same output
shape ({ buffer: Buffer.concat(...), truncated }) and reference collect,
handle.stdout, opts.maxOutputBytes and handle.kill in your changes.
packages/opencode/src/project/vcs.ts (2)

15-17: 💤 Low value

MAX_PATCH_BYTES === MAX_TOTAL_PATCH_BYTES lets a single file exhaust the batch budget.

With both caps set to 10 MB, a single 10 MB patch can fill the entire batch budget and force every subsequent file to render as emptyPatch(...). If that's the intended fail-safe behavior (front-loaded budget, explicit cap), consider adding a brief comment so the equality isn't read as a copy-paste mistake. Otherwise, set the per-file cap below the total (e.g. per-file = total / N or a fixed fraction) so one large file can't starve the rest of the diff.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/project/vcs.ts` around lines 15 - 17, The per-file and
total patch caps are both set equal (MAX_PATCH_BYTES and MAX_TOTAL_PATCH_BYTES),
allowing a single file to consume the entire batch budget and force other files
to become emptyPatch; change the per-file cap (MAX_PATCH_BYTES) to be strictly
less than the total (e.g., set MAX_PATCH_BYTES =
Math.floor(MAX_TOTAL_PATCH_BYTES / N) or a fixed fraction of
MAX_TOTAL_PATCH_BYTES) or add an explicit comment next to PATCH_CONTEXT_LINES /
MAX_* constants explaining the intentional equality; update the constants
PATCH_CONTEXT_LINES, MAX_PATCH_BYTES and MAX_TOTAL_PATCH_BYTES (and any logic
that assumes per-file vs total caps) accordingly so one large patch cannot
starve the batch.

32-52: ⚡ Quick win

Dead patches Map field on PatchBatch.

PatchBatch.patches is declared and constructed at every call site (lines 57, 89, 115) but never read or written by boundedPatch or any caller. It looks like a leftover from an earlier "collect into a map then merge" design.

♻️ Drop the unused field
-  type PatchBatch = { patches: Map<string, string>; total: number; capped: boolean }
+  type PatchBatch = { total: number; capped: boolean }
-    const batch: PatchBatch = { patches: new Map(), total: 0, capped: false }
+    const batch: PatchBatch = { total: 0, capped: false }

(apply at all three construction sites)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/project/vcs.ts` around lines 32 - 52, PatchBatch
currently declares an unused patches field; remove that dead field and its
initializations so the type and its constructors only include total and capped
(i.e., change type PatchBatch = { total: number; capped: boolean } and drop
patches from every place where a PatchBatch is created), and ensure boundedPatch
(and callers like the sites that build the batch) continue to use total and
capped with existing symbols (boundedPatch, emptyPatch, MAX_TOTAL_PATCH_BYTES,
Git.Item, Git.Patch) unchanged.
packages/core/test/npm.test.ts (1)

78-78: ⚡ Quick win

Assertion only checks isSome, not the actual entrypoint value.

The test verifies Option.isSome(entry.entrypoint) but doesn't inspect what the Some contains. If the fallback logic returns Some("") or Some(anyWrongPath), this assertion still passes, giving no protection against regressions in the path-computation logic. Strengthening to check the expected node_modules/fixture-provider suffix would make the test meaningful:

✅ Proposed stronger assertion
-    expect(Option.isSome(entry.entrypoint)).toBe(true)
+    expect(Option.isSome(entry.entrypoint)).toBe(true)
+    expect(
+      Option.getOrThrow(entry.entrypoint).includes(path.join("node_modules", "fixture-provider"))
+    ).toBe(true)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/npm.test.ts` at line 78, The test currently only asserts
Option.isSome(entry.entrypoint) which allows wrong or empty entrypoint values;
update the assertion to unwrap and verify the actual value of entry.entrypoint
(e.g., check that the Some string endsWith or equals the expected
"node_modules/fixture-provider" entrypoint path) so the test fails on incorrect
fallback paths; locate the assertion using Option.isSome and entry.entrypoint in
packages/core/test/npm.test.ts and replace or add an assertion that inspects the
Option's contained string for the expected suffix/value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/opencode/src/git/index.ts`:
- Around line 144-153: The current Result object mixes stdout and stderr
truncation (truncated: stdout.truncated || stderr.truncated), which causes
stderr noise to invalidate otherwise-complete stdout patches; change the result
to expose truncation separately (e.g., stdoutTruncated and stderrTruncated) and
set truncated (or the existing truncated field used by patch helpers) to reflect
only stdout truncation. Concretely, update the block that collects handle.stdout
and handle.stderr (used with collect and handle.exitCode) to keep stdout.buffer
and stderr.buffer but emit stdoutTruncated = stdout.truncated and
stderrTruncated = stderr.truncated, and set truncated: stdout.truncated (or
remove the combined OR) so downstream helpers like patchResult/boundedPatch only
react to stdout truncation; you can still apply a separate cap to stderr via
collect but ensure its flag is distinct.

In `@packages/opencode/src/session/message-v2.ts`:
- Around line 782-789: The current empty-text placeholder check
(hasAnthropicSignedReasoning) only tests part.metadata?.anthropic?.signature but
must also detect Bedrock-signed or redacted reasoning; update the predicate used
to set hasAnthropicSignedReasoning (or rename it to reflect both providers) to
also return true when part.metadata?.bedrock?.signature or
part.metadata?.bedrock?.redactedData is present, so the empty text branch in the
loop over msg.parts injects the synthetic space when Bedrock-signed/redacted
reasoning blocks are present; adjust the variable name if helpful and update any
related tests to cover Bedrock replay of empty reasoning blocks.

In `@packages/opencode/src/session/session.ts`:
- Around line 798-800: The compaction part update currently overwrites
p.tail_start_id with undefined when idMap.get(...) has no entry; instead, only
replace tail_start_id if a remap exists. In the block that checks p.type ===
"compaction" and p.tail_start_id, change the logic to test the presence of a
mapping (e.g., idMap.has(p.tail_start_id) or check that idMap.get(...) !==
undefined) and assign p.tail_start_id only when that mapping exists, leaving the
original p.tail_start_id intact otherwise; refer to the variables p,
p.tail_start_id and the idMap used in this function to locate and update the
code.

In `@packages/opencode/src/tool/agent.ts`:
- Around line 360-363: The cancel action ops.cancel(nextSession.id) is invoked
twice via cancelChild/runCancel.fork in the onParentAbort handler and again in
the Exit.hasInterrupts(exit) release branch; make the cancellation idempotent by
ensuring cancelChild is created once and guarded so it only executes a single
time across both paths. Concretely, construct a single cancel effect (the
existing cancelChild from ops.cancel(nextSession.id)) and share it between the
onParentAbort handler and the release branch, or wrap its invocation with a
one-time guard (e.g., an atomic/boolean "called" flag or a once/Promise-based
latch) so runCancel.fork(cancelChild) and the release-path call both delegate to
the same guarded cancel invocation; update the references around runCancel,
cancelChild, onParentAbort, and the Exit.hasInterrupts(exit) release branch
accordingly.

In `@packages/opencode/src/tool/read.ts`:
- Around line 159-160: Update the parameter documentation for params.offset in
the read function to reflect that offset 0 is accepted as an alias for the first
line/entry: change any wording that says strictly "1-indexed" to something like
"1-indexed (0 is accepted as an alias for the first entry)" and ensure the doc
block/comments immediately above the read function (and any exported API docs
that describe params.offset) explicitly mention this 0 alias while leaving the
existing validation logic for params.offset unchanged.

---

Nitpick comments:
In `@packages/core/test/npm.test.ts`:
- Line 78: The test currently only asserts Option.isSome(entry.entrypoint) which
allows wrong or empty entrypoint values; update the assertion to unwrap and
verify the actual value of entry.entrypoint (e.g., check that the Some string
endsWith or equals the expected "node_modules/fixture-provider" entrypoint path)
so the test fails on incorrect fallback paths; locate the assertion using
Option.isSome and entry.entrypoint in packages/core/test/npm.test.ts and replace
or add an assertion that inspects the Option's contained string for the expected
suffix/value.

In `@packages/opencode/src/git/index.ts`:
- Around line 475-490: statUntracked's parsing of result.text() is fragile to
extra whitespace or formatting changes; update the parser in the statUntracked
Effect.fn to trim the output and extract additions/deletions via a strict regex
(e.g. match /^(\d+|-)\t(\d+|-)\t/) against result.text() instead of relying on
split("\t"), then parse the captured groups into numbers (treat "-" as 0) before
returning the Stat-shaped object; continue to respect result.truncated and
ensure Number.isFinite checks remain in place.
- Around line 127-143: The current collect implementation using Stream.runFold
continues draining handle.stdout even after opts.maxOutputBytes is reached;
change it to stop consuming and abort the child process when truncation is hit:
replace the runFold usage with a stream pipeline that tracks bytes (e.g.,
Stream.mapChunks/Stream.takeWhile or Stream.scan + Stream.takeWhile) so the
stream short-circuits once accumulated bytes >= opts.maxOutputBytes, and on
truncation call handle.kill?.('SIGKILL' or appropriate) from the
interrupt/finalizer (or spawn a watcher fiber that interrupts the spawn scope)
so git is terminated early; keep the same output shape ({ buffer:
Buffer.concat(...), truncated }) and reference collect, handle.stdout,
opts.maxOutputBytes and handle.kill in your changes.

In `@packages/opencode/src/project/vcs.ts`:
- Around line 15-17: The per-file and total patch caps are both set equal
(MAX_PATCH_BYTES and MAX_TOTAL_PATCH_BYTES), allowing a single file to consume
the entire batch budget and force other files to become emptyPatch; change the
per-file cap (MAX_PATCH_BYTES) to be strictly less than the total (e.g., set
MAX_PATCH_BYTES = Math.floor(MAX_TOTAL_PATCH_BYTES / N) or a fixed fraction of
MAX_TOTAL_PATCH_BYTES) or add an explicit comment next to PATCH_CONTEXT_LINES /
MAX_* constants explaining the intentional equality; update the constants
PATCH_CONTEXT_LINES, MAX_PATCH_BYTES and MAX_TOTAL_PATCH_BYTES (and any logic
that assumes per-file vs total caps) accordingly so one large patch cannot
starve the batch.
- Around line 32-52: PatchBatch currently declares an unused patches field;
remove that dead field and its initializations so the type and its constructors
only include total and capped (i.e., change type PatchBatch = { total: number;
capped: boolean } and drop patches from every place where a PatchBatch is
created), and ensure boundedPatch (and callers like the sites that build the
batch) continue to use total and capped with existing symbols (boundedPatch,
emptyPatch, MAX_TOTAL_PATCH_BYTES, Git.Item, Git.Patch) unchanged.

In `@packages/opencode/test/skill/skill.test.ts`:
- Around line 253-290: Add a sibling test that creates the same temp layout
(.agents/skills/agent-skill and .claude/skills/claude-skill) but sets
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=true before invoking Skill.all(); assert
that the agent-skill is present and claude-skill is not. Use the same tmpdir
setup and Instance.provide pattern, set
process.env.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = "true" (and restore/clear it
after the test), call Skill.all(), then expect skills.find(s => s.name ===
"agent-skill") toBeDefined() and expect(skills.find(s => s.name ===
"claude-skill")).toBeUndefined().

In `@packages/ui/src/components/session-diff.ts`:
- Around line 30-54: The try block currently relies on a TypeError from
accessing patch.hunks to detect parse failures; instead explicitly guard the
parse result: call parsePatch(diff.patch), check the returned array (e.g., const
parsed = parsePatch(diff.patch); const patch = parsed[0];) and if !patch or
!patch.hunks immediately return the fallback { before: "", after: "", patch:
diff.patch } (or throw a clear parse error), then proceed to build
beforeLines/afterLines; keep the try/catch narrowly around parsePatch if you
still want to catch parser exceptions, and don't swallow unrelated TypeErrors
from the rest of the logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8aa60ab2-58d0-422b-8050-75f42ba96cd8

📥 Commits

Reviewing files that changed from the base of the PR and between e00780c and e27b86d.

📒 Files selected for processing (39)
  • packages/core/src/flag/flag.ts
  • packages/core/src/global.ts
  • packages/core/src/npm.ts
  • packages/core/test/fixture/effect-flock-worker.ts
  • packages/core/test/npm.test.ts
  • packages/core/test/util/effect-flock.test.ts
  • packages/opencode/src/agent/agent.ts
  • packages/opencode/src/format/index.ts
  • packages/opencode/src/git/index.ts
  • packages/opencode/src/mcp/index.ts
  • packages/opencode/src/project/vcs.ts
  • packages/opencode/src/provider/error.ts
  • packages/opencode/src/provider/transform.ts
  • packages/opencode/src/session/message-v2.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/session.ts
  • packages/opencode/src/skill/index.ts
  • packages/opencode/src/tool/agent.ts
  • packages/opencode/src/tool/bash.ts
  • packages/opencode/src/tool/bash.txt
  • packages/opencode/src/tool/read.ts
  • packages/opencode/src/tool/registry.ts
  • packages/opencode/src/util/timeout.ts
  • packages/opencode/src/worktree/index.ts
  • packages/opencode/test/agent/agent.test.ts
  • packages/opencode/test/git/git.test.ts
  • packages/opencode/test/mcp/headers.test.ts
  • packages/opencode/test/project/vcs.test.ts
  • packages/opencode/test/project/worktree.test.ts
  • packages/opencode/test/provider/transform.test.ts
  • packages/opencode/test/session/message-v2.test.ts
  • packages/opencode/test/session/messages-pagination.test.ts
  • packages/opencode/test/session/retry.test.ts
  • packages/opencode/test/skill/skill.test.ts
  • packages/opencode/test/tool/agent.test.ts
  • packages/opencode/test/tool/read.test.ts
  • packages/opencode/test/util/timeout.test.ts
  • packages/ui/src/components/session-diff.test.ts
  • packages/ui/src/components/session-diff.ts

Comment thread packages/opencode/src/git/index.ts
Comment thread packages/opencode/src/session/message-v2.ts
Comment thread packages/opencode/src/session/session.ts
Comment thread packages/opencode/src/tool/agent.ts
Comment thread packages/opencode/src/tool/read.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/opencode/src/tool/external-directory.ts`:
- Around line 17-18: resolveForPermission and assertExternalDirectoryEffect
currently normalize the incoming path before resolving filesystem symlinks,
which collapses `..` segments and can bypass symlink boundaries; change both
functions to call realpath (e.g., fs.realpathSync or your AppFileSystem realpath
equivalent) on the raw target first to resolve symlinks, then apply
normalization/AppFileSystem.normalizePath (with the base option for Windows) to
the realpath result so symlink traversal is preserved for the external-directory
permission check.

In `@packages/opencode/test/skill/skill.test.ts`:
- Around line 321-335: The test restores OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
incorrectly by assigning the string "undefined" back when the variable was
originally absent; change the finally block so it checks the saved original
value and if original === undefined then delete
process.env.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS, otherwise set
process.env.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = original; update the
restoration logic near the Instance.provide / Skill.all test to avoid leaking
env state between tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f9634d76-06e3-4db0-8118-658015004a96

📥 Commits

Reviewing files that changed from the base of the PR and between e27b86d and c808e77.

📒 Files selected for processing (15)
  • packages/core/src/global.ts
  • packages/core/test/global.test.ts
  • packages/core/test/npm.test.ts
  • packages/opencode/src/git/index.ts
  • packages/opencode/src/project/vcs.ts
  • packages/opencode/src/session/session.ts
  • packages/opencode/src/skill/index.ts
  • packages/opencode/src/tool/agent.ts
  • packages/opencode/src/tool/external-directory.ts
  • packages/opencode/src/tool/read.ts
  • packages/opencode/test/git/git.test.ts
  • packages/opencode/test/session/messages-pagination.test.ts
  • packages/opencode/test/skill/skill.test.ts
  • packages/opencode/test/tool/external-directory.test.ts
  • packages/ui/src/components/session-diff.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/ui/src/components/session-diff.ts
  • packages/opencode/src/session/session.ts
  • packages/core/test/npm.test.ts
  • packages/opencode/test/session/messages-pagination.test.ts
  • packages/opencode/src/project/vcs.ts
  • packages/opencode/src/tool/read.ts
  • packages/opencode/src/tool/agent.ts
  • packages/opencode/src/git/index.ts

Comment thread packages/opencode/src/tool/external-directory.ts Outdated
Comment thread packages/opencode/test/skill/skill.test.ts
@Astro-Han
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/opencode/src/tool/external-directory.ts`:
- Around line 20-34: The Windows branch currently pre-normalizes the input via
AppFileSystem.normalizePath (assigned to normalized) which lexically collapses
'..' before calling realpathSync.native, allowing junction boundaries to be
escaped; change it to mirror the POSIX segment-walk: split the input path into
segments and iterate from the root, calling realpathSync.native on each
accumulated segment (handle ENOENT/ENOTDIR the same way you do now), process
'..' by popping the last resolved segment rather than letting win32.normalize
collapse them up-front, and only call AppFileSystem.normalizePath once the final
resolved realpath (the variable currently named resolved) is obtained (or when
returning normalized as fallback). Update logic around missing, current, and the
while-loop so resolution occurs per-segment and lexical normalization is
deferred until after symlink/junction resolution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d85d2e9-f9b6-4855-9caa-862d071fdb2b

📥 Commits

Reviewing files that changed from the base of the PR and between c808e77 and 5bff71a.

📒 Files selected for processing (5)
  • packages/opencode/src/tool/bash.ts
  • packages/opencode/src/tool/external-directory.ts
  • packages/opencode/test/skill/skill.test.ts
  • packages/opencode/test/tool/bash.test.ts
  • packages/opencode/test/tool/external-directory.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/opencode/test/skill/skill.test.ts

Comment thread packages/opencode/src/tool/external-directory.ts Outdated
@Astro-Han
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/opencode/src/tool/external-directory.ts (1)

132-146: ⚡ Quick win

Move bypass short-circuit before path resolution work.

resolveExternalPathForPermission is invoked three times here (once for full, once for scope.directory, optionally for scope.worktree), each performing per-segment lstatSync calls — including for stable, instance-level paths (ins.directory, ins.worktree) that don't depend on target. When options?.bypass is true, all of that work is discarded. Hoisting the bypass check immediately after full avoids the sync fs traversals on the bypass hot path (e.g., callers that already trust the target).

♻️ Proposed change
   const ins = yield* InstanceState.context
   const full =
     process.platform === "win32"
       ? windowsPermissionPath(target, ins.directory)
       : path.isAbsolute(target)
       ? target
       : `${ins.directory.replace(/\/+$/, "")}/${target}`
+  if (options?.bypass) return full
   const resolved = resolveExternalPathForPermission(full, ins.directory)
   const scope = {
     ...ins,
     directory: resolveExternalPathForPermission(ins.directory, ins.directory),
     worktree: ins.worktree === "/" ? ins.worktree : resolveExternalPathForPermission(ins.worktree, ins.directory),
   }
-  if (options?.bypass) return full
   if (Instance.containsPath(resolved, scope)) return full
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/tool/external-directory.ts` around lines 132 - 146, The
bypass short-circuit should be moved to run immediately after computing full to
avoid unnecessary filesystem traversal; after computing full (using
InstanceState.context and windowsPermissionPath/path.isAbsolute logic) check
options?.bypass and return full if true, before calling
resolveExternalPathForPermission or building scope, and therefore avoid calling
resolveExternalPathForPermission for full, scope.directory, or scope.worktree
and avoid invoking Instance.containsPath when bypass is set.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/opencode/src/tool/external-directory.ts`:
- Around line 132-146: The bypass short-circuit should be moved to run
immediately after computing full to avoid unnecessary filesystem traversal;
after computing full (using InstanceState.context and
windowsPermissionPath/path.isAbsolute logic) check options?.bypass and return
full if true, before calling resolveExternalPathForPermission or building scope,
and therefore avoid calling resolveExternalPathForPermission for full,
scope.directory, or scope.worktree and avoid invoking Instance.containsPath when
bypass is set.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b7215a1-6bab-448b-981a-86ffd025429f

📥 Commits

Reviewing files that changed from the base of the PR and between 5bff71a and c0f6d06.

📒 Files selected for processing (4)
  • packages/opencode/src/tool/bash.ts
  • packages/opencode/src/tool/external-directory.ts
  • packages/opencode/test/tool/bash.test.ts
  • packages/opencode/test/tool/external-directory.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/opencode/src/tool/bash.ts

@Astro-Han Astro-Han merged commit 24809a7 into dev May 8, 2026
20 checks passed
@Astro-Han Astro-Han deleted the codex/i477-runtime-safety-plan branch May 8, 2026 12:44
Astro-Han added a commit that referenced this pull request May 8, 2026
Remove the broken upstream `codesearch` tool from PawWork as the PR 3a follow-up for #477.

Root cause:
- Upstream opencode removed this tool in anomalyco/opencode#24992 because Exa MCP no longer exposes `get_code_context_exa`.
- PawWork still had the stale tool registered after PR #504 intentionally split it out from the broader runtime safety slice.
- Leaving it exposed meant the model could still call a tool that fails at runtime.

Scope included:
- Deleted `packages/opencode/src/tool/codesearch.ts` and `codesearch.txt`.
- Removed registry initialization, builtin exposure, provider gating, default explore-agent permission, CLI rendering, and permission schema fields.
- Removed stale SDK/OpenAPI permission schema entries.
- Removed app settings labels and UI/i18n dedicated rendering for `codesearch`, so historical unknown parts fall back to generic rendering.
- Removed the stale Effect migration checklist entry so future sync work does not try to migrate a deleted tool.
- Added a registry regression test proving the built-in tool list no longer contains `codesearch`.

Explicitly out of scope:
- Web search behavior and Exa web-search quota/auth flows.
- Desktop/app manual ports planned for the next #477 slice.
- Effect foundation, effectCmd, HttpApi/listener migration, generated SDK regeneration beyond the removed field, dependencies, lockfile, CI/workflow, and packaging changes.

Verification:
- `bun install --frozen-lockfile` in the fresh worktree with no lockfile changes.
- Red test confirmed the new registry assertion failed while production still exposed `codesearch`.
- `bun --cwd packages/opencode test test/tool/registry.test.ts` -> 19 passed.
- Residual scan showed only negative registry-test assertions mention `codesearch`.
- `bun run --cwd packages/opencode typecheck` -> passed.
- `bun run --cwd packages/ui typecheck` -> passed.
- `bun run --cwd packages/app typecheck` -> passed.
- `git diff --check` -> passed.
- `packages/sdk/openapi.json` parsed successfully.
- PR CI/status checks were green before merge, including ci, typecheck, unit-app, unit-desktop, unit-opencode, desktop-smoke, e2e-artifacts, CodeQL, dependency-review, commit-lint, PR title lint, action-semantic-pull-request, and CodeRabbit.
- Review thread check returned no unresolved threads.

Follow-up:
- #477 remains open. The next sync work should move to the consolidated desktop/app manual-port slice unless a newer live upstream/PR scan shows an already-open PR covering it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working harness Model harness, prompts, tool descriptions, and session mechanics P1 High priority upstream Tracked upstream or vendor behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant