Skip to content

feat(plugins): B3 safe_uninstall — 3-step wizard, orphan table preservation, sandboxed pre-hook#54

Merged
DavidsonGomes merged 1 commit into
developfrom
host/b3-safe-uninstall
Apr 25, 2026
Merged

feat(plugins): B3 safe_uninstall — 3-step wizard, orphan table preservation, sandboxed pre-hook#54
DavidsonGomes merged 1 commit into
developfrom
host/b3-safe-uninstall

Conversation

@DavidsonGomes
Copy link
Copy Markdown
Member

Summary

  • plugin_schema.pyPluginSafeUninstall, PluginPreUninstallHook, PluginUserConfirmation Pydantic models; 4 validators (block_uninstall gate, preserved_tables slug-prefix, enabled-requires-confirmation, no _orphan_* refs in readonly SQL)
  • routes/plugins.py — full B3 enforcement: admin-only gate, confirmation_phrase check, exported_at file existence, zip_password forwarding; sandboxed pre-hook subprocess (no secrets, read-only DB copy, 600 s timeout, must_produce_file check); cascade-DELETE filtering for preserved_host_entities; orphan table rename (_orphan_{slug}_{table}); EVONEXUS_ALLOW_FORCE_UNINSTALL=1 escape hatch with mandatory audit; reinstall SHA256 check against plugin_orphans before recovery
  • app.pyplugin_orphans migration (id, slug, tablename, orphaned_at, user, original_version, original_sha256, original_publisher_url, recovered_at)
  • PluginUninstall.tsx (new) — 3-step React wizard: regulatory reason + checkbox → ZIP password → typed phrase; force-uninstall orange banner
  • PluginDetail.tsx — wizard integration; showUninstallWizard state; falls back to legacy window.confirm() for plugins without safe_uninstall.enabled
  • docs/plugin-contract.md (new) — full plugin.yaml contract spec for public_pages + safe_uninstall capabilities

Vault mitigations covered

Code Control
B3.S1 block_uninstall: true gate (409 Conflict)
B3.S2 Admin-only enforcement (403 for non-admin)
B3.S3 Sandboxed pre-hook — no BRAIN_REPO_MASTER_KEY, read-only DB copy
B3.S4 Schema validator blocks _orphan_* table refs in readonly SQL
B3.S5 ZIP password required + forwarded to hook for AES-256 export
B3.S6 Force-uninstall audit trail (plugin_uninstall_force audit action)

Test plan

  • Plugin with block_uninstall: trueDELETE /api/plugins/<slug> returns 409
  • Plugin with safe_uninstall.enabled: true — non-admin returns 403
  • Admin without confirmation_phrase → 400
  • Admin with wrong phrase → 400
  • Admin with exported_at path to non-existent file → 400
  • Pre-hook that exits non-zero → uninstall aborted, 400 with stderr
  • Pre-hook with must_produce_file: true but empty output_dir → 400
  • Successful uninstall: preserved tables renamed to _orphan_{slug}_{table}, plugin_orphans row inserted
  • Reinstall with matching SHA256 → orphans recovered, install proceeds
  • Reinstall with mismatched SHA256 → 409 blocked unless confirmed_sha256_change: true
  • EVONEXUS_ALLOW_FORCE_UNINSTALL=1 → bypasses all gates, logs plugin_uninstall_force
  • React wizard: Step 1 checkbox gates Step 2; Step 2 password mismatch (<8 chars) gates Step 3; wrong typed phrase disables button
  • Plugin without safe_uninstall.enabled → fallback to window.confirm()

🤖 Generated with Claude Code

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @DavidsonGomes, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@DavidsonGomes DavidsonGomes changed the base branch from host/b2-public-pages-readonly to develop April 25, 2026 18:31
@DavidsonGomes DavidsonGomes force-pushed the host/b3-safe-uninstall branch from 32af339 to dc679fc Compare April 25, 2026 18:35
…vation, sandboxed hook

- plugin_schema.py: PluginSafeUninstall, PluginPreUninstallHook, PluginUserConfirmation
  models + validators (block_uninstall enforcement, preserved_tables slug-prefix check,
  safe_uninstall_enabled_requires_confirmation, no _orphan_* refs in readonly SQL)
- routes/plugins.py: uninstall gate (admin-only, confirmation_phrase, exported_at check,
  zip_password); sandboxed pre-hook subprocess (no secrets, read-only DB copy, 600s timeout);
  cascade-DELETE filtering for preserved_host_entities; orphan table rename
  (_orphan_{slug}_{table}); EVONEXUS_ALLOW_FORCE_UNINSTALL=1 escape hatch with audit;
  reinstall SHA256 check against plugin_orphans before orphan recovery
- app.py: plugin_orphans table migration (id, slug, tablename, orphaned_at,
  orphaned_by_user_id, original_plugin_version, original_sha256, original_publisher_url,
  recovered_at, UNIQUE(slug, tablename))
- PluginUninstall.tsx: 3-step wizard (regulatory reason+checkbox → ZIP password → typed
  phrase); force-uninstall orange banner; integrated in PluginDetail.tsx
- docs/plugin-contract.md: full plugin.yaml contract for public_pages + safe_uninstall

Vault §B3 mitigations: S1 block_uninstall gate, S2 admin enforcement, S3 sandboxed hook,
S4 no _orphan_* SQL refs, S5 AES-256 ZIP password, S6 force-uninstall audit trail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@DavidsonGomes DavidsonGomes force-pushed the host/b3-safe-uninstall branch from dc679fc to ad7cbec Compare April 25, 2026 18:40
@DavidsonGomes DavidsonGomes merged commit d49f474 into develop Apr 25, 2026
4 checks passed
@DavidsonGomes DavidsonGomes deleted the host/b3-safe-uninstall branch April 25, 2026 18:41
jbmendonca pushed a commit to jbmendonca/evo-nexus that referenced this pull request May 15, 2026
…vation, sandboxed hook (evolution-foundation#54)

- plugin_schema.py: PluginSafeUninstall, PluginPreUninstallHook, PluginUserConfirmation
  models + validators (block_uninstall enforcement, preserved_tables slug-prefix check,
  safe_uninstall_enabled_requires_confirmation, no _orphan_* refs in readonly SQL)
- routes/plugins.py: uninstall gate (admin-only, confirmation_phrase, exported_at check,
  zip_password); sandboxed pre-hook subprocess (no secrets, read-only DB copy, 600s timeout);
  cascade-DELETE filtering for preserved_host_entities; orphan table rename
  (_orphan_{slug}_{table}); EVONEXUS_ALLOW_FORCE_UNINSTALL=1 escape hatch with audit;
  reinstall SHA256 check against plugin_orphans before orphan recovery
- app.py: plugin_orphans table migration (id, slug, tablename, orphaned_at,
  orphaned_by_user_id, original_plugin_version, original_sha256, original_publisher_url,
  recovered_at, UNIQUE(slug, tablename))
- PluginUninstall.tsx: 3-step wizard (regulatory reason+checkbox → ZIP password → typed
  phrase); force-uninstall orange banner; integrated in PluginDetail.tsx
- docs/plugin-contract.md: full plugin.yaml contract for public_pages + safe_uninstall

Vault §B3 mitigations: S1 block_uninstall gate, S2 admin enforcement, S3 sandboxed hook,
S4 no _orphan_* SQL refs, S5 AES-256 ZIP password, S6 force-uninstall audit trail.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant