Skip to content

Clarify extension catalog trust model in docs, help, and messaging - #4177

Merged
mnriem merged 4 commits into
github:mainfrom
mnriem:mnriem-shiny-couscous
Aug 17, 2026
Merged

Clarify extension catalog trust model in docs, help, and messaging#4177
mnriem merged 4 commits into
github:mainfrom
mnriem:mnriem-shiny-couscous

Conversation

@mnriem

@mnriem mnriem commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Closes #4176

Problem

The issue author found extension catalog management confusing: the "default"/"community" catalogs were unexplained, and after discovering the community catalog they tried to make it installable — hitting the deliberate install_allowed: false boundary with no explanation of why it exists or what the correct path is.

Investigating confirmed the root cause is documentation + messaging, not the design:

  • Neither docs/reference/extensions.md nor --help explained that community is discovery-only by design (a security/vetting boundary), or that community is already active for search by default.
  • Worse, the not-installable error text (add to an approved catalog with install_allowed: true) reads like an invitation to flip the community catalog to install_allowed — exactly the wrong move.

Changes

  • Docs: add a "discovery-only vs. install sources" trust-model section; document add --from <url> as the lightweight vetted-install path; stop implying community should be made installable.
  • --help: expand the catalog app and --install-allowed help to state the vetting intent instead of bare mechanics.
  • Messaging: rewrite the not-installable errors in add, search, and info to point at --from and self-curated catalogs, and to say explicitly not to flip a discovery-only catalog to install_allowed.
  • catalog list: prints trust-model guidance when a discovery-only catalog is active.
  • Tests: cover the new catalog list guidance (present and absent).

Deliberately out of scope

No verb to toggle install_allowed on an existing catalog. Discovery-only is a security boundary, not an inconvenience — the fix is making the correct path (--from, self-curated catalog) obvious, not making it easy to weaken the boundary.

Validation

tests/test_extensions.py and tests/integrations/test_cli.py — 633 passed.


This PR was authored by GitHub Copilot (model: Claude Opus 4.8) under the supervision of @mnriem.

…ithub#4176)

Extension catalog management gave no explanation of why the community
catalog is discovery-only, and the install-error text nudged users to
flip a discovery catalog to install_allowed — exactly the wrong move.

- Docs: add a "discovery-only vs. install sources" trust-model section,
  document `add --from <url>` as the lightweight vetted-install path, and
  stop implying you should make community installable.
- Help: expand the `catalog` app and `--install-allowed` help to state the
  vetting intent instead of bare mechanics.
- Messaging: rewrite the not-installable errors in `add`, `search`, and
  `info` to point at `--from` and self-curated catalogs, and to say
  explicitly not to flip a discovery-only catalog to install_allowed.
- `catalog list` now prints trust-model guidance when a discovery-only
  catalog is active.
- Tests cover the new list guidance (present/absent).

Deliberately does not add a verb to toggle install_allowed on an existing
catalog: discovery-only is a security boundary, not an inconvenience.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a86c498e-f129-4422-9983-d1a33513fd4d
Copilot AI balanced review requested due to automatic review settings August 17, 2026 18:25

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.

Pull request overview

Clarifies the extension catalog trust model across documentation and CLI messaging.

Changes:

  • Documents discovery-only versus trusted install catalogs.
  • Improves CLI help and installation guidance.
  • Tests catalog-list trust guidance.
