Skip to content

fix(agent): resolve DAG file references by the host's path rules - #1066

Merged
0xKT merged 1 commit into
mainfrom
fix/dag_workdir_refs_windows
Oct 27, 2026
Merged

0xKT merged 1 commit into
mainfrom
fix/dag_workdir_refs_windows

Conversation

@LivXue

@LivXue LivXue commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Summary

On native Windows every file reference a DAG node or a spawn made into the session working directory or its sub-agent history was refused as outside them. check_confined resolved a reference with posixpath and compared it with each root through within, which tested for a / prefix, so a drive path, a backslash, and even a relative notes.md landed outside every root. This is item 7 of #1008.

  • resolve_reference resolves by the host's path rules, the ones the read uses, and within compares with the host's separators. It keeps case as realpath leaves it, deliberately: on Windows realpath already gives every existing component its on-disk case, so folding would change the answer only for a path that is not there to read, and would take two directories differing only in case, which a case-sensitive Windows directory can hold, for one.
  • The lexical check on @nodes/, and on a reference checked without roots, now refuses what escapes under POSIX or Windows rules, on every host. On Windows @nodes/..\..\x and @nodes/C:\...\x passed it and the read returned the file they named: a probe read a file outside the node root through both spellings, as a ref and as a file input, 4 of 4 on the base and on the 0.2.0 and 0.2.5 release code; on this branch it reads none. This is GHSA-8mqc-c629-9qc8.
  • LocalFileBackend.abspath joins a reference to its base even when the reference is absolute, which os.path.join keeps whole. Python 3.12's ntpath.isabs calls \notes.md absolute, so the read opened it on the process's current drive while the check had resolved it against the working directory's drive. 3.13 no longer calls it absolute.
  • The tests feat(*): let a dag decision node choose which of its branches run #1001 marked POSIX-only for this reason run everywhere again, and the Windows CI job gains a step running the reference tests, which the Linux shards cannot exercise.
  • CONTEXT.md's Reference roots entry says which path rules apply and why case is not folded; CHANGELOG.md has the entry.

CI runs on this public pull request, including the new Windows file-reference step. The Verification below records local runs.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

  • Native Windows 11, CPython 3.12.13, the seven files that reach references (tests/test_subagent_dag_core.py tests/test_subagent_dag_guard.py tests/test_subagent_dag_runner.py tests/test_subagent_dag_verdict.py tests/test_playbook_executor.py tests/test_playbook_validate.py tests/test_subagent_prompt_template.py -n 8): base 26 failed / 1076 passed / 17 skipped, this branch 4 failed / 1126 passed / 5 skipped. By failing ID: 22 fixed, 0 introduced. The 4 left fail the same way on the base: TestBackgroundRun's two process-group tests (os.killpg), and two replan tests whose fixture splits a native path on /. The 5 skips are symlink tests this account cannot create a link for. (The branch figure re-measured after the rebase onto the main of that
    date: on e8332e5d, the same five files gave 4 failed / 1,029
    passed / 5 skipped on the branch against 25 failed / 1,022 passed /
    17 skipped on origin/main, the same 4 failing IDs on both sides
    (os.killpg, the two process-group reaping tests and the two replan
    tests whose fixture splits a native path on /), so the branch fixed
    21 and introduced none; the 5 skips there are the symlink tests this
    account cannot create a link for. Rebased again onto 471842132, the
    same five files under CPython 3.12 on Linux give 1,043 passed / 3
    skipped on the branch against 1,030 passed on origin/main, no
    failing ID on either side, so nothing is introduced; the 3 skips are
    the Windows-only reference cases a Linux host cannot exercise.)

  • The new CI step's command, run under CPython 3.13.12 with only the dev extra and Git's usr\bin first on PATH: 168 passed, 5 skipped (the same symlink tests; the runner can create links).

  • Linux, Docker, CPython 3.12, full suite, rebased on the current main: base 23 failed / 34188 passed / 247 skipped, this branch 23 failed / 34201 passed / 250 skipped, identical failing IDs (container artifacts: process trees in a container without an init process, and root reading a file it was denied).

  • Each part of the fix was reverted on its own and the new CI step's selection run. On Windows, under 3.12 and 3.13 alike: within back to posixpath 37 failed, its child's separators left unnormalised 2, case folded 1, resolve_reference back to posixpath 18, the shape check on POSIX rules alone 12, realpath dropped from the physical path 12. The shape check on the host's rules alone survives there, since ntpath already reads /, and fails 10 on Linux, as do the POSIX-only shape check and the dropped realpath. The within and resolve_reference reverts survive on Linux, where os.path is posixpath. Reverting abspath fails 1 under 3.12 and survives under 3.13, where the old code already joined \notes.md: it is the one revert neither the Windows step, which runs 3.13, nor the Linux shards catch.

  • Diff coverage: scripts/coverage_gate.py diff --base-ref origin/main over a Linux coverage run of the four files the new CI step selects from (--cov=raven --cov-branch, then coverage json): 100.00% (11/11 changed executable lines), as a Windows run of the same four gave. No changed line depends on the platform, so the Linux shards reach every one.

  • ruff check, ruff format --check and ty check on the changed files; lint-imports: 10 kept, 0 broken.

  • Not run: macOS.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

On a POSIX host, a @nodes/ reference holding a backslash .. or a drive, and a reference checked without roots that holds one, is now refused; no node artifact's name holds either. On Windows, references that were all refused now resolve, inside the same roots.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

#1008 (item 7), GHSA-8mqc-c629-9qc8

@LivXue
LivXue requested review from 0xKT and gloryfromca October 22, 2026 16:07

@gloryfromca gloryfromca 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.

No blockers; this can merge as far as I am concerned.

Reviewed the full github/main...HEAD diff and the surrounding validation, rendering, backend-read, node-artifact, and DAG dispatch callers. I also checked the change history; the AGENTS.md/CLAUDE.md constraints and CONTEXT-MAP.md routing; domain terminology and architecture boundaries; backward compatibility on POSIX paths; Windows relative, absolute, drive-relative, rooted-without-drive, UNC, separator, case, and symlink semantics; and that the tests remove only obsolete platform skips rather than weakening assertions.

Verification on Python 3.12: uv run --python 3.12 --extra dev pytest tests/test_subagent_prompt_template.py tests/test_playbook_validate.py tests/test_subagent_dag_core.py tests/test_subagent_dag_runner.py -k 'template or validate or reference or node_root or nodes_prefix or may_not_execute or both_ref_forms or history or addressable or reachable_by_path' -q -n 0 --tb=short passed with 170 passed, 3 Windows-only skipped, and 706 deselected. The checked-in large-file and source-language scripts also passed for github/main..HEAD; I invoked them directly because make is unavailable in this environment.

@LivXue

LivXue commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

No Windows compatibility regression found in the inspected scope.

Reviewed head a84f63dcd54ca893951bbc0a42d6a99db86689c1, base/merge base 4315993300e538ec954c79565cbc8585006c8053. Read the complete 8-file local diff, both path helpers, rendering/validation callers, tests, CI step, and existing discussions.

resolve_reference and LocalFileBackend.abspath now agree on the host's path rules; within normalizes native separators while retaining filesystem case; the @nodes/ shape check rejects anchored and parent-escaping spellings under both Windows and POSIX rules.

Native Windows verification: Python 3.13.12: 168 passed, 5 skipped, 706 deselected with the new CI selection below. The five skips are symlink creation denied on this account. A separate Python 3.12.13 head/base probe passed: for \notes.md with cwd=Z:\work, head returns the checked Z:\notes.md; base returns the drive-less \notes.md.

Commands ran in an isolated detached checkout with a frozen dev environment and isolated Raven/profile/OAuth directories:

uv sync --frozen --python 3.13 --extra dev
uv run --frozen --no-sync pytest tests/test_subagent_prompt_template.py tests/test_playbook_validate.py tests/test_subagent_dag_core.py tests/test_subagent_dag_runner.py -k 'template or validate or reference or node_root or nodes_prefix or may_not_execute or both_ref_forms or history or addressable or reachable_by_path' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\07e3ab35a8ca4ce8a79558f02fad7b30\checks\1066-windows313\pytest' --junitxml 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\07e3ab35a8ca4ce8a79558f02fad7b30\checks\1066-windows313\junit.xml'
uv run --no-project --python 3.12 --with 'pytest>=9,<10' pytest 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\07e3ab35a8ca4ce8a79558f02fad7b30\probes\test_rooted_backend.py' -c 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\07e3ab35a8ca4ce8a79558f02fad7b30\probes\pytest.ini' -x -s --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\07e3ab35a8ca4ce8a79558f02fad7b30\checks\1066-python312-probe'

Limitations: symlink cases could not run; no full suite, live sub-agent/model calls, or remote-filesystem verification. This is a Windows-scoped review, not a general merge verdict.

@LivXue

LivXue commented Oct 22, 2026 •

Copy link
Copy Markdown
Member Author

Native Windows re-verification at head a84f63dcd54ca893951bbc0a42d6a99db86689c1, base 4315993300e538ec954c79565cbc8585006c8053. Both are unchanged upstream; nothing was pushed or edited in this run.

Correction to the first version of this comment: the 173 passed figure below is the CI runner's, not this host's. Locally the same selection gives 168 passed / 5 skipped / 706 deselected, because this account cannot create symlinks. Exact figures:

  • uv run pytest tests/test_subagent_prompt_template.py -q: 30 passed, 4 skipped in 8.47s. All 4 skips are tests\test_subagent_prompt_template.py:153, "this host cannot create a symlink".
  • tests/test_subagent_dag_core.py, the reference and node-root cases (reference_landing, without_roots, nodes_prefix, split_reference, windows_path_rules) - 24 passed, 0 skipped, 408 deselected in 6.31s. This covers lines 1531, 1544, 1563 and 1584, including the backslash-parent, backslash-through-a-child, drive-absolute, drive-relative and unc parametrizations, plus @nodes/.. and @nodes//etc/passwd.
  • The CI step's own selection on this host: 168 passed, 5 skipped, 706 deselected in 7.03s. The fifth skip is tests\test_subagent_dag_core.py:3619, another symlink case.

This agrees with the earlier 3.13.12 run on the same head, which gave 168 passed / 5 skipped over the selection; the two interpreters agree on the pass and skip sets. The "Test DAG file-reference confinement" step in the Windows ACP stdio job is green on this head: 173 passed, 706 deselected in 6.77s under CPython 3.13.12, where the runner can create the symlinks this account cannot. All 21 reported checks pass and 2 skip; reviewDecision is REVIEW_REQUIRED and merge state BLOCKED, which is the outstanding approval rather than a failing check.

No code change: this run only re-measured the existing head.

@0xKT

0xKT commented Oct 23, 2026

Copy link
Copy Markdown
Member

Merge conflict: @LivXue, this PR no longer merges onto main. git merge-tree --write-tree of head a84f63dc onto the current tip a7bf9f89 reports a conflict in: CHANGELOG.md. Please merge or rebase onto the latest main and resolve it; the review stands on the code once it merges cleanly again.

@gloryfromca

Copy link
Copy Markdown
Member

Blocking: resolve the CHANGELOG.md merge conflict against current main.

I independently confirmed this after fetching main at a7bf9f8: git merge-tree --write-tree HEAD github/main exits 1 with a content conflict in CHANGELOG.md. This changes my standing stance from clean to blocker on mergeability; the prior code review remains unchanged. Tests on the unchanged head would not validate the as-yet-unwritten conflict resolution, so I did not repeat them.

@LivXue
LivXue requested a review from gloryfromca October 23, 2026 07:53
@LivXue
LivXue force-pushed the fix/dag_workdir_refs_windows branch from a84f63d to d4b4da2 Compare October 24, 2026 10:51

@gloryfromca gloryfromca 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.

No blockers; this can merge as far as I am concerned.

The rebase resolves the prior CHANGELOG.md conflict and git merge-tree --write-tree HEAD github/main now succeeds against current main. I compared the rebased commit with the previously reviewed patch using git range-diff, re-read the resulting eight-file github/main...HEAD diff, and checked the affected callers, history, AGENTS.md/CLAUDE.md and CONTEXT-MAP.md rules, architecture boundaries, backward compatibility, and test assertions. The implementation patch is preserved; only the changelog insertion context changed.

Verification on Python 3.12: the relevant four-file pytest selection passed with 171 passed, 3 Windows-only skipped, and 741 deselected. check_large_files.py, check_source_language.py, and git diff --check passed for github/main..HEAD; the worktree remains clean.

@LivXue

LivXue commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review at head d4b4da2af5896bb94341d2460c8273fff9e8bbb4, base/merge base e8332e5d4c9690215e6962899064d07648d9badf.

The earlier Windows review on this PR was at head a84f63dcd54ca893951bbc0a42d6a99. The branch has since moved to a new head.

Verdict: the PR's own change is unchanged, so the earlier review still stands. This is a rebase onto the current main, not a change of substance, and it is worth being precise about why.

The PR's diff against its merge base is 8 files, +175/-51 - the same eight files and the same line counts as at the reviewed head. Comparing the two PR diffs line by line, the added and removed lines are identical: zero differing lines out of 226. What did change is the context around them, because main moved (the base is now e8332e5d4c rather than 4315993300), so the hunk headers and the surrounding lines of the same hunks read differently. The change this PR carries has not moved.

That is the whole of the delta, and it means the Windows review does not have to be redone from scratch: nothing in the code changed. For the record, that review's findings still stand as written - no new Windows regression was found, and the two items it did raise were reported as inherited and as a Windows CI coverage note, not as defects in this PR.

Native Windows verification: the two PR diffs were compared line by line on this machine (226 changed lines on each side, 0 differing), the eight changed files were confirmed to be the same eight as at the reviewed head, and the merge bases were confirmed to be the ones named above. No test suite was re-run for this rebase, because no line of the change under test differs from the line the earlier review verified. This is a scoped compatibility report, not a merge verdict.

Exact verification commands and results
$root = 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\run-20261024'
$repo = "$root\repo"

# the PR's diff at the reviewed head and at the current head
git -C $repo diff --no-color "43159933..a84f63dcd54ca893951bbc0a42d6a99" > "$root\scratch\rb1066-old.patch"
git -C $repo diff --no-color "e8332e5d4c..d4b4da2af5896bb94341d2460c8273fff9e8bbb4" > "$root\scratch\rb1066-new.patch"

# compare the added and removed lines only, ignoring the hunk context that main's move changed
$o = Get-Content "$root\scratch\rb1066-old.patch" | Where-Object { ($_ -match '^[+-]') -and ($_ -notmatch '^(\+\+\+|---)') }
$n = Get-Content "$root\scratch\rb1066-new.patch" | Where-Object { ($_ -match '^[+-]') -and ($_ -notmatch '^(\+\+\+|---)') }
"changed lines old=$($o.Count) new=$($n.Count)"
Compare-Object $o $n
# no output: the two sets are identical

0xKT pushed a commit that referenced this pull request Oct 26, 2026
## Summary

A still-running DAG node is labelled Finished in the TUI. Truncated node
output also includes the system's cut marker inside the node's own
Markdown, and expanding a long delivery body has no height limit. This
follow-up addresses the three review findings on #1063.

- Keep the first 20,000 characters of node text in `output` and carry a
separate optional `output_truncated` boolean through the manager, spine,
three RPC delivery shapes, generated clients and web live/replay paths.
Render the translated system notice outside the node's Markdown. A
whitespace-only truncated prefix still exposes the fold; older records
and node-authored suffixes retain their contents.
- Bound expanded delivery bodies with `max-height: min(50vh, 32rem)` and
scrolling. The labelled region accepts keyboard focus, including
navigation to its final line. This applies to every delivery fold.
- Use the existing Still running translation for TUI notice rows in both
live and replay views.
- Replace the foreground test helper's external cat command with a real
Python echo process and platform-aware argument quoting. The
release/re-send regression explicitly refuses cat and uses a script path
containing a space.
- Apply the pinned formatter to two existing TUI component expressions
so the whole-tree formatting hook passes. These changes only reflow
whitespace.

The model-facing report and untrusted-content handling remain unchanged.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

After applying the patch to current main, using Python 3.13.13 on Linux:

- `uv run --frozen --no-sync pytest -q -n 0 --tb=short` with branch
coverage enabled: **1,575 passed** across
`test_subagent_dag_adjudication.py`, `test_subagent_dag_runner.py`,
`test_subagent_manager.py`, `test_playbook_stint.py`,
`test_rpc_spine.py`, `test_rpc_schema_match.py`,
`test_spine_scheduler_lane.py`, `test_spine_events.py`,
`test_rpc_turn_cancel.py`, `test_spine_scheduler.py` and
`test_security_untrusted_context.py` under `tests/`. This includes the
newly merged cancellation path.
- `node node_modules/vitest/vitest.mjs run --maxWorkers=1
--no-file-parallelism`: web **253 files, 4,234 passed**; TUI **144
files, 2,251 passed, 13 skipped**. Both exited 0.
- The repository diff-coverage gate passed: **100% (10/10 executable
changed Python lines)**. Coverage was collected for the five changed
runtime/RPC modules; this is diff coverage, not repository-wide
coverage.
- `uv run --frozen --no-sync ruff check`, `ruff format --check`, `ty
check`, `lint-imports` and `deptry raven` passed. Ruff covered the
repository lint targets; ty covered the source targets. All 10 import
contracts were kept.
- In `ui-web`: `npm run gen:check`, `npm run type-check`, `npm run lint`
and `npm run build` passed. In `ui-tui`: `npm run lint:rpc`, `npm run
lint:i18n`, `npm run type-check`, `npm run lint` and `npm run build`
passed; lint retains 23 existing warnings and no errors.
- `uv run --frozen --no-sync pre-commit run --files` over every changed
file passed, including the whole-tree TUI formatting hook.
- `UV_NO_SYNC=1 make check-commits check-large-files
check-source-language PYTHON_VERSION=3.13.13
COMMIT_RANGE=origin/main...HEAD` passed, including commitlint and the
source-language carrier rules.
- Real Chromium on the offline page, driving the production history
renderer: long output is bounded at 500px in a 1440x1000 viewport and
422px in 780x844 and 390x844 viewports. End reaches the last line, the
cut notice is outside Markdown, and collapse works. No page errors. The
page's existing 780px minimum width still limits its narrow layout.
- Patch preparation included failing regressions before each fix and
four rejected single-construct truncation mutants.
- Native Windows validation and a fresh Python suite on the follow-up
base have since been run, on the branch rebased onto `e8332e5d`: `pytest
tests/test_subagent_manager.py tests/test_subagent_dag_runner.py
tests/test_rpc_spine.py tests/test_rpc_schema_match.py
tests/test_spine_events.py tests/test_spine_turn.py
tests/test_rpc_turn_cancel.py tests/test_playbook_stint.py` -> the same
11 failures as `origin/main` on the same files, 0 introduced, all of
them this Windows account's (a `~/.raven/oauth` permission refusal,
symlink privilege, `os.killpg`, and the POSIX path rules #1066 fixes).
The full `ui-web` suite passed (254 files, 4,278 tests) and the full
`ui-tui` suite passed in CI. `ruff check`, `ruff format --check`, `ty
check`, `lint-imports`, `npm run gen:check`, `npm run type-check`, `npm
run lint` (ui-web and ui-tui), `npm run lint:rpc`, `npm run lint:i18n`
and the repo's commit, large-file and source-language gates all pass on
the rebased branch.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

## Risk

The truncation flag is additive. Older clients still display the output
prefix but do not gain the separate notice. Previously stored records
with an embedded suffix are preserved because the client cannot
distinguish that suffix from node-authored text.

The height cap changes scrolling for spawn and whole-graph results as
well as node reports. Original run files retain the complete output.
Delivered text follows the existing content policy; no dependency,
migration or permission change is introduced.

Rollback: revert this change. Previously stored marks may retain the
optional field, which the older code ignores.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

#1063
@0xKT

0xKT commented Oct 26, 2026

Copy link
Copy Markdown
Member

Merge conflict: @LivXue this PR no longer merges cleanly into main.

Measured just now with git merge-tree --write-tree of main dacfc64c and this head d4b4da2a. Main moved with the merge batch #1098, #1095, #1084, #1087, #1078, #1081, #1100 and #1031. Conflicting files: CHANGELOG.md only.

After a rebase onto current main I re-check what moved before the grade comes back.

@gloryfromca

Copy link
Copy Markdown
Member

Blocking: resolve the renewed CHANGELOG.md conflict against current main.

Confirmed independently after fetching main at dacfc64: git merge-tree --write-tree HEAD github/main exits 1 with the reported content conflict. This changes my stance on d4b4da2af589 from clean to blocker on mergeability. The reviewed head itself is unchanged, so rerunning its tests would not validate the future conflict resolution.

On native Windows every reference into the working directory or the
sub-agent history was refused as outside its roots: check_confined
resolved it with posixpath and within tested for a "/" prefix, so a
drive, a backslash and even a plain notes.md landed outside every root.

- resolve_reference and within follow the host's path rules, the ones
  the read uses. Case is kept as realpath leaves it, so two directories
  that differ only in case stay two.
- The lexical check on @nodes/, and on a reference checked without
  roots, refuses what escapes under POSIX or Windows rules on every
  host. On Windows, @nodes/..\..\x and @nodes/C:\...\x passed it and the
  read returned the file they named.
- LocalFileBackend.abspath joins even an absolute reference to its
  base, so a rooted path without a drive is read on the drive the check
  resolved it against, under Python 3.12 as under 3.13.
- The tests marked POSIX-only for this run everywhere again, and the
  Windows CI job runs the reference tests.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
@LivXue
LivXue force-pushed the fix/dag_workdir_refs_windows branch from d4b4da2 to e396c4e Compare October 27, 2026 02:56
@LivXue

LivXue commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Rebased onto current main (4718421324ee) as e396c4e69. The renewed conflict was CHANGELOG.md only, a same-spot insertion under ### Fixed; the resolution keeps both sides, this branch's entry and the three main gained since.

Re-checked at the new head:

  • The new Windows step's selection on Linux: 171 passed, 3 skipped (the Windows-only cases), 745 deselected.
  • The five-file selection the description records, CPython 3.12 on Linux: 1,043 passed and 3 skipped on the branch against 1,030 passed on origin/main, no failing ID on either side, so nothing is introduced.
  • check_commit_messages.py, check_large_files.py and check_source_language.py pass over origin/main..HEAD; ruff check and ruff format --check pass on the changed Python files.

The description's rebase base and verification numbers are updated to match.

@0xKT @gloryfromca the renewed conflict is resolved at the new head; please re-check it when you get a chance.

@gloryfromca gloryfromca 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.

No blockers; this can merge as far as I am concerned.

The rebase resolves the renewed CHANGELOG.md conflict and git merge-tree --write-tree HEAD github/main succeeds against current main at 4718421324ee. I compared the commit to the previously reviewed patch with git range-diff, read the complete resulting github/main...HEAD diff, and rechecked the affected callers, history, AGENTS.md/CLAUDE.md and CONTEXT-MAP.md rules, architecture boundaries, backward compatibility, and test assertions. Only the changelog insertion context changed; both sides of the conflict are retained.

Verification on Python 3.12: the relevant four-file pytest selection passed with 171 passed, 3 Windows-only skipped, and 745 deselected. The large-file and source-language gates plus git diff --check passed, and the worktree is clean.

@LivXue

LivXue commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review — PR #1066 @ e396c4e69ad5066abcd5a4a56d7293951a8342d6

Scope: full base 4718421324 → this head (8 files), re-verified at the new head; the source fix (raven/agent/subagent/prompt_paths.py, raven/agent/subagent/prompt_backend.py) and tests/test_subagent_prompt_template.py are byte-identical to the previously Windows-reviewed head a84f63dc, so the delta is main's merge in ci.yml and the shared DAG test files.

Result: the host-path-rule fix is correct on native Windows and removes the inherited ref-path outage; no new Windows issue found. This is also the fix that unblocks the seven new path-gate tests in PR #1118.

What I checked:

  • resolve_reference now uses os.path.join/os.path.normpath (host rules), so a relative reference resolves against the root with backslashes on Windows, and an absolute or drive-rooted reference keeps its own drive. within compares with os.path.normpath and os.sep; case is deliberately compared as written (realpath canonicalises existing components), which the docstring explains.
  • _check_shape now judges a reference by both POSIX and Windows rules (_escapes over posixpath/ntpath), so @nodes/..\.., C:\..., C:notes.md and UNC spellings are refused on every host, not only where they might escape.
  • LocalFileBackend.abspath joins even when the path is absolute, so a Windows root-relative \x takes the cwd's drive — matching what confinement resolved.
  • The new Windows CI step runs the confinement suite on windows-latest, so this surface stays covered.

Verification (native Windows 11, CPython 3.12.13, isolated worktree D:\tmp\rv-1066 at this head, scratch HOME/USERPROFILE, PYTHONUTF8=1):

pytest tests/test_subagent_prompt_template.py tests/test_subagent_dag_core.py -q
→ 2 failed, 460 passed, 5 skipped        (both failures also fail at base)

pytest tests/test_subagent_dag_runner.py -q
→ 2 failed, 395 passed                  (both are the inherited reaping tests)

same nine-file DAG/instance set:
  base 4718421324: 16 failed, 1234 passed, 19 skipped
  head e396c4e6:    5 failed, 1269 passed,  3 skipped

All five remaining failures are in the base failing set (TestBackgroundRun reaping, two dag_core cases, the rpc_instances clock); the PR's un-skipped reference tests now pass on Windows. Direct probe at this head:

accepted: notes.md
accepted: D:\tmp\pp-...\notes.md
accepted: D:/tmp/pp-.../notes.md
resolve: ('D:\tmp\pp-...\notes.md', 'D:\tmp\pp-...\notes.md')
refused shape: ..\elsewhere\notes.md
refused shape: C:\work\notes.md
refused shape: C:notes.md

Limitations: symlink-based cases skip on this host (no privilege); a live reference to a UNC share inside a root was not exercised; no full DAG run through a remote backend.

@0xKT
0xKT merged commit cf5c8c5 into main Oct 27, 2026
24 of 27 checks passed
@0xKT
0xKT deleted the fix/dag_workdir_refs_windows branch October 27, 2026 06:11
0xKT pushed a commit that referenced this pull request Oct 27, 2026
… the docs (#1064)

## Summary

Items 1 to 6 of #1008, the follow-ups the review of #1001 left out. Item
7, file references on Windows, is handled separately.

1. **A decision node's route is no longer the first thing cut on a
tasks-board card.** The agent's name kept its full width (`min-width:
auto`) and the tool-count chip never shrinks, so the selection took all
of the line's overflow: with "Raven-Research" and "3 tools" a 196px card
showed only the arrow. The selection now renders as the arrow with its
first route, which keeps its width, then the routes after it. Those, and
the instance handle, start from nothing and grow into the room the line
has left (`flex: 1 1 0; max-width: max-content`), and the agent's name
gives way after them, with an ellipsis. The later routes keep room for a
letter and an ellipsis: narrower, a line's first character is clipped
rather than replaced by one. The 6px spacing separates fields only: the
two selection spans have no additional gap, and the comma-space
separator stays with the first route, outside the text that can be cut.
A card with no selection lays out as before, a long minted handle
included.
2. **Route conditions are filled and validated for params.** The
executor filled `${params.X}` only in `promptTemplate`, and
`validate_graph_nodes` checked only `promptTemplate` and `inputs`, so
"the deal is above ${params.threshold}" reached the judge as written and
`${params.nobody}` passed validation. Conditions now go through the same
`_fill_text` as the prompt, a secret withheld with the same log line,
naming the route, and a reference to no declared param is an error. The
filling and the check are on `main` now, carried by #1058; this branch
holds the executor's docstring and the tests that pin both.
3. **Hand-written route names `yes`, `no`, `on`, `off`, `true`, `false`
and bare numbers load.** The store parsed the spec block with
`yaml.safe_load`; YAML 1.1 read those keys as booleans or integers,
validation refused them, and the runtime dropped the playbook from the
library. Every mapping is now keyed by a name, and the value keeps its
YAML type. The loader is the one #1057 drafted, so the store carries one
key loader rather than two: a node that is not a mapping is left to
PyYAML to answer, which keeps the error the validate paths catch, and a
key node is retagged through a copy, so an anchor that shares one scalar
between a key and a value leaves the value's type alone. #1057 is closed
as superseded by this branch.
4. **The generator's dag-mode instructions describe branches.** That
section called `dependsOn` the whole graph language and never named
`routes`, `select` or `when`; it now describes a decision node and the
rule that a node depending on one names it in its `when`, and says a
condition may use a param. A new test reads that section on its own.
5. **An ordinary node's judge request is held to a literal golden.**
`test_judge_without_routes_sends_the_old_request_verbatim` compared the
request with the module's own constants, so changing the instruction or
a tool schema's description passed. It now compares the whole request --
messages, tools, model, tool choice -- with the fence nonce fixed,
against `tests/data/dag_verdict_plain_request.json`. The golden is the
request from before #1001: the tree before it and this one send
byte-identical requests over 32 input combinations.
6. **Domain terms around branches.** `CONTEXT.md` gains three `_Avoid_`
notes: "decision" alone for a decision node or for what it chose, since
the word already names the main agent's answer to an exception report in
a DAG run and a choice the host puts to the user; "skipped" for a node a
branch left out; and "branch" for any other part of a graph. The uses
those notes retire are rewritten in the files this pull request touches:
the DAG orchestration skill (the `abandon` paragraphs and the
conditional-branch section), the orchestration and playbook pages in
both languages, a runner docstring, and test docstrings, one test helper
and one test name; two ui-tui test comments that called a path through a
drawn graph a branch now say path and edge. The playbook page's "not a
runtime branching language" had read, since #1001, as saying a
prompt-mode playbook cannot branch; it now says the guidance decides
nothing while the graph runs, and a branch is chosen by a decision node
in the graph it composes.

Left as they are, outside the files this pull request touches:
"decision" alone still names a decision node or its selection in text a
model reads -- the `routes` description in the `run_subagent_dag` schema
and on `DagNodeSpec`, which the playbook generator's tool carries; the
guard-size error in `raven/agent/subagent/dag_guard.py`; the note
`raven/agent/subagent/dag_render.py` writes into a sub-agent's prompt;
the workflow compiler's prompt -- and in the RPC model's field
description and both generated clients, the "Decision missing" reason in
`i18n/messages.json`, the DAG Panel entry of `ui-tui/CONTEXT.md` (which
`test_the_tui_glossary_says_what_the_turned_in_line_carries` quotes),
and docstrings, comments and test names across the DAG modules and both
UIs. Retiring those reaches the RPC schema, the generated clients, a
translated label and pinned tests, so it is left to #1065. Python test
function names, which predate the note, were left alone in these files
too.

Two things the review of this pull request left open are closed here:

- **The card's layout test now lives where a real-environment test
belongs.** `tests/test_web_task_card_route_real_browser.py` measured a
card in a real Chromium, so it moved to
`tests/integration/test_web_task_card_real_browser.py`, and a new
`web-browser` CI job runs it on a runner that ships a system Chrome. It
is the only place the tasks board card's agent line is measured:
happy-dom lays nothing out, so the stylesheet gate beside it can only
read the declarations. Without a browser it skips with the reason,
except under `CI`, where a skip would leave the line unmeasured on every
push and so fails instead.
- **The store's loader answers a node that is not a mapping the way
PyYAML does.** `raven playbook validate` now answers a YAML syntax error
in a spec block as a finding, the way the RPC validator already did,
instead of a traceback.

The branch is rebased onto `main` at `471842132` (#1114). The
`CHANGELOG.md` conflict that rebasing raised is resolved by keeping both
sides' entries, and the branch merges clean.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

Native Windows 11, CPython 3.12.13:

- `uv run --frozen --no-sync pytest tests/test_playbook_store.py
tests/test_playbook_executor.py tests/test_playbook_validate.py
tests/test_subagent_dag_verdict.py tests/test_subagent_dag_core.py
tests/test_cli_playbook_commands.py
tests/integration/test_web_task_card_real_browser.py -m "integration or
not integration" -q -p no:randomly --tb=short`: 770 passed, 11 failed,
15 skipped. The same 11 fail at the merge base on this host, with the
same IDs:
`test_playbook_executor.py::test_a_playbook_run_s_node_attempts_nest_under_the_call_that_ran_it`,
`test_playbook_validate.py::test_path_confinement_waits_for_dispatch_roots`,
`test_subagent_dag_core.py::test_replanning_refuses_a_redeclared_id`,
`test_subagent_dag_core.py::test_a_refused_submission_still_records_the_link_as_unstarted`,
the five
`test_subagent_dag_core.py::test_a_reference_landing_in_either_root_is_accepted`
parameters, and
`test_cli_playbook_commands.py::test_run_judges_its_nodes_so_a_branching_playbook_takes_its_branch`
and `::test_run_judges_on_the_users_own_subagent_dag_settings`. The
first group is the file-reference rules #1066 fixes, and the last two
shell out to `cat`. The branch adds the passing cases.
- The baseline comparison above, run over the same six files at the
merge base: 746 passed, 11 failed, 15 skipped -- the same failing IDs.
- `uv run --frozen --no-sync pytest
tests/integration/test_web_task_card_real_browser.py -m integration -q
-n 0 -p no:randomly --tb=short`: 10 passed, the board's real 196px card
measured in a real Chromium. Playwright could not create its artifacts
directory under this host's default temp path, so the run needed a
writable `TEMP`; with the default one the nine browser cases skip and
the path-lookup case still passes.
- After the rebase onto `main` at `471842132`, with `-n 0`: the six
playbook/DAG files (`tests/test_playbook_store.py`,
`tests/test_playbook_executor.py`, `tests/test_playbook_validate.py`,
`tests/test_subagent_dag_verdict.py`, `tests/test_subagent_dag_core.py`,
`tests/test_cli_playbook_commands.py`): 790 passed;
`tests/integration/test_web_task_card_real_browser.py -m integration`:
10 passed; `scripts/check_commit_messages.py`,
`scripts/check_source_language.py` and `scripts/check_large_files.py`
over `origin/main..HEAD`, and `git diff --check`: passed.
- `uv run --frozen --no-sync ruff check raven/playbook/store.py
raven/cli/playbook_commands.py tests/test_playbook_store.py
tests/test_cli_playbook_commands.py
tests/integration/test_web_task_card_real_browser.py` and `ruff format
--check` over the same files: passed.
- `uv run --frozen --no-sync ty check raven/playbook/store.py
raven/cli/playbook_commands.py`: passed.
- `uv run --frozen --no-sync lint-imports`: 10 kept, 0 broken.
- `uv run --frozen --no-sync --extra dev python
scripts/check_commit_messages.py origin/main..HEAD`,
`scripts/check_source_language.py origin/main..HEAD` and
`scripts/check_large_files.py origin/main..HEAD`: passed.
- `git merge-tree` against `origin/main`: no conflict, through
`--write-tree` on the Windows host and through the three-argument form
on the Linux workstation, whose git predates `--write-tree`. Before the
rebase the same check reported one conflict, in `CHANGELOG.md`.

ui-web, run in `ui-web/`:

- `node node_modules/vitest/vitest.mjs run
src/features/tasks/BoardCard.test.tsx
scripts/gates/task-card-selection-css.test.mjs --no-file-parallelism`: 2
files, 36 tests passed.
- `node node_modules/vitest/vitest.mjs run src/features/tasks
scripts/gates --no-file-parallelism --testTimeout=60000`: 62 files, 560
tests passed. The same command over `main` at the time of the run
reported 61 files and 552 tests passed; the branch adds the stylesheet
gate and the cases beside it. Without the longer timeout two of the
gates time out on a loaded host; they pass on their own and the files
they read are byte-identical to `main`.

At head `211d226d`, the branch's CI run: 23 checks passed, 2 skipped
(`claude`, `docs deploy`), 0 failed. The rebased head re-runs the same
workflow, including the `web layout in a real browser` job, which runs
the browser cases on a runner that ships a system Chrome.

Not run here: macOS, a browser other than Chromium, and the full
repository suite.

## Risk

A stored playbook whose route condition names an undeclared param now
fails validation, and so leaves the library, as one doing the same in
`promptTemplate` already did. On the board, a crowded agent line now
shortens the agent's name where it used to cut the selection. The
store's loader now reads the frontmatter through the same path as the
spec block; a frontmatter key that YAML 1.1 would have resolved to a
boolean or a number is now read as the text it was written as, which the
frontmatter's two string fields never were.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

#1008 (items 1 to 6)
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.

3 participants