Skip to content

feat(#7824)!: add OpenCode sandbox hook adapter - #8852

Open
yvonnedevlinrh wants to merge 14 commits into
fullsend-ai:mainfrom
yvonnedevlinrh:feat/7824-opencode-hook-probe
Open

yvonnedevlinrh wants to merge 14 commits into
fullsend-ai:mainfrom
yvonnedevlinrh:feat/7824-opencode-hook-probe

Conversation

@yvonnedevlinrh

@yvonnedevlinrh yvonnedevlinrh commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds the sandbox security adapter for OpenCode 1.19.1, enabling the shared hook controls for write-capable agents.

  • Bridge security.HookPlan through native before/after tool hooks.
  • Scan failed-tool messages before model requests, including nested Task sessions.
  • Enforce Bash restrictions and preserve the trusted permission policy as a ceiling.
  • Match native worktree-relative permission paths and validate every patch target, including move destinations.
  • Preserve native Bash exit status so failed verification output is not summarized as successful.
  • Prevent loaded or inherited OPENCODE_PURE values from disabling the hook plugin.
  • Support LS-only directory access without granting file reads.
  • Require security to remain enabled for every OpenCode role.
  • Add SDK dependency preparation, scoped image smoke checks, and documentation updates.

Deployment decision

The adapter follows pi's existing bootstrap deployment pattern. The runner uploads the Go-embedded adapter and selected shared scripts into /sandbox/opencode-config, retains their expected digests, and verifies them before loading.

This keeps the deployment consistent with the existing runtime architecture. Hash checks detect modifications, but the same-user verification-to-load race remains explicitly unresolved. Stronger OS-backed storage is left for separate cross-runtime review.

Config-directory skills remain agent-writable, persist across iterations, and are not covered by the adapter's integrity checks. This behavior predates the adapter and is consistent with pi's existing deployment model. The trust model for inherited skill changes is tracked separately in #8899.

Breaking changes

  • OpenCode runs now reject security.enabled: false. Existing harnesses using that workaround must enable security.
  • Bash allowlists are now enforced by OpenCode, unlike pi's advisory default. Agent tool declarations must include the commands their instructions require. The scribe declaration mismatch is tracked in Reconcile scribe input commands with its declared tools agents#1821 without relaxing runtime enforcement.
  • Additional native OPENCODE_CONFIG_CONTENT.plugin entries are rejected.
  • The sandbox must provide OpenCode 1.19.1 and its pinned plugin SDK.

The SDK is installed in the image; the adapter and selected scripts are supplied during bootstrap.

Validation

  • All 16 native integration scenarios pass against OpenCode 1.19.1 using a scripted local provider, across the full run and targeted reruns.
  • Runtime, CLI, config, and security packages pass with the race detector.
  • Regression coverage verifies hostile environment handling, native Bash failure output, patch-header variants, and path-scoped trusted permissions.
  • The bootstrap guard assertion passes with escape-interpreting shell behavior; fixture logging now preserves literal backslash escapes.
  • Lint, Go vet, and commit hooks pass.
  • Approximate cumulative Go patch coverage: 88.4%.
  • Startup with the actual pinned SDK succeeds without a registry install after global SDK bookkeeping is prepared.
  • Revised image smoke checks pass in CI at the updated PR head.
  • Live OpenShell and capability-gated runtime-opencode E2E validation pass with the matching runner and candidate image.

Raw session/transcript artifact redaction remains tracked separately in #7320.

Closes #7824.

Related: unbound-force/unbound-force#515.

Runtime implementation guide

Consulted docs/contributing/runtime-implementation.md. This PR updates the OpenCode security matrix, shared hook contract, and sandbox workspace layout to describe the native adapter and selected shared scripts. Deployment follows pi's bootstrap-uploaded, runner-digest-checked pattern. The same-user verification-to-load race remains explicitly documented as unresolved.

Install Go-embedded native hooks at bootstrap using the pi deployment
pattern. Enforce Bash and tool policies, sanitize successful and failed
tool output, and keep hook configuration and digests on the runner.

Pin utilities for standalone bootstrap and agent-restore hook guards.
Add native OpenCode 1.19.1 regression coverage and scoped image smoke
checks. Document the remaining same-user verification race.

BREAKING CHANGE: OpenCode requires security.enabled: true, OpenCode
1.19.1 and the pinned plugin SDK. Remove the security-disabled workaround
and additional OPENCODE_CONFIG_CONTENT.plugin entries.

Assisted-by: gpt-5.6-terra
Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>
@yvonnedevlinrh
yvonnedevlinrh requested a review from a team as a code owner October 27, 2026 10:21
@github-actions

Copy link
Copy Markdown

E2E tests did not run

E2E tests run automatically for pull requests authored by users with write access or higher.

For other contributors, someone with write access must add the ok-to-test label after the latest push.

Reason: unauthorized

See E2E testing guide for details.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add OpenCode 1.19.1 sandbox security-hook adapter

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Enable shared sandbox hooks for write-capable OpenCode agents, including failed tools and nested
 sessions.
• Enforce trusted tool permissions and require security for every OpenCode role.
• Pin image dependencies and add native probes, image smoke checks, and migration guidance.
Diagram

graph TD
  CLI["CLI security gate"] --> Bootstrap["OpenCode bootstrap"] --> Policy["Runner-held policy"] --> Adapter["Native hook adapter"] --> Scripts["Shared hook scripts"]
  Bootstrap --> Engine["OpenCode tool loop"] --> Adapter
  Adapter --> Model["Model requests"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. OS-protected, image-owned hook assets
  • ➕ Would strengthen protection against same-user changes between verification and loading.
  • ➖ Requires cross-runtime privilege and image changes; couples hook updates to image releases.

Recommendation: The bootstrap-uploaded, runner-digest-checked approach matches pi and allows hook changes to ship with the runner. Accept it with the documented same-user verification-to-load race; consider OS-protected storage in separate cross-runtime hardening.

Files changed (30) +1892 / -197

Enhancement (8) +518 / -67
opencode_security.goReject security-disabled OpenCode runs +12/-0

Reject security-disabled OpenCode runs

• Adds a runtime-specific gate so no OpenCode role can use the former disabled-security workaround.

internal/cli/opencode_security.go

run.goApply the OpenCode security gate during run setup +4/-0

Apply the OpenCode security gate during run setup

• Validates the selected runtime against the composed harness security setting before proceeding.

internal/cli/run.go

opencode.goHarden OpenCode's exported runtime environment +9/-5

Harden OpenCode's exported runtime environment

• Clears dangerous inherited variables and disables default plugins and model fetching. Clarifies that the uploaded config directory is not an OS security boundary.

internal/runtime/opencode.go

opencode_bootstrap.goInstall hooks and constrain agent permissions +30/-25

Install hooks and constrain agent permissions

• Installs and verifies the adapter before agent inspection, adds compatible Bash permission patterns, and requires the pinned runtime version.

internal/runtime/opencode_bootstrap.go

opencode_hooks.goEmbed and deploy the OpenCode hook policy +162/-0

Embed and deploy the OpenCode hook policy

• Records runner-held plans, permissions, environment, and script digests; installs the embedded adapter and scripts; and defines integrity guards and SDK-cache restoration.

internal/runtime/opencode_hooks.go

opencode_integrity.goRetain hook policy and reject additional native plugins +8/-0

Retain hook policy and reject additional native plugins

• Adds runner-held hook configuration to trusted OpenCode state and rejects nonempty native plugin entries.

internal/runtime/opencode_integrity.go

fullsend-hooks.tsBridge OpenCode native events to shared security hooks +262/-0

Bridge OpenCode native events to shared security hooks

• Enforces permissions and write-path restrictions before tools, runs shared sanitizers after tools, and scans failed-tool history before model requests. Checks script digests and withholds unsafe results on failure.

internal/runtime/opencode_plugin/fullsend-hooks.ts

opencode_run.goRequire verified hooks on every OpenCode iteration +31/-37

Require verified hooks on every OpenCode iteration

• Guards installed assets around environment loading, restores trusted settings and SDK bookkeeping, and pins binary execution. Refuses runs without bootstrap-installed hook policy.

internal/runtime/opencode_run.go

Tests (8) +1054 / -45
opencode.featureExercise secure OpenCode configuration in behavior tests +24/-3

Exercise secure OpenCode configuration in behavior tests

• Adds rejection coverage for disabled security and enables security in the capability-gated OpenCode run.

e2e/behaviour/features/runtime/opencode.feature

opencode_security_test.goTest the runtime-specific security gate +23/-0

Test the runtime-specific security gate

• Checks that OpenCode requires security while other runtimes retain their existing behavior.

internal/cli/opencode_security_test.go

opencode_bootstrap_test.goAdapt bootstrap tests to hook-enabled OpenCode +29/-15

Adapt bootstrap tests to hook-enabled OpenCode

• Updates the fake sandbox and assertions for version pinning, installed hooks, Bash patterns, and run restoration.

internal/runtime/opencode_bootstrap_test.go

opencode_hooks_test.goTest hook policy, deployment, and integrity +196/-0

Test hook policy, deployment, and integrity

• Runs adapter unit tests and covers bootstrap failures, SDK preparation, plugin rejection, missing policy, version checks, and tampered assets.

internal/runtime/opencode_hooks_test.go

fullsend-hooks.test.mjsUnit-test native hook enforcement and sanitization +221/-0

Unit-test native hook enforcement and sanitization

• Covers tool and Bash ceilings, LS-only access, write paths, script chaining, tamper failures, denial diagnostics, failed-tool history, and shell isolation.

internal/runtime/opencode_plugin/fullsend-hooks.test.mjs

opencode_plugin_integration_test.goProbe hooks against a real OpenCode process +504/-0

Probe hooks against a real OpenCode process

• Adds opt-in scenarios using a scripted local provider to inspect execution and model-visible continuations, including nested tasks, permissions, failed tools, and config isolation.

internal/runtime/opencode_plugin_integration_test.go

opencode_test.goUpdate run-command tests for mandatory hooks +21/-27

Update run-command tests for mandatory hooks

• Checks integrity-guard placement, environment cleanup, binary pinning, override restoration, and missing bootstrap state.

internal/runtime/opencode_test.go

hook-probe.tsProvide an observation plugin for native hook probes +36/-0

Provide an observation plugin for native hook probes

• Records real hook events and supplies a test-only throwing tool without making security-policy decisions.

internal/runtime/testdata/opencode/hook-probe.ts

Documentation (12) +205 / -62
architecture.mdShow OpenCode's native security-hook integration +2/-2

Show OpenCode's native security-hook integration

• Replaces the read-only, no-hooks description with the versioned native-plugin architecture.

docs/architecture.md

run.mdDocument OpenCode's mandatory security setting +4/-1

Document OpenCode's mandatory security setting

• Explains that security-disabled OpenCode runs are rejected before sandbox creation.

docs/cli/run.md

runtime-implementation.mdDocument the OpenCode hook contract and native probe +122/-19

Document the OpenCode hook contract and native probe

• Updates the runtime security matrix and deployment layout. Describes probe coverage, the version boundary, and the unresolved same-user race.

docs/contributing/runtime-implementation.md

sandbox-topology.mdList OpenCode and its matching SDK in the sandbox image +1/-1

List OpenCode and its matching SDK in the sandbox image

• Adds the version-matched plugin SDK to the base-image inventory.

docs/contributing/sandbox-topology.md

glossary.mdUpdate the OpenCode runtime definition +1/-1

Update the OpenCode runtime definition

• States that the opt-in runtime has shared hooks on version 1.19.1 and requires security.

docs/glossary.md

choosing-a-runtime.mdRevise OpenCode pilot guidance +1/-1

Revise OpenCode pilot guidance

• Replaces the read-only limitation with version and security prerequisites.

docs/guides/getting-started/choosing-a-runtime.md

config-reference.mdClarify write-capable OpenCode runtime support +2/-3

Clarify write-capable OpenCode runtime support

• Documents support for read-only and write-capable roles with security enabled.

docs/reference/config-reference.md

harness-reference.mdSpecify the OpenCode security requirement +3/-0

Specify the OpenCode security requirement

• Adds the security setting to the harness example and explains its OpenCode-specific constraint.

docs/reference/harness-reference.md

runtimes.mdUpdate OpenCode capabilities and security matrix +10/-7

Update OpenCode capabilities and security matrix

• Describes native hooks, Bash restrictions, runner-held digests, and the writable config tree's limits.

docs/runtimes.md

opencode.mdReplace the read-only workaround with hook-enabled setup +56/-21

Replace the read-only workaround with hook-enabled setup

• Documents image and security requirements, enforced tool behavior, failed-tool scanning, troubleshooting, and the residual deployment race.

docs/runtimes/opencode.md

runtime_prompt.goRefresh interactive OpenCode selection guidance +1/-1

Refresh interactive OpenCode selection guidance

• Advertises shared hooks and the security requirement instead of the former read-only limitation.

internal/cli/runtime_prompt.go

config.goCorrect the OpenCode runtime security description +2/-5

Correct the OpenCode runtime security description

• Replaces the obsolete no-adapter warning with the mandatory-security behavior.

internal/config/config.go

Other (2) +115 / -23
sandbox-images.ymlSmoke-test the sandbox image and handle merge queues +98/-12

Smoke-test the sandbox image and handle merge queues

• Adds offline checks for OpenCode, its SDK, binary ownership, Python, and Tirith. Builds relevant merge-queue changes without publishing their images.

.github/workflows/sandbox-images.yml

ContainerfileBake the matching OpenCode plugin SDK into the image +17/-11

Bake the matching OpenCode plugin SDK into the image

• Installs the version-matched SDK under the OpenCode config directory and explicitly sets ownership of selected pinned binaries.

images/sandbox/Containerfile

@qodo-code-review

qodo-code-review Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Code and fix agents lose the Go toolchain in Bash ✓ Resolved
Description
The adapter's shell.env hook replaces PATH with
/usr/local/bin:/usr/bin:/bin:/sandbox/workspace/bin for every OpenCode Bash call. That drops
/usr/local/go/bin and /sandbox/go/bin, which the code image and the runner's .env add, along
with any PATH entries a harness adds through .env.d. On the fullsend-code image, write-capable
agents (code, fix) then fail to run go build, go test or gopls — the very roles this PR is
meant to enable.
Code

internal/runtime/opencode_plugin/fullsend-hooks.ts[R199-202]

+        for (const key of ["BASH_ENV", "ENV", "SHELLOPTS", "BASHOPTS", "CDPATH", "PS4"]) output.env[key] = "";
+        for (const key of Object.keys(process.env)) if (key.startsWith("BASH_FUNC_")) output.env[key] = "";
+        output.env.PATH = `${env.PATH}:${path.join(cfg.workspace, "bin")}`;
+        output.env.HOME = cfg.shellHome;
Evidence
The code image installs Go under /usr/local/go/bin and puts GOPATH binaries in /sandbox/go/bin, and
reaches them only through PATH; nothing links them into /usr/local/bin. The runner's .env PATH
also starts with /usr/local/go/bin. The plugin overwrites PATH using env.PATH, which defaults to a
fixed system PATH (line 120), so neither directory is on the Bash tool's PATH.

images/code/Containerfile[144-149]
internal/cli/run.go[3484-3496]
internal/runtime/opencode_plugin/fullsend-hooks.ts[118-121]
internal/runtime/opencode_plugin/fullsend-hooks.ts[196-208]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The OpenCode adapter's `shell.env` hook sets the Bash tool's PATH to a fixed `/usr/local/bin:/usr/bin:/bin` plus `/sandbox/workspace/bin`. This removes `/usr/local/go/bin` and `/sandbox/go/bin` (fullsend-code image) and the runner's `.env` PATH, so `go` and `gopls` are not found by code/fix agents.

## Fix Focus Areas
- internal/runtime/opencode_plugin/fullsend-hooks.ts[196-208]
- internal/runtime/opencode_hooks.go[67-71]

## Recommended Fix
On the runner side, capture the trusted command PATH at bootstrap: the image PATH as seen by `sandbox.Exec`, or the runner's `runnerPathLines` value with trusted system dirs first and writable dirs last. Store it in the hook config (e.g. `shellPath`) and use it in `shell.env` instead of the hard-coded value. Keep the fixed PATH for the Python hook subprocesses only. Add a test that a Bash tool call on the code image can resolve `go`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Triage GraphQL calls get blocked ✓ Resolved
Description
checkBash rejects braces and dollar signs anywhere in a command, including inside the
single-quoted GraphQL argument prescribed by the agents repository’s GitHub skill. When an OpenCode
triage agent retrieves native sub-issues using that command, the adapter denies the call before gh
executes.
Code

internal/runtime/opencode_plugin/fullsend-hooks.ts[R43-45]

+  if (typeof command !== "string" || !command.trim()) deny("empty Bash command");
+  if (/[`$(){}\\]|<<|<\(|>\(/.test(command)) deny("Bash allowlist rejects substitutions, groups, escapes and heredocs");
+  for (const segment of command.split(/\r?\n|&&|\|\||;|\|&|\||(?<![<>|])&(?!>)/)) {
Evidence
Triage permits gh, and the shared skill specifies a gh api graphql command whose single-quoted
query contains $ and braces; the new check rejects those characters without considering quotes.

fullsend -> agents
internal/runtime/opencode_plugin/fullsend-hooks.ts[41-52]
internal/runtime/opencode_hooks.go[37-49]
External repo: fullsend-ai/agents, agents/triage.md [1-9]
External repo: fullsend-ai/agents, skills/github-forge/SKILL.md [30-42]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The OpenCode Bash check treats literal GraphQL syntax inside single quotes as executable shell syntax and blocks an agent-owned GitHub command.
## Fix Focus Areas
- internal/runtime/opencode_plugin/fullsend-hooks.ts[41-52]
- /cross_repos/agents/skills/github-forge/SKILL.md[30-42]
## Recommended Fix
Make the syntax check quote-aware so literal content in single-quoted arguments is permitted while executable substitutions and unapproved commands remain denied. Test the GitHub skill’s GraphQL command with the triage allowlist.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Runtime changes lack guide acknowledgment ✗ Dismissed
Description
OpenCodeRuntime.Bootstrap now installs and verifies the native hook adapter before agent
verification, changing the runtime backend's security and workspace behavior. Because the PR
description does not state that docs/contributing/runtime-implementation.md was consulted, the
backend change lacks the required review record even though the guide's matrix and workspace
sections were updated.
Code

internal/runtime/opencode_bootstrap.go[R129-132]

+	trustedEnv.Hooks, err = r.installOpenCodeHooks(input, def, trustedRules)
+	if err != nil {
+		return err
+	}
Evidence
The checklist requires an explicit PR-description reference to the runtime implementation guide when
adding or modifying a runtime backend. The changed bootstrap code installs and verifies the OpenCode
hook adapter, while the guide changes document the new OpenCode hook contract and opencode-config
workspace layout; the supplied PR description does not explicitly say the guide was consulted.

Rule 2889480: Consult runtime implementation guide when modifying runtime.Runtime backends
internal/runtime/opencode_bootstrap.go[129-134]
docs/contributing/runtime-implementation.md[159-174]
docs/contributing/runtime-implementation.md[574-576]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The OpenCode runtime backend now adds native hook installation, verification, and workspace behavior, but the PR description does not explicitly state that `docs/contributing/runtime-implementation.md` was consulted as required.

## Fix Focus Areas
- internal/runtime/opencode_bootstrap.go[129-132]
- docs/contributing/runtime-implementation.md[159-174]
- docs/contributing/runtime-implementation.md[574-576]

## Recommended Fix
Add an explicit statement to the PR description confirming that `docs/contributing/runtime-implementation.md` was consulted, and summarize that the OpenCode security matrix, hook contract, and workspace layout were updated consistently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Every model request rescans all past tool errors ✓ Resolved
Description
experimental.chat.messages.transform runs the full PostToolUse chain, spawning a Python process
for each matching script, on every errored tool part in output.messages before each model request.
Already-sanitized parts are not marked or cached, so they are scanned again on every request. A long
session with many failed commands pays a cost that grows with history size on every turn, with up to
30 s per spawn, and adds to the iteration timeout.
Code

internal/runtime/opencode_plugin/fullsend-hooks.ts[R214-220]

+      for (const message of output.messages) {
+        for (const part of message.parts) {
+          if (part.type !== "tool" || part.state?.status !== "error") continue;
+          const state = part.state;
+          try {
+            const sanitized = chain("PostToolUse", names[part.tool] ?? part.tool,
+              state.input ?? {}, { output: state.error, metadata: state.metadata ?? {}, exit_code: 1 });
Evidence
The transform loops over every message and every part with status error, calling chain (which
spawns cfg.python per script with a 30 s timeout) without skipping parts handled in an earlier
request. The comment confirms the hook runs before every model send, including in Task sessions.

internal/runtime/opencode_plugin/fullsend-hooks.ts[160-194]
internal/runtime/opencode_plugin/fullsend-hooks.ts[209-233]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The messages-transform hook re-runs the Python PostToolUse chain for every errored tool part in the full history on every model request.

## Fix Focus Areas
- internal/runtime/opencode_plugin/fullsend-hooks.ts[209-233]

## Recommended Fix
Keep an in-memory Map in the plugin closure, keyed by part id/callID plus a digest of `state.error`, that stores the sanitized text. Run the chain only for parts not yet seen, and apply the cached result to the rest. Keep the fail-closed behaviour (throw) for parts whose scan failed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (6)
5. Failed checks look successful to agents ✓ Resolved
Description
experimental.chat.messages.transform passes failed-tool errors to the normal PostToolUse chain
with exit_code: 1, but the context-suppression stage does not use that field to recognize a
failure. When a failed verification command emits text such as “no secrets,” suppression can replace
its error with “scan-secrets: passed (no findings),” hiding the failure detail from the model.
Code

internal/runtime/opencode_plugin/fullsend-hooks.ts[R219-220]

+            const sanitized = chain("PostToolUse", names[part.tool] ?? part.tool,
+              state.input ?? {}, { output: state.error, metadata: state.metadata ?? {}, exit_code: 1 });
Evidence
The new transform supplies a nonzero exit code while selecting PostToolUse. The shared chain
reserves its failure handler for PostToolUseFailure, and its suppression guard checks only an `Exit
code text prefix or interrupted, not exit_code`. The scan-secrets summarizer can therefore
replace qualifying error text with a passing summary.

internal/runtime/opencode_plugin/fullsend-hooks.ts[219-224]
internal/security/hooks/posttool_chain.py[317-319]
internal/security/hooks/posttool_chain.py[357-366]
internal/security/hooks/hook_io.py[139-150]
internal/security/hooks/context_suppress_posttool.py[121-125]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Failed OpenCode tool errors are sent through PostToolUse with an `exit_code` that context suppression ignores, allowing a failed verification command to receive a success-looking summary.
## Fix Focus Areas
- internal/runtime/opencode_plugin/fullsend-hooks.ts[219-224]
- internal/security/hooks/hook_io.py[139-150]
## Recommended Fix
Make the shared suppression failure check recognize the synthetic nonzero exit code, or mark these errors as failures in a form it already recognizes. Add a failed-verification regression test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Pinned agent images block OpenCode ✗ Dismissed
Description
Bootstrap now calls openCodeSDKCacheRestore, which requires SDK files baked into the new sandbox
image, but the agents repository still pins its harnesses to existing image digests. An OpenCode run
using those harnesses reaches the new check during bootstrap and exits before the adapter can load.
Code

internal/runtime/opencode_bootstrap.go[R119-120]

+	if err := codexExecOK(sandboxName, r.openCodeSDKCacheRestore(), "preparing OpenCode plugin SDK cache"); err != nil {
+		return err
Evidence
The PR adds both the image installation and an unconditional bootstrap check for its files; the
consuming harness still selects an immutable, previously pinned image.

fullsend -> agents
internal/runtime/opencode_bootstrap.go[115-120]
internal/runtime/opencode_hooks.go[155-161]
images/sandbox/Containerfile[374-378]
External repo: fullsend-ai/agents, harness/triage.yaml [10-12]
External repo: fullsend-ai/agents, harness/code.yaml [19-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
OpenCode bootstrap requires an SDK added only to the new image, while agents harnesses retain immutable image digests.
## Fix Focus Areas
- internal/runtime/opencode_bootstrap.go[116-120]
- images/sandbox/Containerfile[374-378]
- /cross_repos/agents/harness/triage.yaml[12-12]
## Recommended Fix
Publish the SDK-bearing sandbox images and coordinate updates to the corresponding pinned digests in fullsend-ai/agents before enabling OpenCode runs against those harnesses.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Scribe cannot read meeting notes ✗ Dismissed
Description
The new checkBash enforces scribe’s Bash(jq) declaration against every command segment, while
the agents repository instructs scribe to run tar, cat, and cat | jq to initialize its inputs.
Selecting OpenCode for the scribe harness now denies those mandatory commands, including extraction
of the notes archive.
Code

internal/runtime/opencode_plugin/fullsend-hooks.ts[R45-50]

+  for (const segment of command.split(/\r?\n|&&|\|\||;|\|&|\||(?<![<>|])&(?!>)/)) {
+    if (!segment.trim()) continue;
+    const first = segment.trim().split(/\s+/)[0];
+    if (!allowlist.includes(first) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(first) ||
+        ["eval", "exec", "sh", "bash", "source", ".", "command", "builtin", "env", "nohup", "nice", "timeout", "xargs", "time"].includes(first)) {
+      deny("command is outside the Bash allowlist");
Evidence
Scribe’s declared Bash list contains only jq, whereas its mandatory startup and context steps
invoke tar and cat; the new adapter denies first tokens absent from that list.

fullsend -> agents
internal/runtime/opencode_plugin/fullsend-hooks.ts[41-52]
internal/runtime/opencode_hooks.go[37-49]
External repo: fullsend-ai/agents, agents/scribe.md [1-5]
External repo: fullsend-ai/agents, agents/scribe.md [35-48]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The scribe prompt requires shell programs excluded from its Bash allowlist, which the new OpenCode adapter enforces.
## Fix Focus Areas
- internal/runtime/opencode_plugin/fullsend-hooks.ts[41-52]
- /cross_repos/agents/agents/scribe.md[1-5]
- /cross_repos/agents/agents/scribe.md[35-48]
## Recommended Fix
Coordinate a change in fullsend-ai/agents to allow the specific commands scribe requires, or change its instructions to use permitted tools. Retain the adapter’s restriction rather than silently widening the declared allowlist.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Merge-queue code images use an old base ✓ Resolved
Description
The new merge_group exclusions prevent build-base from publishing a digest, so merge-base
supplies the existing :dev tag to build-code instead of the base image built for the queue
commit. On a merge-queue run with relevant image changes, the code-image build and smoke test
therefore exercise an older sandbox base, or fail if that tag is unavailable.
Code

.github/workflows/sandbox-images.yml[127]

+          outputs: type=image,name=${{ env.BASE_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' && github.event_name != 'merge_group' }}
Evidence
The merge-group conditions disable the base push and all manifest-publication steps. With no
manifest digest, Compute base image ref falls back to :dev, which is passed unchanged as the
code image’s BASE_IMAGE.

.github/workflows/sandbox-images.yml[124-127]
.github/workflows/sandbox-images.yml[219-244]
.github/workflows/sandbox-images.yml[292-299]
.github/workflows/sandbox-images.yml[349-355]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Merge-queue runs smoke-test a candidate sandbox image but build the dependent code image from the previously published `:dev` base.
## Fix Focus Areas
- .github/workflows/sandbox-images.yml[124-127]
- .github/workflows/sandbox-images.yml[219-244]
- .github/workflows/sandbox-images.yml[292-299]
- .github/workflows/sandbox-images.yml[349-355]
## Recommended Fix
Provide `build-code` with an immutable reference to the base built by the same merge-queue run, without publishing the normal release tags. Ensure both platform builds and their smoke tests use that reference.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Plugin SDK dependencies are not pinned in the image ✓ Resolved
Description
The Containerfile runs npm install @opencode-ai/plugin@${OPENCODE_VERSION} with no committed
lockfile or integrity check, so transitive dependencies such as zod resolve to whatever matching
versions the registry serves at build time. Every other tool in this image is SHA256-verified, and
this tree is copied into the runtime's config and linked from $HOME/.config/opencode, so a
compromised or changed transitive release reaches every OpenCode sandbox without review.
Code

images/sandbox/Containerfile[R377-378]

+RUN npm install --prefix /sandbox/opencode-config --save-exact --ignore-scripts --no-audit --no-fund \
+      "@opencode-ai/plugin@${OPENCODE_VERSION}"
Evidence
Only the top-level package version is pinned with --save-exact; no package-lock.json is supplied
to the build context, and the npm install runs without npm ci. The binaries in the same file are
each checked against a pinned SHA256.

images/sandbox/Containerfile[341-366]
images/sandbox/Containerfile[374-378]
internal/runtime/opencode_hooks.go[155-162]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The baked OpenCode plugin SDK is installed with `npm install` and no lockfile, so transitive dependencies float between builds.

## Fix Focus Areas
- images/sandbox/Containerfile[374-378]

## Recommended Fix
Add an `images/sandbox/opencode-sdk/package.json` and `package-lock.json` (with integrity hashes) pinned to the OpenCode version. COPY them into `/sandbox/opencode-config` and run `npm ci --ignore-scripts --no-audit --no-fund` there. Update the pin comment so bumping OPENCODE_VERSION also regenerates the lockfile.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. The runtime guide leaves hooks undefined ✓ Resolved
Description
The changed runtime table calls OpenCode's feature shared sandbox hooks without defining it or
linking to a glossary or architectural reference. Readers reaching this first mention cannot tell
what the hooks control or where to learn the term before choosing the runtime.
Code

docs/guides/getting-started/choosing-a-runtime.md[20]

+| `opencode` | Experimental | [OpenCode](https://opencode.ai/) — reads `AGENTS.md` natively | Opt-in pilots on version 1.19.1, with shared sandbox hooks and security enabled for every role; see [OpenCode](../../runtimes/opencode.md) |
Evidence
The guide's changed table row introduces shared sandbox hooks but contains no inline definition or
documentation link for that term, which fails the requirement that guide jargon be defined on first
use.

docs/guides/getting-started/choosing-a-runtime.md[20-20]
Skill: writing-user-docs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The runtime guide introduces the domain-specific term `shared sandbox hooks` without an inline definition or a link to the glossary or architectural documentation.

## Fix Focus Areas
- docs/guides/getting-started/choosing-a-runtime.md[20-20]

## Recommended Fix
Add a glossary or architecture link to the first occurrence of `shared sandbox hooks`, or define the term inline by briefly explaining that these hooks enforce shared sandbox security controls around runtime tool calls.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

11. Reading a missing path is reported as tampering ✓ Resolved
Description
For an LS-only agent, validate calls statSync on the read target, and a nonexistent path
throws ENOENT. The catch block rewrites any error without the [fullsend-hooks] prefix to `hook
integrity check failed`. A typo'd path therefore shows the agent and operator an integrity failure,
pointing them to sandbox tampering instead of a missing file.
Code

internal/runtime/opencode_plugin/fullsend-hooks.ts[R145-148]

+      if (input.tool === "read" && cfg.tools?.includes("LS") && !cfg.tools.includes("Read")) {
+        const target = args.filePath ?? args.file_path;
+        if (typeof target === "string" && statSync(path.resolve(cwd, target)).isDirectory()) canonical = "LS";
+      }
Evidence
statSync(path.resolve(cwd, target)) throws for missing files, and the catch at lines 154-157
rewrites any non-prefixed error to the integrity message.

internal/runtime/opencode_plugin/fullsend-hooks.ts[139-159]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An LS-only `read` of a missing path is reported as `hook integrity check failed`.

## Fix Focus Areas
- internal/runtime/opencode_plugin/fullsend-hooks.ts[145-148]

## Recommended Fix
Wrap the `statSync` call: on ENOENT/ENOTDIR, call `deny("read target does not exist")`. Do the same for a non-directory target, which falls through to the allowlist denial. Keep the generic integrity message only for checkFiles failures.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
❌ Tickets: failed to load — Ticket collection could not establish complete evidence
✅ Compliance rules (platform): 95 rules
✅ Skills: 5 invoked
  writing-user-docs
  writing-adrs
  check-patch-coverage
  cutting-releases
  adr-corner
✅ Cross-repo context — repo relationships
  Explored: repo: fullsend-ai/agents (sha: 332a2ec8) — View relationship
Review mode: Auto: 🧠 Deep: Security-critical adapter spans many independent Go, TypeScript, bootstrap, and runtime paths.

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread docs/guides/getting-started/choosing-a-runtime.md Outdated
Comment thread internal/runtime/opencode_bootstrap.go
Comment thread internal/runtime/opencode_plugin/fullsend-hooks.ts Outdated
Comment thread .github/workflows/sandbox-images.yml Outdated
Comment thread internal/runtime/opencode_plugin/fullsend-hooks.ts
Comment thread images/sandbox/Containerfile Outdated
Comment thread internal/runtime/opencode_plugin/fullsend-hooks.ts
Comment thread internal/runtime/opencode_bootstrap.go
Comment thread internal/runtime/opencode_plugin/fullsend-hooks.ts Outdated
Comment thread internal/runtime/opencode_plugin/fullsend-hooks.ts Outdated
Preserve the native hook adapter while adopting the shared hook
environment and trusted interpreter APIs from main. Keep the data-only
.env loader, trusted binary launch, and both hook integrity checks.

Verify the pinned hook PATH during bootstrap and retain override-reset
ordering checks. Update the runtime documentation for data-only loading.

Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Site preview

Preview: https://3da24be3-site.fullsend-ai.workers.dev

Commit: f866ac49d25427bcda8dfbe26008dc30d1e5b12f

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review notes on head 2347294: one failing required check and two smaller points, all inline.

Comment thread internal/runtime/opencode_hooks_test.go
Comment thread internal/runtime/opencode_run.go
Comment thread internal/runtime/opencode_plugin/fullsend-hooks.ts Outdated
Honor nonzero exit_code on structured OpenCode errors so context
suppression cannot replace failure details with a passing summary.
Cover failure passthrough and successful suppression.

Addresses PR fullsend-ai#8852 review feedback from @qodo-code-review[bot].

Assisted-by: gpt-6.1-sol
Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>
Permit single-quoted Bash literals without hiding executable syntax.
Cache successful error sanitization by identity and complete payload,
and distinguish missing LS targets from hook integrity failures.

Addresses PR fullsend-ai#8852 review feedback from @qodo-code-review[bot].

Assisted-by: gpt-6.1-sol
Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>
Link the term directly from the runtime-selection table.

Addresses PR fullsend-ai#8852 review feedback from @qodo-code-review[bot].

Assisted-by: gpt-6.1-sol
Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>
Commit the SDK manifest and transitive integrity hashes, install with
npm ci, and reject SDK pins that differ from the OpenCode binary pin.
Document lockfile regeneration when updating the runtime.

Addresses PR fullsend-ai#8852 review feedback from @qodo-code-review[bot].

Assisted-by: gpt-6.1-sol
Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>
Publish base platform digests and a run-scoped queue index. Pass its
immutable digest to both code-image builds and smoke tests without
updating normal release tags. Fail closed if that digest is missing.

Addresses PR fullsend-ai#8852 review feedback from @qodo-code-review[bot].

Assisted-by: gpt-6.1-sol
Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review notes on head 624928d: one critical and three medium findings, all inline. The critical one: OPENCODE_PURE set from .env stops the hook plugin from loading, and the digest guards still pass.

Comment thread internal/runtime/opencode_run.go
Comment thread internal/security/hooks/hook_io.py
Comment thread internal/runtime/opencode_plugin/fullsend-hooks.ts Outdated
Comment thread internal/runtime/opencode_plugin/fullsend-hooks.ts Outdated
Use printf so shell escape handling cannot alter recorded guard commands.

Addresses PR fullsend-ai#8852 review feedback from @waynesun09.

Assisted-by: gpt-6.1-sol
Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>
Keep the hook plugin enabled after hostile environment loads. Normalize
Bash exit metadata, patch targets, and path-scoped permission inputs.

Cover the native failure and permission paths with the pinned runtime.
Addresses PR fullsend-ai#8852 review feedback from @waynesun09.

Assisted-by: gpt-6.1-sol
Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>
Explain the stronger Bash default and the retained pi-style skills layout.
Reference agents#1821 for the scribe tool-declaration follow-up.

Addresses PR fullsend-ai#8852 review feedback from @waynesun09.

Assisted-by: gpt-6.1-sol
Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>
Keep local PR feedback and triage records out of commits.

Addresses PR fullsend-ai#8852 feedback workflow requirements.

Assisted-by: gpt-6.1-sol
Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review notes on head 85d7e75: one medium finding, inline.

Comment thread internal/runtime/opencode_plugin_integration_test.go
Run the existing pre-block probe in each candidate sandbox image as the
sandbox user. Include runtime and security changes in the image workflow
triggers.

Addresses PR fullsend-ai#8852 review feedback from @waynesun09.

Assisted-by: gpt-6.1-sol
Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>
Preserve the native OpenCode hook description alongside upstream's
human_min_role overlay guidance.

Assisted-by: gpt-6.1-sol
Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>
@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review notes on head 3171a19: three medium findings, all inline.

// Scan every string field, not just stdout: native metadata, titles
// and attachments can otherwise carry an alternate copy of a secret.
const original = { ...output };
if (input.tool === "bash" && Number.isInteger(output.metadata?.exit)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

MEDIUM: Bash timeout or abort (metadata.exit null) still looks like success to context suppression

The fix in dcd6839 sets exit_code only when Number.isInteger(output.metadata?.exit). In OpenCode v1.19.1 packages/opencode/src/tool/shell.ts:542-557, the race between exitCode, abort and timeout returns code: null for the abort and timeout cases. The tool still returns normally with metadata: { exit: null }, and the termination notice is appended only as a <shell_metadata> block in the output text (lines 561-584). No exit_code reaches the hooks.

hook_io.looks_failed (hook_io.py:147-152) checks only an Exit code prefix, interrupted, or an integer non-zero exit_code, so it reports success. suppress_scan_secrets matches by substring ("no secrets" in lower), and the go test and pytest matchers look for positive evidence in the same way. A verification command that prints partial success text and is then killed by timeout or user abort is rewritten to ...: passed, and the timeout notice is dropped. This is a different case from the integer non-zero exit thread on hook_io.py:151, which is resolved.

Suggestion: in tool.execute.after, when input.tool === "bash" and metadata.exit is not an integer, mark the payload as failed, for example with original.interrupted = true or a non-zero sentinel exit_code. Add a regression test: bash output containing "No secrets found" followed by the shell_metadata timeout notice, with exit: null, must not be suppressed.

function checkPermission(cfg, tool, args, cwd, worktree, targets) {
const id = ["write", "apply_patch", "multiedit"].includes(tool) ? "edit" : tool;
const rule = cfg.permission[id] ?? cfg.permission["*"];
const match = (pattern, text) => new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$", "s").test(text);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

MEDIUM: the plugin's trusted-permission ceiling uses different wildcard rules and match values than OpenCode for bash, skill, glob/grep and websearch

The earlier thread on line 91 was fixed only for path tools (read/edit/write/apply_patch). Other tools still differ from native OpenCode 1.19.1:

  1. Native Wildcard.match (packages/opencode/src/util/wildcard.ts) turns a trailing * into ( .*)?, so "git *" matches a bare git. The local match here does not, so it denies that command.
  2. Native bash checks each parsed command node separately (shell.ts collect, which skips cwd commands such as cd). The plugin matches the whole command string, so a trusted map like {"*":"deny","git *":"allow"} denies cd repo && git status, which native OpenCode allows.
  3. For skill, websearch and glob/grep, the plugin tests args.path or the literal "*" instead of native's pattern, name or query, so a trusted skill or websearch allow-list by name denies every call.
  4. Rule selection uses cfg.permission[id] ?? cfg.permission["*"] instead of native's ordered evaluation.

Native checks still run, so this over-denies and is not a bypass. But it breaks valid trusted pattern maps that the docs say the plugin keeps as a ceiling.

Suggestion: either port OpenCode's Wildcard.match (including the trailing * rule) and per-tool value selection, with table tests against native results, or limit the plugin-side ceiling to the path tools that are already verified and document that native evaluation is authoritative for bash, skill, glob, grep and websearch.

try {
const tool = validate(input, input.args);
const sanitized = chain("PostToolUse", tool, input.args, original);
output.output = sanitized.output;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

MEDIUM: MCP tool results bypass sanitization. Only output.output is rewritten, but OpenCode builds the model text from result.content[]

In OpenCode v1.19.1 packages/opencode/src/session/tools.ts:419-458, MCP tools trigger tool.execute.after with the raw CallToolResult. After the hook, OpenCode builds the model-visible text and attachments from result.content[] (text items, resource.text, image and blob data). This hook writes back only output.output = sanitized.output and, on rewrite, clears metadata and attachments. It never touches content, so the redaction, canary and unicode rewrites the chain makes on content[].text are thrown away.

For an unrestricted agent (openCodePermissionRecord(nil) returns an empty record and cfg.tools is null), MCP tools are allowed. validateOpenCodeTrustedEnv (opencode_integrity.go:89-114) rejects a non-empty plugin but accepts an mcp block in OPENCODE_CONFIG_CONTENT. Any MCP server configured there therefore returns results to the model without the shared post-tool sanitization.

Suggestion: fail closed on result shapes the adapter does not handle: deny when the after payload has a content array or no string output, or when the tool is not a known built-in. Also reject a non-empty mcp key in validateOpenCodeTrustedEnv next to plugin. Add a test for each.

Signed-off-by: Yvonne Devlin <ydevlin@redhat.com>

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review notes on head f866ac4: one medium finding, inline.

// call. The native hook adapter additionally checks file-tool write paths.
func (r OpenCodeRuntime) EnvExports() []string {
return []string{
"unset " + strings.Join(openCodeDangerousEnvVars, " "),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

MEDIUM: EnvExports() writes a non-assignment unset ... line into .env, so every OpenCode .env read logs an "ignored .env line" warning

This PR adds "unset " + strings.Join(openCodeDangerousEnvVars, " ") as the first entry of OpenCodeRuntime.EnvExports(). bootstrapEnv (internal/cli/run.go:3802) appends rt.EnvExports() line by line into /sandbox/workspace/.env. That file is now read as data by envDataLoad. In env_data.go, statement() matches unset as a NAME. The next character is not = and the line has no export prefix, so it raises "not a NAME=value assignment". read_file then calls warn() and prints fullsend: ignored '/sandbox/workspace/.env' line N: not a NAME=value assignment.

This happens on every OpenCode launch and on every probe that loads .env. That warning channel is how operators see tampered or malformed .env lines, so a warning on every run makes the real ones easy to miss.

The unset has no effect in .env. It is also redundant at launch: opencode_run.go joins EnvExports() into the prelude after the .env load, and openCodeTrustedEnvRestore() already unsets the same openCodeDangerousEnvVars. No other runtime's EnvExports returns a non-assignment line, and main's opencode.go had none, so this is new in the diff.

Suggestion: make EnvExports() return only export or assignment lines. Keep the unset in the launch-time shell fragment (openCodeTrustedEnvRestore already covers it) and list the names in openCodeEnvDataDrop. Add a test that runs EnvExports() through envDataLoad and checks that no "ignored" warning is printed.

This branch was successfully deployed

1 active deployment
site-preview — f866ac49 Deployed Oct 29, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(runtime/opencode): implement hook adapter plugin for sandbox security enforcement

2 participants