Skip to content

Persist project symlink policy at command boundaries - #4934

Merged
mnriem merged 3 commits into
github:mainfrom
mnriem:mnriem-filehelper-policy-wiring
Oct 12, 2026
Merged

mnriem merged 3 commits into
github:mainfrom
mnriem:mnriem-filehelper-policy-wiring

Conversation

@mnriem

@mnriem mnriem commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator

Description

Persist project filesystem policy through the init-only --allow-symlinks option and strictly reload allow_symlinks from .specify/init-options.json at project command boundaries. Missing configuration/key defaults to deny; malformed, unreadable or non-boolean configuration fails explicitly. Configuration bootstrap reads and writes always deny symlinks, regardless of the operational setting.

This bounded stage adds the explicit FileHelper handoff API, protects event cwd and bundle ancestor-selection boundaries, and wires the current upstream init lifecycle, optional-project check, and shared artifact CLI/MCP operations. Init validates its original target before adapter/package preparation and retains existing transaction/rollback ownership. Configuration updates preserve unrelated options and the stored policy.

The compatibility overwrite_text operation is deliberately non-atomic: it preserves existing inode/hard-link identity, permissions, umask and native text behavior. Encoding/I/O errors propagate and can leave partial writes; symlink policy is not hard-link confinement or a race-free sandbox. The existing atomic APIs are unchanged.

Migration limit: other filesystem/runtime/storage/script operations are not migrated by this PR. Loading policy alone does not enforce symlink behavior on those operations.

The validated branch is based on merged foundation 03de661789b74405ae93b8b532d52d2cf8af422e. Publication preflight found newer upstream commits in non-overlapping files; the PR merge-base remains that foundation and the delta contains only this policy stage.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (if applicable)

Tests use the repository-documented worktree interpreter rather than bare uv run pytest, avoiding another worktree's editable install. Dependencies were already present; UV_NO_SYNC=1 kept CLI smoke checks in the same validated environment.

Command / evidence Result
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests -q 12,026 passed, 71 skipped
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests/specify_cli/test_file_policy.py tests/specify_cli/artifacts tests/specify_cli/events/test_command_run.py tests/specify_cli/bundles/test_security_paths.py tests/specify_cli/test_command_check.py tests/test_file_helper.py -q 760 passed, 53 skipped; independently repeated by the coordinating review agent
uvx ruff@0.15.0 check src tests Passed
git diff --check HEAD Passed
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests --collect-only -q 12,097 collected versus 11,874 from an isolated unchanged foundation snapshot: +223
UV_NO_SYNC=1 uv run specify --help Passed
UV_NO_SYNC=1 uv run specify init <temporary-project> --integration generic --integration-options='--commands-dir .agent/commands' --non-interactive --ignore-agent-tools --allow-symlinks Passed; verified literal persisted true and generated command directory; temporary project removed

Before fixes, the new-base boundary regressions produced 30 failures; original dangling/missing-parent target evidence produced two additional failures. Positive/negative coverage includes repeated policy edits in one process, strict documents/values and permission errors, bootstrap links, init-only exposure, selected roots/overrides, JSON/plain and MCP failure contracts, no handler/mutation before failure, observer ordering, existing-config rollback and new-root cleanup, plus both-mode overwrite behavior and configuration metadata/hard-link compatibility.

Windows limitation: validation above ran on macOS. The 53 focused native Windows cases were skipped. The merged foundation's Windows CI does not validate these newly changed policy/overwrite operations; native Windows policy-stage verification is pending this PR's CI.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (fill in the disclosure below)

AI disclosure: Implemented with GitHub Copilot using GPT-6.1 Sol in agent/autonomous mode under user-directed scope and coordinating-agent review. AI assistance covered implementation, tests, documentation, validation and this PR description. No sub-agents were launched.

Add strict per-invocation policy loading, init-only persistence, deny-mode configuration bootstrap, and explicit FileHelper handoff. Preserve init lifecycle rollback and config metadata compatibility with a bounded non-atomic overwrite operation. Other filesystem migrations remain deferred.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 661d79b5-8b35-41ce-b903-db64815502bc
Copilot AI balanced review requested due to automatic review settings October 12, 2026 16:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Security-sensitive cross-platform filesystem semantics require human review and native Windows CI confirmation.

0 open findings

What changed in this PR

Persists project symlink policy and enforces strict policy loading at project command boundaries.

Changes:

  • Adds deny-by-default policy loading and FileHelper handoff APIs.
  • Persists init-only --allow-symlinks while preserving configuration metadata.
  • Wires policy checks into project, artifact, bundle, workflow, event, and integration boundaries with extensive tests.
File Description
src/​specify_cli/​__init__.py Preflights policy in shared project resolution.
src/​specify_cli/​_file_policy.py Implements strict policy persistence and loading.
src/​specify_cli/​_init_options.py Delegates configuration writes to policy-aware storage.
src/​specify_cli/​_project.py Adds policy-aware project resolution and error rendering.
src/​specify_cli/​file_helper.py Adds inode-preserving text overwrite support.
src/​specify_cli/​command_init.py Adds and persists --allow-symlinks.
src/​specify_cli/​command_check.py Preflights optional project policy.
src/​specify_cli/​events/​command_run.py Protects the event cwd boundary.
src/​specify_cli/​workflows/​command_run.py Preflights direct workflow roots.
src/​specify_cli/​integrations/​_lifecycle.py Validates init roots before preparation.
src/​specify_cli/​integrations/​_helpers.py Uses strict configuration reads during updates.
src/​specify_cli/​artifacts/​_commands.py Preserves CLI policy-error contracts.
src/​specify_cli/​artifacts/​_operation_info.py Validates policy in artifact info operations.
src/​specify_cli/​artifacts/​_operation_list.py Validates policy in artifact list operations.
src/​specify_cli/​bundles/​_commands.py Adds JSON errors and init policy defaults.
src/​specify_cli/​bundles/​project.py Enforces policy during ancestor discovery.
src/​specify_cli/​bundles/​command_info.py Emits structured policy failures.
src/​specify_cli/​bundles/​command_list.py Emits structured policy failures.
src/​specify_cli/​bundles/​command_search.py Emits structured policy failures.
tests/​specify_cli/​test_file_policy.py Covers policy parsing, lifecycle, CLI, and MCP behavior.
tests/​test_file_helper.py Covers overwrite and symlink semantics.
tests/​test_presets.py Updates configuration encoding coverage.
tests/​specify_cli/​events/​test_command_run.py Tests event policy boundaries.
tests/​specify_cli/​bundles/​test_security_paths.py Tests bundle marker-boundary behavior.
tests/​specify_cli/​integrations/​test_command_install.py Verifies corrupt configuration fails before installation.
tests/​specify_cli/​integrations/​test_command_status.py Updates root-resolution failure expectations.
docs/​reference/​core.md Documents persisted filesystem policy.
design/​file-helper.md Defines the policy and overwrite contracts.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Reject missing and non-directory prefixes before parent traversal can be normalized into an existing init root. Preserve trusted existing directory and root aliases, propagate I/O failures, and cover the Windows behavior with portable regressions and native init cases.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 661d79b5-8b35-41ce-b903-db64815502bc
Copilot AI balanced review requested due to automatic review settings October 12, 2026 19:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The new artifact lookup policy-error path lacks the repository-required regression coverage.

0 open findings

Previously missed (1)

In code that hasn't changed since last review

Low severity Add artifact lookup policy-error coverage for malformed init options

src/​specify_cli/​artifacts/​_commands.py:57

This branch adds policy-error translation for artifact lookup, the sole caller of this resolver, but the new policy tests cover only artifact list/info. Add lookup coverage for a malformed or symlinked init-options.json, asserting exit 1, empty stdout, and the JSON error envelope; this deterministic boundary behavior requires positive/negative coverage under CONTRIBUTING.md:227-240.

🧠 Review effort: Balanced

@mnriem

mnriem commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed the Windows init-path CI failure in commit 07d9b3746885b0ed08a8f33af1f0d1130585c0e6.

Windows presence/strict-resolution probes can normalize missing/../existing into an existing root before accessing the missing prefix. Init now checks that each original prefix before .. is an existing directory, before policy reads, project preparation, or root creation. Existing directory and symlink-root aliases remain supported; permission/I/O failures propagate. The change is limited to init policy-boundary validation, regression tests, and its design documentation. It does not claim race-free filesystem protection or migrate other filesystem operations.

The original Windows Python 3.14 job failed test_new_init_target_preserves_evidence_before_project_reads[missing-parent-traversal]; the original Python 3.13 job was cancelled by matrix fail-fast, not an independently demonstrated test failure. Both fresh Windows jobs were inspected separately on the exact new head:

Native CI job Result
Windows Python 3.13 11,613 passed, 581 skipped; original failing case and new traversal/alias cases passed
Windows Python 3.14 11,613 passed, 581 skipped; original failing case and new traversal/alias cases passed

All six OS/Python pytest jobs in run 38299687391 passed. All other current-head checks completed successfully, except the non-applicable scheduled dependency-audit job, which was skipped.

Local macOS evidence (not local native Windows execution):

Command Result
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests/specify_cli/test_file_policy.py -q -k 'normalized_traversal or existing_parent_traversal_root' Before correction: six failed, two passed
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests/specify_cli/test_file_policy.py tests/test_file_helper.py tests/specify_cli/test_command_init.py -q After correction: 369 passed, 53 native Windows skips
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests/specify_cli/integrations/test_installed_adapters.py -q -k init 80 passed, 588 deselected
uvx ruff@0.15.0 check src tests Passed
git diff --check Passed
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests --collect-only -q 12,110 collected, up 13 from the published stage

Review 5493458559 reported zero findings and no inline comments. Native Windows evidence is now available, but human maintainer review of the filesystem semantics remains outstanding; this automated follow-up does not satisfy or replace it.

AI disclosure: Posted on behalf of contributor @mnriem by GitHub Copilot, powered by GPT-6.1 Sol, operating in agent/autonomous mode under the contributor's approved scope. The agent authored the bounded code, regression tests, design-document update, and this summary; executed local validation; and inspected the native CI results. This is not a claim of personal human review or testing.

Verify missing and valid persisted policies permit lookup, while malformed settings and bootstrap links emit actionable JSON errors before installed-adapter or catalog access. Add seven net cases without changing runtime behavior.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 661d79b5-8b35-41ce-b903-db64815502bc
Copilot AI balanced review requested due to automatic review settings October 12, 2026 20:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

It changes security-sensitive filesystem boundaries across many commands, while native Windows verification remains pending.

0 open findings

🧠 Review effort: Balanced

@mnriem

mnriem commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the coverage request in review 5495328871 with commit 54688810a586e8ea021c74ee64eb45167b1a271c. Only tests/specify_cli/artifacts/test_command_lookup.py changed; there are no runtime or documentation changes.

Real lookup success now covers a missing policy file, missing setting, and literal false/true. Negative cases cover malformed JSON, a non-boolean setting, a contained configuration symlink whose target records allow=true, and a dangling configuration symlink. They assert exit 1, empty stdout, the sole JSON error envelope with an actionable configuration diagnostic, and no installed-integration loader or artifact catalog access. This adds seven net collected cases (12,110 to 12,117 locally).

Regression evidence: substituting the actual pre-policy _resolve_project_root function from commit 03de661789b74405ae93b8b532d52d2cf8af422e into the lookup adapter in memory, while retaining current project preflight, produces four failing negative cases and four passing positive cases. The failures demonstrate the old translator losing policy diagnostics and incorrectly reporting a missing project. This is a focused translator comparison, not a full old-tree rerun; no source or checkout was changed for it. With the current translator, the tests pass.

Local macOS validation:

Command Result
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests/specify_cli/artifacts tests/specify_cli/test_file_policy.py -q 536 passed
uvx ruff@0.15.0 check tests/specify_cli/artifacts/test_command_lookup.py Passed
git diff --check Passed
LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests --collect-only -q 12,117 collected, seven net additions

Fresh exact-head CI run 38303244931 passed all six OS/Python pytest jobs. Both native Windows logs independently confirm that all eight positive/negative lookup cases passed:

Native CI job Result
Windows Python 3.13 11,620 passed, 581 skipped
Windows Python 3.14 11,620 passed, 581 skipped

All other current-head checks completed successfully except the non-applicable scheduled dependency-audit job, which was skipped. Native Windows execution occurred in CI, not locally. Human maintainer review remains outstanding; no review conversations were resolved.

AI disclosure: Posted on behalf of contributor @mnriem by GitHub Copilot, powered by GPT-6.1 Sol, operating in agent/autonomous mode within the contributor-approved scope. The agent authored the tests-only change and this summary, ran local validation and the qualified in-memory regression comparison, and inspected CI evidence. No claim of personal human review or testing is made.

@mnriem
mnriem merged commit 117fb57 into github:main Oct 12, 2026
16 checks passed
@mnriem
mnriem deleted the mnriem-filehelper-policy-wiring branch October 12, 2026 21:35
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.

2 participants