Skip to content

feat(plugins): publisher trust, signed catalog installs, and lock entries - #504

Merged
Ayinkx merged 1 commit into
AyinkxLab:mainfrom
olatechy0:feat/phase-9-publisher-trust-install-lock
Oct 17, 2026
Merged

Ayinkx merged 1 commit into
AyinkxLab:mainfrom
olatechy0:feat/phase-9-publisher-trust-install-lock

Conversation

@olatechy0

@olatechy0 olatechy0 commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

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 PluginLockEntry model with canonical lockfile serialization.

Closes #385
Closes #386
Closes #389
Closes #394

Changes

#386 — trusted publisher keys (app/services/plugin_trust.py)

  • one strict loader validates PLUGIN_TRUSTED_KEYS: canonical lowercase publisher ids plus real 32-byte Ed25519 keys (base64 raw, PEM SPKI, or raw bytes).
  • invalid key material or an unknown PLUGIN_TRUST_POLICY now aborts startup with a clear error (create_app → validate_trust_config) instead of silently treating signatures as invalid.
  • secret-free read-only surfaces: flask plugins publishers [--json] and GET /plugins/api/trusted-publishers return publisher ids + policy only (no key bytes or fingerprints).

#385 — detached publisher signatures (app/services/plugin_signatures.py)

  • Ed25519 verification over the canonical manifest body (compact, key-sorted JSON excluding signature / trust_state / trust_publisher); the embedded and detached forms share one verifier.
  • policy: required rejects unsigned entries; forged, malformed, or unknown-publisher signatures are refused under both policies (absence is not forgery).
  • app/services/plugins.py now 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-catalog is owner-only (manage_plugins) and accepts only {plugin_id, version?} — never a client manifest, artifact URL, or key.
  • trust refusals are audited (plugin.denied with a static reason, no artifact bytes); success registers metadata only, downloads nothing, executes no code, and creates no capability grant. The response reports the resolved source and integrity.

#394 — plugin lock entries (PluginLockEntry)

  • owner/workspace-scoped rows, unique per (workspace_id, plugin_id), storing plugin id, version, source, sha256 integrity, dependencies, and resolved_at.
  • rows serialize through the existing canonical dumps_lockfile (sorted, normalized, no volatile timestamps); no capability grants are implied.

Docs: docs/plugins.md, docs/security.md, .env.example updated.

Scope notes:

Validation

  • New change suite: 107 passed (tests/test_plugin_trust.py, tests/test_plugin_signatures.py, tests/test_plugin_catalog_install.py, tests/test_plugin_lock_entries.py)
  • Existing plugin suites: 124 passed (test_plugins_manifest.py, test_plugin_lockfile.py, test_plugins_api.py, test_plugins_cli.py), including the embedded-signature regression tests
  • ruff check . — All checks passed!
  • black --check . — 270 files would be left unchanged.
  • Full pytest run complete — 2353 passed, 2 failed; both failures are pre-existing on main and unrelated (see below)
  • Docs updated if user-visible

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 import app.services.budget_alerts,
which does not exist on origin/main either.

git ls-tree origin/main app/services/budget_alerts.py   # no output
git show origin/main:tests/test_notifications.py | grep budget_alerts
# from app.services.budget_alerts import check_budget_alerts

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 main run 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 every
plugin test (new and existing) is green.

Notes for reviewers

  • Security behaviour: a forged signature is refused even under if-present; only absence is tolerated, which keeps local-path installs working under the default policy.
  • Operator-visible change: a malformed PLUGIN_TRUSTED_KEYS used to make every signature Invalid silently; 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.
  • Identity binding: the catalog route reuses the local install helper, so entry-point binding, duplicate handling, and the "no implicit grant" invariant cannot diverge between the two install paths.
  • Version selection: omitting version picks the newest catalogued version by PEP 440 ordering; an explicitly requested version must exist.
  • Key rotation: installing an existing plugin refreshes the persisted trust_state / trust_publisher from the verified signature.

…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.
@drips-wave

drips-wave Bot commented Oct 17, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

@Ayinkx
Ayinkx merged commit 78becf4 into AyinkxLab:main Oct 17, 2026
4 of 6 checks passed
Felamsy pushed a commit to Felamsy/ai-code-assistant that referenced this pull request Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants