Commit Graph

3 Commits

Author SHA1 Message Date
ethernet 5aa121ecfd refactor(deps): read the lazy-install specs from the pyproject extras
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.
2026-08-09 17:33:14 -04:00
Teknium 6b81590c55
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):

- DELETE: source-reading tests (read_text/getsource on prod files),
  change-detector tests (exact catalog counts, model-name snapshots,
  config version literals), mock-echo tests (assert a mock returns what
  it was told), assertion-free/trivial tests, near-duplicate
  parametrizations (boundaries + one representative kept), async/sync
  twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
  alternation invariants, prompt-caching/deterministic-call-id
  invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
  redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
  models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
  tests performed on every session create — test_server.py 147s → 3.4s,
  and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
  compression_concurrent_fork, etc.); no wall-clock assertion tightened.

Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
2026-07-29 13:10:23 -07:00
Ben Barclay 1289f12812 fix(memory): lazy-install supermemory + mem0 SDKs like honcho/hindsight
The supermemory and mem0 memory providers shipped third-party SDKs
(supermemory / mem0ai) that are not core dependencies, but — unlike the
honcho and hindsight providers — they imported those SDKs directly with
no tools.lazy_deps.ensure() preflight and had no LAZY_DEPS allowlist
entry. On the published Docker image the agent venv is sealed
(HERMES_DISABLE_LAZY_INSTALLS=1) and lazy installs are redirected to a
writable durable target (HERMES_LAZY_INSTALL_TARGET). honcho/hindsight
route through ensure() and install fine there; supermemory/mem0 never
called it, so their SDK was never installed on a hosted instance and the
provider silently reported itself unavailable even with the API key set.

Fixes:
- Add memory.supermemory + memory.mem0 to the LAZY_DEPS allowlist
  (tools/lazy_deps.py), pinned to current PyPI releases.
- Call ensure('memory.<x>', prompt=False) at each SDK-import chokepoint
  (_SupermemoryClient.__init__; Mem0MemoryProvider._create_backend),
  mirroring honcho's wrapped try/except shape.
- Drop the SDK-import gate from supermemory's is_available() — it was a
  chicken-and-egg trap (provider never loaded on a sealed venv, so
  ensure() never ran). Now key-presence only, like honcho/mem0.
- Add matching pyproject extras [supermemory]/[mem0]; update the
  lazy-covered-extras contract test (excluded from [all] by policy).

Tests prove each path fails without the fix and the real sealed-venv
durable-target gate accepts both features.
2026-06-29 00:25:36 -07:00