Skip to content

fix(drive-local): declare acceptance inputs as immutable (#284) - #294

Merged
kjgbot merged 1 commit into
mainfrom
fix/284-immutable-acceptance-inputs
Sep 10, 2026
Merged

kjgbot merged 1 commit into
mainfrom
fix/284-immutable-acceptance-inputs

Conversation

@miyaontherelay

@miyaontherelay miyaontherelay commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

An implementation agent could replace a checkout acceptance script with process.exit(0) and obtain PACKAGE_VERIFIED while the implementation remained broken. This adds a reconstructed verification manifest alongside #244's existing pinned baseline and argv: script inputs must come from full Git commit IDs or absolute paths outside implementation write scope. Git inputs are extracted afresh; undeclared scripts, missing pins, writable paths, symlink aliases, and hard-linked external inputs fail closed with an immutability diagnostic.

Existing inline Node assertions remain supported. Verification emits PACKAGE_VERIFIED only after its final scope check, and refusal diagnostics name the DoD. The launcher now uses the SDK's socketPathFor, matching the daemon socket relocation in #262 so the real journal tests can reach these gates.

Validation: npx --no-install tsc --noEmit; 68 local package/launcher tests; 51 SDK backlog/spec tests. The regression probe exits 1 with ACCEPTANCE_IMMUTABILITY_VIOLATION and IMPLEMENTATION=broken; isolated reporting refuses the outside edit and names the DoD. Literal commands and output: verification transcript.

This enforces declared acceptance-input ownership. It does not sandbox trusted assertion programs or infer their dynamic dependencies; the existing same-user filesystem limitation remains documented.

Closes #284.


Note

High Risk
Changes verification gates that decide whether local drive work is complete; bugs could falsely pass broken implementations or block valid packages, though behavior is heavily tested.

Overview
Closes the #284 gap where an agent could swap in-scope acceptance scripts for no-ops and still get PACKAGE_VERIFIED. Drive-local now builds a pinned verification: { ref, checks: [{ argv, inputs }] } manifest from the backlog (alongside existing verificationCommands) and enforces it on initial scope, verify, and report.

Script checks must declare inputs with full Git commit IDs (or absolute paths outside write scope). Git bytes are extracted to a temp dir for execution; undeclared scripts, bad pins, writable/symlink/hard-link paths, and shell -c escapes fail with ACCEPTANCE_IMMUTABILITY_VIOLATION before any check runs. Inline node -e assertions stay as backlog-pinned code. PACKAGE_VERIFIED is emitted only after acceptance plus a final scope pass; errors include the DoD.

gate-snapshot now pins local-work-acceptance.mjs. The local workflow launcher uses SDK socketPathFor for daemon sockets (#262). Docs and broad tests cover the new contract and journal failure paths.

Reviewed by Cursor Bugbot for commit 3e166fb. Bugbot is set up for automated code reviews on this repo. Configure here.

Session-Id: 01a08cf3-1384-7810-bdc7-6cfe60d0a4c1
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 0cdc8a8c-bdf9-4970-8510-e06967b0c9ef


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Review swarm: maintainability

Maintainability Review: PR #294 - Immutable Acceptance Inputs

PR: #294
Branch: fix/284-immutable-acceptance-inputs
Reviewer lens: Maintainability
Review date: 2026-09-10

Summary

This PR addresses issue #284 by implementing an ownership contract for acceptance test inputs, preventing implementation code from mutating the checks that verify it. The change introduces a new module (local-work-acceptance.mjs) that extracts acceptance scripts from pinned Git refs or validates external absolute paths, ensuring checks cannot be edited mid-implementation.

Maintainability Assessment

1. UNCLEAR BOUNDARY: Acceptance vs. Verification Separation

Finding: The division of responsibility between local-work-verification.mjs and local-work-acceptance.mjs creates an unclear contract.

  • local-work-verification.mjs:runChecks() now does nothing but delegate to runAcceptance(pkg) from the acceptance module
  • The verification module still owns checkScope() but acceptance validation is in a separate module
  • The naming suggests verification owns all checking, but it now only owns scope checking
  • A maintainer reading runChecks() in six months would need to trace through two modules to understand what "checks" means

What would break: If someone adds a new kind of check, they could reasonably assume it belongs in local-work-verification.mjs since that's where runChecks() lives, creating inconsistent placement.

Location: ops/local-work-verification.mjs:640-642

2. IMPLICIT CONTRACT: Git Input Validation Happens Twice

Finding: Git input validation occurs in both acceptanceContract() (parse-time, ref: 'HEAD' resolution) and validateInput() (execution-time, full validation).

The contract is:

  • acceptanceContract() replaces ref: 'HEAD' with the full commit sha
  • validateInput() later asserts it's a 40-char hex commit sha

Why it's implicit: Nothing documents that inputs with ref: 'HEAD' are mutated during contract creation. The mutation happens in line 184:

return { ...input, ...(input.ref === 'HEAD' ? { ref } : {}) };

What would break: If someone adds a new ref type (e.g., 'HEAD~1'), they might add it to the assertion at line 216 without realizing they also need expansion in acceptanceContract(). The assertion would fail with an unclear error about "missing pinned Git ref" even though a ref was provided.

Locations:

  • ops/local-work-acceptance.mjs:184 (mutation)
  • ops/local-work-acceptance.mjs:216 (validation assumes mutation happened)

3. MISSING FAILURE HANDLING: Git Command Errors Become Opaque

Finding: Git errors are wrapped in generic violation messages that lose critical diagnostic context.

In validateInput(), lines 220-226:

try {
  assert.equal(git('cat-file', '-t', input.ref).toString().trim(), 'commit');
  const mode = git('ls-tree', input.ref, '--', input.path).toString().split(' ')[0];
  assert(mode === '100644' || mode === '100755');
  return git('show', `${input.ref}:${input.path}`);
} catch (cause) {
  throw new Error(violation(`pinned Git input unavailable: ${input.ref}:${input.path}`), { cause });
}

The problem: This catch block conflates:

  • Git object not found (ref doesn't exist)
  • File not at that path in that commit
  • File is a symlink (wrong mode)
  • Git command failed for environmental reasons (corrupt repo, permissions)

All produce: ACCEPTANCE_IMMUTABILITY_VIOLATION: pinned Git input unavailable: <ref>:<path>

What would break: A stranger debugging why their acceptance input fails won't know if they:

  • Misspelled the path
  • Used the wrong ref
  • Have a corrupt Git object database
  • Hit a symlink that's not executable source

The cause chain exists but the outer message provides no guidance on which failure mode occurred.

Location: ops/local-work-acceptance.mjs:220-227

4. COMMENT THAT ASSERTS WHAT CODE DOES NOT DO: "Do not materialize a symlink blob"

Finding: Line 221 claims:

// Do not materialize a symlink blob as executable source.

But the code that follows does NOT prevent materialization. It checks the Git mode and refuses symlinks, but if a symlink passes (somehow), the next line return git('show', ...) WOULD materialize it.

The comment asserts a safety property that depends on the mode check being exhaustive, but:

  • Git has more than three modes (100644, 100755, 120000 for symlink, plus others)
  • The assertion only allows two modes
  • If Git adds a new mode or someone modifies the check, the comment's claim becomes false

What a maintainer would miss: They might assume "do not materialize" means there's explicit handling to refuse materialization. In fact, it means "we assert the mode, so materialization won't happen." If the assertion is wrong, there's no second line of defense.

Location: ops/local-work-acceptance.mjs:221-224

5. UNCLEAR BOUNDARY: What "Writable Paths" Includes

Finding: The writablePaths() function traverses symlinks and collects both lexical paths and their realpath targets, but its contract is unclear.

Lines 192-212 implement a visited-set traversal that:

  • Adds resolve(path) (lexical)
  • Adds realpathSync(path) (target)
  • Recursively visits symlink targets
  • Recursively visits directory children

Why it's unclear:

  1. The function returns a flat array of paths with no indication which are lexical vs. real
  2. Callers check within(resolve(input.path), scope) OR within(target, scope) but the "OR" logic is in the caller, not in this function's contract
  3. A maintainer adding a new check might not realize they need to check BOTH the resolved path and the real path
  4. The visited set prevents infinite loops but doesn't document what happens with circular symlinks (the answer: they're safe, but that's not stated)

What would break: If someone needs to distinguish "paths that are lexically writable" from "paths that are writable through symlink aliases," they can't. The function mixes both into one array without tagging them.

Location: ops/local-work-acceptance.mjs:192-212

6. TEST THAT WOULD NOT FAIL IF BEHAVIOR BROKE: Hard Link Test

Finding: The test at ops/local-work-acceptance.test.mjs:429-438 asserts that hard links are refused:

test('an external input with an in-scope hard link is refused', t => {
  const f = fixture(t);
  f.put('checks/check.cjs', 'process.exit(0);');
  const path = join(f.root, 'checks/check.cjs');
  linkSync(path, join(f.root, 'src/check.cjs'));
  f.put('ops/BACKLOG.md', entry('Fix value', 'src/', [{ argv: ['node', path], inputs: [{ path }] }]));
  commit(f);
  pass(f.run('select'));
  fail(f.run('verify'), /ACCEPTANCE_IMMUTABILITY_VIOLATION: external input has writable hard-link aliases/);
});

The problem: This test creates a hard link from src/check.cjs (in write scope) to checks/check.cjs (external), then validates the external path. But the assertion at line 235:

assert(stat.nlink === 1, violation(`external input has writable hard-link aliases: ${input.path}`));

Only checks nlink === 1. This would PASS for a file with ANY number of hard links if someone changed the operator to >= 1 or removed the check entirely. The test doesn't verify that the SPECIFIC hard link to an in-scope path is the problem—it just checks that verification fails with that message.

What would not be caught: If the code changed to allow hard links but still failed for some other reason (e.g., wrong path), this test would still pass because it only asserts the error message matches a regex.

Location: ops/local-work-acceptance.test.mjs:429-438

7. UNCLEAR BOUNDARY: When DoD Gets Appended to Errors

Finding: The DoD (Definition of Done) is appended to errors in three places with different trigger conditions:

  1. ops/local-work-acceptance.mjs:305 - during acceptance execution, ONLY for PACKAGE_CHECK_FAILED
  2. ops/local-work-package.mjs:549 - during validation, for ANY error caught in the try-block
  3. Test assertions check for ; DoD: in various error scenarios

Why it's unclear:

  • Line 549 wraps ALL errors (scope violations, acceptance immutability violations) with DoD
  • Line 305 only appends DoD to check execution failures
  • The contract is: "DoD appears when validation or checks fail" but there's no single source of truth for when this should happen
  • If a new error type is added, the maintainer must remember to thread through the DoD or it won't appear in user-facing output

What would break: If someone adds a new validation step (e.g., verifying backlog integrity), they might throw an error that doesn't include DoD context, creating inconsistent error quality.

Locations:

  • ops/local-work-acceptance.mjs:305
  • ops/local-work-package.mjs:546-550

8. IMPLICIT CONTRACT: Package Verification Order Matters

Finding: The verification flow in ops/local-work-package.mjs:238-260 has a specific order:

  1. Load verified package (reconstructs from backlog)
  2. Compare all fields to submitted package
  3. Run checkScope(pkg)
  4. Run validateAcceptance(pkg)
  5. Run runChecks(pkg) (executes tests)
  6. Load verified package AGAIN
  7. Emit PACKAGE_VERIFIED

Why it's implicit:

  • The second verifiedPackage() call at line 258 appears redundant but is actually checking that acceptance commands didn't mutate scope (per the earlier contract)
  • Nothing documents WHY the order is: scope check → acceptance validation → check execution → scope recheck
  • The contract is that acceptance validation must happen before execution (line 369 comment says "validate every input before executing any command"), but this isn't enforced—it's just how the code is currently ordered

What would break: If someone reorders these steps (e.g., runs checks before validation to "fail fast"), they would execute untrusted code before validating immutability, defeating the security property.

Location: ops/local-work-package.mjs:238-260

9. MISSING FAILURE HANDLING: Extraction Directory Cleanup on Assertion Failure

Finding: In runAcceptance(), lines 284-307, each check creates a temp directory:

for (const check of checks) {
  const directory = mkdtempSync(join(tmpdir(), 'drive-acceptance-'));
  try {
    // ... extraction and execution ...
  } finally { rmSync(directory, { recursive: true, force: true }); }
}

The problem: If the process crashes or receives SIGKILL during check execution (line 303's spawnSync), the finally block may not run. The temp directory leaks.

Why this matters for maintainability: The code LOOKS like it cleans up (there's a finally block), but:

  • Kill signals don't trigger finally blocks
  • The 120-second timeout on spawnSync could expire, but if the timeout is hit, control returns to the finally, so that's OK
  • But external termination (user Ctrl-C, OOM kill, system shutdown) would leave /tmp/drive-acceptance-* directories behind

What would break: Over time on a long-running CI system, leaked temp directories would accumulate. A maintainer debugging disk usage wouldn't know these directories are from failed acceptance runs because they're in /tmp with no identifying metadata.

Location: ops/local-work-acceptance.mjs:284-307

10. UNCLEAR BOUNDARY: When Ambient NODE_OPTIONS/NODE_PATH Are Excluded

Finding: Lines 300-302 delete NODE_OPTIONS and NODE_PATH from the environment:

const env = { ...process.env };
delete env.NODE_OPTIONS;
delete env.NODE_PATH;

This happens for ALL checks, not just Node checks. The comment at line 258-259 says "Runtime options that could preload checkout code are deliberately excluded" but this applies to:

  • Node -e checks
  • Node script checks
  • Non-Node executable checks (shell scripts)

Why it's unclear: A shell script acceptance check doesn't care about NODE_OPTIONS, so why is it being deleted? The answer is "because we use one env for all checks," but that's not documented. If someone wants to add a new environment exclusion (e.g., PYTHON_PATH for Python checks), they have to:

  1. Realize these deletions exist
  2. Understand they apply to all checks
  3. Add language-specific exclusions alongside Node-specific ones

The implicit contract: The environment is "sanitized for all checks regardless of check type" but this isn't stated. It's implemented as "delete Node-specific vars for all checks."

Location: ops/local-work-acceptance.mjs:297-302

Positive Observations

  1. Well-isolated change: The new acceptance module is cleanly separated from existing verification logic
  2. Comprehensive test coverage: 14 new tests cover edge cases (symlinks, hard links, Git modes, missing refs)
  3. Evidence file included: runtime-evidence/drive-284-immutable-acceptance-0910.txt shows thorough verification
  4. Fail-closed design: Unknown input types are refused rather than defaulting to permissive behavior

Maintainability Risk Assessment

High Risk:

  • F2 (Implicit Git input mutation contract)
  • F8 (Verification order dependency)

Medium Risk:

  • F1 (Acceptance/verification boundary)
  • F3 (Git error opacity)
  • F7 (DoD appending inconsistency)

Low Risk:

  • F4 (Comment over-promises)
  • F5 (Writable paths boundary)
  • F6 (Hard link test fragility)
  • F9 (Temp directory leak on kill)
  • F10 (Environment sanitization scope)

Could a Stranger Read This in Six Months and Change It Safely?

Partial.

A stranger could:

  • Add a new acceptance input type (e.g., S3 paths) by following the Git/external pattern
  • Add new validation checks to validateAcceptance()
  • Extend test coverage for new edge cases

A stranger could NOT safely:

  • Reorder verification steps without understanding the scope-check-before-and-after contract
  • Add a new Git ref type without understanding the HEAD resolution mutation
  • Change error handling without understanding which errors should include DoD
  • Optimize by removing the "redundant" second verifiedPackage() call
  • Add language-specific environment exclusions without understanding the all-checks scope

The code's core safety properties (immutability of acceptance inputs, scope enforcement) are sound, but the sequencing dependencies and implicit contracts create maintenance hazards.

REVIEW_FAILED

@github-actions

Copy link
Copy Markdown

Review swarm: history

PR #294 — history review

Reviewed head: 3e166fbe2306b95d82a7cd5fcc60c77d66a604bc.
Title: fix(drive-local): declare acceptance inputs as immutable (#284).
Lens: fit with code history and settled decisions only.

Assessment

No blocking history finding. This change follows the acceptance-input decision left open by #244 rather than restoring the superseded working-tree snapshot design.

  • Recorded mistake addressed: DRIVE-LOG.md:8780 and :9072 distinguish immutable argv from the mutable assertion file it executes. The new acceptance contract declares inputs, resolves HEAD against the submitted baseline, extracts Git bytes outside the checkout, and reconstructs the contract before scope/verification/report operations. An undeclared script entry point is refused. This directly addresses the recorded node src/check.mjs failure class.
  • Latest decision, not the discarded fix: DRIVE-LOG.md:6273 rejected copying working-tree helpers beside their editable SHA256SUMS. The more recent ops/runtime-evidence/drive-threads-0909-decisions.md explicitly chooses between declared immutable inputs and a preserved acceptance workspace, and warns against silently narrowing arbitrary argv. This PR takes the first option, documents the new syntax and refusals, and keeps Git objects as authority. The new helper joins both the pinned extraction list and protected paths. It does not resurrect .drive-gate/ checksums or trusted checkout dist.
  • Prior guarantees retained: The work-branch guard, baseline reconstruction, atomic package writer, committed-scope selection and existing scope checks remain. The prior regression files and workflow ordering listed in the evidence below have no diff. In particular, this does not undo the post-implementation SDK rebuild recorded at DRIVE-LOG.md:5782/:7936 or restore the obsolete hardcoded apply operation discussed at :7844. The success marker now follows the post-check scope validation, consistent with the log's fail-closed reporting rule.
  • Socket change fits its predecessor: 7f45f572 deliberately moved the daemon socket out of the data directory and introduced the shared socketPathFor derivation. Updating the local launcher and its cleanup assertions to use that helper completes that migration; it does not restore the removed inline path. This small ancillary change is explained in the PR's runtime-evidence file.
  • RFC fit: This extends the evidence-layer gate ownership intended by settled decision regressions: relaycast workspace-key repair answers an untyped 500 #6, without moving product logic into the kernel or bypassing the journal protocol (regressions: red/green flows for the 2026-08-27 platform bugs (dormant until gates 2+6) #5/drive: WP-11: repair PR #9 under review before anything else #11). The declared-input model relies on trusted assertion authors to declare dependencies and avoid delegating assertions into mutable code. The documentation explicitly retains the same-user temporary-file limitation and adds the arbitrary-program limitation. This review does not interpret the title's “immutable” as an OS isolation or complete transitive dependency guarantee. Those stronger claims would contradict the recorded boundary; this diff expressly disclaims them. No change to merge authority (drive: cloud run a1055874 #16) or the review swarm's own judging gate appears here.
  • Commit truthfulness: The sole PR commit's subject accurately describes the main change: declared acceptance input ownership. It makes no runtime-test or mutation-verification claim in its message. The socket alignment is secondary and documented. The checked-in runtime transcript is author-provided evidence, not a test run performed by this reviewer.

ops/NEXT.md is the older cloud review-swarm brief and explicitly says its listed work is already done; it supplies no reason to redo that work here. ops/DIRECTIVES.md contains its explanatory preamble and no outstanding directive. The targeted local-drive entries and their subsequent corrections in DRIVE-LOG informed this assessment; early claims about snapshot immutability were not treated as settled after their retraction.

Review environment and limits

The original git log attempt failed with the literal output:

fatal: not a git repository: /home/daytona/.project-git

The /tmp/pr-294.diff handoff was absent. I used .review-target/pr.diff, then recovered the repository's Git objects from https://github.com/AgentWorkforce/flows.git into the missing gitdir. I set a local review/pr294-history branch at the metadata's exact head and populated only the index with git read-tree HEAD; no checkout/reset of supplied files was performed. The supplied diff is compared byte-for-byte to that commit below. Existing executable-mode differences in the sandbox were left alone and are not PR findings. Only this review is staged.

This is a static history review, not a new runtime acceptance certification, a claim that the full RFC gates are green, or authorization to merge. No application test suite or mutation test was run by this reviewer. The commands below capture the actual provenance and preservation checks used for this verdict.

Captured commands and output

$ git rev-parse HEAD
3e166fbe2306b95d82a7cd5fcc60c77d66a604bc
[exit 0]
$ git log --oneline -40
3e166fbe fix(drive-local): declare acceptance inputs as immutable (#284)
3ae6c24e feat(testdata): promote shakedown scenarios into testdata/shakedown/ (#288)
ff2f23c5 fix(review-swarm): emit safe terminal diagnostics (#290)
1d153070 fix(review-swarm): wrapper guard + rebased #285 with Bugbot fixes (#289)
4d08f9ae fix(review-swarm): validate candidate without self-judging (#265)
f72e2bad fix(observer-link): split mint and dashboard hosts; grace to 5s (#286)
1aad3e81 feat(cli): emit Observer: URL on run start when a workspace key is present (#264) (#269)
90edeb04 fix(drive-local): enforce scope and selected package acceptance (#244)
19cc188d spec(rfc-0001): specify the wake-time context contract (gate 2) (#251)
028aa490 docs(scoreboard): gate 7 is AMBER — #227 landed the suite it was waiting on (#240)
5f17b62f fix(docs): clarify inline model behavior without project config (#280)
53396750 fix(cli): make help and single-step summaries readable (#279)
7f45f572 fix(daemon): bind the unix socket outside the data dir at a short hashed path (#262) (#268)
a42ca161 fix(preflight): skip model_unknown for inline named agents when no flows.json is present (#263) (#266)
78ae4b8e fix(review-swarm): make the wait step's timed_out sentinel reachable (#258)
4dd9277e fix(review-swarm): give the lens retry budget a delay that can span a 60s backoff (#259)
d9377d17 ops(drive-log): -0910 online; closed relayfile#492, re-ran flows#258
8790e002 ops(drive-log): corrected flows#260 -- I truncated the quote that disproved it
ecaf6b86 ops(drive-log): lenses never received the diff; filed flows#260
3cfbd061 ops(drive-log): recovered lens transcripts; two lenses passed #259
5fd56fbe ops(drive-log): opened cloud#3527 -- run export 400s for every caller
ec014740 ops(drive-log): gate failure moved off infrastructure onto the agent step
f5f97e53 ops(drive-log): quiet tick, nothing moved
17c413ec ops(drive-log): #259 cannot be validated by its own gate; audit complete
069789bd ops(drive-log): audited remaining PRs -- all three still valid
4c2b0ab1 ops(drive-log): closed cloud#3517 as obsolete -- main deleted what it extended
fb73faf3 ops(drive-log): verified the #3516 classifier claim against three literal inputs
a32dc6d3 ops(drive-log): mount fault CONFIRMED FIXED; two corrections
8ab1ab2b ops(drive-log): the in-flight run shows the wedge signature, not progress
7999b28e ops(drive-log): re-ran the gate to test v0.10.56; in flight past 16 minutes
3bb84add ops(drive-log): v0.10.56 promoted; Khaliq had fixed the transport 3h before I filed
7cecffd8 ops(drive-log): opened cloud#3525 -- guard against an empty snapshot name
4bb9f865 ops(drive-log): named the masking secret -- RELAYFILE_SMOKE_BASE_URL
9b26383d ops(drive-log): root cause -- a secret valued "-" masks every hyphen (cloud#3524)
bdcaf415 ops(drive-log): retracted most of relayfile#492 -- read a 95-commit-stale checkout
c3dfe269 ops(drive-log): relayfile#492 -- the full-reconcile remedy exists, nothing triggers it
b58ce471 ops(drive-log): failures converged on one mode; retracting the rotation claim
4519a701 ops(drive-log): broke #3510's build with backticks in a template literal
7124cade ops(drive-log): caught myself reporting an unpushed fix as pushed
e717971b ops(drive-log): Bugbot findings on #3510 -- fixed the race, contested the heartbeat
[exit 0]
$ git show -s --format=fuller HEAD
commit 3e166fbe2306b95d82a7cd5fcc60c77d66a604bc
Author:     Miya <khaliqgant+miya@gmail.com>
AuthorDate: Thu Sep 10 22:31:08 2026 +0200
Commit:     Miya <khaliqgant+miya@gmail.com>
CommitDate: Thu Sep 10 22:31:08 2026 +0200

    fix(drive-local): declare acceptance inputs as immutable (#284)
    
    Session-Id: 01a08cf3-1384-7810-bdc7-6cfe60d0a4c1
[exit 0]
$ git diff HEAD^ HEAD | cmp - .review-target/pr.diff
[exit 0]
$ git diff HEAD^ HEAD --stat
 ops/DRIVE-LOCAL.md                                 |  57 ++++++-
 ops/drive-local-flow.test.mjs                      |  24 ++-
 ops/local-work-acceptance.mjs                      | 154 ++++++++++++++++++
 ops/local-work-acceptance.test.mjs                 | 173 +++++++++++++++++++++
 ops/local-work-package.mjs                         |  24 ++-
 ops/local-work-snapshot.mjs                        |   1 +
 ops/local-work-test-fixture.mjs                    |   6 +-
 ops/local-work-verification.mjs                    |  28 +---
 .../drive-284-immutable-acceptance-0910.txt        | 144 +++++++++++++++++
 scripts/run-local-workflow.mjs                     |   3 +-
 scripts/run-local-workflow.test.mjs                |   3 +-
 11 files changed, 569 insertions(+), 48 deletions(-)
[exit 0]
$ git log --oneline -12 -- ops/local-work-package.mjs ops/local-work-verification.mjs ops/local-work-snapshot.mjs
3e166fbe fix(drive-local): declare acceptance inputs as immutable (#284)
90edeb04 fix(drive-local): enforce scope and selected package acceptance (#244)
c9bf155f Add local relayflow launcher and execute backlog F8b (#231)
[exit 0]
$ git show -s --format="%h %s" 7f45f572
7f45f572 fix(daemon): bind the unix socket outside the data dir at a short hashed path (#262) (#268)
[exit 0]
$ git diff HEAD^ HEAD -- ops/local-work-package.test.mjs ops/local-work-review.test.mjs ops/local-work-gate.test.mjs ops/local-work-git-input.test.mjs workflows/drive-local.yaml ops/local-work-gate.mjs
[exit 0]
$ cat ops/runtime-evidence/drive-threads-0909-decisions.md
# PR #244: pinned Git gate inputs and remaining acceptance-input decision

The user clarified the H1 design: copying working-tree bytes does not create a
trust boundary. The earlier command embedding at `4d88ac2` is superseded because
it also took its helper and compiled-picker inputs from the working tree.

The launcher now pins HEAD before submission. `gate-snapshot` extracts the
package helper, verifier and picker TypeScript source from that commit with
`git --no-replace-objects show`, and builds the picker in a temporary directory
outside the checkout before selection and implementation. The snapshot script
itself is loaded from the same ref. The package records it as `head`; checks
compare it to the submitted pin so repinning metadata cannot hide a moved HEAD.
No SHA256SUMS or adjacent ref file is used as authority.

The installed TypeScript compiler and local launcher remain trusted tools. A
same-user agent can still write to the extracted temp files. The claim is about
the origin of gate inputs, not same-user filesystem isolation or runtime-file
immutability. The kernel and journal protocol are unchanged.

The contract is summarized in `../DRIVE-LOCAL.md`. The fixture uses the same
preparer and actual snapshot step, commits source into its disposable Git repo,
and invokes the installed compiler without symlinking host node_modules.

`drive-threads-0909-git-input-before.txt` captures two failures before this fix:
a substituted working-tree helper or ignored compiled picker returned success
without selecting any package. The current regression tests also assert that
extracted bytes match Git, dirty TypeScript is ignored as input, committed
invalid TypeScript fails the snapshot build, and fixed implementation passes.
The combined captured run is `drive-threads-0909-git-input-after.txt`.

**Remaining decision: how are acceptance-check inputs declared?** Immutable
argv alone does not freeze a script or its imports. A command such as
`node src/check.mjs` can execute an agent-edited judge. Inferring its transitive
inputs from arbitrary argv is not reliable. Choose a declared immutable
acceptance-input contract or an independently preserved acceptance workspace
that tests the changed source. Do not silently narrow the existing arbitrary
argv authoring contract. The flow remains blocked for unattended use; no swarm
pass is claimed.

`drive-threads-0909-acceptance-input-probe.mjs` reproduces this remaining bypass:
an unchanged broken value fails its original check, but replacing an allowed
check script with a no-op makes verification exit zero. The adjacent capture
records the literal command and output. A zero probe exit means the known
bypass was reproduced, not that this boundary is safe.

## Captured evidence lifecycle

- At commit `34349b2`, `drive-threads-0909-trust-probe.mjs` produced the adjacent
  `drive-threads-0909-trust-probe.txt`: repinning HEAD and rewriting a snapshot
  plus manifest were both accepted. That file is historical failure evidence.
  To reproduce those exact bytes, use that commit's probe and fixture together.
- The updated probe now asserts HEAD repinning is refused and a forged legacy
  snapshot cannot bless broken implementation. It then fixes the value and
  verifies the good case. `drive-threads-0909-owned-trust-after.txt` captures it.
- `drive-threads-0909-owned-gate-after.txt` captures the combined package, review,
  gate and real-daemon journal tests. The daemon cases assert failed completion
  reasons and that reporting never starts after a bypass attempt.
- Earlier `baseline`, `review-before`, `post-suite-before` and `after` captures
  retain their historical meaning. The old fixture compiled a snapshot, which
  is superseded by the preparing launcher. These are before/after reproductions;
  none is labeled mutation verification.
[exit 0]
$ git diff HEAD^ HEAD -- scripts/run-local-workflow.mjs
diff --git a/scripts/run-local-workflow.mjs b/scripts/run-local-workflow.mjs
index fc896d31..2457a596 100644
--- a/scripts/run-local-workflow.mjs
+++ b/scripts/run-local-workflow.mjs
@@ -21,6 +21,7 @@ async function main() {
   const { classifyOutcome } = await import('../packages/sdk/dist/cli/run.js');
   const { toKernelSpec } = await import('../packages/sdk/dist/compile.js');
   const { JournalClient } = await import('../packages/sdk/dist/journal-client.js');
+  const { socketPathFor } = await import('../packages/sdk/dist/daemon-connection.js');
   const { AgentWorker } = await import('../packages/sdk/dist/worker.js');
   const checked = checkFlow(path);
   for (const diagnostic of checked.report.diagnostics) console.error(JSON.stringify(diagnostic));
@@ -47,7 +48,7 @@ async function main() {
   });
   await mkdir('.relayflow', { recursive: true });
   const dataDir = await mkdtemp(join(root, '.relayflow', 'local-'));
-  const socket = join(dataDir, 'relayflowd.sock');
+  const socket = socketPathFor(dataDir);
   if (Buffer.byteLength(socket) >= 104) throw new Error(`LOCAL_SOCKET_PATH_TOO_LONG: ${socket}`);
   console.log(`LOCAL_DATA_DIR=${dataDir}`);
   const daemon = spawn(binary, ['--data-dir', dataDir, 'serve'], { stdio: ['ignore', 'ignore', 'inherit'] });
[exit 0]

REVIEW_PASSED

@github-actions

Copy link
Copy Markdown

Review swarm: structure

No fresh transcript was produced for run 3a78c3b0-d745-4b96-b2ff-401aafd684c6 (MISSING).

@github-actions

Copy link
Copy Markdown

Review swarm: FAILED

  • maintainability: FAILED
  • history: PASSED
  • structure: MISSING

Cloud run: 3a78c3b0-d745-4b96-b2ff-401aafd684c6

@kjgbot
kjgbot merged commit 6e376d8 into main Sep 10, 2026
4 of 5 checks passed
@kjgbot
kjgbot deleted the fix/284-immutable-acceptance-inputs branch September 10, 2026 20:53
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.

flows: acceptance argv is mutable by the implementation agent (drive-local class fix)

2 participants