Skip to content

fix: one output_dir boundary check for run_skill admission and recipe rules - #5257

Merged
Trecek merged 12 commits into
developfrom
impl-rectify_bounded_output_dir_write_target_authority-20261009-205759
Oct 10, 2026
Merged

Trecek merged 12 commits into
developfrom
impl-rectify_bounded_output_dir_write_target_authority-20261009-205759

Conversation

@Trecek

@Trecek Trecek commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator

Refs #5256

Summary

Bundled recipes passed output_dir: . to skills whose write_paths is BOUNDED. run_skill correctly rejects those dispatches, because . would widen the grant to the whole worktree. The static rules never caught it: four places each answered "is this output_dir admissible for this skill?" on their own, and they had drifted apart. Since #5211 moved 12 bundled skills to BOUNDED scopes, 17 bundled steps have been rejected at runtime, while every static check stayed silent or even required the rejected value.

This PR gives that question one answer and fixes the dispatches.

Changes

  • One boundary check. output_dir_widens_scope(scope, output_dir, project_dir) and the shared diagnostic widened_output_dir_message(...) now live in the stdlib-only write-scope authority, hooks/_write_scope.py.
    • run_skill admission (_extend_closure_write_scope) and the skill-write-path-recipe-alignment rule both decide through it.
    • bounded_scope_contains is registered in POLICY_FUNCTIONS, so this check stays its only caller.
  • Runtime diagnostic. A rejected dispatch now names the skill, the requested output_dir, and the declared directories. Admission behavior and the closure union extension (Cook sessions cannot compose skills: write boundary intersects across loaded skills, and composable sub-skills like prepare-issue are unusable #5154 / feat: compose skill write scopes by union with typed write_paths declarations #5211) are unchanged.
  • Alignment rule. The pre-filter that hid ., ${{ context.work_dir }} and slash-only values is gone.
    • Each value is resolved to the path run_skill will judge: ., slash-only, or rooted at the step's cwd.
    • Only values that can't be resolved statically stay silent: another template root, or an absolute path.
  • Write-skill rule. write-skill-requires-source-output-dir now fires only for skills declared write_paths: unrestricted that omit output_dir. The duplicate test_write_skill_steps_have_output_dir is removed, since the real rule's bundled sweep covers it.
  • Recipes. output_dir: . is dropped from the 17 BOUNDED dispatches in implementation, implementation-groups, remediation, merge-prs, research, research-implement and research-review. Those skills are make-plan, prepare-pr, prepare-research-pr and compose-research-pr. The default floor <cwd>/.autoskillit/temp/<skill> equals each one's declared scope.
  • vis-lens fix.
    • vis_apply passed output_dir: run-vis-lenses to vis-lens-{slug} skills, so the runtime rejected every lens dispatch. It now passes no output_dir.
    • synthesize-vis-plan previously read a directory no skill writes. It now consumes the captured vis_lens_output_paths through a new optional lens_output_paths input.
    • The contract, SKILL.md and phoropter docs are updated to match.
  • Regenerated files. Compiled recipe JSON, diagram recipe hashes, the pinned pre-delivery structure digests, and the pretty-output baseline maxima.

Drift guard

  • tests/server/test_bundled_dispatch_write_scope_admission.py dry-runs every tracked bundled run_skill step through the real _resolve_dispatch_paths and _extend_closure_write_scope. It asserts that each step is admitted and that the alignment rule flags exactly the steps the runtime would reject.
    • Planner steps run through a reviewed probe value for ${{ context.planner_dir }}.
    • Templated-skill dispatches must pass no output_dir.
    • Positive controls keep the sweep non-vacuous.
  • tests/server/test_attested_dispatch_write_scope_admission.py drives the real attested run_skill for implementation:plan and implementation:prepare_pr. It pins the narrowed write-watch directory.
  • test_rule_is_exercised_not_vacuously_silent proves that the bundled zero-findings test is earned.

Verification

  • Red before green. Before the fix, the new tests failed for the stated reasons: 17 runtime rejections that the rule did not flag, and both vis_apply templated-skill violations.
  • Guards catch breakage. Each deliberate break failed the right test: removing the reviewed planner root, adding a bogus root key, restoring output_dir on vis_apply, and disabling the rule's step selection.
  • User-recipe parity. A scratch copy of implementation.yaml with output_dir: . restored on plan reports one skill-write-path-recipe-alignment ERROR that carries the runtime's own diagnostic.
  • Isolated Docker admission probe (the issue's probe, adapted to the shared check) on a9a8ed994 exits 0:
    • configured_output_dir: "" gives configured_output_dir_widens: false;
    • . gives dot_output_dir_widens: true.
  • Targeted suites. tests/recipe, tests/arch, tests/hooks, tests/workspace, tests/infra, tests/contracts, tests/skills_extended and the recipe-loading tests/server files pass. pre-commit run --all-files and the complexity ratchet against develop pass.
  • Not yet run. The full task test-local-gate has not been run.

Commissioning still pending (post-merge)

Per #5256, run the PR #5255 implementation E2E scenario on MiniMax twice against sandbox seed TalonT-Org/autoskillit-e2e-sandbox#1, and confirm that both pass plan and prepare_pr admission. Do that before claiming commissioning. This PR does not close the issue for that reason.

Notes for reviewers

  • User-recipe impact. User recipes that pass ., /, or a cwd-equal template to a BOUNDED skill now get a static ERROR carrying the runtime's diagnostic. The git-excluded local .autoskillit/recipes/remediation-fable.yaml will report it on its make-plan, prepare-pr and compose-pr steps. Its owner should drop output_dir there.
  • Narrowing. For the 17 steps, allowed_write_prefixes and the write-watch snapshot narrow from the worktree to the skill's own directory. Each skill's NEVER block already confines it there, and none of these skills is in WORKTREE_SKILLS.

🤖 Generated with Claude Code

Trecek and others added 12 commits October 9, 2026 21:08
…hority

output_dir_widens_scope decides whether a dispatch output_dir widens a
skill's declared write scope, matching the scope kind before calling
bounded_scope_contains. widened_output_dir_message is the diagnostic that
names the skill, the requested value, and the declared directories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_extend_closure_write_scope now decides through output_dir_widens_scope and
reports widened_output_dir_message, so a rejected dispatch says which skill,
which output_dir, and which declared directories were involved. Admission
semantics and the closure union extension are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elper

skill_write_scope resolves a SKILL.md, reads it once, and decodes its
write_paths declaration. The alignment rule now reads SKILL.md through it;
its findings are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
write-skill-requires-source-output-dir keyed on write_behavior and demanded an
output_dir from BOUNDED skills too, recommending a worktree-root value that
run_skill rejects for them. It now fires only when the skill declares
write_paths: unrestricted and the step omits output_dir; a BOUNDED skill's
temp floor is already its declared scope.

The duplicate test_write_skill_steps_have_output_dir is removed; the real
rule's bundled sweep in test_rules_contracts covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…outputs

vis_apply passed output_dir run-vis-lenses to vis-lens-{slug} skills, each of
which may write only to its own vis-lens-<slug>/ directory, so run_skill
rejected every lens dispatch; vis_synthesize then read run-vis-lenses, a
directory no skill writes.

vis_apply now passes no output_dir, and synthesize-vis-plan takes the
captured vis_lens_output_paths list (lens_output_paths, optional) instead of
a capture directory. research-design declares the capture optional because
vis_apply routes failures to vis_synthesize.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ktree

Seventeen bundled steps passed output_dir: . to make-plan, prepare-pr,
prepare-research-pr and compose-research-pr. Those skills declare BOUNDED
write scopes, so run_skill rejects a worktree-root output_dir for them. With
output_dir omitted, the default floor <cwd>/.autoskillit/temp/<skill> equals
each skill's declared scope; captures, plan placement and attestation are
unchanged.

Regenerates the compiled recipes, the diagram recipe hashes and the pinned
pre-delivery structure digests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t rule

skill-write-path-recipe-alignment pre-filtered '.', '${{ context.work_dir }}'
and slash-only values before its boundary check, so it stayed silent on the
exact dispatches run_skill rejects. The rule now resolves each value to the
path run_skill will judge -- '.', slash-only, or a value rooted at the step's
cwd -- and decides through output_dir_widens_scope, reporting the runtime's
own diagnostic. Only statically undecidable values (another template root,
an absolute path) stay silent.

bounded_scope_contains is registered as a single-enforcement-point policy
function: output_dir_widens_scope is now its only caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_bundled_dispatch_write_scope_admission dry-runs every tracked bundled
run_skill step through the real _resolve_dispatch_paths and
_extend_closure_write_scope, asserting each is admitted and that the
alignment rule flags exactly what the runtime would reject. Planner steps
run through a reviewed probe for ${{ context.planner_dir }}, templated-skill
dispatches must pass no output_dir, and positive controls keep the sweep
non-vacuous.

test_attested_dispatch_write_scope_admission drives the real attested
run_skill for implementation:plan and implementation:prepare_pr and pins the
narrowed write-watch directory. ADR 0015 names the shared predicate and the
drift guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dropping output_dir: . from remediation's make_plan and prepare_pr steps
shrinks the rendered remediation recipe; the canonical baseline maxima are
re-measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Trecek
Trecek added this pull request to the merge queue Oct 10, 2026
Merged via the queue into develop with commit 317fbd9 Oct 10, 2026
11 checks passed
@Trecek
Trecek deleted the impl-rectify_bounded_output_dir_write_target_authority-20261009-205759 branch October 10, 2026 17:28
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.

1 participant