Skip to content

feat(agent): resolve review feedback with evidence - #16

Merged
kodiakhq[bot] merged 37 commits into
mainfrom
claude/issue-8-implementation-r7kd66
Aug 9, 2026
Merged

feat(agent): resolve review feedback with evidence#16
kodiakhq[bot] merged 37 commits into
mainfrom
claude/issue-8-implementation-r7kd66

Conversation

@RedStar071

@RedStar071RedStar071 commented Aug 9, 2026

Copy link
Copy Markdown
Member

Implements the v0.1 feedback-to-verification loop: ingest GitHub review
feedback, validate whether the claim is actually supported, apply a narrowly
scoped fix through the runner boundary when authorized, verify with the
repository's own checks, and publish deterministic evidence for accepted and
rejected findings alike.

Verdicts and evidence

  • Add a runtime verdict (accepted, rejected, inconclusive) decided by
    packages/agent/src/validation.ts rather than by the reviewer or the model. A
    finding is accepted only when it cites evidence, names a file that exists,
    and quotes repository content that is really there, so fabricated paths and
    invented code quotes are rejected instead of fixed.
  • Preserve rejection reasons in full and render an EvidenceBundle to
    deterministic Markdown, shared by the CLI and GitHub checks.
  • Derive TaskResult.verified in exactly one place: completed state, an applied
    change, and every executed check passing. No branch can claim verification it
    did not earn.

Lifecycle

  • Add LifecycleMachine with the full transition table, so executing cannot
    reach completed without verifying and an undefined move throws rather than
    producing a result that looks finished.
  • Enforce the repair budget, narrow-scope change limits, and a distinct
    reportable outcome for every authorization refusal.

Runner boundary

  • Split the boundary into process, boundary, local, and container modules.
    Refuse writes on a read-only runner, refuse anything reaching into .git, and
    re-validate paths against resolved symlinks so a planted link cannot escape
    the checkout.
  • Add ContainerRunner for production: ephemeral, all capabilities dropped,
    no-new-privileges, CPU and memory limits, no forwarded environment, and the
    configured egress policy mapped to a concrete container network.
  • Bound and redact captured output; reject commands that would need a shell.

GitHub

  • Parse pull_request_review and pull_request_review_comment into normalized
    feedback, validating every field of the untrusted payload and ignoring
    approvals, empty bodies, and the agent's own account.
  • Publish check runs whose conclusion can never report a failing check as
    success, with the token sent only as a header and redacted from errors.

Verification and configuration

  • Discover the checkout's own lint, typecheck, test, and build scripts when
    checks is empty, and refuse to change files when no check can verify them.
  • Add validation, runner, and maxChangedFiles policy with full validation.
  • Require callers to pass a GitHub token explicitly so no code path reaches
    GitHub implicitly, and keep every test off the network.

Verified with pnpm check:repo, lint:ci, typecheck, test (170 tests), and build.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01LBHWebMnjZkofzCY7Biawm


View with [code]smith
Need help on this PR? Tag @codesmith with what you need. Autofix is enabled.

Confidence Score: 3/5

The change is not safe to merge until the non-/proc/self/fd write path preserves checkout containment for the entire filesystem mutation.

One verified security-sensitive checkout escape remains: the fallback turns a held directory capability back into a mutable pathname before creating and renaming the replacement file.

Files Needing Attention: packages/runner/src/boundary.ts

Security Review

On platforms without usable /proc/self/fd, an actor able to modify the writable checkout can replace a verified parent directory with a symlink after its identity check. Temporary-file creation and the final rename then follow the replacement path, allowing a repository-relative write to create or overwrite a file outside the checkout.

T-Rex T-Rex Logs

What T-Rex did

  • A forced fallback reproduction workflow was produced to support the posted P1 finding, including control and adversarial runs and a runtime harness.
  • The Doctor discovery harness was run against the current runDoctor implementation and observed lockfile checks via inspector.exists and package metadata loads via inspector.read, with the assertions passing twice.
  • Boundary behavior was validated with control and adversarial fallback runs, noting escaped and outsideTargetContent values and the stillPresent flags.
  • Harness sources and startup captures were collected, including before/after comparisons and the blocker shown by the doctor CLI runtime attempt.

View all artifacts

T-Rex Ran code and verified through T-Rex

Fix All in Claude CodeFix All in CursorFix All in Cursor Cloud Agents

Prompt To Fix All With AI
### Issue 1
packages/runner/src/boundary.ts:375-379
**Fallback directory anchor can be redirected outside the checkout**
When `/proc/self/fd` is unavailable, `directoryAnchor` verifies that `fallback` names the held directory and then returns that mutable pathname. A concurrent task can rename the verified directory and replace it with a symlink immediately after the inode comparison. `replaceInside` subsequently creates its temporary file and renames the result through that symlink, allowing a repository-relative write to overwrite a file outside the checkout. Use descriptor-relative create/rename operations for this path, or fail closed when those operations are unavailable.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (4): Last reviewed commit: "fix(runner): keep the adversarial-rename..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Comment threadpackages/runner/src/boundary.ts Outdated
Comment threadpackages/cli/src/index.ts Outdated
@socket-security

socket-securityBot commented Aug 9, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Added@​vite-hub/​shell@​0.0.2771008889100
Addedmagic-regexp@​0.11.01001008781100
Added@​ai-sdk/​openai-compatible@​3.0.279910010098100
Addedai@​7.0.589910010099100

View full report

@socket-security

socket-securityBot commented Aug 9, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

ActionSeverityAlert (click "▶" to expand/collapse)
WarnHigh
Obfuscated code: npm json-schema is 90.0% likely obfuscated

Confidence: 0.90

Location:Package overview

From:pnpm-lock.yamlnpm/ai@7.0.58npm/@ai-sdk/openai-compatible@3.0.27npm/json-schema@0.4.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/json-schema@0.4.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

WarnHigh
Obfuscated code: npm strtok3 is 90.0% likely obfuscated

Confidence: 0.90

Location:Package overview

From:pnpm-lock.yamlnpm/@vite-hub/shell@0.0.2npm/strtok3@10.3.5

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/strtok3@10.3.5. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Comment threadpackages/runner/src/boundary.ts Outdated
greptile-apps[bot]
greptile-appsBot previously approved these changes Aug 9, 2026
@greptile-apps
greptile-appsBot dismissed their stale reviewAugust 9, 2026 16:42

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

claudeand others added 24 commits August 9, 2026 17:00
Implements the v0.1 feedback-to-verification loop: ingest GitHub review
feedback, validate whether the claim is actually supported, apply a narrowly
scoped fix through the runner boundary when authorized, verify with the
repository's own checks, and publish deterministic evidence for accepted and
rejected findings alike.
Verdicts and evidence
- Add a runtime verdict (accepted, rejected, inconclusive) decided by
packages/agent/src/validation.ts rather than by the reviewer or the model. A
finding is accepted only when it cites evidence, names a file that exists,
and quotes repository content that is really there, so fabricated paths and
invented code quotes are rejected instead of fixed.
- Preserve rejection reasons in full and render an EvidenceBundle to
deterministic Markdown, shared by the CLI and GitHub checks.
- Derive TaskResult.verified in exactly one place: completed state, an applied
change, and every executed check passing. No branch can claim verification it
did not earn.
Lifecycle
- Add LifecycleMachine with the full transition table, so executing cannot
reach completed without verifying and an undefined move throws rather than
producing a result that looks finished.
- Enforce the repair budget, narrow-scope change limits, and a distinct
reportable outcome for every authorization refusal.
Runner boundary
- Split the boundary into process, boundary, local, and container modules.
Refuse writes on a read-only runner, refuse anything reaching into .git, and
re-validate paths against resolved symlinks so a planted link cannot escape
the checkout.
- Add ContainerRunner for production: ephemeral, all capabilities dropped,
no-new-privileges, CPU and memory limits, no forwarded environment, and the
configured egress policy mapped to a concrete container network.
- Bound and redact captured output; reject commands that would need a shell.
GitHub
- Parse pull_request_review and pull_request_review_comment into normalized
feedback, validating every field of the untrusted payload and ignoring
approvals, empty bodies, and the agent's own account.
- Publish check runs whose conclusion can never report a failing check as
success, with the token sent only as a header and redacted from errors.
Verification and configuration
- Discover the checkout's own lint, typecheck, test, and build scripts when
checks is empty, and refuse to change files when no check can verify them.
- Add validation, runner, and maxChangedFiles policy with full validation.
- Require callers to pass a GitHub token explicitly so no code path reaches
GitHub implicitly, and keep every test off the network.
Verified with pnpm check:repo, lint:ci, typecheck, test (170 tests), and build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LBHWebMnjZkofzCY7Biawm
Validation alone raced the filesystem operation: a concurrent task could
swap a validated directory for a symlink before mkdir or writeFile used
the unresolved target, redirecting the write outside the checkout.
Reads and writes now re-resolve the parent immediately before opening,
open the final component with O_NOFOLLOW, and re-check containment on
the descriptor itself (via /proc on Linux, inode identity elsewhere)
before any content moves through it. Missing directories are created one
validated component at a time instead of with a recursive mkdir.
Deterministic regression tests reproduce the validate-then-swap race for
both read and write.
Doctor's check discovery read lockfiles and package.json with direct
filesystem helpers, splitting checkout access policy across packages. It
now inspects the checkout through a read-only LocalRunner, so the same
path validation and boundary enforcement apply. A local runner is used
deliberately so doctor can still diagnose a misconfigured container
isolation setup.
A write previously truncated and wrote through a descriptor whose
containment was proven only at open time; a concurrent task could rename
the opened inode outside the checkout in between and receive the write.
Writes no longer mutate the target inode at all. Content is staged into
a fresh exclusively-created temporary inode and committed with an atomic
rename, with both names resolved through a held, containment-verified
directory descriptor (/proc/self/fd on Linux), so the mutation stays
anchored to the validated directory inode for its full lifetime. The
final-component symlink refusal is kept explicit and the previous file
mode is carried over the rename. A deterministic regression test renames
the validated target over an external victim after every check has
passed and proves the victim receives nothing.
ShellAnalyzeResult exposes commands, not commandNames, so lint and typecheck
failed. assertSimpleCommand now throws CommandRejectedError, preserving the
boundary's rejection contract; the class moves to process.ts where the lower
layer can raise it.
The config layer intentionally validates only that a check command is
non-empty; ViteHub Shell analysis in the runner rejects shell expressions at
execution time. Update the stale tests to that contract.
… API error bodies
Distinguish unparseable JSON from schema mismatches, append the redacted
response body to API call failures, enable structured outputs on the provider,
and give mocked chat completions a finish_reason so AI SDK parses their
output.
aube's no-downgrade trust policy refuses @vite-hub/shell@0.0.2 because it
carries no trust evidence while 0.0.1 was published with SLSA provenance.
0.0.3 restores provenance and keeps the same analyze API, so the boundary
code is unchanged. Regenerates the lockfile for the rebased manifests.
Main introduced @e18e/eslint-plugin. Hoist per-call regular expressions to
module scope and build the task list with Array.from's map callback.
The command-policy refactor made replaceInside private while the descriptor
race regression test still overrides it to interleave a rename. Keep the
method protected, matching its documented test seam, and align the override
with the new parameter names.
@RedStar071
RedStar071force-pushed the claude/issue-8-implementation-r7kd66 branch from 0a4034a to 24f8312CompareAugust 9, 2026 17:10
@kodiakhq
kodiakhqBot merged commit 6330834 into mainAug 9, 2026
11 of 12 checks passed
@kodiakhq
kodiakhqBot deleted the claude/issue-8-implementation-r7kd66 branch August 9, 2026 17:10
Comment on lines +375 to +379
const real = await realpath(fallback);
const [expected, current] = await Promise.all([directory.stat(), stat(real)]);
if (expected.dev !== current.dev || expected.ino !== current.ino)
throw new PathEscapeError(original, 'parent replaced while writing');
return real;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1securityFallback directory anchor can be redirected outside the checkout

When /proc/self/fd is unavailable, directoryAnchor verifies that fallback names the held directory and then returns that mutable pathname. A concurrent task can rename the verified directory and replace it with a symlink immediately after the inode comparison. replaceInside subsequently creates its temporary file and renames the result through that symlink, allowing a repository-relative write to overwrite a file outside the checkout. Use descriptor-relative create/rename operations for this path, or fail closed when those operations are unavailable.

Artifacts

Forced fallback race reproduction script

  • Node script that forces the current directory-anchor fallback and interleaves the directory-to-symlink swap after identity verification, demonstrating the tested condition.

Control run with forced fallback and no directory swap

  • Executed control command shows forced fallback writes the target inside the checkout and does not create an outside file, establishing the comparison baseline.

Adversarial forced-fallback run after directory swap

  • Executed adversarial command shows the post-verification symlink swap causes the target write to appear outside the checkout, reproducing the candidate.

Runtime harness for the two prior boundary claims

  • Node harness implements the current boundary write ordering and deterministically interleaves each previously reported race condition.

Runtime classification of prior boundary claims

  • Executed checks show the symlink validation race is rejected without an outside file and the renamed target inode retains old content while the checkout receives the payload, so both prior roots are no longer present.

Unavailable runner test command output

  • Captured attempted Vitest command exits 127 because the repository has no node_modules test binary, explaining why the narrow Node runtime harness was used.

Unavailable runner typecheck command output

  • Captured attempted TypeScript command exits 127 because the repository has no node_modules compiler binary, confirming the environment limitation.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/runner/src/boundary.ts
Line: 375-379
Comment:
**Fallback directory anchor can be redirected outside the checkout**
When `/proc/self/fd` is unavailable, `directoryAnchor` verifies that `fallback` names the held directory and then returns that mutable pathname. A concurrent task can rename the verified directory and replace it with a symlink immediately after the inode comparison. `replaceInside` subsequently creates its temporary file and renames the result through that symlink, allowing a repository-relative write to overwrite a file outside the checkout. Use descriptor-relative create/rename operations for this path, or fail closed when those operations are unavailable.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude CodeFix in CursorFix in Cursor Cloud Agents

Sign up for freeto 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: v0.1 resolve review feedback with evidence

2 participants

@RedStar071@claude