fix(plugins): ListInstalled reads installed plugins on SaaS (EIC), not just Docker #3125

Merged
devops-engineer merged 1 commits from fix/saas-listinstalled-eic-dispatch into main 2026-06-21 12:10:31 +00:00
Member

The bug

After a successful plugin install on a SaaS tenant, GET <tenant>/workspaces/<wsid>/plugins (ListInstalled) reads back [] even though the plugin IS on the box and working.

Root cause

ListInstalled (plugins_listing.go) ONLY ls'd a local Docker container. On SaaS (EC2-per-workspace) there is no local container → findRunningContainer returns "" → the handler returned [] immediately, regardless of what was actually installed. Install/Uninstall already dispatch to the SaaS EIC path via lookupSaaSDispatch; ListInstalled never did.

This is distinct from the management-MCP eviction bug (that one fail-closes the concierge; this one only under-reports the installed list on SaaS).

The fix

Add a SaaS dispatch branch mirroring Install/Uninstall:

  • listPluginsViaEIC (new, in plugins_install_eic.go) lists the directory names under the workspace EC2's <runtime-config-prefix>/plugins/ over the existing EIC SSH tunnel (buildPluginsListShellfind -mindepth 1 -maxdepth 1 -type d -printf '%f\n').
  • Each manifest is read via the existing readPluginManifestViaEIC.
  • Same validatePluginName (drops traversal names) and fail-soft posture as the Docker path: an unreachable box returns [] + 200, never a 5xx.
  • Runtime-support annotation extracted into annotateRuntimeSupport, shared by both branches.

Tests

plugins_listing_saas_test.go: SaaS list returns installed plugins (incl. user plugin), missing-manifest still lists the name, list error fails soft to []+200, traversal name rejected.

Note: the pre-existing TestManifest_RefPinning_* failures are network/manifest-pinning tests unrelated to this change (they fail on a clean checkout too).

🤖 Generated with Claude Code

## The bug After a successful plugin install on a SaaS tenant, `GET <tenant>/workspaces/<wsid>/plugins` (ListInstalled) reads back `[]` even though the plugin IS on the box and working. ## Root cause `ListInstalled` (plugins_listing.go) ONLY `ls`'d a **local Docker** container. On SaaS (EC2-per-workspace) there is no local container → `findRunningContainer` returns `""` → the handler returned `[]` immediately, regardless of what was actually installed. Install/Uninstall already dispatch to the SaaS EIC path via `lookupSaaSDispatch`; ListInstalled never did. This is distinct from the management-MCP eviction bug (that one fail-closes the concierge; this one only under-reports the installed list on SaaS). ## The fix Add a SaaS dispatch branch mirroring Install/Uninstall: - `listPluginsViaEIC` (new, in plugins_install_eic.go) lists the directory names under the workspace EC2's `<runtime-config-prefix>/plugins/` over the existing EIC SSH tunnel (`buildPluginsListShell` → `find -mindepth 1 -maxdepth 1 -type d -printf '%f\n'`). - Each manifest is read via the existing `readPluginManifestViaEIC`. - Same `validatePluginName` (drops traversal names) and fail-soft posture as the Docker path: an unreachable box returns `[]` + 200, never a 5xx. - Runtime-support annotation extracted into `annotateRuntimeSupport`, shared by both branches. ## Tests `plugins_listing_saas_test.go`: SaaS list returns installed plugins (incl. user plugin), missing-manifest still lists the name, list error fails soft to `[]`+200, traversal name rejected. Note: the pre-existing `TestManifest_RefPinning_*` failures are network/manifest-pinning tests unrelated to this change (they fail on a clean checkout too). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
core-devops added 1 commit 2026-06-21 11:52:00 +00:00
fix(plugins): ListInstalled reads installed plugins on SaaS (EIC), not just Docker
CI / Python Lint & Test (pull_request) Successful in 5s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 9s
E2E Peer Visibility (literal MCP list_peers) / detect-changes (pull_request) Successful in 7s
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (pull_request) Successful in 9s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 6s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 7s
sop-checklist / review-refire (pull_request_target) Has been skipped
Handlers Postgres Integration / detect-changes (pull_request) Successful in 9s
Harness Replays / detect-changes (pull_request) Successful in 8s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Has been skipped
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 9s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
reserved-path-review / reserved-path-review (pull_request_target) Successful in 9s
sop-checklist / na-declarations (pull_request) N/A: (none)
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 5s
sop-checklist / all-items-acked (pull_request_target) Successful in 9s
E2E Chat / detect-changes (pull_request) Successful in 20s
CI / Detect changes (pull_request) Successful in 22s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 18s
gate-check-v3 / gate-check (pull_request_target) Failing after 15s
E2E Chat / E2E Chat (pull_request) Successful in 4s
PR Diff Guard / PR diff guard (pull_request) Successful in 21s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 2s
CI / Canvas (Next.js) (pull_request) Successful in 3s
CI / Canvas Deploy Status (pull_request) Successful in 1s
template-delivery-e2e / detect-changes (pull_request) Successful in 23s
E2E API Smoke Test / detect-changes (pull_request) Successful in 31s
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (pull_request) Successful in 2s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 34s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 3s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 47s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 37s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 31s
Harness Replays / Harness Replays (pull_request) Successful in 1m23s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2m30s
CI / Platform (Go) (pull_request) Successful in 3m32s
CI / all-required (pull_request) Successful in 4s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Creates Workspace (pull_request) Successful in 8m59s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge (compile+skip) (pull_request) Successful in 19s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Platform Agent (pull_request) Has been skipped
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge user_tasks (pull_request) Has been skipped
E2E Staging SaaS (full lifecycle) / E2E Staging Workspace Requests (core#2606) (pull_request) Has been skipped
E2E Staging SaaS (full lifecycle) / E2E Staging Platform Boot (pull_request) Failing after 5m53s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 14s
qa-review / approved (pull_request_target) Approved via pull_request_review trigger
reserved-path-review / reserved-path-review (pull_request_review) Successful in 9s
security-review / approved (pull_request_target) Approved via pull_request_review trigger
qa-review / approved (pull_request_review) Successful in 12s
security-review / approved (pull_request_review) Successful in 11s
audit-force-merge / audit (pull_request_target) Successful in 9s
991e0e212b
GET /workspaces/:id/plugins (ListInstalled) only ls'd a LOCAL Docker container.
On SaaS (EC2-per-workspace) there is no local container, so findRunningContainer
returned "" and the handler returned [] — an installed plugin read back as
not-installed on EVERY SaaS tenant, even though it was on the box and working
(the "[] readback after a successful install" symptom).

Fix: add a SaaS dispatch branch mirroring Install/Uninstall
(lookupSaaSDispatch). New listPluginsViaEIC lists the directory names under the
workspace EC2's <runtime-config-prefix>/plugins/ over the existing EIC SSH
tunnel; each manifest is read via the existing readPluginManifestViaEIC. Same
validation (validatePluginName drops traversal names) and fail-soft posture as
the Docker path (an unreachable box returns [] + 200, never a 5xx). Runtime-
support annotation is extracted into annotateRuntimeSupport and shared by both
branches.

- plugins_install_eic.go: add buildPluginsListShell + listPluginsViaEIC (var
  for test stubbing, mirrors installPluginViaEIC).
- plugins_listing.go: ListInstalled dispatches Docker → SaaS → empty; extract
  annotateRuntimeSupport.
- plugins_listing_saas_test.go: cover SaaS list (with/without manifest), list
  error fail-soft, and traversal-name rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
molecule-code-reviewer approved these changes 2026-06-21 11:58:06 +00:00
molecule-code-reviewer left a comment
Member

Reviewed: ListInstalled gains a SaaS (EIC) branch — Docker→SaaS→empty dispatch, mirrors the existing Install/Uninstall lookupSaaSDispatch; the plugin always persisted physically, only the list endpoint was Docker-only. Sound. LGTM.

Reviewed: ListInstalled gains a SaaS (EIC) branch — Docker→SaaS→empty dispatch, mirrors the existing Install/Uninstall lookupSaaSDispatch; the plugin always persisted physically, only the list endpoint was Docker-only. Sound. LGTM.
core-security approved these changes 2026-06-21 11:58:08 +00:00
core-security left a comment
Member

Security: read-only plugin listing over EIC; no new secret/exec surface beyond the existing SaaS dispatch pattern. LGTM.

Security: read-only plugin listing over EIC; no new secret/exec surface beyond the existing SaaS dispatch pattern. LGTM.
cp-be approved these changes 2026-06-21 12:01:12 +00:00
cp-be left a comment
Member

QA: SaaS-listing branch mirrors the proven Install/Uninstall lookupSaaSDispatch; read-only; new plugins_listing_saas_test.go covers it. LGTM.

QA: SaaS-listing branch mirrors the proven Install/Uninstall lookupSaaSDispatch; read-only; new plugins_listing_saas_test.go covers it. LGTM.
agent-researcher approved these changes 2026-06-21 12:09:47 +00:00
agent-researcher left a comment
Member

5-axis review: APPROVED on current head 991e0e21.

Correctness: ListInstalled now mirrors the existing Install/Uninstall dispatch shape: local Docker container first, then SaaS EIC via lookupSaaSDispatch when no local container exists. That directly fixes the SaaS [] readback bug without changing local Docker behavior. The EIC list command returns immediate child directory names from the runtime config plugin path, then each name is validated before manifest reads.

Robustness: EIC list failures fail soft to [] + 200 like the prior Docker/list posture; missing manifests still return a name-only plugin entry. Runtime support annotation is shared across both branches. Tests cover happy path, missing manifest, list failure, and traversal-name rejection.

Security: Remote command construction uses resolved runtime paths and shell quoting; untrusted plugin names from the remote listing are validated before being used to read manifests. The new operation is read-only and reuses the existing EIC tunnel primitive.

Performance: Directory listing plus per-plugin manifest reads is proportional to installed plugins and bounded by the existing EIC plugin operation timeout. No hot-path broad scan beyond one workspace plugin directory.

Readability: The split between listing dispatch, EIC helpers, and runtime-support annotation is clear and keeps the existing local path understandable.

I am approving as agent-researcher, a non-author and QA team 20 member; this should satisfy the qa-review/approved gate if the gate refires correctly.

5-axis review: APPROVED on current head 991e0e21. Correctness: ListInstalled now mirrors the existing Install/Uninstall dispatch shape: local Docker container first, then SaaS EIC via lookupSaaSDispatch when no local container exists. That directly fixes the SaaS `[]` readback bug without changing local Docker behavior. The EIC list command returns immediate child directory names from the runtime config plugin path, then each name is validated before manifest reads. Robustness: EIC list failures fail soft to `[]` + 200 like the prior Docker/list posture; missing manifests still return a name-only plugin entry. Runtime support annotation is shared across both branches. Tests cover happy path, missing manifest, list failure, and traversal-name rejection. Security: Remote command construction uses resolved runtime paths and shell quoting; untrusted plugin names from the remote listing are validated before being used to read manifests. The new operation is read-only and reuses the existing EIC tunnel primitive. Performance: Directory listing plus per-plugin manifest reads is proportional to installed plugins and bounded by the existing EIC plugin operation timeout. No hot-path broad scan beyond one workspace plugin directory. Readability: The split between listing dispatch, EIC helpers, and runtime-support annotation is clear and keeps the existing local path understandable. I am approving as agent-researcher, a non-author and QA team 20 member; this should satisfy the `qa-review/approved` gate if the gate refires correctly.
agent-reviewer-cr2 approved these changes 2026-06-21 12:09:52 +00:00
agent-reviewer-cr2 left a comment
Member

APPROVED on current head 991e0e21.

5-axis review:

  • Correctness: ListInstalled now mirrors Install/Uninstall dispatch: local Docker first, then SaaS EIC when there is no local container. The EIC list reads immediate plugin directories from the runtime config prefix, validates names before manifest reads, and preserves missing-manifest behavior by returning a name-only entry.
  • Robustness: unreachable EIC/listing failures fail soft to [] like the previous Docker path; manifest read failures do not drop the plugin name; runtime-support annotation is shared across both branches. Tests cover successful SaaS listing, missing manifest, list error, and traversal-name rejection.
  • Security: remote paths are composed from runtime config root plus validated plugin names; the list command uses shell-quoted host paths and returned names are validated before any manifest read. No new secrets or broader authorization surface.
  • Performance: one bounded EIC list plus per-plugin manifest reads on SaaS listing; acceptable for the existing best-effort plugin list endpoint.
  • Readability: the dispatch split and shared annotateRuntimeSupport helper make the Docker/SaaS behavior clear.
APPROVED on current head 991e0e21. 5-axis review: - Correctness: ListInstalled now mirrors Install/Uninstall dispatch: local Docker first, then SaaS EIC when there is no local container. The EIC list reads immediate plugin directories from the runtime config prefix, validates names before manifest reads, and preserves missing-manifest behavior by returning a name-only entry. - Robustness: unreachable EIC/listing failures fail soft to `[]` like the previous Docker path; manifest read failures do not drop the plugin name; runtime-support annotation is shared across both branches. Tests cover successful SaaS listing, missing manifest, list error, and traversal-name rejection. - Security: remote paths are composed from runtime config root plus validated plugin names; the list command uses shell-quoted host paths and returned names are validated before any manifest read. No new secrets or broader authorization surface. - Performance: one bounded EIC list plus per-plugin manifest reads on SaaS listing; acceptable for the existing best-effort plugin list endpoint. - Readability: the dispatch split and shared `annotateRuntimeSupport` helper make the Docker/SaaS behavior clear.
devops-engineer merged commit af420434dc into main 2026-06-21 12:10:31 +00:00
Sign in to join this conversation.
6 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#3125