feat(portfolio): K10 — notification/notifier seam, opt-in webhook egress (parity row 23) [skip-docs]

S5.2-analog. New notify.py: Notifier protocol + console/file/webhook sinks. The
webhook (the one transport that leaves the machine) fires ONLY behind an explicit
per-run opt-in flag (--allow-webhook-egress), mirroring ingest-spec §8 (the flag
is a run argument, never a config field). Transport is injected — canned in the
suite (NULL socket), real transport behind one seam function default_webhook_transport;
an AST grep-guard proves no network path exists outside that seam. run.py (both
outcomes — a budget stop notifies too) and hitl.py (read-only preserved) share the
same opt-in-gated CLI seam, refusing a webhook-without-opt-in before any spend.
Payload shape is stack-local (no shared notification spec; divergence documented).

Two new load-bearing test files (18 tests): opt-in gate + payload structure + the
grep-guard + run/hitl emit wiring + run-level opt-in threading, each detach-proven
RED. 544 -> 562 green, full gate clean (ruff+format+mypy strict, 26 src files).
README sync (test count x2 + notify.py module note + load-bearing omtale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
This commit is contained in:
Kjell Tore Guttormsen 2026-07-24 20:16:56 +02:00
commit a2acfc0f98
6 changed files with 863 additions and 11 deletions

View file

@ -13,7 +13,7 @@ human-in-the-loop, and the system learns from the verdicts.
> **Status:** the D7 build (S5S10) is complete, and the deterministic **ingest layer**
> (CSV and SQL source types) has since been added in front of the loop. The deterministic
> backbone, the agentic loop, the learning loop, and the ingest connectors are wired seam by
> seam, each proven by load-bearing tests (544 tests, all running offline without an API
> seam, each proven by load-bearing tests (562 tests, all running offline without an API
> key). The programme's single budgeted **live model run has been executed and validated**
> its artifacts are committed under [`runs/s10/`](runs/s10/) (see below).
@ -141,6 +141,17 @@ description, never from its code)
now; K13 formalizes the dimension catalog) to an expert via a schema-validated table
(`nøkkel→ekspert`, fail-fast) with an optional default; an unmatched measure is UNROUTED.
`uv run python -m portfolio_optimiser_claude.hitl pending|route`.
- `notify.py` — deliverable notification sinks that never break the no-silent-egress
invariant (§8): `console` and `file` deliver locally, `webhook` is the one transport that
leaves the machine and fires ONLY behind an explicit per-run opt-in flag
(`--allow-webhook-egress`) — mirroring the ingest-spec §8 rule that *the flag is a run
argument, never a config field, so the config cannot grant itself network access*. The
webhook transport is injected: the suite passes a canned transport (no socket is ever
opened), and the real transport lives behind one seam function that the suite never calls (a
grep-guard proves no network path exists elsewhere in the module). `run.py` (on both
outcomes — a budget stop notifies too) and `hitl.py` (read-only preserved) share the same
opt-in-gated CLI seam, refusing a webhook-without-opt-in *before* any spend. The payload
shape is stack-local (no shared notification spec across the siblings).
### Load-bearing tests (§11)
@ -164,7 +175,12 @@ artifacts and stops before the first model call — a call-counting client prove
red the moment the stop seam is detached),
`test_hitl_loadbearing.py` (a proposal with no verdict is listed pending and disappears once
an inbox or promoted verdict shares its id — red the moment the id-join filter is detached —
and hitl never writes any layer, proven by a before/after byte snapshot), and
and hitl never writes any layer, proven by a before/after byte snapshot),
`test_notify_loadbearing.py` and `test_notify_seam_loadbearing.py` (a webhook without the
per-run opt-in flag refuses fail-fast and its transport never fires — red the moment the gate
is detached — the canned transport receives the structured payload, an AST grep-guard proves
no network path lives outside the one injectable seam function, and the run/hitl entrances
emit on their outcomes while hitl stays read-only), and
`test_sdk_isolation.py` (local config cannot capture the checker).
## The ingest layer — CSV and SQL, in front of the loop
@ -227,7 +243,7 @@ Python ≥3.10 · [`claude-agent-sdk`](https://pypi.org/project/claude-agent-sdk
```bash
uv sync # install dependencies
uv run pytest # 544 tests — run without any API key and without network
uv run pytest # 562 tests — run without any API key and without network
uv run ruff check . && uv run ruff format --check .
uv run mypy src # strict
```