Show a summary per file
File Description
docs/reference/extensions.md Documents catalog trust and vetted installation paths.
src/specify_cli/extensions/_commands.py Updates catalog help and user-facing guidance.
tests/test_extensions.py Tests conditional catalog-list guidance.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread src/specify_cli/extensions/_commands.py Outdated
Comment thread src/specify_cli/extensions/_commands.py Outdated
…rning (github#4176)

- The discovery-only "install directly" hint used the user-typed argument,
  which can be a display name with spaces (resolved via search) and would
  break when copied as a shell command. Emit the resolved catalog ID
  (ext_info['id']) instead. Added a regression test.
- The `--from` untrusted-source warning claimed the URL was "not listed in
  any of your configured extension catalogs", which is false for a URL
  copied from a discovery-only catalog — the exact flow this PR documents.
  Reword it to state the install is bypassing trusted (install-allowed)
  catalogs, which is accurate regardless of discovery-catalog membership.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a86c498e-f129-4422-9983-d1a33513fd4d
Copilot AI review requested due to automatic review settings August 17, 2026 18:33
@mnriem

mnriem commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed both review comments in a9299f1:

  1. Copy-pasteable install hint — the discovery-only "install directly" suggestion now uses the resolved catalog ID (ext_info['id']) instead of the typed argument, so a display name with spaces no longer produces a command that splits into multiple args. Added test_add_discovery_only_error_suggests_resolved_id as a regression test.
  2. Accurate --from warning — reworded the untrusted-source panel so it says the install is bypassing your trusted (install-allowed) catalogs rather than claiming the URL is absent from all configured catalogs. That claim was false for a URL copied from a discovery-only catalog (the flow this PR documents).

Full suite green: tests/test_extensions.py, tests/integrations/test_cli.py, tests/test_workflows.py — 1564 passed.

Posted by GitHub Copilot (model: Claude Opus 4.8) on behalf of @mnriem.

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.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

docs/reference/extensions.md:113

  • The documented path still leaves users without a way to obtain <archive-url>. Community catalog entries contain download_url, but extension search only shows the repository and extension info omits download_url, so following this instruction currently requires inspecting the raw catalog—the confusion this PR is intended to remove. Document how to locate and vet the release archive, or expose the candidate URL through the CLI before recommending this command.
> 1. **Install a single vetted extension directly** with `--from` (no catalog authoring needed):
>    ```bash
>    specify extension add <name> --from <archive-url>
>    ```

src/specify_cli/extensions/_commands.py:1539

  • This newly suggested command interpolates an ID from an explicitly unvetted catalog. _escape_markup only escapes Rich syntax, not shell metacharacters, so copying the command can execute catalog-controlled shell text. Validate the ID against the extension manifest's lowercase-alphanumeric-and-hyphen rule before showing it as a command, or render command arguments through a shell-safe helper.
            f"Once you've vetted it, install directly: specify extension add {safe_id} --from <archive-url>"
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/specify_cli/extensions/_commands.py Outdated
…RL (github#4176)

Second review round on github#4177.

Shell-safety: catalog entry IDs (especially from discovery-only catalogs)
are not validated during catalog merge, and rich.markup.escape only
neutralizes Rich markup, not shell metacharacters. A malicious ID like
`foo; rm -rf ~` was interpolated into the `specify extension add ... --from`
command we encourage the user to copy. Add `_command_safe_id`, which only
emits an ID matching the manifest rule `^[a-z0-9-]+$` (via
VALID_EXTENSION_ARTIFACT_NAME_PATTERN) and otherwise falls back to a literal
`<extension-id>` placeholder. Applied to every suggested command in `add`,
`search`, and `info`.

Discoverability: the documented `--from <archive-url>` flow gave no CLI path
to obtain the URL. `extension info` now prints the candidate `download_url`
for a discovery-only entry (clearly flagged as needing vetting), and the docs
show `extension info <name>` as the way to get the archive URL.

Tests cover the resolved-ID hint, the unsafe-ID neutralization, and pass the
full extensions + CLI suites (635).

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a86c498e-f129-4422-9983-d1a33513fd4d
Copilot AI review requested due to automatic review settings August 17, 2026 20:13
@mnriem

mnriem commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the second review round in 8d41177:

  1. Shell-safety of interpolated catalog IDs (inline + suppressed comment on _commands.py:1539) — catalog entry IDs aren't validated during merge, and rich.markup.escape only handles Rich markup, not shell metacharacters, so an ID like foo; rm -rf ~ could land in a command the user is nudged to copy. Added _command_safe_id, which emits the ID only when it matches the manifest rule ^[a-z0-9-]+$ (VALID_EXTENSION_ARTIFACT_NAME_PATTERN) and otherwise substitutes a literal <extension-id>. Applied to every suggested command in add, search, and info.
  2. No CLI path to <archive-url> (suppressed comment on docs/reference/extensions.md:113) — extension info now prints the candidate download_url for a discovery-only entry (clearly flagged as needing vetting), and the docs point to specify extension info <name> as the way to obtain the URL before --from.

New tests: test_add_discovery_only_error_neutralizes_unsafe_id plus the existing resolved-ID test. Full extensions + CLI suites green — 635 passed.

Posted by GitHub Copilot (model: Claude Opus 4.8) on behalf of @mnriem.

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread src/specify_cli/extensions/_commands.py
Comment thread src/specify_cli/extensions/_commands.py
…ub#4176)

Third review round on github#4177.

_command_safe_id: an ID like `--force` satisfies the manifest character rule
`^[a-z0-9-]+$` but Typer parses a leading hyphen as an option rather than the
positional extension argument, so an untrusted catalog could still yield a
non-copyable or option-altering suggested command. Reject a leading hyphen and
fall back to the `<extension-id>` placeholder.

Tests: cover the new `extension info` discovery-only branch that surfaces the
candidate `download_url` (plus the no-URL fallback), and the leading-hyphen
rejection. Full extensions suite green (528).

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a86c498e-f129-4422-9983-d1a33513fd4d
Copilot AI review requested due to automatic review settings August 17, 2026 20:37
@mnriem

mnriem commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the third review round in 74a7e0a:

  1. Leading-hyphen catalog IDs (_commands.py:99) — an ID like --force satisfies ^[a-z0-9-]+$ but Typer would parse it as an option rather than the positional extension argument. _command_safe_id now rejects any ID starting with - before the pattern check and substitutes the <extension-id> placeholder.
  2. Missing coverage for the extension info archive-URL branch (_commands.py:1564) — added test_info_discovery_only_shows_candidate_archive_url (discovery-only entry with download_url renders the flagged candidate URL + vetted --from guidance) and test_info_discovery_only_without_url_falls_back (no-URL fallback). Also added test_command_safe_id_rejects_leading_hyphen.

Full extensions suite green — 528 passed.

Posted by GitHub Copilot (model: Claude Opus 4.8) on behalf of @mnriem.

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.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/specify_cli/extensions/_commands.py:104

  • re.Pattern.match() with a $ anchor accepts a match immediately before one trailing newline, so a catalog ID such as acme\n is returned verbatim. Every generated hint then becomes a two-line, non-copyable shell command despite this helper's safety contract. Use fullmatch() so the complete catalog key must satisfy the slug rule.
    if VALID_EXTENSION_ARTIFACT_NAME_PATTERN.match(text):
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@mnriem
mnriem merged commit fa3a5c5 into github:main Aug 17, 2026
14 checks passed
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.

[Feature]: Extension management makes no sense

2 participants