Repository navigation
feat(plugins): publisher trust, signed catalog installs, and lock entries - #504
Merged
Ayinkx merged 1 commit intoOct 17, 2026
Merged
Conversation
…ries Implements the Phase 9 trust/install/lock slice: - AyinkxLab#386: strict PLUGIN_TRUSTED_KEYS loading (canonical ids, real Ed25519 keys), startup validation via create_app, and secret-free publisher listings (flask plugins publishers, GET /plugins/api/trusted-publishers). - AyinkxLab#385: detached Ed25519 manifest signature verification over the canonical manifest body, shared with the embedded-signature path; forged/untrusted signatures always fail, unsigned entries fail under PLUGIN_TRUST_POLICY=required. - AyinkxLab#389: validated operator catalog (PLUGIN_CATALOG_FILE) and owner-only POST .../plugins/install-from-catalog; registers metadata only, audits trust refusals, never downloads code or grants capabilities. - AyinkxLab#394: PluginLockEntry model (owner/workspace scoped) serialized through the canonical deterministic lockfile builder. Docs and .env.example updated; tests cover keys, signatures, catalog install failure/authorization paths, and lock serialization.
|
@olatechy0 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Implements the Phase 9 plugin trust/install/lock slice: detached Ed25519 publisher signature verification, strict trusted-publisher key management, an owner-only catalog-backed remote install endpoint, and the
PluginLockEntrymodel with canonical lockfile serialization.Closes #385
Closes #386
Closes #389
Closes #394
Changes
#386 — trusted publisher keys (
app/services/plugin_trust.py)PLUGIN_TRUSTED_KEYS: canonical lowercase publisher ids plus real 32-byte Ed25519 keys (base64 raw, PEM SPKI, or raw bytes).PLUGIN_TRUST_POLICYnow aborts startup with a clear error (create_app→validate_trust_config) instead of silently treating signatures as invalid.flask plugins publishers [--json]andGET /plugins/api/trusted-publishersreturn publisher ids + policy only (no key bytes or fingerprints).#385 — detached publisher signatures (
app/services/plugin_signatures.py)signature/trust_state/trust_publisher); the embedded and detached forms share one verifier.requiredrejects unsigned entries; forged, malformed, or unknown-publisher signatures are refused under both policies (absence is not forgery).app/services/plugins.pynow delegates to the shared helpers instead of duplicating the crypto path.#389 — remote install from the catalog (
app/services/plugin_catalog.py+ route)PLUGIN_CATALOG_FILE(empty by default = feature disabled) points at an operator-controlled catalog. Entries are validated at load: canonical id/version, https source, sha256 integrity, manifest id/version match, signature shape, duplicate detection, size/entry bounds.POST /plugins/api/workspaces/<id>/plugins/install-from-catalogis owner-only (manage_plugins) and accepts only{plugin_id, version?}— never a client manifest, artifact URL, or key.plugin.deniedwith a static reason, no artifact bytes); success registers metadata only, downloads nothing, executes no code, and creates no capability grant. The response reports the resolvedsourceandintegrity.#394 — plugin lock entries (
PluginLockEntry)(workspace_id, plugin_id), storing plugin id, version, source, sha256 integrity, dependencies, andresolved_at.dumps_lockfile(sorted, normalized, no volatile timestamps); no capability grants are implied.Docs:
docs/plugins.md,docs/security.md,.env.exampleupdated.Scope notes:
docs/security.md).Validation
tests/test_plugin_trust.py,tests/test_plugin_signatures.py,tests/test_plugin_catalog_install.py,tests/test_plugin_lock_entries.py)test_plugins_manifest.py,test_plugin_lockfile.py,test_plugins_api.py,test_plugins_cli.py), including the embedded-signature regression testsruff check .—All checks passed!black --check .—270 files would be left unchanged.pytestrun complete — 2353 passed, 2 failed; both failures are pre-existing onmainand unrelated (see below)New tests:
tests/test_plugin_trust.py(37) — key encodings, invalid configuration, startup rejection, secret-free API/CLI.tests/test_plugin_signatures.py(24) — canonical payload, valid/forged/unsigned policy matrix, embedded-signature regression.tests/test_plugin_catalog_install.py(39) — catalog validation plus endpoint authorization, trust policy, 4xx paths, audit of refusals, no grants, no code execution.tests/test_plugin_lock_entries.py(7) — scoping, uniqueness, canonical lockfile bytes.Pre-existing failures (not from this PR)
The full suite reports two failures, both in
tests/test_notifications.py(
TestBudgetAlertDeduplication): they importapp.services.budget_alerts,which does not exist on
origin/maineither.So the failure reproduces without this branch and is unrelated to the plugin
trust/install/lock changes. It looks like leftover wiring from the budget-alert
dedup work (#471) and should be fixed in its own PR.
CI confirms that this is pre-existing: the
mainrun after #503(
gh run view 39044006397 --log-failed) reports the same two failures(
2 failed, 2246 passed) and the same PostgreSQL coverage gate(
total of 84 is less than fail-under=85). This PR's run(https://github.com/AyinkxLab/ai-code-assistant/actions/runs/39047731044)
reports
2 failed, 2353 passed— the only failures are those two, and everyplugin test (new and existing) is green.
Notes for reviewers
if-present; only absence is tolerated, which keeps local-path installs working under the default policy.PLUGIN_TRUSTED_KEYSused to make every signatureInvalidsilently; it now fails startup with a descriptive error. That is the point of Phase 9: Trusted publisher key management #386, but worth a release note.versionpicks the newest catalogued version by PEP 440 ordering; an explicitly requested version must exist.trust_state/trust_publisherfrom the verified signature.