feat(hermes): boxed Hermes stack — 2012 MBP containerized agent + ledger MCP broker - #53
Draft
alycda wants to merge 6 commits into
Draft
feat(hermes): boxed Hermes stack — 2012 MBP containerized agent + ledger MCP broker#53alycda wants to merge 6 commits into
alycda wants to merge 6 commits into
Conversation
Containerized Hermes (gateway+dashboard, workspace UI, isolated one-shot draft
runner) with a host-side MCP broker as the privilege boundary: the agent has no
host shell and reaches the ledger only through fixed verbs, each gated by
bean-check and git-revert rails.
- boxed/: compose unit, ledger skill, env examples. Gateway holds no ledger
mount; the draft runner gets /ledger ro + import rw on an isolated network
because it consumes untrusted PDF text.
- mbp12-host/ledger-broker: stdlib-python MCP server (loopback, bearer token)
exposing status/check/ingest/draft/read_import/approve.
- mbp12-host/ledger-ingest: inbox watcher -> classify -> contained draft ->
bean-check gate -> commit -> Signal notify, plus payment-due reminders.
* pdfocr.swift: macOS Vision OCR fallback. PayPal statements are image-only;
pypdf returned ~200B of mail-barcode junk that was NOT empty and so slipped
past the empty-text guard. Triggers under 400B. Verified 201B -> 16867B.
* preflight.sh: Venice returns HTTP 402 but hermes reports only "no final
response was produced", indistinguishable from an agent failure. draft.sh
exits 75 EX_TEMPFAIL logging OUT OF CREDITS, leaving the PDF pending.
* draft.sh asserts gateway_state=stopped per run: container_boot auto-starts
a gateway in any HERMES_HOME marked running, which fights the one-shot for
the s6 log lock.
* duedates.sh scans all ledger *.beancount so rate/promo notes are seen.
- agents/skills/here-now: repo-canonical copy.
- README documents the skills prune (107 -> 6, ~11.3k tokens/call of
scaffolding) and the opt-out marker that stops the per-boot re-seed.
History rewritten to remove a bank loan account number that had been used as a
statement-classifier discriminator; replaced with a non-sensitive one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aAWaFMpE4vzKyLauk3Z8y
Prevention for the incident that put a bank loan account number in this public repo. It was used as a statement-classifier string match; the pre-push sweep at the time checked for API keys, tokens, and phone numbers but not bare long digit strings. GitHub secret scanning would not have caught it either — an account number is an identifier, not a credential, so it matches no partner pattern. - Scans ADDED lines across EVERY commit in the push, not just the tip: fixing a secret in a later commit leaves it in the history being published. - Flags private keys, sk-/ghp_/github_pat_/gho_/xox*/AKIA/tskey-/AIza, bearer tokens, key=value secrets, SSN shape, and 9+ digit runs. - .secretscanignore for false positives; SKIP_SECRET_SCAN=1 to override. Verified against the real leaked line (blocks, 2 patterns), a real ghp_ token (blocks), and a clean tree containing a unix timestamp, a port, a short SHA and version numbers (passes). Enable per clone: git config core.hooksPath tools/githooks Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aAWaFMpE4vzKyLauk3Z8y
The ledger holds every account, balance, paystub and statement reference. A content scanner is the wrong tool there — the sensitive data IS the content, so it would fire on every push and get bypassed by reflex. Guard the DESTINATION instead: refuse any remote that is not a local path or file:// URL. Install: install -m 755 ledger-pre-push.sh ~/ledger/.git/hooks/pre-push Override: ALLOW_REMOTE_PUSH=1 git push <remote> Verified: https and git@ forge URLs block (exit 1), local bare repo passes, override passes with a warning. Caveat: git contacts the remote to discover refs BEFORE running pre-push, so an unreachable remote fails on its own first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aAWaFMpE4vzKyLauk3Z8y
The gateway mounted com~apple~CloudDocs/Hermes at /artifacts. That is a different iCloud container from the one Obsidian uses, so agent markdown never appeared in Obsidian on the phone — the reason for writing it to iCloud at all. Obsidian on iOS only sees vaults under iCloud~md~obsidian/Documents/. Repointed to iCloud~md~obsidian/Documents/Hermes, a standalone vault rather than a folder inside an existing one, so agent output cannot reach the personal notes (the Claudia vault alone holds ~3,170 files). Verified: the container resolves the new source, and a write as the `hermes` user succeeds. No skill change needed — the ledger skill already says "deliverables to /artifacts"; only the host side of the mount moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aAWaFMpE4vzKyLauk3Z8y
…gest docker-watchdog.sh — Docker Desktop's VM has now died twice on this always-on box (once after sleep, fixed with pmset; once with no identified cause). The failure is silent and total: the Docker.app UI keeps running and logging while the VM is gone, so nothing looks wrong, but `docker ps` HANGS and every container is unreachable — fava, signal-cli, the Hermes gateway, the ingest pipeline and the 08:30 reminder all at once. Detection bounds the `docker info` call with a background kill, because hanging IS the failure mode and a plain exit-code check never returns. Recovery quits and relaunches Docker Desktop, waits up to 300s, and Signals the outcome. Restarts are rate-limited to one per 30 minutes so a genuinely broken VM escalates to a human instead of thrashing. Runs every 10 min from launchd. duedates.sh — add a 60-day actionable window. import/ now holds every statement back to 2022 from the reconciliation batch, and the reminder was reporting 100+ statements from 2022-2025 as "OVERDUE". That is exactly the kind of noise that gets a notification muted, which would silently cost the payment reminders their whole purpose. ingest.sh — carries the OCR fallback and paypal classifier added earlier, both of which proved out on real input: six PayPal statements arrived with 215-byte text layers (image-only PDFs, mail-barcode junk) and OCR turned each into ~17KB of readable statement, correctly classified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aAWaFMpE4vzKyLauk3Z8y
The old version scraped a due date out of every statement in import/ and
reported it. Once four years of statements were loaded for the reconciliation
that meant 100+ permanently-"overdue" lines. Of the eleven it showed, ONE
mattered; every other was already paid or on autopay. A reminder that cries
wolf gets muted, and then the line that mattered is lost with it.
Now alerts only when:
- the NEWEST statement for an account has a due date inside the horizon
- AND no ledger payment covers it, OR the covering entry is #unscheduled
- or the statement shows a Past Due amount (reported regardless of horizon —
a balance past due today should not wait for the next due date to come
within N days before anyone hears about it)
Suppressed: autopay (detected from "WILL BE DEDUCTED" in the statement text and
from the #autopay tag), superseded statements, anything already covered.
Still surfaced: hand-written future-dated `note` directives.
Also fixed:
- payment matching now includes `#`-flagged forecast entries. Scheduled
payments in this ledger use the forecast flag, so a check for [*!] found
NOTHING and would have called every scheduled payment missing.
- `Minimum Payment Due $ 466.46` (space after the $) broke the amount regex,
silently dropping every HELOC amount and printing a bare "due".
- the launchd job wrapped the script in notify.sh "$(...)", which sent an
empty Signal message every day regardless of content. It now runs the
script directly; the script notifies itself and exits silent when quiet.
On real data this went from 11 lines (1 useful) to 2, and immediately surfaced
a genuine miss: the NFCU card shows Past Due $293.60 with Payments -$0.00 for
the July cycle, while the only payment in the ledger is dated 2026-09-18 and
tagged #unscheduled — a month after the 08/20 due date.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aAWaFMpE4vzKyLauk3Z8y
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces #45, which was closed to drop references to a bank loan account number
that had been committed as a statement-classifier discriminator. This branch is a
single clean commit; the number appears in no commit here.
What this adds
Containerized Hermes on the 2012 MBP, with a host-side MCP broker as the
privilege boundary — the agent has no host shell and reaches the ledger only
through fixed verbs, each gated by bean-check and git-revert rails.
deadlock on the shared state's s6 log locks), workspace UI, and an isolated
one-shot
draftrunner with/ledgerro +importrw on its own network,because it consumes untrusted PDF text.
token, exposing exactly status/check/ingest/draft/read_import/approve.
bean-check gate → commit → Signal notify, plus payment-due reminders.
Fixes worth calling out
pdfocr.swift— macOS Vision OCR fallback. PayPal statements areimage-only; pypdf returned ~200B of mail-barcode junk which is not empty and
so slipped past the empty-text guard, meaning the bookkeeper would have been
handed garbage as if it were a statement. Triggers under 400B. 201B → 16867B.
preflight.sh— Venice returns HTTP 402, but hermes surfaces onlyno final response was produced, indistinguishable from an agent failure.draft.shnow exits 75 (EX_TEMPFAIL) logging OUT OF CREDITS and leaves the PDFpending rather than blaming the agent.
draft.shassertsgateway_state=stoppedper run —container_bootauto-starts a gateway in any HERMES_HOME marked
running, which then fightsthe one-shot for the s6 log lock.
duedates.shscans all ledger*.beancount, not just main + yearlytransactions, so rate/promo notes are actually seen.
Known gaps (documented in README, not fixed here)
tools/agents/skills/is not mounted into the gateway. An earlier revisionclaimed it was; it never existed in compose. Skills live in untracked runtime
state and must be copied in at provision time.
hermes -zone-shot mode works at all is unverified — every testso far coincided with an exhausted Venice balance.
🤖 Generated with Claude Code
https://claude.ai/code/session_012aAWaFMpE4vzKyLauk3Z8y