Skip to content

feat(integrations): add Nexora Code (nexora) skills integration - #5316

Open
monyverse wants to merge 3 commits into
github:mainfrom
monyverse:feat/5315-nexora-integration
Open

monyverse wants to merge 3 commits into
github:mainfrom
monyverse:feat/5315-nexora-integration

Conversation

@monyverse

Copy link
Copy Markdown

Closes #5315.

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.

🤖 Generated with Claude Code

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>
Copilot AI auto review requested due to automatic review settings November 8, 2026 16:53

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

A critical write-before-validation flaw allows pre-existing skill-file symlinks to overwrite files outside the project before the containment check runs.

1 open issue
What changed in this PR

Adds Nexora Code as a built-in skills integration for Specify CLI, addressing #5315 without requiring the optional Nexora CLI.

Changes:

  • Installs .nexora/skills/speckit-<name>/SKILL.md files with hyphenated slash commands and core argument hints.
  • Registers Nexora in integration discovery, documentation, and issue templates.
  • Adds coverage for setup, invocation, extensions, status, and multi-install behavior.
File Description
tests/​test_agent_config_consistency.py Checks Nexora agent and registrar configuration.
tests/​integrations/​test_registry.py Includes Nexora in expected built-ins.
tests/​integrations/​test_integration_nexora.py Tests skill generation and integration lifecycle behavior.
src/​specify_cli/​integrations/​nexora/​__init__.py Implements the Nexora skills adapter and argument hints.
src/​specify_cli/​integrations/​__init__.py Registers the built-in adapter.
src/​specify_cli/​_invocation_style.py Classifies Nexora for slash-style skill invocation.
integrations/​catalog.json Adds Nexora discovery metadata.
docs/​integrations/​nexora.md Documents setup, skills, and dispatch limitations.
docs/​integrations/​index.md Links the Nexora guide.
.github/​ISSUE_TEMPLATE/​feature_request.yml Adds Nexora to agent choices.
.github/​ISSUE_TEMPLATE/​bug_report.yml Adds Nexora to agent choices.
.github/​ISSUE_TEMPLATE/​agent_request.yml Lists Nexora among supported agents.

Review effort: Balanced (auto)

}


class NexoraIntegration(SkillsIntegration):
@mnriem mnriem added the triage-must-have Verdict: high-value, important work for Spec Kit — do first label Nov 9, 2026
@mnriem

mnriem commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

@mnriem mnriem added the author-awaiting Waiting on author response label Nov 9, 2026
@monyverse

Copy link
Copy Markdown
Author

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.)

@monyverse
monyverse marked this pull request as ready for review November 9, 2026 23:31
@monyverse
monyverse requested a review from mnriem as a code owner November 9, 2026 23:31
@mnriem
mnriem requested an auto review from Copilot November 9, 2026 23:43

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 installation path writes SKILL.md before validating symlinks, allowing a malicious repository to overwrite files outside the project with the user's permissions.

1 open issue

Review effort: Balanced (auto)

@mnriem

mnriem commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

…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>
@monyverse

Copy link
Copy Markdown
Author

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).

Copilot AI auto review requested due to automatic review settings November 10, 2026 00:06

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

Critical destination-validation gaps allow symlinks and directory targets to redirect integration and extension writes, potentially overwriting unrelated files inside or outside the project.

2 open issues
3 previously missed issues

In code that hasn't changed since last review

High severity 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.

High severity 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).

High severity Validate extension destination symlinks before writing

src/​specify_cli/​integrations/​nexora/​__init__.py:55

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.

Review effort: Balanced (auto)

Comment on lines +652 to +653
@staticmethod
def resolve_destination_inside(dest: Path, project_root: Path) -> Path:
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>
@monyverse

Copy link
Copy Markdown
Author

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.

Copilot AI auto review requested due to automatic review settings November 10, 2026 00:23

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 shared write-helper and signature changes affect all integrations and violate the documented requirement to deliver shared runtime changes in a separate prerequisite PR.

2 open issues

Review effort: Balanced (auto)

@mnriem

mnriem commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author-awaiting Waiting on author response 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.

[Agent]: Add support for Nexora Code

4 participants