BINKY-WP-0007: close out against qonto-assistant's actual delivery
Some checks failed
Work Records / validate (push) Has been cancelled
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:
parent
56f9abfee8
commit
ea568558a8
2 changed files with 181 additions and 31 deletions
99
scripts/qonto-costrunrate-refresh.py
Executable file
99
scripts/qonto-costrunrate-refresh.py
Executable 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())
|
||||||
|
|
@ -4,11 +4,11 @@ type: workplan
|
||||||
title: "Qonto Governed Assistant: policy kernel + REST (Phase 1)"
|
title: "Qonto Governed Assistant: policy kernel + REST (Phase 1)"
|
||||||
domain: infotech
|
domain: infotech
|
||||||
repo: binky-control
|
repo: binky-control
|
||||||
status: active
|
status: finished
|
||||||
owner: claude
|
owner: claude
|
||||||
topic_slug: the-custodian
|
topic_slug: the-custodian
|
||||||
created: "2026-07-22"
|
created: "2026-07-22"
|
||||||
updated: "2026-07-22"
|
updated: "2026-07-23"
|
||||||
state_hub_workstream_id: "8a691acb-3500-4f17-84fe-dcfc7a979fdc"
|
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,
|
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).
|
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
|
This workplan tracks **Phase 1** (policy kernel + REST — minimum useful
|
||||||
product, per blueprint §6). Phase 2 (MCP surface), Phase 3 (flex-auth/fleet),
|
product, per blueprint §6) plus the binky-control-side consumer cutover.
|
||||||
and Phase 4 (productization) are follow-on work, opened as residuals or a new
|
Phase 3 (flex-auth/fleet) and Phase 4 (productization) remain follow-on work.
|
||||||
workplan once Phase 1 lands.
|
|
||||||
|
**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 T02–T04 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)
|
## Task: Ratify architecture decisions (blueprint §9)
|
||||||
|
|
||||||
|
|
@ -58,52 +70,91 @@ the ratified answers.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: BINKY-WP-0007-T02
|
id: BINKY-WP-0007-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "8978dbd9-c7a1-4b5b-9c0c-acf3e08c74bf"
|
state_hub_task_id: "8978dbd9-c7a1-4b5b-9c0c-acf3e08c74bf"
|
||||||
```
|
```
|
||||||
|
|
||||||
In the `qonto-assistant` repo (per T01 decision): shared `decide(tool, args,
|
**2026-07-23:** Delivered in the `qonto-assistant` repo as `QONTO-WP-0002`
|
||||||
claims) -> Allow|Deny` module backed by `policy/qonto-v1.yaml` (blueprint
|
(T01–T07, all done), not as new work in binky-control — the decision from T01
|
||||||
§4.6); OpenBao fetch of `tenants/binky/qonto-api` only inside this service
|
already routed the implementation there. Shipped: protocol-neutral capability
|
||||||
(AppRole/OIDC role, short TTL) — never in harness env. Hard deny list for
|
core, declarative `policy/qonto-v1.yaml` + pure `PolicyEngine.decide()`
|
||||||
every write/spend path, even unimplemented ones. Green lane (no live traffic
|
(default-deny; allows `org_summary`, `list_transactions`,
|
||||||
yet).
|
`cost_run_rate_hints`, `snapshot_bundle`; hard-denies `spend`,
|
||||||
|
`volume_cost`, `credential_exfil`), a Qonto REST client with
|
||||||
Done when: policy module has unit tests for allow/deny cases including the
|
`Authorization: login:key` injection and no secret logging, plus guardrails
|
||||||
deny-classes (`spend`, `volume_cost`, `credential_exfil`).
|
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: REST surface + CostRunRate consumer cutover
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: BINKY-WP-0007-T03
|
id: BINKY-WP-0007-T03
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "a35e2565-e520-491e-8587-8ec4961f898c"
|
state_hub_task_id: "a35e2565-e520-491e-8587-8ec4961f898c"
|
||||||
```
|
```
|
||||||
|
|
||||||
Implement `GET /v1/accounts`, `GET /v1/transactions`, `GET /v1/snapshot`
|
**2026-07-23:** REST surface (`GET /v1/accounts`, `/v1/transactions`,
|
||||||
behind the policy gate from T02. Replace the ad-hoc first-pull script used
|
`/v1/snapshot`, `/v1/health`) shipped in `qonto-assistant` as
|
||||||
for `finance/CostRunRate.md` with a call through this REST surface. Blue lane
|
`QONTO-WP-0002-T04`, behind the same policy kernel as T02. That repo
|
||||||
(reads live account data; no writes).
|
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
|
Verified end-to-end against a locally started `qonto-assistant` instance
|
||||||
once, with call metadata (actor, tool, decision) logged to evidence, not the
|
using `QONTO_FIXTURE_DIR` (no real Qonto credentials): `/v1/health` reachable,
|
||||||
raw account data.
|
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: Smoke tests and closure
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: BINKY-WP-0007-T04
|
id: BINKY-WP-0007-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "a16ef7df-ddd7-474b-8327-0eca5c89ede7"
|
state_hub_task_id: "a16ef7df-ddd7-474b-8327-0eca5c89ede7"
|
||||||
```
|
```
|
||||||
|
|
||||||
CI smoke against a mocked Qonto backend (deny-path and allow-path cases);
|
**2026-07-23:** REST smoke (`scripts/smoke_rest_api.py`) and MCP smoke
|
||||||
one manual live read against the dogfood account through the deployed
|
(`scripts/smoke_mcp.py`) both live in `qonto-assistant` and both pass against
|
||||||
assistant. Close this workplan when Phase 1 is proven; hand off Phase 2 (MCP
|
fixtures with no real credentials (`QONTO-WP-0002-T07`, `QONTO-WP-0003-T06`).
|
||||||
surface for all harnesses) as a residual intake or new workplan.
|
Phase 2 (MCP surface for all harnesses) was **not** held for a hand-off — it
|
||||||
|
was already built and closed as `QONTO-WP-0003` (T01–T07, 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
|
This workplan (Phase 1 + consumer cutover) is closed. **Known gaps carried
|
||||||
either as an AutopilotWorkQueue residual or a follow-on workplan file.
|
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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue