Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c96ea82
fix: add missing timeline/artifacts stubs, fix recipe routing and err…
Emperiusm Apr 15, 2026
2d29942
fix: bridge API container to security hub tool containers
Emperiusm Apr 15, 2026
925dc7c
feat: add scan UI page with target type selector and file upload
Emperiusm Apr 15, 2026
43fc47d
feat: wire scan execution as background task + file upload endpoint
Emperiusm Apr 15, 2026
9ad6102
feat: complete scan pipeline - workspace volume, auth fix, editor fix
Emperiusm Apr 15, 2026
7f9679f
fix: serialize ReactiveEdge/RetryPolicy Pydantic models in scan task …
Emperiusm Apr 15, 2026
bd15cba
docs: add Phase 3E plugin marketplace design spec
Emperiusm Apr 15, 2026
3455c39
fix: register Docker executor for scan tasks + add error logging
Emperiusm Apr 15, 2026
aabec8b
feat: redesign scan UI with PrimeVue components
Emperiusm Apr 15, 2026
4c8a2c6
feat: auto-start/stop tool containers for scans
Emperiusm Apr 15, 2026
a12d29d
fix: wait for container readiness + auto-poll scan status in UI
Emperiusm Apr 15, 2026
04f2b13
fix: use 'tasks' not 't' in background scan closure
Emperiusm Apr 15, 2026
5ed3928
fix: run scan tools in parallel, remove auto-stop, fix parser import
Emperiusm Apr 15, 2026
b297900
fix: correct tool command flags for container versions
Emperiusm Apr 15, 2026
bb673f1
feat: wire full parsing pipeline with ParserRouter + generic_json fal…
Emperiusm Apr 15, 2026
d97c992
feat: JSONL + text parsers, live task status overlay
Emperiusm Apr 15, 2026
9c27829
fix: mount SecLists wordlists, copy into ffuf container before scan
Emperiusm Apr 15, 2026
b37d383
fix: run nikto via docker run (MCP server crash-loops), skip docker e…
Emperiusm Apr 15, 2026
55a0fd7
fix: nikto container stays alive (tail -f), revert docker run workaround
Emperiusm Apr 15, 2026
fdc2f60
fix: nikto output to temp file then cat (avoids /dev/stdout.json)
Emperiusm Apr 15, 2026
49574f6
fix: nikto maxtime 180s, executor timeout 600s
Emperiusm Apr 15, 2026
06bd62c
fix: remove nikto maxtime cap, let 10min executor timeout handle it
Emperiusm Apr 15, 2026
703b4ce
docs: add Phase 3E plugin marketplace implementation plan
Emperiusm Apr 15, 2026
b0f678b
feat: add rebuild button to global chain page, fix empty engagement_i…
Emperiusm Apr 15, 2026
1acdf71
feat(plugin-core): scaffold package with pyproject.toml and shared fi…
Emperiusm Apr 15, 2026
4bab940
feat(plugin-core): add PluginError hierarchy with hint system
Emperiusm Apr 15, 2026
0f3281a
feat(plugin-core): add Pydantic v2 manifest, catalog, and registry mo…
Emperiusm Apr 15, 2026
c4a63b8
feat(plugin-core): add SQLite plugin index with integrity tracking
Emperiusm Apr 15, 2026
f99ad64
feat(plugin-core): add content-addressable plugin cache
Emperiusm Apr 15, 2026
0eafd94
feat(plugin-core): add sandbox policy with mount blocklist and org ov…
Emperiusm Apr 15, 2026
6875bfb
feat(plugin-core): add recipe command enforcement with shlex parsing
Emperiusm Apr 15, 2026
8aa2dbd
feat(plugin-core): add skill content advisory scanner
Emperiusm Apr 15, 2026
f1d0666
feat(plugin-core): add sigstore and SHA256 verification
Emperiusm Apr 15, 2026
0469abb
feat(plugin-core): add per-plugin compose generator with sandbox inje…
Emperiusm Apr 15, 2026
987341d
feat(plugin-core): add registry client with catalog caching and search
Emperiusm Apr 15, 2026
ca58121
feat(plugin-core): add dependency resolver with cycle and conflict de…
Emperiusm Apr 15, 2026
8958d41
feat(plugin-core): add transactional installer with staging and atomi…
Emperiusm Apr 15, 2026
1556c90
feat(plugin-core): add updater with rollback and version pruning
Emperiusm Apr 15, 2026
1686ce3
feat(cli): add opentools plugin subcommand with 22 marketplace commands
Emperiusm Apr 15, 2026
04340a9
feat(cli): add skill/recipe search paths for marketplace plugin integ…
Emperiusm Apr 15, 2026
bff85dd
feat(cli): add plugin container status integration
Emperiusm Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,614 changes: 4,614 additions & 0 deletions docs/superpowers/plans/2026-04-15-phase3e-plugin-marketplace.md

Large diffs are not rendered by default.

890 changes: 890 additions & 0 deletions docs/superpowers/specs/2026-04-15-phase3e-plugin-marketplace-design.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ dependencies = [
"orjson>=3.10.0",
"sqlalchemy>=2.0.30",
"aiosqlite>=0.21",
"opentools-plugin-core>=0.1.0",
"filelock>=3.16",
]

[project.optional-dependencies]
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/opentools/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

from opentools.chain.cli import app as chain_app # noqa: E402
from opentools.scanner.scan_cli import app as scan_app # noqa: E402
from opentools.plugin_cli import plugin_app # noqa: E402

app.add_typer(engagement_app)
app.add_typer(findings_app)
Expand All @@ -47,6 +48,7 @@
app.add_typer(config_app)
app.add_typer(chain_app)
app.add_typer(scan_app)
app.add_typer(plugin_app)


# ---------------------------------------------------------------------------
Expand Down
46 changes: 46 additions & 0 deletions packages/cli/src/opentools/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,49 @@ def _get_profiles(self, container_name: str) -> list[str]:
if tool:
return tool.profiles
return []


def get_plugin_container_statuses() -> list[ContainerStatus]:
"""Get status of all plugin containers across installed plugins."""
from opentools.plugin import _marketplace_plugin_dirs

statuses: list[ContainerStatus] = []
for version_dir in _marketplace_plugin_dirs():
compose_dir = version_dir / "compose"
if not compose_dir.is_dir():
continue

compose_file = compose_dir / "docker-compose.yaml"
if not compose_file.exists():
compose_file = compose_dir / "docker-compose.yml"
if not compose_file.exists():
continue

try:
result = subprocess.run(
["docker", "compose", "-f", str(compose_file), "ps", "--format", "json"],
capture_output=True, timeout=10,
cwd=str(compose_dir),
)
if result.returncode != 0:
continue

stdout = result.stdout.decode(errors="replace").strip()
for line in stdout.splitlines():
line = line.strip()
if not line:
continue
try:
data = json.loads(line)
statuses.append(ContainerStatus(
name=data.get("Name", data.get("Service", "")),
state=data.get("State", "unknown"),
health=data.get("Health"),
profile=["plugin"],
))
except json.JSONDecodeError:
continue
except (subprocess.TimeoutExpired, FileNotFoundError):
continue

return statuses
63 changes: 63 additions & 0 deletions packages/cli/src/opentools/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,66 @@ def discover_plugin_dir(cli_package_root: Path | None = None) -> Path:
raise FileNotFoundError(
"Plugin directory not found. Set OPENTOOLS_PLUGIN_DIR or run from the OpenTools repo."
)


def _marketplace_plugin_dirs() -> list[Path]:
"""Scan ~/.opentools/plugins/ for active plugin version directories."""
marketplace = Path.home() / ".opentools" / "plugins"
if not marketplace.is_dir():
return []

dirs: list[Path] = []
for plugin_dir in marketplace.iterdir():
if not plugin_dir.is_dir():
continue
active_file = plugin_dir / ".active"
if active_file.exists():
version = active_file.read_text(encoding="utf-8").strip()
version_dir = plugin_dir / version
if version_dir.is_dir():
dirs.append(version_dir)
return dirs


def skill_search_paths() -> list[Path]:
"""Return search paths for skills: built-in + marketplace."""
paths: list[Path] = []

try:
plugin_dir = discover_plugin_dir()
paths.append(plugin_dir / "skills")
except FileNotFoundError:
pass

for version_dir in _marketplace_plugin_dirs():
skills_dir = version_dir / "skills"
if skills_dir.is_dir():
paths.append(skills_dir)

marketplace = Path.home() / ".opentools" / "plugins"
if marketplace.is_dir():
paths.append(marketplace)

return paths


def recipe_search_paths() -> list[Path]:
"""Return search paths for recipes: built-in + marketplace."""
paths: list[Path] = []

try:
plugin_dir = discover_plugin_dir()
paths.append(plugin_dir)
except FileNotFoundError:
pass

for version_dir in _marketplace_plugin_dirs():
recipes_dir = version_dir / "recipes"
if recipes_dir.is_dir():
paths.append(recipes_dir)

marketplace = Path.home() / ".opentools" / "plugins"
if marketplace.is_dir():
paths.append(marketplace)

return paths
Loading
Loading