Add sandboxed OpenCode review worker #1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/sandboxed-review-worker"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Verification
go test ./...go vet ./...sh scripts/adversarial-check.sh/oc rerunonlambadalambda/pleromanet#1completed with a success status/oc review
OpenCode Review
Execution completed for
lambadalambda/opencode-forgejo-botat5c67364bc091(job #10).Findings
[low] Coarse-grained dispatcher mutex blocks all transitions during network I/O (
internal/app/app.go)The single
a.transitionmutex is held acrosshandleIssueComment,handleClaim,handleResult, andhandleFailure, each of which performs multiple Forgejo API calls that can take seconds. A slow or unresponsive Forgejo instance will block all webhook processing, job claiming, and result publication simultaneously. WhileSetMaxOpenConns(1)already serializes DB access, the application-level lock adds network-call latency to the critical path. Consider per-PR locking or moving Forgejo calls outside the mutex where the store transaction itself provides atomicity. This is an operational/availability concern rather than a correctness or security defect.[low]
postBestEffortTerminalStatusIfCurrentreturns terminal errors despite "best-effort" name (internal/app/app.go)When the pre-status
GetPullRequestfreshness check fails due to a transient Forgejo API error, the method returns a non-nil error that surfaces as HTTP 502 to the launcher even though the canonical comment was already published and the job iscompletedin the store. The launcher logs the error and moves on (no retry of an already-completed job), so no duplicate work occurs, but the naming understates the impact: the only truly best-effort part is the finalCreateCommitStatuscall. This is consistent with the documented design ("statuses are best-effort decoration") but the function-level error contract is slightly misleading for future maintainers.[low]
strings.Titleis deprecated (internal/app/app.go,renderFailureComment)strings.Titlehas been deprecated since Go 1.18 because it does not handle Unicode word boundaries correctly. Sincejob.Kindis always"review"or"summary", the output is correct today, but the linter (go vet) may flag it and a future Kind value with non-ASCII could behave unexpectedly. Replace with a manualstrings.ToUpper(kind[:1]) + kind[1:]orgolang.org/x/text/cases.[low] Test helper hardcodes PR HTML URL regardless of owner/name (
internal/app/app_test.go,pullRequestResponseForRepo)pullRequestResponseForRepoacceptsownerandnameparameters for the repository fields but hardcodesHTMLURL: baseURL + "/lambadalambda/pleromanet/pulls/42"for the pull request URL. This does not affect test correctness today because the organization tests construct payloads viaorganizationIssueCommentPayload, but a future test that asserts onPRHTMLURLfor a different repo would silently pass with the wrong URL.[informational] Tar path validation accepts trailing-slash directory names (
internal/launcher/snapshot.go,safeArchivePath)safeArchivePath("dir/")returns"dir"without error becausepath.Clean("dir/")equalsstrings.TrimSuffix("dir/", "/"). The duplicate-seen check then prevents a separate"dir"entry from also appearing. This is safe (the extracted path is identical), but the trailing-slash acceptance is subtle; a comment documenting the intentional behavior would help future reviewers.[informational]
ParseOpenCodeJSONLerror-event detection relies onjson.RawMessagestring comparison (internal/launcher/worker.go)The check
string(event.Error) != "null"correctly distinguishes JSONnull(4 bytes) from the JSON string"null"(6 bytes with quotes), but the logic is non-obvious. A brief comment explaining that a JSONnullbody forerrormeans "no error" would improve maintainability.[informational] Preparer
writePatchAttachmentsline-splitting can break mid-content for pathological patches (internal/preparer/preparer.go)Individual diff lines exceeding
patchAttachmentMaxLineBytes(1800 bytes) are split at UTF-8 rune boundaries across multiple attachments. The worker reassembles them by concatenation, so the original patch is preserved exactly. However, an attacker crafting a patch with many oversized lines could generate many small parts (bounded bypatchAttachmentMaxParts = 128), which is handled by fail-closed behavior. This is correctly bounded but worth documenting as an adversarial resilience point.[informational]
container.go—--env=HOME=/job/homefor preparer but no tmpfs mount for that path (internal/launcher/preparer.go)The preparer Podman args set
HOME=/job/homeand mount--tmpfs=/job:...size=512m. Since/job/homeis inside the/jobtmpfs, the home directory exists on tmpfs. Git is configured withHOME=+ home directory ingitEnvironment, so.gitconfigwrites go to/job/home. This is correct and self-consistent; just noting that the home directory is implicit under the job tmpfs rather than explicitly declared.Overall
This is a substantial, well-architected security-focused rewrite that replaces a monolithic bot with a three-boundary design (trusted dispatcher, host launcher, disposable preparer). The trust separation is clean: the dispatcher never touches Git/Node/OpenCode, the launcher never holds Forgejo credentials, and the preparer receives no Z.AI key. Defense in depth is consistently applied — anonymous HTTPS-only clone validation,
core.hooksPath=/dev/null, no redirects, no submodules, no LFS, bounded I/O at every layer, strict tar extraction rejecting traversal/symlinks/devices/duplicates, snapshot sanitization removing instruction and config files at every depth, a worker with every tool and plugin disabled, an egress proxy limited to two HTTPS CONNECT destinations, and lease-token CAS semantics preventing duplicate generations or stale-result publication.The publication race-condition handling is particularly thorough: the dispatcher refreshes the PR before and after comment mutation, rolls back created comments or restores previous bodies when the PR changes mid-publication, and treats compensation failures as terminal. The store-level idempotency model (unique
delivery_idon jobs, random CAS delivery tokens, generation counters, lease expiry requeue) is sound and well-tested under concurrent access.The findings are all low or informational severity — no correctness bugs, data-integrity violations, or security weaknesses were identified. The coarse mutex and best-effort naming are the most actionable items for future iteration. No blocking issues found.
/oc rerun
/oc rerun
/oc rerun