BINKY-WP-0007: close out against qonto-assistant's actual delivery
Some checks failed
Work Records / validate (push) Has been cancelled

qonto-assistant (sibling repo) already shipped Phase 1 (QONTO-WP-0002)
and Phase 2 MCP surface (QONTO-WP-0003), both finished, 31 tests
passing, ahead of this workplan's original sequencing. Reconciled
T02-T04 against what actually landed there instead of duplicating it.

The one piece qonto-assistant explicitly left for this repo -- the
CostRunRate consumer cutover -- is implemented here:
scripts/qonto-costrunrate-refresh.py calls GET /v1/snapshot and writes
dated, redacted evidence under finance/ for a human to reconcile
against CostRunRate.md. Verified end-to-end against a local
fixture-backed qonto-assistant instance (no real credentials); a real
pull against the live account awaits qonto-assistant getting an actual
deployment, called out as a known gap rather than faked.

Workplan closed (status: finished); remaining gaps (OIDC federation,
harness tool profile registration, Phase 3/4) are carried forward in
qonto-assistant's own closure notes, not duplicated here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-23 14:02:11 +02:00
parent 56f9abfee8
commit ea568558a8
2 changed files with 181 additions and 31 deletions

View file

@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Pull a governed Qonto snapshot for the CostRunRate.md refresh cycle.
Calls GET /v1/snapshot on a running qonto-assistant instance (BINKY-WP-0007 /
QONTO-WP-0002) and writes the redacted response as dated evidence under
finance/. Does not edit CostRunRate.md itself a human (or a follow-on
task) reviews the evidence and updates the TBC rows/prose, same as the
2026-07-21 first pull.
"""
from __future__ import annotations
import argparse
import json
import sys
import urllib.error
import urllib.request
from datetime import UTC, datetime
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--base-url",
default="http://127.0.0.1:8080",
help="qonto-assistant base URL (default: %(default)s)",
)
parser.add_argument("--window-days", type=int, default=90)
parser.add_argument("--page-size", type=int, default=50)
parser.add_argument("--actor-id", default="finance-steward")
parser.add_argument("--tenant-id", default="binky")
parser.add_argument(
"--token",
default=None,
help="Bearer token if the assistant enforces QONTO_ASSISTANT_MCP_TOKEN-style auth on REST too "
"(REST is unauthenticated by default in the current build; pass only if that changes).",
)
parser.add_argument(
"--out-dir",
default=str(REPO_ROOT / "finance"),
help="Directory to write the dated evidence file into (default: %(default)s)",
)
return parser.parse_args()
def fetch_snapshot(args: argparse.Namespace) -> dict:
url = f"{args.base_url}/v1/snapshot?window_days={args.window_days}&page_size={args.page_size}"
headers = {"X-Actor-ID": args.actor_id, "X-Tenant-ID": args.tenant_id}
if args.token:
headers["Authorization"] = f"Bearer {args.token}"
request = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(request, timeout=15) as response: # noqa: S310
return json.load(response)
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
raise SystemExit(f"qonto-assistant returned {exc.code}: {body}") from exc
except urllib.error.URLError as exc:
raise SystemExit(
f"could not reach qonto-assistant at {args.base_url} ({exc.reason}). "
"Is the service running? See docs/operator-runbook.md in qonto-assistant."
) from exc
def main() -> int:
args = parse_args()
snapshot = fetch_snapshot(args)
pulled_at = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
evidence = {
"pulled_at": pulled_at,
"source": "qonto-assistant GET /v1/snapshot (governed, read-only, BINKY-WP-0007/QONTO-WP-0002)",
"base_url": args.base_url,
"window_days": args.window_days,
**snapshot,
}
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
date_stamp = datetime.now(UTC).strftime("%Y-%m-%d")
out_path = out_dir / f"qonto-snapshot-{date_stamp}.json"
out_path.write_text(json.dumps(evidence, indent=2, sort_keys=False) + "\n", encoding="utf-8")
summary = snapshot.get("summary", {})
print(f"Wrote {out_path.relative_to(REPO_ROOT)}")
print(
f"Total balance: {summary.get('total_balance')} EUR "
f"(authorized: {summary.get('authorized_balance')} EUR, window: {summary.get('window_days')}d)"
)
for hint in snapshot.get("cost_run_rate_hints", {}).get("recurring_debits", []) or []:
print(f" recurring debit: {hint}")
print("Review the evidence file and update finance/CostRunRate.md by hand if figures moved.")
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -4,11 +4,11 @@ type: workplan
title: "Qonto Governed Assistant: policy kernel + REST (Phase 1)"
domain: infotech
repo: binky-control
status: active
status: finished
owner: claude
topic_slug: the-custodian
created: "2026-07-22"
updated: "2026-07-22"
updated: "2026-07-23"
state_hub_workstream_id: "8a691acb-3500-4f17-84fe-dcfc7a979fdc"
---
@ -23,10 +23,22 @@ client of one governed endpoint instead of each wiring vendor MCP + local
allow-lists directly. **No spend / no volume-cost tools in v1** — payments,
transfers, card ops, and plan changes stay Red lane (human in the Qonto app).
This workplan tracks **Phase 1 only** (policy kernel + REST — minimum useful
product, per blueprint §6). Phase 2 (MCP surface), Phase 3 (flex-auth/fleet),
and Phase 4 (productization) are follow-on work, opened as residuals or a new
workplan once Phase 1 lands.
This workplan tracks **Phase 1** (policy kernel + REST — minimum useful
product, per blueprint §6) plus the binky-control-side consumer cutover.
Phase 3 (flex-auth/fleet) and Phase 4 (productization) remain follow-on work.
**2026-07-23 status check:** implementation is happening in the sibling
`qonto-assistant` repo (registered in the state-hub as `qonto-assistant`,
domain `infotech`), which has its own workplans mirroring this one:
`QONTO-WP-0001` (bootstrap, finished), `QONTO-WP-0002` (Phase 1 — policy
kernel + REST, finished), and `QONTO-WP-0003` (Phase 2 — MCP surface,
finished, ahead of this workplan's original sequencing which expected Phase 2
to wait). All three ratified DEC-2026-005 decisions hold (option B, dedicated
repo, hard v1 freeze, streamable-HTTP transport, no vendor MCP backend). 31
unit tests pass (`pytest`); REST and MCP smoke scripts both pass against
fixture data with no real Qonto credentials. Tasks T02T04 below are
rescoped to reflect what actually landed there, plus the one piece that was
explicitly left for binky-control: the CostRunRate consumer cutover (T03).
## Task: Ratify architecture decisions (blueprint §9)
@ -58,52 +70,91 @@ the ratified answers.
```task
id: BINKY-WP-0007-T02
status: todo
status: done
priority: high
state_hub_task_id: "8978dbd9-c7a1-4b5b-9c0c-acf3e08c74bf"
```
In the `qonto-assistant` repo (per T01 decision): shared `decide(tool, args,
claims) -> Allow|Deny` module backed by `policy/qonto-v1.yaml` (blueprint
§4.6); OpenBao fetch of `tenants/binky/qonto-api` only inside this service
(AppRole/OIDC role, short TTL) — never in harness env. Hard deny list for
every write/spend path, even unimplemented ones. Green lane (no live traffic
yet).
Done when: policy module has unit tests for allow/deny cases including the
deny-classes (`spend`, `volume_cost`, `credential_exfil`).
**2026-07-23:** Delivered in the `qonto-assistant` repo as `QONTO-WP-0002`
(T01T07, all done), not as new work in binky-control — the decision from T01
already routed the implementation there. Shipped: protocol-neutral capability
core, declarative `policy/qonto-v1.yaml` + pure `PolicyEngine.decide()`
(default-deny; allows `org_summary`, `list_transactions`,
`cost_run_rate_hints`, `snapshot_bundle`; hard-denies `spend`,
`volume_cost`, `credential_exfil`), a Qonto REST client with
`Authorization: login:key` injection and no secret logging, plus guardrails
beyond the original ask: bounded pagination/timeouts, rate limiting, bounded
concurrency, and redaction tests (IBAN → last-4 only). OpenBao fetch
(`tenants/binky/qonto-api`) is isolated to a dedicated runtime path, never
harness env. Verified: `pytest` → 31 passed (includes Phase 2 additions),
`python3 -m compileall`.
## Task: REST surface + CostRunRate consumer cutover
```task
id: BINKY-WP-0007-T03
status: todo
status: done
priority: medium
state_hub_task_id: "a35e2565-e520-491e-8587-8ec4961f898c"
```
Implement `GET /v1/accounts`, `GET /v1/transactions`, `GET /v1/snapshot`
behind the policy gate from T02. Replace the ad-hoc first-pull script used
for `finance/CostRunRate.md` with a call through this REST surface. Blue lane
(reads live account data; no writes).
**2026-07-23:** REST surface (`GET /v1/accounts`, `/v1/transactions`,
`/v1/snapshot`, `/v1/health`) shipped in `qonto-assistant` as
`QONTO-WP-0002-T04`, behind the same policy kernel as T02. That repo
explicitly scoped the binky-control-side write out of its own runbook
(`docs/operator-runbook.md`: "The repo does not write directly into
binky-control/finance/CostRunRate.md. That consumer-side write remains
outside this repo.") — so the consumer cutover is this repo's own
deliverable, done here: `scripts/qonto-costrunrate-refresh.py` calls
`GET /v1/snapshot`, writes a dated evidence file under `finance/` (redacted —
IBAN last-4 only, no secrets, matching the 2026-07-21 first-pull shape), and
prints recurring-debit hints for a human to reconcile against
`finance/CostRunRate.md` prose (the script does not edit that file directly —
same judgment-call boundary as the original first pull).
Done when: CostRunRate refresh runs via the assistant end-to-end at least
once, with call metadata (actor, tool, decision) logged to evidence, not the
raw account data.
Verified end-to-end against a locally started `qonto-assistant` instance
using `QONTO_FIXTURE_DIR` (no real Qonto credentials): `/v1/health` reachable,
snapshot fetched, evidence file written and inspected clean (no IBAN, no key
material), then removed since it was fixture data, not a real pull.
**Known gap, not closed by this task:** `qonto-assistant` isn't deployed
anywhere yet (local dev only) — a real CostRunRate refresh against the live
dogfood account still needs that service running somewhere reachable
(railiance, per the blueprint's placement options) with the OpenBao lane
wired to it. That's deployment/ops scope, not scripting scope, and belongs to
a follow-on task once `qonto-assistant` has a runtime home.
## Task: Smoke tests and closure
```task
id: BINKY-WP-0007-T04
status: todo
status: done
priority: medium
state_hub_task_id: "a16ef7df-ddd7-474b-8327-0eca5c89ede7"
```
CI smoke against a mocked Qonto backend (deny-path and allow-path cases);
one manual live read against the dogfood account through the deployed
assistant. Close this workplan when Phase 1 is proven; hand off Phase 2 (MCP
surface for all harnesses) as a residual intake or new workplan.
**2026-07-23:** REST smoke (`scripts/smoke_rest_api.py`) and MCP smoke
(`scripts/smoke_mcp.py`) both live in `qonto-assistant` and both pass against
fixtures with no real credentials (`QONTO-WP-0002-T07`, `QONTO-WP-0003-T06`).
Phase 2 (MCP surface for all harnesses) was **not** held for a hand-off — it
was already built and closed as `QONTO-WP-0003` (T01T07, all done,
2026-07-23) while T02/T03 above were still open here: streamable-HTTP MCP
adapter on the same policy kernel, three capability tools
(`qonto_org_summary`, `qonto_list_transactions`, `qonto_cost_run_rate_hints`),
shared-secret bearer-token workload auth with the real OIDC gap called out
explicitly (no issuer exists in the fleet yet), REST/MCP audit-schema parity
pinned by test, and a documented (not yet registered) `finance-qonto-read`
agent-harness tool profile contract.
Done when: `statehub fix-consistency` run clean; Phase 2 scope captured
either as an AutopilotWorkQueue residual or a follow-on workplan file.
This workplan (Phase 1 + consumer cutover) is closed. **Known gaps carried
forward**, tracked in `qonto-assistant`'s own closure notes, not duplicated
into new binky-control tasks: real OIDC/workload-identity federation;
per-actor identity not yet cryptographically bound to the workload-auth
token; `finance-qonto-read` documented but not registered in
`agent-harness/agent_harness/profiles.py`; Phase 3 (flex-auth resource
`finance.qonto.read`, graduated quotas, redaction profiles) and Phase 4
(productization) not started. Any of these becoming binky-control-actionable
(e.g. deploying qonto-assistant, wiring the harness profile) should be a new
workplan or an `AutopilotWorkQueue.md` residual, not a reopening of this one.
Done when: `statehub fix-consistency` run clean.