--- id: BINKY-WP-0007 type: workplan title: "Qonto Governed Assistant: policy kernel + REST (Phase 1)" domain: infotech repo: binky-control status: finished owner: claude topic_slug: the-custodian created: "2026-07-22" updated: "2026-07-23" state_hub_workstream_id: "8a691acb-3500-4f17-84fe-dcfc7a979fdc" --- Follow-on from BINKY-WP-0005 (custody lane + first read-only pull, finished). Blueprint: `integrations/qonto-governed-assistant/ArchitectureBlueprint.md`. **Recommendation being executed:** build a small **Qonto Governed Assistant** service — the only component allowed to hold the bank API key — exposing a default-deny policy-shaped MCP server and thin REST API, so every harness (Claude Code, Codex, Cursor, Grok, agent-harness sessions, scripts) is a dumb 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** (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 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 id: BINKY-WP-0007-T01 status: done priority: high state_hub_task_id: "34474d61-0238-4321-a6c0-27c3ad3b8fe3" ``` **2026-07-22:** DEC-2026-005 approved as recommended (all five). See `DecisionQueue.md` for the ratified answers. Bring the five decision requests from the blueprint to the founder via DecisionQueue.md: 1. Adopt the domain-assistant architecture (option B) for Qonto. 2. Repo home: new dedicated `qonto-assistant` repo (already present in the state-hub repo registry) vs. waiting for a generic `finance-connect`. 3. v1 policy freeze: hard deny on all spend / volume-cost tools. 4. MCP transport: remote streamable-HTTP only for production clients. 5. Whether to ever run vendor `qonto-mcp-server` as an internal-only backend (blueprint default: no). Done when: DEC entry filed, founder decision recorded, blueprint updated with the ratified answers. ## Task: Service skeleton — decision function + custody wiring ```task id: BINKY-WP-0007-T02 status: done priority: high state_hub_task_id: "8978dbd9-c7a1-4b5b-9c0c-acf3e08c74bf" ``` **2026-07-23:** Delivered in the `qonto-assistant` repo as `QONTO-WP-0002` (T01–T07, 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: done priority: medium state_hub_task_id: "a35e2565-e520-491e-8587-8ec4961f898c" ``` **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). 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: done priority: medium state_hub_task_id: "a16ef7df-ddd7-474b-8327-0eca5c89ede7" ``` **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` (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. 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.