You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds Nexora Code as a built-in skills-style integration (key = "nexora"): a SkillsIntegration writing .nexora/skills/speckit-<name>/SKILL.md with the hyphen invoke separator, argument hints as in the Claude integration, multi_install_safe = True, requires_cli = False (the editor extension needs no CLI; the nexora CLI is optional and reads the same directory). Nexora Code and the CLI invoke any installed skill as /<name> [args], so the commands are /speckit-specify, /speckit-bug-assess, … exactly as for the other skills layouts.
Changes: src/specify_cli/integrations/nexora/__init__.py, registry import, _invocation_style.py (CONDITIONAL_SLASH_AGENTS), integrations/catalog.json, docs/integrations/nexora.md + index row, tests (tests/integrations/test_integration_nexora.py, registry and agent-config consistency lists), and the three issue templates' agent lists (required by test_agent_config_consistency.py).
Testing: pytest tests/integrations/test_integration_nexora.py → 74 passed; full suite 9953 passed / 166 skipped / 1 failed (test_check_prerequisites_python_parity[composed], reproduces on a clean main here — needs pwsh). End-to-end: specify init demo --integration nexora --script sh --ignore-agent-tools, then specify extension add bug / assess → 17 .nexora/skills/speckit-*/SKILL.md; specify integration status OK.
Opened as a draft per the contribution guidance (prior discussion in #5315). If an external adapter + catalog listing is the preferred route for new agents, I am glad to convert this.
AI disclosure: drafted with AI assistance (Claude Code) under my direction; reviewed, tested and submitted by me.
Nexora Code is the nexora-intelligence.nexora-code extension for VS Code,
Antigravity and Cursor, with an optional companion CLI (@nexoracode/cli).
Both read skills from .nexora/skills/<name>/SKILL.md (and the shared
.agents/skills/ root) and invoke them as /<name>, so Spec Kit commands are
/speckit-specify, /speckit-bug-assess, etc.
- SkillsIntegration subclass: key `nexora`, folder .nexora/, skills under
.nexora/skills, hyphen invoke separator, multi_install_safe, requires_cli
False (no workflow CLI dispatch), inline argument-hint frontmatter for
the core commands.
- Register in the built-in registry and catalog, classify as a conditional
slash agent, list in the integration index and issue templates.
- Add docs/integrations/nexora.md and tests mirroring the existing skills
integrations (setup/teardown, hints, status, bug/assess extensions,
multi-install alongside claude).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
A critical write-before-validation flaw allows pre-existing skill-file symlinks to overwrite files outside the project before the containment check runs.
Thanks — addressed in ce3f12a. The shared write helpers (write_file_and_record(), copy_command_to_directory(), install_scripts(), record_file_in_manifest()) now resolve each destination with a new IntegrationBase.resolve_destination_inside() and raise the existing "escapes project root" error before any directory is created or bytes are written, so a symlink planted at an output path (or on an ancestor) can no longer redirect a write outside the project; inside-project links keep working and no integration is special-cased. Regression coverage: TestNexoraSymlinkContainment plants .nexora/skills/speckit-specify/SKILL.md and its speckit-specify/ parent as links to outside files and asserts the error is raised and the outside files are unchanged, plus shared helper/markdown/scripts cases in TestDestinationContainment. Branch is merged with current main. (copy_command_to_directory()'s new project_root= is optional for external adapters that call it directly — happy to make it mandatory if you prefer.)
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The installation path writes SKILL.md before validating symlinks, allowing a malicious repository to overwrite files outside the project with the user's permissions.
…g files
Addresses the Copilot review on github#5316: write_file_and_record() created the
parent directory and wrote the file before checking that the resolved path
stayed inside the project, so a symlink planted at an output path (for
example .nexora/skills/speckit-specify/SKILL.md, or its speckit-specify/
parent) redirected the write to a file outside the project before the
containment error was raised. copy_command_to_directory() and the bundled
script copy had the same ordering.
- Add IntegrationBase.resolve_destination_inside(): resolve the destination
(following an existing or dangling link at the path and any symlinked
ancestor, with the not-yet-existing tail appended to the nearest existing
ancestor's real path) and raise the existing "escapes project root"
ValueError when it leaves the project.
- Call it before before_file_change()/mkdir/write in write_file_and_record(),
in copy_command_to_directory() (new optional project_root, passed by the
markdown setup), in install_scripts(), and in record_file_in_manifest().
Symlinks that stay inside the project keep working. No integration is
special-cased.
- Regression tests: shared helper and write-path tests in
tests/integrations/test_base.py (file symlink, dangling symlink, symlinked
ancestor, traversal, markdown setup, bundled scripts) and Nexora setup
tests that plant .nexora/skills/speckit-specify/SKILL.md and the
speckit-specify/ directory as links to outside files, assert the error is
raised and the outside files are unchanged, and that an inside link is
still honoured. Skipped where the platform cannot create symlinks.
- Document the guard in docs/integrations/api.md and design/integration.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The fix is now pushed as 9197cb6 (rebased onto the PR base instead of merged with main so the diff stays limited to the integration; content identical to the change described above — resolve_destination_inside() + the containment regression tests).
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Critical destination-validation gaps allow symlinks and directory targets to redirect integration and extension writes, potentially overwriting unrelated files inside or outside the project.
Reject symlinked destinations and ancestors before resolving paths
src/specify_cli/integrations/base.py:671
A skill path such as SKILL.md -> <project>/user-notes.md passes this guard, allowing the writer to overwrite the notes and record them as integration-owned. Reject symlinked destinations and ancestors before resolving them, including project-local links, as required by design/integration.md (“Storage, loading, and lifecycle”). Preserve lexical paths through setup and manifest validation, and replace the new allow-internal-symlink tests with rejection and file-preservation cases.
Reject directory destinations before copying files
src/specify_cli/integrations/base.py:676
An existing directory passes this check, but shutil.copy2() appends the source basename when its destination is a directory; a symlink at that unchecked child can therefore redirect the copy outside the project. Reject directory destinations before copying, since these helpers expect a file path. This affects both copy_command_to_directory() and install_scripts() (base.py:791–794).
Validate extension destination symlinks before writing
With .nexora/skills/speckit-bug-assess symlinked to an outside directory, installing the bundled bug extension can overwrite that directory's SKILL.md. The registrar only checks lexical containment, so the new core-setup checks do not protect extension installation. Validate the registrar's destination and ancestors before creating directories or writing, and add an extension-install regression that verifies the outside file remains unchanged.
Round 2 of the github#5316 review. The first round checked each destination
before writing, but copy_command_to_directory() only did so when the
caller passed project_root (a fail-open default), and both helpers still
had a check-then-write window: a symlink planted at the destination after
the check would be followed by write_bytes()/copy2().
- copy_command_to_directory(): project_root is now required; the in-repo
caller (MarkdownIntegration.setup) and tests pass it.
- New IntegrationBase._write_bytes_nofollow(): after the containment check,
an existing symlink at the destination is unlinked rather than written
through, and the file is opened with O_WRONLY|O_CREAT|O_TRUNC|O_NOFOLLOW
(plus O_BINARY/O_CLOEXEC where defined), so a link planted between the
check and the write is refused (ELOOP -> ValueError) instead of followed.
write_file_and_record(), copy_command_to_directory() and install_scripts()
all write through it; copies preserve the source mode and timestamps via
fchmod/utime on the descriptor (exec bit still added for .sh/.py scripts).
- Tests: omitting project_root is a TypeError; an existing symlink at the
destination is replaced, not followed (target unchanged) for both helpers
and for a Nexora SKILL.md; a link planted between the check and the write
is not followed; O_NOFOLLOW refuses a link that survives the unlink step;
copied mode/mtime are preserved. The external-adapter leaf-symlink test
passes project_root to the now-mandatory parameter.
- Docs: api.md row and containment paragraph, design/integration.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Round 2 (e3f4173): copy_command_to_directory() now requires project_root (no fail-open default), and it, write_file_and_record() and install_scripts() write through a new _write_bytes_nofollow() that unlinks an existing symlink at the destination and opens with O_NOFOLLOW, so a link planted between the containment check and the write is refused rather than followed. Tests cover the TypeError, link replacement with the former target unchanged, a link planted after the check, O_NOFOLLOW refusing a surviving link, and preserved copy mode/mtime. Scope note: O_NOFOLLOW protects the final path component; a parent directory swapped for a link after the check is still caught by the existing post-write re-check — closing that fully would need openat-style traversal, which felt disproportionate here but I can add it if you want.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The shared write-helper and signature changes affect all integrations and violate the documented requirement to deliver shared runtime changes in a separate prerequisite PR.
Please make sure that any changes in base integration are funneled into a new PR as if shared behavior needs to change it should be done as a separate PR. So if Copilot is asking for it please address it in a separate PR and/or flag it out of scope. So can you please refine this PR along those lines. Thanks!
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #5315.
Adds Nexora Code as a built-in skills-style integration (
key = "nexora"): aSkillsIntegrationwriting.nexora/skills/speckit-<name>/SKILL.mdwith the hyphen invoke separator, argument hints as in the Claude integration,multi_install_safe = True,requires_cli = False(the editor extension needs no CLI; thenexoraCLI is optional and reads the same directory). Nexora Code and the CLI invoke any installed skill as/<name> [args], so the commands are/speckit-specify,/speckit-bug-assess, … exactly as for the other skills layouts.Changes:
src/specify_cli/integrations/nexora/__init__.py, registry import,_invocation_style.py(CONDITIONAL_SLASH_AGENTS),integrations/catalog.json,docs/integrations/nexora.md+ index row, tests (tests/integrations/test_integration_nexora.py, registry and agent-config consistency lists), and the three issue templates' agent lists (required bytest_agent_config_consistency.py).Testing:
pytest tests/integrations/test_integration_nexora.py→ 74 passed; full suite 9953 passed / 166 skipped / 1 failed (test_check_prerequisites_python_parity[composed], reproduces on a cleanmainhere — needspwsh). End-to-end:specify init demo --integration nexora --script sh --ignore-agent-tools, thenspecify extension add bug/assess→ 17.nexora/skills/speckit-*/SKILL.md;specify integration statusOK.Opened as a draft per the contribution guidance (prior discussion in #5315). If an external adapter + catalog listing is the preferred route for new agents, I am glad to convert this.
AI disclosure: drafted with AI assistance (Claude Code) under my direction; reviewed, tested and submitted by me.
🤖 Generated with Claude Code