Skip to content

fix: correct server-memory pin to an existing version - #134

Merged
github-actions[bot] merged 1 commit into
mainfrom
claude/fix-server-memory-pin-16199
Aug 1, 2026
Merged

fix: correct server-memory pin to an existing version#134
github-actions[bot] merged 1 commit into
mainfrom
claude/fix-server-memory-pin-16199

Conversation

@dizhaky

@dizhaky dizhaky commented Aug 1, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

One-line fix: .claude/settings.json pins the project memory MCP server at @modelcontextprotocol/server-memory@0.6.3, a version that does not exist on npm — the published version line jumps 0.6.2 → 2025.4.25. The npx -y launch therefore 404s and the server has never been able to start from this config on any machine (Mac or remote). Repinned to 0.6.2, the nearest existing version to the intended pin.

Related Issue

Surfaced while diagnosing why the repo's configured MCP servers don't start in Claude Code remote sessions.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • .claude/settings.json@modelcontextprotocol/server-memory@0.6.3@0.6.2

How to Test

npm view @modelcontextprotocol/server-memory@0.6.3 version   # E404 — does not exist
npm view @modelcontextprotocol/server-memory@0.6.2 version   # 0.6.2
python3 -c "import json; json.load(open('.claude/settings.json'))"  # still valid JSON

Generated by Claude Code

@modelcontextprotocol/server-memory@0.6.3 does not exist on npm (the
version line jumps 0.6.2 -> 2025.4.25), so the memory MCP server has
never been able to start from this config on any machine. Pin the
nearest existing version.
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

🔎 Lint report: claude/fix-server-memory-pin-16199 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9238 on HEAD, 9238 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5103 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@dizhaky
dizhaky marked this pull request as ready for review August 1, 2026 21:03
@github-actions
github-actions Bot merged commit 04d0598 into main Aug 1, 2026
26 checks passed
@github-actions
github-actions Bot deleted the claude/fix-server-memory-pin-16199 branch August 1, 2026 21:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 657d8144c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .claude/settings.json
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory@0.6.3"]
"args": ["-y", "@modelcontextprotocol/server-memory@0.6.2"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move the memory server pin into the MCP template

On a fresh checkout, changing this pin still does not make the memory server available because project MCP definitions belong in .mcp.json, while this repository keeps .claude/settings.json for hooks only; .mcp.json.example still defines only codebase-memory. Move this server definition, including the corrected pin, into .mcp.json.example so copying the template actually registers it.

AGENTS.md reference: AGENTS.md:L26-L30

Useful? React with 👍 / 👎.

github-actions Bot pushed a commit that referenced this pull request Aug 2, 2026
…#137) (#138)

* feat(deploy): persistent gateway hosting + env-passthrough log hardening

Re-lands the reviewed-good content of #137 on a branch cut from current main.
#137 could not merge: it was mergeable_state dirty, and it sat on
claude/slack-session-94aae0 — the branch of closed #106, which #130 said
should not be continued.

Two things ship here.

tools/env_passthrough.py — CodeQL clear-text-logging fixes. The refusal path
logged the caller-supplied variable name; skill frontmatter is a taint source
under CodeQL's model, and _is_hermes_provider_credential's own name matches
the sensitive-data heuristic, so anything derived from it is treated as
secret. Replaced with counts and static strings. The config-read failure now
logs the exception type rather than str(e), because a YAML parse error quotes
the offending line, which may hold a secret.

deploy/, docs/DEPLOYMENT.md, website/docs/guides/persistent-hosting.md,
Dockerfile, docker-compose.yml, README.md — running the gateway 24/7 with no
long-lived credentials on the host. Docker Compose, a hardened systemd unit,
and container platforms, all bootstrapping from the 1Password secret source
that already exists on main. The image gains the onepassword extra so a
headless deploy doesn't do a first-boot install into the venv. Only
placeholder tokens (ops_...your-token...) appear anywhere.

Dropped from #137: .claude/settings.json, which reverted
@modelcontextprotocol/server-memory from 0.6.2 to 0.6.3. That version does not
exist on npm — the published line jumps 0.6.2 to 2025.4.25 — so the revert
re-breaks the memory MCP server and undoes #134. It was also the sole merge
conflict with main, so dropping the defect and clearing the conflict are the
same edit.

hermes_cli/config.py is not touched, so the GHSA-mv8x-fg99-32mf
_sanitize_env_lines regression #130 warned about is not in play.

Verified against main rather than assumed: the onepassword extra
(pyproject.toml), every secrets.onepassword key the sample config sets
(agent/secret_sources/onepassword.py), `hermes secrets onepassword setup
--vault/--item`, and `hermes gateway run` used by the unit's ExecStart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT

* docs(website): register the persistent-hosting guide in the sidebar

website/sidebars.ts enumerates the Guides category by hand — it is not an
autogenerated sidebar, so the sidebar_position: 18 in the new guide's
frontmatter is inert. Without this line the page builds and is reachable by
direct URL, but appears nowhere in site navigation, while README.md and
docs/DEPLOYMENT.md both link its published URL. docusaurus.config.ts sets
onBrokenLinks: 'warn', so nothing fails — it just quietly isn't there.

Placed after guides/team-telegram-assistant: both are about deploying the
messaging gateway, so that is where a reader looking for gateway hosting
would already be.

Not in #137; found while reviewing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT

* test(env-passthrough): pin that registration never logs a variable name

The clear-text-logging fix in the previous commit had nothing guarding it.
Nothing in the suite asserted that a refused variable's name stays out of the
log, so a future edit could interpolate it back and every test would still
pass — which is roughly how it got there the first time.

Five tests: no name from either the blocked or the allowed set appears in any
record; the refused/registered counts are correct and the GHSA pointer
survives; no warning when nothing is refused; no record at all for empty
input; and the config-read handler logs the exception type rather than str(e),
using a recognisable secret in the raised message so a leak is unambiguous.

Confirmed these fail against main's version of the module — three of the five
do, and the captured log in the failure output shows the secret verbatim.
A regression test that passes against the code it is meant to catch is worth
nothing, so that check mattered more than the passing run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT

* docs(system-log): record the #137 salvage

Per docs/system-log/README.md. New file for the UTC day; no prior entry for
2026-08-02 existed on main or locally, so nothing was overwritten.

Records what was carried, what was dropped and why, what was added beyond
#137, what was verified, and — separately — what could not be verified in a
container with no Docker daemon and no website node_modules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jy1tjok1XKgpTUK69b8dKT

---------

Co-authored-by: Claude <noreply@anthropic.com>
dizhaky pushed a commit that referenced this pull request Aug 12, 2026
…ngs.json

The block was removed deliberately on 2026-07-28 (see docs/system-log) in
favour of .mcp.json.example: Claude Code documents ${VAR} expansion for
.mcp.json and ~/.claude.json but not for settings.json, so a server
declared there needs a literal absolute path and starts on exactly one
machine, failing silently everywhere else. An unrelated dependabot npm
bump (7e38fa5) re-added a copy carrying a hardcoded /Users/<name>/ path.

The `memory` server moves into .mcp.json.example so #134's
@modelcontextprotocol/server-memory@0.6.2 pin survives the removal (0.6.3
does not exist on npm). The `hooks` block is untouched — AGENTS.md is
explicit that settings.json keeps hooks only, and $CLAUDE_PROJECT_DIR
*does* expand there, which is how the tracked
.claude/hooks/session-start.sh is reached.

test_no_repository_local_claude_permissions_file asserted the file must
not exist at all, which would orphan that hook. It now asserts what
AGENTS.md actually requires: no `permissions` and no `mcpServers` key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8
github-actions Bot pushed a commit that referenced this pull request Aug 12, 2026
* fix: repair two NameError crashes in the Python suite

Both are environment-independent source bugs — code paths that could never have
run — surfaced while investigating why main's Python suite is red (six of eight
CI slices), which blocks the aggregate gate in #186.

plugins/memory/mem0: _start_prefetch(self, query) computed
`effective_user_id = user_id or self._user_id`, but user_id is not one of its
parameters, so any call raised NameError. It is a copy of the identical line in
sync_turn fifty lines below, where user_id genuinely is a parameter. The value
was never read — the closure filters via self._read_filters() — so the line is
deleted rather than the parameter added.

hermes_cli/gateway: the launchd plist builder assembled `core_args`, computed a
`use_wrapper` flag, then called `prog_args.append(...)` — a name never
assigned. All three were mutually inconsistent, so the function could not run
to completion. The surrounding comment is unambiguous about intent (the wrapper
execs the python + module args passed after it; fall back to direct-python when
it is missing) and the three failing tests assert nothing about wrapper shape,
so the comment is the spec: the wrapper path goes in front of core_args when
usable, core_args alone otherwise.

Deliberately not included: the tests importing _expand_value_from_environ and
_redact_command_for_display, neither of which exists. Implementing them means
inventing semantics in a security-adjacent config loader, and whether they were
removed or never written is not answerable from a shallow clone.

Also records a correction in the system log: my earlier "69 failing tests"
figure was this sandbox, not the repo. Most of it is missing optional extras
and a different aiohttp than uv.lock, and some of those failures would be wrong
to "fix" — the lazy-deps pin test is correctly detecting local drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* fix: unshadow the gateway command-line matcher, repair two test harnesses

Second instalment of the Python-suite repair. CI confirmed the first: slice 6
went 5 files/20 tests -> 4 files/17 tests with test_mem0_v3.py gone. This
clears the remaining four files in that slice.

gateway/status.py had TWO definitions of looks_like_gateway_command_line — the
canonical one at line 444 delegating to _gateway_command_subcommand (shlex
tokenization, profile-selector stripping, case-folding), and a hand-rolled
substring scan at line 569. Python keeps the last, so every caller silently got
the naive one. looks_like_gateway_runtime_command_line was duplicated the same
way. The substring version fails precisely the cases the canonical one exists
for: `--profile work gateway run` (the flag splits the substring), `--profile
gateway gateway run` (the profile value shadows the subcommand), bare
`gateway`, and `GATEWAY RUN`. _scan_venv_blockers' docstring warns about this
in as many words, so the regression it describes had reappeared one module
over. Deleting the duplicates restores the canonical behaviour unchanged —
14 tests across two files.

test_kanban_db referenced bare `hermes_state` at line 907 while line 922 does a
plain `import hermes_state`, making the name function-local for the whole body,
so the earlier reference could never resolve. It was redundant anyway: line 903
already clears that same set via the _hermes_state alias.

test_modal_snapshot_isolation died before reaching its subject: modal.py calls
lazy_deps.ensure("terminal.modal") first, and that asks whether the
distribution is installed, which a sys.modules stub cannot satisfy. The test
already stubs hermes_cli, tools, tools.environments and modal, so lazy_deps is
one more stub, not the thing under test. The production check is deliberately
untouched — it is security-adjacent, and loosening it to accept pre-imported
modules is not a change to make for a test's convenience.

Also corrects a claim in the log: I said test_gateway_command_line_matcher
fails in CI but passes locally, and used it to argue the environments disagree.
It fails locally too — verified by stashing the fix. It was missing from my
inventory because the background capture kept only the tail of the full-suite
output, so the file list was truncated.

No regressions: tests/gateway/ + tests/hermes_cli/ run 9,102 passing / 24
failing across 12 files, none of them files touched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* fix: define the cron staleness check, stop provider labels collapsing to ids

cron/scheduler: gateway/run.py's cron-ticker supervisor imports
get_ticker_heartbeat_age and ticker_heartbeat_is_stale from cron.scheduler.
Neither existed there — the age reader lives in cron/jobs.py and the staleness
predicate was defined nowhere in the repo. The import sits inside the
supervisor loop, so the watchdog meant to restart a wedged ticker would itself
have died on ImportError the first time it checked. Added the predicate and
re-exported the age reader beside it, since the supervisor and its tests both
treat cron.scheduler as the ticker's public surface.

The rule is taken from the existing docstrings rather than invented:
age > interval * stale_multiplier, and an unknown age is NOT stale —
get_ticker_heartbeat_age returns None on a missing or torn read and documents
that callers treat that as "cannot determine", not "dead". Restarting on a torn
read would turn a filesystem hiccup into a restart loop.

hermes_cli/providers: get_label("xai") returned "xai", which collapsed the xAI
API-key entry onto its own id and made it indistinguishable from the OAuth
sibling in the model picker. My first fix was wrong and the test caught it — I
added a fallback at the end of get_label, but that line is unreachable for this
case: get_provider returns a synthesized def for Hermes-only providers, with
name=_LABEL_OVERRIDES.get(canonical, canonical), so the id was already baked in.
Fixed at that source via one _display_name() helper shared by both call sites:
overrides, then the registry in hermes_cli.models (which already carries
ProviderEntry("xai", "xAI", ...)), then the id.

Not fixed by adding an _LABEL_OVERRIDES entry: that duplicates a name the
registry owns, which is exactly the duplication behind the gateway/status.py
bug in the previous commit. models.dev is a remote catalog, so this path is
also what every provider hits whenever it is unreachable.

No regressions: 774 tests across 90 provider/model files pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* docs(system-log): record the missing config-integrity cluster

Five names are imported from hermes_cli.config and none exist; two of them by
production code in hermes_cli/backup.py. The restore path swallows the
ImportError, so a config restore silently never reseals the integrity baseline
and the watchdog reads an authorized restore as tampering.

The machinery exists unwired in skills/devops/config-integrity-watchdog/, whose
seal(quiet=True) parameter is documented for exactly this caller. Reconstructing
it is four functions of security-adjacent behaviour, so it is reported for a
decision rather than folded into a make-CI-green pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* fix(slack): assign as_yaml, add the channels:join bot scope

hermes_cli/slack_cli.py used `as_yaml` in two places — the render call and the
file extension — without ever assigning it, so `hermes slack manifest` raised
NameError on every invocation. Every other option in that function is read via
getattr(args, ..., default); as_yaml now is too, defaulting to False (JSON),
which is what the default write target (slack-manifest.json) and the test's
json.loads(stdout) both assume.

The generated app manifest also omitted the channels:join bot scope, so
`hermes slack invite --all` would fail per-channel with missing_scope on an
otherwise valid install — conversations.join requires it.

Not fixed here: test_gateway_platform_gating asserts _builtin_setup_fn returns
bespoke setup functions for telegram/slack/matrix. Those return None by design
— all three moved into plugins/platforms/*/adapter.py, and I verified each
registers setup_fn=interactive_setup, so the invariant the test documents (they
must not fall through to the generic wizard, which would break Matrix's
empty-token password login) still holds. The test asserts the removed route
rather than the invariant; replacing it properly needs the plugin registry's
lookup API, so it is documented rather than deleted to make CI green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* fix(config): restore the missing config-integrity cluster

Eight functions were referenced by tests and production but did not exist in
hermes_cli/config.py: get_config_lock_path, get_config_seal_path,
config_write_lock, config_read_lock, seal_config, verify_config_integrity,
_reseal_git_backed_integrity_baseline and restore_config. Three test files
failed at collection because of it, and hermes_cli/backup.py imported two of
them behind `except ImportError: pass` — so a config restore silently never
resealed and the watchdog read it as tampering.

They were deleted, not never written: config.py still imports `fcntl as _fcntl`
and `msvcrt as _msvcrt` at module scope and uses neither, which is exactly what
the lock helpers needed, and hermes_cli/config_integrity_cli.py still provides
the _find_core_module bridge the tests import.

save_config() now seals both baselines after a successful write — the local
config.yaml.sha256 sidecar and the git-backed JSONL log in the operator's
dotfiles repo. A write through save_config is authorized by definition, so
leaving those stale is what made the watchdog flag the model scanner.

Decisions worth calling out. A missing config and a missing seal both verify OK:
absence of a baseline is not a mismatch, and failing there would flag every
pre-existing install as compromised. Sealing never fails a write — the
git-backed half shells into a repo outside Hermes' control that can be absent,
non-git, mid-rebase or read-only. That reseal is a no-op unless the dotfiles
directory already exists, since the skill's seal() would otherwise mkdir a
baseline on machines that never opted in. Locks degrade to no-lock rather than
raising, and Windows readers go unlocked because msvcrt has no shared mode.
restore_config always writes a pre-restore backup, including when the live file
is unreadable — a corrupt config is the usual reason a restore is happening.

_expand_value_from_environ leaves an unset reference verbatim rather than
blanking it, so a typo'd variable stays legible instead of becoming an empty API
key. That one rule also resolves the apparent contradiction in its tests:
$TEST_VAR expands and literal-$var does not, purely because one name is exported
and the other is not. It is not wired into config loading — _expand_env_vars
walks whole documents and resolves ${env:NAME} SecretRefs with different
warning behaviour, so delegating would change semantics rather than restore a
helper.

Verified against the same baseline command as before: tests/gateway/ +
tests/hermes_cli/ go from 9,102 passing / 24 failing across 12 files to 9,139
passing / 11 failing across 8, with collection errors down from 3 to 1. Strictly
better on every axis, so the new sealing side effect regressed nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* fix(config): open the lock file in binary mode

Ruff (PLW1514) and the Windows-footgun checker both flagged the new
`open(lock_path, "a+")`: text mode without an explicit encoding, which on
Windows also brings newline translation. The file is only ever a lock handle —
never read or written as text — so binary sidesteps both concerns rather than
papering over them with an encoding argument.

Both checks pass locally: `ruff check hermes_cli/ cron/ gateway/` is clean and
`scripts/check-windows-footguns.py --all` reports no footguns across 927 files.

Unrelated, for the record: the `gitleaks (diff)` failure on 25b74f9 is not a
secret. Its install step died on `curl: (22) ... error: 503` fetching the
release tarball, so the scanner never ran — but the job's failure-notice step
prints "gitleaks flagged a secret in this diff" unconditionally, making a
transient CDN failure look identical to a real detection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* fix(model-metadata): define the Ollama /api/show probe gate

Ninth instance of the same defect class: _should_probe_ollama_api_show was
referenced by tests but defined nowhere, so the file raised NameError.

Regression guard for NousResearch#31555 — /api/show is an Ollama-server endpoint, so
firing it at a hosted provider costs a doomed HTTP roundtrip (up to the 3s
probe timeout) on a path that runs during agent startup. The rule comes from
the three tests: skip for a known hosted provider, infer the provider from the
base URL when none is named (which is how the OpenRouter case reached the probe
at all), otherwise allow.

The first attempt gated on _PROVIDER_PREFIXES and was wrong — that set contains
plain `ollama`, because it answers "can this appear as a model prefix?", not
"is this hosted?". test_allows_ollama_provider caught it. Local providers are
now checked first via an explicit _LOCAL_OLLAMA_PROVIDERS; ollama-cloud stays
out of it, being the hosted service that serves no /api/show.

Found by reading CI's slice list rather than my sandbox: tests/agent/ — 351
files — had never been in any of my local sweeps, so this bug was absent from
every inventory I had built. That also corrects my earlier claim that the
remaining failures were all environmental: the environmental ones are real
(root defeats chmod-based permission tests, no IPv6 for bind assertions), but
that was a statement about a set I had not measured.

tests/agent/: 3,926 passing / 12 failing across 7 files, none of them this one.
ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* docs(system-log): complete local inventory and two corrections

First full-suite run with a repaired venv — the first complete enumeration this
session has had. 62 failures across 28 files, roughly half of them this
sandbox's missing optional extras rather than repo bugs.

Two of the real ones are features that do not exist rather than bugs:
_redact_command_for_display (tools/approval) and per-model credential lockout
(CredentialPool.select/_mark_exhausted take no model kwarg and the module has
zero lockout machinery, against a 185-line spec). Both left for a decision. The
contrast with the config-integrity cluster is the reason: that one had orphaned
fcntl/msvcrt imports, a surviving bridge module and a skill implementing the
machinery, so restoring it was recovery. There is no such trace here, and
inventing cooldown semantics beside a DEAD/EXHAUSTED state machine that governs
billing-bearing credentials is a feature decision.

Also records two corrections. My first regression check was vacuous — git stash
takes only uncommitted work, so with everything committed I compared the branch
against itself and read the identical numbers as evidence. A worktree at
origin/main gives the real answer: the tests/agent/ failures are pre-existing.
And 'same compromised run' was wrong; runs are mixed, and log length (~180 vs
~1000+ lines) discriminates a setup death from a real test run, not the run id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* fix(deps): stop lazy installs downgrading aiohttp below its CVE patch

pyproject.toml and uv.lock both pin aiohttp==3.14.3; tools/lazy_deps.py still
pinned 3.14.1 in four places, and cbor2==6.1.2 against a uv.lock of 6.1.3.

The consequence is in the failing test's own message: pyproject extras pins must
match LAZY_DEPS pins for every shared package, otherwise `hermes update`
downgrades the package. Every one of those lines carries a CVE list including an
RCE (CVE-2026-34993), so installing any lazy feature — a Discord, Teams or
Matrix platform, or the Modal terminal — would have pulled aiohttp back below
the patched floor on a machine that already had the fix.

Bumped the four aiohttp pins to 3.14.3 and cbor2 to 6.1.3, matching uv.lock
exactly. Also corrected the comments that named the old version beside the new
pin (`# aiohttp 3.14.1: CVE-…` next to `aiohttp==3.14.3`, once here and three
times in pyproject.toml) — a version string that disagrees with the pin it
annotates is the drift that produced this.

Correction to my own triage: I had filed these two tests under "sandbox drift"
because the message mentions an aiohttp version, assuming it reflected my stale
venv. It does not — they compare declarations across pyproject/lazy_deps/uv.lock
and never inspect what is installed, and CI fails them identically.

test_project_metadata + test_packaging_metadata: 13 tests, 0 failures.
pyproject.toml parses; ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* feat(credential-pool): restore per-model credential lockout

A 429 for one model says nothing about the others, but the pool could only
bench a credential globally — so one rate-limited model removed the key from
rotation everywhere.

Correction to my own earlier triage: I reported that credential_pool.py had
"zero model-lockout machinery" and deferred this as inventing a feature. That
was wrong — I grepped for model_lockout/locked_models/model_locks; the feature
is called exhausted_models, and two pieces were already shipping.
_model_exhausted_until() lives in credential_pool.py and reads exactly the
record shape the tests specify, and hermes_cli/auth_commands.py renders
entry.extra["exhausted_models"] in auth status output. The display side has
been live against a field nothing ever wrote — the same partial-deletion
signature as the config-integrity cluster, consumers and helpers surviving
without the producer.

_mark_exhausted(model=...) now records the bench in extra["exhausted_models"]
and leaves the credential-wide status alone. Terminal auth failures are
deliberately excluded from that path: a revoked or invalid token is dead for
every model, and recording it per-model would keep the credential in rotation
for the rest, failing instantly on each. The existing _is_terminal_auth_failure
classifier makes that call, so the DEAD/EXHAUSTED distinction is unchanged.

select(model=...) filters entries benched for that model, keyword-only so
existing no-argument callers are untouched.

_prune_expired_model_lockouts() drops lapsed records, mirroring what
_available_entries(clear_expired=True) does for the credential-wide status, and
runs before filtering so a just-expired lockout frees its credential in the
same call. reset_statuses() now counts a per-model lockout as status — a
credential can be benched for a model with every credential-wide field clear,
and reset must free that too rather than reporting "0 reset".

tests/agent/test_model_lockout.py 4/4. Credential, pool and auth suites: 477
tests across 67 files, 0 failures. tests/agent/ overall 3,930 passing / 8
failing, up from 3,926 / 12 — no regressions in a module governing
billing-bearing credential rotation. ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* fix(approval): redact commands shown for approval; fold single-component homes

Two fixes in tools/approval.py, five tests.

_redact_command_for_display was missing. Searching for consumers first — the
lesson from the credential-pool commit — showed agent/redact.py already carries
the Slack xapp-/xox[baprs]- patterns, URL-userinfo handling and
redact_sensitive_text(), and that it satisfies every assertion in the spec
unchanged. So the missing piece was a thin wrapper, not a redaction engine.

It earns its own name from the contract in the test docstring: the original
command still runs, this is display-only. Motivated by the 2026-07-13 incident
— a heredoc appending a Slack app-level token to ~/.hermes/.env, where the
approval prompt reproduced the command in full, so asking permission published
the credential more widely than running it would have. The variable name
survives so the reviewer can see which secret is being written; only the value
is masked. force=True and redact_url_credentials=True come from
redact_sensitive_text's own docstring — force is documented for "safety
boundaries that must never return raw secrets regardless of the user's global
logging redaction preference", and an approval echo into Slack is an egress
boundary rather than the tool flow the URL default protects.

The fifth failure was TestSensitiveRedirectPattern, and it corrects me twice. I
first checked /home/alice/.ssh/authorized_keys, saw False, and called it a
general security hole — wrong, that is not the current user's home and the fold
is deliberately scoped to the running user's. The real issue is narrower:
_home_prefix_fold_regex required two components below the root, which correctly
rejects / and /home (folding /home would rewrite every user's path to ~), but
/root is a legitimate home with one component. So on any root-run deployment —
the Docker image, CI containers, sudo — ~/.ssh/authorized_keys was guarded while
the identical /root/.ssh/authorized_keys was not.

Replaced the count with the property the guard wants: reject a bare root, and
reject a single component only when it contains other homes (home, users, var,
mnt, …). Verified both directions: /root/.ssh/authorized_keys now flags, ~ still
flags, /home/other/.ssh/authorized_keys is still left unfolded.

test_approval.py 101 passing / 0 failing (was 96/5). tests/tools/ 5,375 passing
/ 18 failing across 3 files, all pre-existing: daytona (optional extra absent),
execution-flag detection, and the chmod-under-root artifact. ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* fix(cron,state): re-enable the privileged-model gate and the PASSIVE WAL checkpoint

Three defects of the same shape: the mechanism survived, the thing that
uses it did not.

cron/model_policy.py is a complete, documented guardrail — it refuses to
route a legal/finance cron job to a denylisted or non-allowlisted
provider/model. It had zero callers. `enforce_privileged_model_route()`
now runs in create_job() on the assembled record and in update_job() on
the *merged* record (the privileged tags come from the stored
prompt/skills while the offending route arrives in the update, so neither
half is conclusive alone). Both raise before save_jobs(), so a rejected
job never reaches disk.

hermes_state._try_wal_checkpoint() executed `PRAGMA
wal_checkpoint(TRUNCATE)` under a 16-line docstring explaining that
TRUNCATE is what caused B-tree corruption on large databases (NousResearch#45383) and
that the periodic path must be PASSIVE. The warning string and the tests
still said PASSIVE; only the pragma had reverted. Now PASSIVE again;
close() and the pre-VACUUM path keep TRUNCATE, as documented.

test_gateway_platform_gating asserted the retired lookup: telegram/slack/
matrix moved to plugin-registered `setup_fn=interactive_setup` in NousResearch#41112,
and _configure_platform() tries the registry entry first. The invariant —
these three never fall through to _setup_standard_platform(), whose
mandatory-first-var behaviour would break Matrix's password-login path —
is now asserted against the mechanism actually in use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* fix(config): drop the resurrected mcpServers block from .claude/settings.json

The block was removed deliberately on 2026-07-28 (see docs/system-log) in
favour of .mcp.json.example: Claude Code documents ${VAR} expansion for
.mcp.json and ~/.claude.json but not for settings.json, so a server
declared there needs a literal absolute path and starts on exactly one
machine, failing silently everywhere else. An unrelated dependabot npm
bump (7e38fa5) re-added a copy carrying a hardcoded /Users/<name>/ path.

The `memory` server moves into .mcp.json.example so #134's
@modelcontextprotocol/server-memory@0.6.2 pin survives the removal (0.6.3
does not exist on npm). The `hooks` block is untouched — AGENTS.md is
explicit that settings.json keeps hooks only, and $CLAUDE_PROJECT_DIR
*does* expand there, which is how the tracked
.claude/hooks/session-start.sh is reached.

test_no_repository_local_claude_permissions_file asserted the file must
not exist at all, which would orphan that hook. It now asserts what
AGENTS.md actually requires: no `permissions` and no `mcpServers` key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* docs(system-log): ninth instalment — CI's failure list and the last five

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* fix(cron): anchor privileged-workload keywords so ordinary jobs aren't refused

Found by the E2E validation AGENTS.md requires before wiring a dead module
into a live path — and it would have shipped a regression.

detect_privileged_workload_tags() scanned for its keywords as plain
substrings. Several of them are acronyms: "je" (journal entry) matches
"project", "subject", "rejected" and "Jenkins", and "books" matches
"notebooks". Harmless while the detector had no callers. Now that the
route gate runs on create_job()/update_job(), an over-broad match doesn't
just mislabel a job — a privileged tag plus any pinned provider/model
outside the six-entry allowlist refuses the job outright. "Summarize the
project changelog" with a model override would have been rejected.

Keywords now match at a token start, so inflections still tag
("reconciliation" -> "reconciliations", "attorney" -> "attorneys") while a
keyword buried inside an unrelated word does not. Acronyms are anchored at
both ends. The over-tagging the module documents as deliberate is intact:
every keyword that matched a real word before still matches.

E2E against a temp HERMES_HOME through the real create_job/update_job
chain: an ordinary pinned job saying "project" lands, a denied route is
refused without persisting, an allowlisted privileged route lands, a bad
update leaves the stored job untouched, and ordinary updates still work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* docs(system-log): tenth instalment — E2E validation caught a regression

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

* test(moa): remove the interrupt-ordering race in the reference fan-out test

test_references_parallel_interrupt_aborts_wait raised the interrupt flag
from inside the "fast" stub, before that stub's response reached its
future. A poll landing in the gap saw an interrupt with the reference
still in flight, so the slot was marked skipped and the assertion failed
— rare locally, hit on a loaded CI runner.

The production path is not at fault: _run_references_parallel() records
completed futures before it checks the flag, and the interrupted sweep
preserves any result that lands in between. The test simply wasn't
creating the scenario it described.

The interrupt now comes from the progress_callback the fan-out invokes
right after recording a completed reference — "the interrupt arrives
right after the fast reference finishes", ordered by the production code
instead of by thread scheduling.

Verified in both directions: forcing the old ordering with a 150ms linger
reproduces the exact CI failure string, and the new ordering under the
same delay returns the real output. agent/moa_loop.py is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants