Files
FabledSteward/steward/templates/settings/_catalog_partial.html
T
bvandeusenandClaude Opus 5.5 d188a673a6
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 44s
CI / unit (push) Successful in 1m5s
CI / publish (push) Successful in 58s
feat(plugins): installs fetch only what the catalog names, verified (#5032)
#5032 "Plugin install takes its URL and checksum from the catalog, requires
the checksum, and extracts only inside the plugin folder". The install form
carried the download URL and checksum as hidden fields, so any admin POST
could make Steward fetch and import code from anywhere, unchecked, and a zip
member like ../x.py was written wherever it pointed.

- The route takes only the plugin name and reads download_url and
  checksum_sha256 from the configured catalogs; a name no repository lists
  is refused. Installs are audited (plugin.install / plugin.install_failed).
- The checksum is required; the name must be a plain identifier.
- extract_plugin refuses any member that resolves outside the plugin
  folder, caps the download at 50 MB and the unpacked size at 200 MB.
- The zip unpacks into a staging folder and is swapped in whole: a refused
  or failed install leaves the installed copy untouched, and an update no
  longer leaves the old version's deleted files behind.
- The catalog shows "Can't install: the catalog lists no checksum" in place
  of the button for an entry without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:47:43 -04:00

115 lines
5.7 KiB
HTML

{# settings/_catalog_partial.html — HTMX partial for plugin catalog #}
<div id="plugin-catalog">
{% if error %}
<div style="color:var(--text-muted);font-size:0.85rem;padding:0.5rem 0.75rem;background:var(--bg);border:1px solid var(--border);border-radius:6px;margin-bottom:0.75rem;">
{{ error }}
</div>
{% endif %}
{% if catalog %}
<div style="display:grid;gap:0.75rem;">
{% for plugin in catalog %}
{% set is_installed = plugin.name in installed_names %}
{% set is_loaded = plugin.name in loaded_names %}
{% set installed_ver = installed_versions.get(plugin.name, "") %}
{# update_available: catalog version is strictly newer than what's on disk #}
{% set update_available = is_installed and installed_ver and installed_ver != plugin.version %}
<div class="card" style="margin-bottom:0;padding:1rem 1.25rem;{% if update_available %}border-color:var(--gold);{% endif %}">
<div style="display:flex;align-items:flex-start;gap:1rem;">
<div style="flex:1;min-width:0;">
<div style="display:flex;align-items:center;gap:0.6rem;flex-wrap:wrap;">
<span style="font-weight:600;color:var(--text);font-size:0.9rem;">{{ plugin.name }}</span>
<span style="font-size:0.75rem;color:var(--text-muted);">v{{ plugin.version }}</span>
{% if update_available %}
<span class="badge badge-gold" style="font-size:0.68rem;">
Update available — installed v{{ installed_ver }}
</span>
{% elif is_loaded %}
<span class="badge badge-green" style="font-size:0.68rem;">Active</span>
{% elif is_installed %}
<span class="badge" style="background:var(--bg-elevated);color:var(--text-muted);font-size:0.68rem;">Installed</span>
{% endif %}
{% for tag in plugin.tags %}
<span style="font-size:0.68rem;padding:0.1em 0.4em;background:var(--bg-elevated);color:var(--text-muted);border-radius:3px;">{{ tag }}</span>
{% endfor %}
</div>
{% if plugin.description %}
<div style="font-size:0.82rem;color:var(--text-muted);margin-top:0.25rem;">{{ plugin.description }}</div>
{% endif %}
<div style="font-size:0.75rem;color:var(--text-dim);margin-top:0.15rem;display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;">
{% if plugin.author %}<span>by {{ plugin.author }}</span>{% endif %}
{% if plugin.license %}<span style="color:var(--text-muted);">{{ plugin.license }}</span>{% endif %}
{% if plugin.min_app_version %}<span style="color:var(--text-muted);">requires v{{ plugin.min_app_version }}+</span>{% endif %}
</div>
</div>
<div style="display:flex;gap:0.5rem;flex-shrink:0;align-items:center;">
{% if plugin.homepage %}
<a href="{{ plugin.homepage }}" target="_blank" rel="noopener"
class="btn btn-ghost btn-sm" style="font-size:0.78rem;">Docs</a>
{% elif plugin.repository_url %}
<a href="{{ plugin.repository_url }}" target="_blank" rel="noopener"
class="btn btn-ghost btn-sm" style="font-size:0.78rem;">Source</a>
{% endif %}
{% if (update_available or not is_installed) and not plugin.checksum_sha256 %}
{# Steward installs only what it can verify against the catalog's checksum #}
<span style="font-size:0.75rem;color:var(--text-muted);max-width:14rem;text-align:right;"
title="Add checksum_sha256 to this plugin's entry in the repository's index.yaml">
Can't {{ "update" if update_available else "install" }}: the catalog lists no checksum
</span>
{% elif update_available %}
{# Installed but outdated — offer update (download + restart) #}
<form method="post"
action="/settings/plugins/install/{{ plugin.name }}"
hx-post="/settings/plugins/install/{{ plugin.name }}"
hx-target="#catalog-result-{{ plugin.name }}"
hx-swap="outerHTML"
style="margin:0;">{{ csrf_field() }}
<button type="submit" class="btn btn-warn btn-sm" style="font-size:0.78rem;">Update</button>
</form>
{% elif is_loaded %}
{# Active and up to date — no action needed #}
{% elif is_installed %}
{# Installed but not active — offer hot-reload #}
<form method="post"
action="/settings/plugins/reload/{{ plugin.name }}"
hx-post="/settings/plugins/reload/{{ plugin.name }}"
hx-target="#catalog-result-{{ plugin.name }}"
hx-swap="outerHTML"
style="margin:0;">{{ csrf_field() }}
<button type="submit" class="btn btn-sm" style="font-size:0.78rem;">Activate</button>
</form>
{% else %}
{# Not installed — offer install #}
<form method="post"
action="/settings/plugins/install/{{ plugin.name }}"
hx-post="/settings/plugins/install/{{ plugin.name }}"
hx-target="#catalog-result-{{ plugin.name }}"
hx-swap="outerHTML"
style="margin:0;">{{ csrf_field() }}
<button type="submit" class="btn btn-sm" style="font-size:0.78rem;">Install</button>
</form>
{% endif %}
</div>
</div>
{# Inline result placeholder — replaced by _install_result.html after action #}
<div id="catalog-result-{{ plugin.name }}"></div>
</div>
{% endfor %}
</div>
{% elif not error %}
<div style="color:var(--text-muted);font-size:0.85rem;">No plugins listed in the catalog.</div>
{% endif %}
</div>