Skip to content

fix: reject symlinked extension registry storage - #5184

Open
mnriem wants to merge 3 commits into
github:mainfrom
mnriem:mnriem-extension-registry-symlinks
Open

mnriem wants to merge 3 commits into
github:mainfrom
mnriem:mnriem-extension-registry-symlinks

Conversation

@mnriem

@mnriem mnriem commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

Description

Reject symlinked extension registry storage by routing registry reads and writes through a deny-mode FileHelper rooted at the project, or its nearest existing ancestor for a new project. Validate the destination before file-change observers run, while retaining registry recovery, write semantics, and preset-resolution diagnostics.

Add regression coverage for linked registry files and parent directories, including dangling links, and document the registry filesystem boundary.

Testing

  • Tested locally with uv run specify --help

  • Ran existing tests with uv sync && uv run pytest

  • Tested with a sample project (if applicable)

  • uv sync --extra test — passed.

  • uv run specify --help — passed.

  • LC_ALL=en_US.UTF-8 .venv/bin/python -m pytest tests -q — passed: 13,677 passed, 130 skipped.

For the existing-tests checklist item, dependencies were installed with uv sync --extra test, and pytest used this worktree's interpreter as required by the contribution guidance rather than bare uv run pytest. No separate manual sample-project run was performed.

AI Disclosure

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

AI disclosure: GitHub Copilot, powered by GPT-6.1 Sol (gpt-6.1-sol), performed the investigation, code and test generation, documentation changes, automated validation, and PR preparation in autonomous agent mode at the contributor's request. The reasoning-effort setting is not exposed to the agent; no human line-by-line review is claimed.

Route registry I/O through deny-mode FileHelper operations and preserve preset resolution diagnostics. Cover linked storage hierarchies and missing-project creation.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI auto review requested due to automatic review settings October 29, 2026 14:34

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.

🟡 Changes recommended

Registry policy failures can still allow disabled extension commands to run, while linked parent paths can bypass validation and several callers mishandle the new errors.

6 open issues
What changed in this PR

This PR hardens Specify CLI’s extension registry storage by moving registry I/O to a project-rooted, deny-mode FileHelper.

Changes:

  • Route registry reads and writes through the helper, validating destinations before file-change observers run.
  • Translate registry policy errors into preset-resolution diagnostics.
  • Add regression coverage for linked and missing storage, and document the filesystem boundary.

Full project tests were not rerun during this review.

AI review assistance: GitHub Copilot performed automated inspection and comment editing for the requesting contributor; contributor identity and model/settings were unavailable.

File Description
tests/​test_extensions.py Tests linked-storage rejection, missing-storage creation, and observer sequencing.
tests/​specify_cli/​presets/​test_resolver.py Updates expectations for dangling registry links.
src/​specify_cli/​presets/​_resolver.py Wraps helper failures in preset-validation diagnostics.
src/​specify_cli/​extensions/​__init__.py Adds scoped registry I/O and write-destination preflight.
design/​file-helper.md Documents the registry’s always-deny boundary and preserved write semantics.

Review effort: Max (auto)

try:
with open(self.registry_path, "r", encoding="utf-8") as f:
data = json.load(f)
data = json.loads(self._files.read_text(self.registry_path))
Comment thread src/specify_cli/extensions/__init__.py Outdated
try:
with open(self.registry_path, "r", encoding="utf-8") as f:
data = json.load(f)
data = json.loads(self._files.read_text(self.registry_path))
try:
with open(self.registry_path, "r", encoding="utf-8") as f:
data = json.load(f)
data = json.loads(self._files.read_text(self.registry_path))
try:
with open(self.registry_path, "r", encoding="utf-8") as f:
data = json.load(f)
data = json.loads(self._files.read_text(self.registry_path))
Comment thread src/specify_cli/presets/_resolver.py
@mnriem mnriem added the triage-must-have Verdict: high-value, important work for Spec Kit — do first label Oct 29, 2026
Validate hierarchy metadata before registry recovery, fail closed during event resolution, preserve artifact and extension command errors, and retain non-fatal dependency and configuration probes.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI auto review requested due to automatic review settings October 29, 2026 16:24
@mnriem

mnriem commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

Updated in e42d18e: registry hierarchy validation now precedes missing-directory and leaf recovery; event resolution stops on forbidden registry state; artifact CLI/MCP and extension commands preserve their error formats; and dependency/configuration probes retain their non-fatal behavior. Regression coverage and the filesystem-boundary documentation were updated. Posted on behalf of @mnriem by GitHub Copilot (GPT-6.1 Sol, autonomous agent mode; reasoning-effort setting not exposed), which implemented the code, tests, and documentation and generated this reply.

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.

Comment thread src/specify_cli/events/__init__.py
Comment on lines +771 to 772
self._files = FileHelper(root)
self.data = self._load()
Reject registry hierarchy links and reparse points before dispatcher delegation or inline fallback. Route extension update through the shared CLI policy-error handler.

Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI auto review requested due to automatic review settings October 29, 2026 16:54
@mnriem

mnriem commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

Updated in c671cc9: generated event dispatchers now reject linked registry storage and unsupported reparse points before importing an installed resolver or entering the inline fallback, with a diagnostic and exit status 2. Extension update now uses the shared CLI policy-error handler. Added coverage for no-package and stale-package execution, live and dangling links, reparse points, and both update invocation forms. Posted on behalf of @mnriem by GitHub Copilot (GPT-6.1 Sol, autonomous agent mode; reasoning-effort setting not exposed), which implemented the code, tests, and documentation and generated this reply.

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

With events enabled and only built-in integrations installed, linked extension storage can cause integration switching to fail after removing the old integration’s files and metadata, without rollback.

5 open issues
1 previously missed issue

In code that hasn't changed since last review

High severity Validate registry before removing the current integration

src/​specify_cli/​extensions/​__init__.py:777

For a project with only built-in integrations and events enabled, a linked extensions directory or .registry now makes integration switch fail after it has removed the old integration's files and metadata. resolve_events() runs before the handler's rollback block (integrations/command_switch.py:295-303), and the outer lifecycle skips its transaction when no external packages are recorded (integrations/_lifecycle.py:468-469), leaving the old integration removed. Validate registry storage before Phase 1 and add a switch regression test that checks files and metadata remain unchanged on rejection.

1 resolved since last review

Review effort: Max (auto)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage-must-have Verdict: high-value, important work for Spec Kit — do first

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants