The admission gate ran the install scanner (dangerous fails) but nothing refused
a compiled binary, committed bytecode, or an archive hiding either — the shapes
the scanner cannot read. This adds that enforcement to pinned-source-validate.
- scripts/scan_plugin_payloads.py (stdlib-only, so the no-install validator and
third-party CI can run it): flags compiled binaries by magic header (ELF,
Mach-O, PE/MZ, WebAssembly — not by extension, which a rename defeats),
.pyc/.pyo bytecode (incl. sourceless/hash-forged caches that run instead of
the .py beside them), and archives whose members are not reviewable source.
Plain-text source bundles and inert assets (fonts, images) pass.
- plugin-catalog-ci.yml: after `hermes plugins validate`, each changed entry is
scanned; a finding fails it. An entry may list `attested_binaries:` paths,
each verified with `gh attestation verify --repo <repo> --source-digest <sha>`
against the pinned commit; verified paths are dropped before the scan, any
other binary still fails. Repo-controlled filenames are newline-stripped in
the log so none can forge a workflow command.
- validate_plugin_catalog.py: `attested_binaries` is a known key, shape-checked
as a list of relative paths.
- Tests: unit tests for the detector and two new workflow-driven bypass cases
(bundled .exe, committed .pyc) in the confinement harness, which runs the real
`run:` block.
Full-content re-sweep of all 543 pins: only tabro carries a native binary (it is
delisted in the companion PR). Three entries (crawl4ai, hermes-vision-inline,
mermail) commit __pycache__/*.pyc build litter and would fail rule 17 on their
next pin bump — pre-existing, not touched here.
`activate` doubled as the thing scripts sourced non-interactively, so it
carried `--` argument guards and every caller had to tolerate its prompt and
`hermes` function. Composition (setup sync, bootstrap Python, pm env) now
lives in scripts/_activation.sh, and there are two consumers:
- `activate`: interactive, reversible (`deactivate`).
- new `scripts/run-in-hermes-env CMD...`: applies the env to one process and
execs into it. Replaces `scripts/_hermes-python`; every Python script's
shebang and `scripts/run_tests.sh` (which re-executes itself under it,
however it was launched) use it.
`pm.environments --format sh` emits the env as shell exports, so the inline
JSON-to-export Python is gone from `activate`. An install that did not build
the test environment now reads as stale for every consumer.
Tests are split along the same seams: activation support fixtures, bash/ps1
activation, env-script quoting, and the runner's staleness.
- Run the data-only guard once, inside the changed-entries step of the
pinned-source job, and only when an entry YAML changed. README, policy,
docs and CI PRs that touch plugin-catalog/ without an entry no longer go
red.
- Allow contributors/emails/** beside plugin-catalog/**: the attribution
fix we push onto catalog PR branches is data the gate never executes
(29 of the last 134 merged catalog PRs carried one).
- Diff GitHub's merge commit against its first parent with fetch-depth: 2
instead of a full-history clone plus merge-base. The structural job,
which only validates the whole directory, goes back to a shallow
checkout with no diff at all.
- `subdir:` with no value means the repo root in both the parse step and
the structural validator, matching how the runtime loader has always
read it.
The pinned-source job joined a PR-controlled subdir onto the clone root
with no validation, accepted file:// repos and ref names as the "pin",
and followed committed symlinks out of the clone, so the gate could
validate attacker-planted files instead of the declared commit's
contents. Harden the step end to end:
- validate repo (https://, whitespace-free), sha (40-hex, commit object),
and subdir (plain relative path) in the parser before any use
- run the parser under python3 -I against installed ruamel.yaml so the
PR checkout cannot shadow its imports, capture parser output, and
reset shell variables per entry so a dead parser cannot replay the
previous entry's fields
- confine PLUGIN_DIR and every symlink beneath it to the clone root
- include renamed and typechanged entry files in the changed set
- feed entries over fd 3 and keep plugin-executed commands off stdin so
validate cannot drain the remaining list
- NUL-delimit both self-updater grep stages and sanitize the echoed
filename list so crafted names cannot forge workflow commands
- mirror the same repo/subdir rules in the runtime catalog loader and
the site extractor, and use \Z anchors so a trailing newline cannot
split the verdict between gates
Relaxes the fail-closed install gate from #124037 per teknium1 review
2026-09-27 (known_issues informational; guard belongs at mode-selection seam):
- dashboard_install_plugin: no longer refuses known_issues entries — the text
flows into the result warnings plus a machine-readable "known_issues" key so
the UI can show it; the memory-provider migration paths (migrate_all_homes /
recover_at_startup via _install_into) install hindsight again in any mode.
- cmd_install: prints the yellow "Known issue:" lines but drops the TTY
requirement and the y/N confirmation; non-interactive installs proceed.
- entry_capability_summary: includes "Known issues: ..." so install prompts and
the catalog UI surface the text.
- scripts/validate_plugin_catalog.py: register known_issues in KNOWN_KEYS.
- tests: keep parse round-trip + install-summary display; drop the live-catalog
prose pin (test_live_catalog_hindsight_declares_known_issues) and the
zoneinfo import hack.
- contributors: map 5318happy@users.noreply.github.com -> happy5318.
./scripts/<name>.py now sources activate when the inherited environment is
missing or stale (scripts/_hermes-python) and runs under the pinned
interpreter, so these work from a bare shell and any cwd. The fast path from
an activated shell costs ~5ms over bare python.
These all import repo or locked third-party code. CI invokes them as
`python3 scripts/<name>.py` under setup-pm's environment, where the shebang
is inert, so no require_activation() guard: that would fail those lanes,
which run without __HERMES_ACTIVATED.
Left out on purpose: keystroke_diagnostic.py (run by users inside their own
install, where activation would provision their home) and
docker_config_migrate.py (runs in the container through its venv python).
The onboarding card needs to list catalog plugins beside the hosted
connectors (NS-960 D1, D4) and grey a plugin whose app is absent (D5).
The catalog had no curated flag, and the manage_catalog row left
app_state empty.
- `onboarding: true` and `title` on catalog entries (loader, validator,
docs); set on blender, nvidia-app and nvidia-broadcast.
- hermes_cli/plugin_catalog_presence.py reads the plugin.json at the
catalog's pinned commit once per pin and judges its app declaration with
the hermes_platform resolver the installer and the Plugins-tab pill use.
No declaration or an unreadable one is `unknown`, never `present`.
- `plugins.manage action=onboarding` lists the curated entries this OS
runs (platform mismatch is the only exclusion) with app_state and the
sentence the card greys the row with.
- manage_catalog plugin rows now carry app_state and the catalog title.
- A live entry that differs from the in-tree entry at the same pin (new
metadata) now follows the same newer-catalog rule as a new pin, so a
checkout that adds `onboarding` is not masked by a published doc that
predates it.
main extracted the launchd backend and the setup wizard out of hermes_cli/gateway.py. The
eight launchd functions pm-clean had changed are ported into gateway_launchd.py in its
_gw() style: runtime_command/installation_command for the gateway argv (no VIRTUAL_ENV in
the plist, XML-escaped args), _prepare_service_launcher before every plist write,
utf-8-sig plist reads, and launchd_restart's refresh-first + bounded bootstrap revival.
The systemd service-unit cluster stays in the facade (no gateway_service_unit sibling).
backup.py takes main's browser_profiles backup-only exclusion on our profile_root_entry
shape. main.ts takes main's attach-first backend block with our explicit types.
The README section was gated on `readme: true` in the catalog YAML and no entry set it, so
all 222 plugin pages shipped without one. READMEs now render for every GitHub/GitLab entry
(fetched at the reviewed sha, subdir first then repo root, common casings and docs/README.md
as fallbacks); `readme: false` opts an entry out. Build proof: 222/222 READMEs rendered.
Branch semantics kept where main and PM disagree: update_cmd_deps.py,
constraints-termux.txt, the Electron update-api-check module and the
post-swap hand-off test stay deleted; the pending-fleet-restart catch-up
and the local_runtime tag/download ladder stay retired (PM owns engines).
Ported from main onto the branch's shape: profile_scoped_chore for the
auto-archive and plugin-update housekeeping chores, the local-runtime
cross-process boot lock and residency cap, the checkpoint tmp_pack sweep,
the cua daemon-liveness status probe, the remote-served Desktop update
flag (posix.sh / windows.ps1), sign-in for env-pinned remote gateways
(urlDisabled on RemoteSetupFields), the uvloop extra split (uvicorn
without [standard]), and the umask-scoping spawn test.
uv.lock regenerated with pm.build_env --lock-only; new utf-8 reads from
main switched to utf-8-sig (check-windows-footguns).
Two optional, submitter-controlled fields on a catalog entry feed the
entry's own page at /docs/plugins/<name>:
- `screenshots:` — up to 6 https URLs on GitHub hosts (same host rule as
`image`, so the site never fetches from third-party hosts and a raw URL
pinned to the sha is as immutable as the code).
- `readme: true` — the docs build renders the README from the PINNED
commit (raw.githubusercontent.com / gitlab.com raw at <sha>), never live
content, so what a user reads is what the reviewer read.
Validator rejects malformed values (admission), the loader parses and
drops off-host screenshots with a warning (client), and the extractor
emits `screenshots`, `readme`, `readmeUrl` and a `maintainerSlug` for the
author pages. Tests on all three.
Reconcile plugin declarations and validation through PM's atomic generation publication; preserve external runtimes, target markers, and conflict refusal. Keep one source-update completion owner and port upstream lifecycle changes to the PM desktop/runtime paths.
The 40-hex sha stays the release, but nobody reads one. Entries may now add
`version: "1.4.0"` (free-form, <=32 chars, never parsed) and `image:` (an https
URL on raw.githubusercontent.com / github.com / *.githubusercontent.com).
Why GitHub-only: the Desktop catalog browser deliberately never fetches from
third-party hosts, and a raw URL pinned to the entry commit is as immutable as
the sha it decorates.
Readers updated together: PluginCatalogEntry + entry_from_mapping (drop with a
warning, entry survives), validate_plugin_catalog.py (admission error), the
site extractor (drop, never fatal), the /docs/plugins card (banner + version
pill + "1.4.0 @ abcd1234" pin), the CLI table/info (pin_label), the TUI-gateway
plugin row (catalog_version -> Desktop "Update to 1.4.0"), and the Desktop
catalog detail header (image).
Teknium's call: most community submissions are Desktop panes, so an entry
without a category lands on the Desktop shelf; "other" becomes "general" for
plugins that genuinely span areas. Shelf order puts Desktop first. The six
entries merged today (pets-all, newswire, auto-titler, live-voice,
metamask-wallet, web-octen) get explicit categories.
The catalog page was one undifferentiated grid filtered only by tier, so a
memory provider sat between two Desktop panes. Entries now carry an optional
``category`` (memory | desktop | platform | web | tools | voice | automation |
models | other, default other) that the loader, the admission validator and
the site extractor all understand.
/docs/plugins renders one shelf per category in browse mode, a category pill
row under the tier pills, a clickable category chip on every card, and a
results bar (active category, count, clear) when a filter or search flattens
the view. ``hermes plugins catalog`` gains a Category column and groups by it.
All 18 shipped entries are categorised. Unknown categories fail admission
(same contract as tier) so a typo cannot create a phantom shelf.
Activation reaches plugin discovery before the application dependencies
exist. Give PM its own locked Python project and runtime so it can install
or repair the application without importing that dependency tree.
Keep PM outside the application workspace. A shared uv workspace resolves
the application graph and cannot provide this isolation. Route mutations
through an isolated worker and preserve transaction callbacks, cancellation,
custom package registrations, and correlated receipts.
Use the same runtime builder for source installs and packaged payloads.
Keep offline wheelhouse support in that builder. Nix builds the independent
PM lock as a separate derivation. Refuse lazy-disabled bootstrap before
installing tools or dependencies.
Move first-party YAML readers and writers to ruamel. Keep the application
lock's transitive PyYAML requirements for third-party packages.
Verification:
- Focused canonical Python suite: 177 passed, 1 host-gated skip.
- Electron backend probes: 12 passed. Electron typecheck passed.
- Both uv locks, scoped lint, Bash syntax, and whitespace checks passed.
- Cold activation, corrupt-app repair, offline staging, and relocation ran.
- Built and exercised the Nix PM runtime and standalone YAML merge script.
Six broader caller test files retain the same 24 failing test IDs as an
archive of HEAD. The existing real-home guard blocks those tests before
they can exercise the affected paths. No full-suite pass is claimed.
Native Windows signing and full Bionic package execution remain unverified.
- scripts/validate_plugin_catalog.py: standalone stdlib+pyyaml structural
validator for plugin-catalog entries and removed.yaml (no hermes install
needed; runtime twin of hermes_cli/plugin_catalog.py). --json support,
unknown top-level keys warn instead of failing for forward compat.
- .github/actions/plugin-validate: composite action plugin authors drop
into their own repo's CI — installs hermes-agent from a chosen ref and
runs 'hermes plugins validate <path>'.
- .github/workflows/plugin-catalog-ci.yml: admission gate on PRs touching
plugin-catalog/** — structural job plus pinned-source job that clones
each changed entry's repo, hard-fails on unreachable pinned sha
(supply-chain gate), and validates the plugin at that exact commit.