tools/lazy_deps.py held a table of about 40 features, each with its own literal pip specs. pyproject.toml declares the same packages as extras, so every pin existed twice and the two copies drifted. Each feature now names an extra, and the specs come from pyproject at run time. The table is 218 lines shorter. A test asserts that each feature names an extra that exists and resolves to at least one spec, so a typo cannot ship. A wheel install, such as Nix, has no pyproject.toml beside the code. There the same table comes from the dist metadata: each spec of an extra is one Requires-Dist line, and its marker names the extra. Without this fallback, each entry point raised on a Nix install, and ensure() raised even for a feature whose packages the build baked in through extraDependencyGroups. That call must be a no-op. is_available() and feature_install_command() catch the failure as well now. Their callers sit in status paths with no try/except, and their contracts are bool and Optional[str]. The security overrides already come from pyproject (the previous commit). This commit moves the reader onto the shared _pyproject() cache and the shared temp-file writer. The tier-0 installer, `uv sync --extra <name>`, names the project with --project. uv reads the project from its working directory, and the agent runs from the user's working directory, not from the install tree. Without the flag the sync failed outside a checkout, and the pip ladder always ran instead. install_specs gets the same managed-install guard as ensure(). A Nix venv is in the read-only store, so the pip ladder could only fail with EROFS after a 15s ensurepip attempt. It reports the Nix remedy instead. A durable install target overrides the guard, as it does in ensure(), because the NixOS container module sets HERMES_MANAGED=true with a writable target. Spec parsing goes to packaging.requirements.Requirement, which is already a core dependency. The hand-written version kept the environment marker attached to the version. SpecifierSet raised on it, so _is_satisfied answered True for every installed version of a marked package. Such a package can never upgrade. Reading the specs from an extra exposed a second fault, in the record of which features are active. active_features read specs[0] as the anchor package, and extra composition put sounddevice there for [voice] and for each wake extra. One local STT install then marked every audio feature active, and `hermes update` installed the wake engines that the user never enabled. ensure() records each feature it serves in $HERMES_HOME/lazy-features.json, and active_features reads that record. A recorded feature still needs its anchor package installed, so an uninstalled backend does not come back. The anchor is the first pin written directly in the extra, not the first spec after expansion. A test asserts that no two extras share an anchor. There is no seeding for an install that predates the record. Its first `hermes update` refreshes nothing. ensure() then repairs a stale pin at each backend's start and records the feature, and the next update covers it. [stt-whisper] splits out of [voice]. faster-whisper transcribes audio files and needs no microphone and no PortAudio, so the Docker image can bake it. [voice] composes [stt-whisper] and [audio-io] and stays the microphone stack. stt.faster_whisper maps to the new extra. Removed with the table: - The literal pin list in plugins/platforms/google_chat/oauth.py. Its pip path targeted /nix/store on a Nix install, which is read-only. - The bare honcho-ai fallback in the honcho setup. An unpinned install accepts whatever PyPI serves, which is the hole this branch closes. Both call sites report the remedy for the deployment instead, through the now-public managed_install_reason. - install_deps() in the google-workspace skill. The SDKs ship in the [google] extra, so a stripped environment is a broken install. The repair is `hermes update`. A pip run from the script writes to whichever interpreter it runs under, which is not always the one Hermes uses. - tests/test_runtime_pins_are_locked.py, which scanned first-party source for pin literals. There are none left to find. - The spec shape check in install_specs. The same plugin.yaml hands external_dependencies[].install to bash with shell=True, and the plugin's __init__.py is imported. Anyone who can write that file already runs code as the user. |
||
|---|---|---|
| .. | ||
| docs | ||
| i18n/zh-Hans/docusaurus-plugin-content-docs/current | ||
| scripts | ||
| src | ||
| static | ||
| .gitignore | ||
| .npmrc | ||
| README.md | ||
| docusaurus.config.ts | ||
| package-lock.json | ||
| package.json | ||
| sidebars.ts | ||
| tsconfig.json | ||
README.md
Website
This website is built using Docusaurus, a modern static website generator.
Installation
yarn
Local Development
yarn start
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
Build
yarn build
This command generates static content into the build directory and can be served using any static contents hosting service.
Deployment
Using SSH:
USE_SSH=true yarn deploy
Not using SSH:
GIT_USER=<Your GitHub username> yarn deploy
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the gh-pages branch.
Diagram Linting
CI runs ascii-guard to lint docs for ASCII box diagrams. Use Mermaid (````mermaid`) or plain lists/tables instead of ASCII boxes to avoid CI failures.