Bootstrap qonto-assistant: intent, blueprint, research, workplans

Author INTENT and SCOPE; persist ArchitectureBlueprint under specs/ and MCP
gateway research under research/. Register with State Hub (QONTO-WP- prefix);
seed QONTO-WP-0001 (bootstrap, finished) and QONTO-WP-0002 (Phase 1 REST
policy kernel, ready).
This commit is contained in:
tegwick 2026-07-21 23:32:24 +02:00
parent bae0734564
commit e1ab23f83e
10 changed files with 1330 additions and 1 deletions

6
.gitignore vendored
View file

@ -162,3 +162,9 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# state-hub: track .claude/rules
# Claude Code local state (track shared rules; ignore machine-specific files)
.claude/*
!.claude/rules/
!.claude/rules/*.md

206
AGENTS.md Normal file
View file

@ -0,0 +1,206 @@
# qonto-assistant — Agent Instructions
## Repo Identity
**Purpose:** Policy-governed Qonto domain REST+MCP assistant: sole bank-key consumer, default-deny no-spend/no-volume-cost policy for multi-harness finance awareness.
**Domain:** infotech
**Repo slug:** qonto-assistant
**Topic ID:** `cee7bedf-2b48-46ef-8601-006474f2ad7a`
**Workplan prefix:** `QONTO-WP-`
---
## State Hub Integration
The Custodian State Hub tracks work across all domains. Interact via HTTP REST —
there is no MCP server for Codex agents.
| Context | URL |
|---------|-----|
| Local workstation | `http://127.0.0.1:8000` |
| Remote via tunnel | `http://127.0.0.1:18000` |
| Optional local edge relay | http://127.0.0.1:18080 |
When an operator has enabled the edge relay, set API_BASE to the relay URL.
Queueable writes return an explicit queued receipt if the central hub is
unreachable. Treat that as pending local evidence, then ask the operator to run
statehub outbox status/replay after connectivity returns.
### Orient at session start
```bash
# Offline brief — works without hub connection
cat .custodian-brief.md
# Active workplans for this domain
curl -s "http://127.0.0.1:8000/workplans/?topic_id=cee7bedf-2b48-46ef-8601-006474f2ad7a&status=active" \
| python3 -m json.tool
# Check inbox
curl -s "http://127.0.0.1:8000/messages/?to_agent=qonto-assistant&unread_only=true" \
| python3 -m json.tool
```
Mark a message read:
```bash
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
-H "Content-Type: application/json" -d '{}'
```
### Log progress (required at session close)
```bash
curl -s -X POST http://127.0.0.1:8000/progress/ \
-H "Content-Type: application/json" \
-d '{
"summary": "what was done",
"event_type": "note",
"author": "codex",
"workplan_id": "<uuid>",
"task_id": "<uuid>"
}'
```
Omit `workplan_id` / `task_id` when not applicable.
### Update task status
```bash
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
-H "Content-Type: application/json" \
-d '{"status": "progress"}'
# values: wait | todo | progress | done | cancel
```
### Flag a task for human review
```bash
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
-H "Content-Type: application/json" \
-d '{"needs_human": true, "intervention_note": "reason"}'
```
---
## Session Protocol
**Start:**
1. `cat .custodian-brief.md` — domain goal and open workplans (offline-safe)
2. Check inbox: `GET /messages/?to_agent=qonto-assistant&unread_only=true`; mark read
3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
**During work:**
- Update task statuses in workplan files as tasks progress
- Record significant decisions via `POST /decisions/`
**Close:**
1. Update workplan file task statuses to reflect progress
2. Log: `POST /progress/` with a summary of what changed
3. After workplan file changes, run:
```bash
statehub fix-consistency
```
Coding agents should run this directly; ask the operator only if the CLI or
State Hub API is unavailable. This syncs task status from files into the hub DB.
---
{CREDENTIAL_ROUTING}
<!-- REPO-AGENTS-EXTENSIONS -->
<!-- Append repo-specific agent instructions below this marker.
The state-hub template sync preserves content after this line. -->
## Repo-specific orientation
| Doc | Role |
| --- | --- |
| `INTENT.md` | Why / never-become |
| `specs/ArchitectureBlueprint.md` | Architecture + phases |
| `research/` | Pattern research |
| `SCOPE.md` | In/out of scope |
**Security:** Never print or commit Qonto API keys. Service alone fetches
`tenants/binky/qonto-api` (`API_KEY`, `API_USER`). Clients use assistant URL +
workload identity only. Policy v1: **no spend / no volume-cost**.
**Local verification (until QONTO-WP-0002 lands runtime):**
```bash
# Workplan frontmatter ↔ hub
statehub fix-consistency
# Docs present
test -f INTENT.md && test -f specs/ArchitectureBlueprint.md
```
After Phase 1 skeleton exists, prefer `make test` / `make lint` as defined in
the Makefile from QONTO-WP-0002-T01.
---
## Workplan Convention (ADR-001)
Work items originate as files in this repo — not in the hub. The hub is a
read/cache/index layer that rebuilds from files.
**File location:** `workplans/QONTO-WP-NNNN-<slug>.md`
**Archived location:** finished workplans may move to
`workplans/archived/YYMMDD-QONTO-WP-NNNN-<slug>.md`. The `YYMMDD` prefix is
the completion/archive date; the frontmatter `id` does not change.
**Ad Hoc Tasks:** small opportunistic fixes discovered during a session use
`workplans/ADHOC-YYYY-MM-DD.md` with task ids `ADHOC-YYYY-MM-DD-T01`, etc. Use
this only for low-risk work completed directly; create a normal workplan for
anything needing analysis, design, approval, dependencies, or multiple phases.
**Frontmatter:**
```yaml
---
id: QONTO-WP-NNNN
type: workplan
title: "..."
domain: infotech
repo: qonto-assistant
status: proposed | ready | active | blocked | backlog | finished | archived
owner: codex
topic_slug: ...
created: "YYYY-MM-DD"
updated: "YYYY-MM-DD"
state_hub_workstream_id: "<uuid>" # fix-consistency — do not edit (legacy field name; workplan UUID)
---
```
Use `proposed` for a new draft, `ready` after review against current repo
state, and `finished` after implementation. `stalled` and `needs_review` are
derived health labels, not frontmatter statuses.
**Terminology:** workplan is the fleet term; `workstream` appears only in legacy
API/MCP/frontmatter bridges until `STATE-WP-0069` retires them — see
`the-custodian/canon/standards/workplan-terminology-fleet_v0.1.md`.
**Task block format** (one per `##` section):
```
## Task Title
` ` `task
id: QONTO-WP-NNNN-T01
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
` ` `
Task description text.
```
Status progression: `todo``progress``done`; use `wait` for waiting/blocked work and `cancel` for stopped work.
To create a new workplan:
1. Write the file following the format above
2. Run `statehub fix-consistency` locally; ask the operator only if the CLI or
State Hub API is unavailable.

135
INTENT.md Normal file
View file

@ -0,0 +1,135 @@
# INTENT
> This file explains why **qonto-assistant** exists — the problem it solves,
> the principle that governs its boundaries, and what it must never become.
> Blueprint: `specs/ArchitectureBlueprint.md`. Origin: binky-control
> BINKY-WP-0005 (Qonto custody + first read-only pull) and the multi-harness
> policy-drift problem (2026-07-21).
## Why it exists
Binky (and later other tenants) need agents to **see** bank reality —
balances, transactions, cost-run-rate signals — so the company control plane
can operate without founder spreadsheet archaeology. Credentials already live
in OpenBao (`tenants/binky/qonto-api`); the thirdparty REST API works.
What fails as scale:
- Wiring vendor MCP or bank keys into **every** coding agent / harness
(Claude, Codex, Cursor, Grok, agent-harness, …) creates **policy drift**.
- Qonto API keys are **not** scope-limited server-side: there is no
“read-only key.” Spend and volume-cost actions must be blocked **before**
the vendor sees them.
- Agent-harness is the session policy shell, not the place to reimplement
bank rules per runtime. llm-connect is LLM routing, not domain tools.
**qonto-assistant** exists so that **one governed domain service** is the
only component allowed to hold the bank credential and speak Qontos
protocol. All harnesses are clients of that service. Policy is code + tests
in one place.
## Purpose
`qonto-assistant` is a **policy-governed Qonto domain assistant**: a dual
surface (**REST + MCP**) for agentic and scripted finance awareness under a
hard **no spend / no volume-cost** policy (v1).
Core principle:
> Harnesses never hold the bank key. The assistant is the choke point.
> Default deny. Reads for control-plane awareness; payments stay Red lane
> (human in the Qonto app).
## Primary utility
### As a multi-harness finance tool
- Streamable-HTTP **MCP** for interactive agents (one client config, no
secrets in MCP configs).
- Stable **REST** for rhythm sessions, CostRunRate refresh, CI smoke, and
non-MCP clients.
- Shared **policy kernel** on every call (tool id + arguments + actor
claims) — not connect-time allow-lists alone.
### As a custody-aware connector
- Fetches secrets only via the sanctioned OpenBao path (ops-warden catalog
`binky-qonto-api` / CCR-2026-0008 pattern).
- Optional flex-auth gate: `finance.qonto.read` (and later finer scopes).
- Audit metadata (who/what/decision) without logging secret material.
### As dogfood for Operational Knowledge
- Internal use on Binky is product evidence (governed bank access for agent
fleets). May graduate to multi-tenant `tenants/<slug>/qonto-api` later.
## Strategic role
```text
OpenBao custody (existing)
+ sole Qonto client identity
+ default-deny tool catalog
+ dual MCP + REST
= consistent finance awareness across every harness
```
It sits **below** agent-harness (session runtime) and **beside** other domain
connectors (email-connect). It does not replace DATEV/StB/DUO; it complements
them with operational bank visibility.
## Governing principles
1. **One policy, many clients** — never reimplement bank rules per harness.
2. **Default deny** — only explicitly allowed read capabilities exist in v1.
3. **No token passthrough** — clients authenticate *to the assistant*; the
assistant alone authenticates *to Qonto*.
4. **Semantic policy, not vendor dump** — we design a safe catalog; we do
not auto-mirror vendor write tools.
5. **Platform fit** — flex-auth, OpenBao, ops-warden, State Hub audit; no
parallel IAM.
6. **Autonomy lanes** — map to binky-control AutonomyPolicy: reads Green/Blue;
plan change / transfers / API key minting remain Red.
## What it must never become
- **Not a payments engine.** No transfers, SEPA, card issuance, or
fee-bearing create operations in v1 (and only via explicit future DEC +
Red/Yellow gates thereafter).
- **Not a full accounting system.** DATEV / StB / DUO stay authoritative for
books.
- **Not a generic MCP gateway.** Fleet gateways may sit *in front* later;
domain policy lives here.
- **Not an LLM router.** Provider selection stays in llm-connect.
- **Not a scheduler.** Cadence stays in activity-core / agent-harness.
- **Not a secret store.** OpenBao remains custody; this service is a
short-lived consumer.
- **Not a per-harness plugin bag.** Client integration is thin: URL +
workload identity + tool profile name.
## Relationship to other repos
| Repo | Relationship |
| --- | --- |
| `binky-control` | First tenant / consumer; CostRunRate, AutonomyPolicy, business policy source |
| `ops-warden` / `railiance-platform` | Credential front door and CCR/policy for `tenants/…/qonto-api` |
| `agent-harness` | Grants profile `finance-qonto-read` pointing at this service only |
| `llm-connect` | Models that *talk*; this service *acts* on bank data |
| `flex-auth` | Authorization decisions for who may call |
| `email-connect` | Sibling pattern: purpose-built connector, not raw vendor wiring |
## Success looks like
1. Every agent that needs Qonto data uses **only** this service.
2. A single policy change (e.g. tighten export) applies fleet-wide.
3. No bank API key appears in harness env, MCP client configs, or chat.
4. Deny decisions are tested and audited; spend tools are not registered.
5. CostRunRate and Finance Steward can refresh from REST without ad-hoc scripts
holding secrets.
## Normative references
- `specs/ArchitectureBlueprint.md` — architecture and phased delivery
- `research/2026-07-21-mcp-gateway-and-governed-domain-assistant.md` — external
pattern research
- binky-control `integrations/qonto-mcp.md` — custody and first-pull evidence
- agent-harness ADR-001 / INTENT — session runtime boundaries

View file

@ -1,3 +1,25 @@
# qonto-assistant
A policy governed qonto domain API and MCP assistant for binky-control.
Policy-governed Qonto domain API and MCP assistant for Binky (and later
multi-tenant dogfood).
**One choke point for bank access across every coding agent and harness.**
Clients never hold the Qonto API key. v1 policy: **read for awareness only —
no spend, no volume-cost actions.**
## Start here
| Doc | What |
| --- | --- |
| [`INTENT.md`](INTENT.md) | Why this exists; boundaries |
| [`specs/ArchitectureBlueprint.md`](specs/ArchitectureBlueprint.md) | Architecture, phases, policy model |
| [`research/2026-07-21-mcp-gateway-and-governed-domain-assistant.md`](research/2026-07-21-mcp-gateway-and-governed-domain-assistant.md) | External + internal research |
## Status
Scaffold + intent + blueprint. Implementation tracked in `workplans/`.
## Related
- OpenBao lane: `tenants/binky/qonto-api` (ops-warden `binky-qonto-api`)
- First pull / CostRunRate: `binky-control` BINKY-WP-0005

55
SCOPE.md Normal file
View file

@ -0,0 +1,55 @@
# SCOPE
> Refined after `statehub register` (2026-07-21). Normative intent in
> `INTENT.md`; architecture in `specs/ArchitectureBlueprint.md`.
## One-liner
Policy-governed Qonto domain **REST + MCP** assistant: sole bank-key consumer
and default-deny **no spend / no volume-cost** policy for multi-harness finance
awareness.
## Core idea
All agents and harnesses talk to **this service** for Qonto. None of them hold
the OpenBao bank secret. Policy is enforced here once, not reimplemented per
client.
## In scope
- Dual surfaces: streamable-HTTP MCP and JSON REST with a **shared policy kernel**
- Read capabilities: org/accounts/balances, transactions (bounded), CostRunRate
hints / snapshot helpers
- OpenBao consumption for `tenants/<tenant>/qonto-api` (dogfood: binky)
- Authn of callers (OIDC / workload identity) and optional flex-auth checks
- Audit metadata (actor, tool, allow/deny) without secret leakage
- Tests for default-deny and spend/volume-cost rejection
- Docs for harness client config (no bank secrets)
## Out of scope (v1 and hard)
- Payments, transfers, SEPA, card operations, invoicing create, plan changes
- Hosting or recommending the hosted Qonto OAuth MCP connector
- DATEV / StB / DUO bookkeeping replacement
- LLM provider routing (llm-connect)
- Agent scheduling (activity-core) or session runtime (agent-harness)
- Generic multi-MCP fleet gateway (may sit *in front* later; not this repos job)
- Storing bank credentials outside OpenBao
## Current state
| Artifact | Status |
| --- | --- |
| INTENT / SCOPE / AGENTS | present |
| Architecture blueprint | `specs/ArchitectureBlueprint.md` |
| Research notes | `research/2026-07-21-…` |
| Runtime implementation | not started (see workplans) |
| Upstream custody | live in platform (`binky-qonto-api`, CCR-2026-0008) |
## Getting oriented
1. `INTENT.md` — why / boundaries
2. `specs/ArchitectureBlueprint.md` — how
3. `research/2026-07-21-mcp-gateway-and-governed-domain-assistant.md` — why this shape
4. `workplans/` — what to do next
5. `AGENTS.md` — State Hub protocol

22
WORK-RECORDS.md Normal file
View file

@ -0,0 +1,22 @@
# Work Records — qonto-assistant
> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record
> stage 3). Do not edit by hand — edit the source file/block listed for
> each record and re-run fix-consistency to refresh this index. Archived
> workplans are omitted; closed decisions/intakes/engagements stay listed
> so recently-resolved work is still visible. [auto]
| Kind | ID | Status | Lane | Source |
| --- | --- | --- | --- | --- |
| workplan | QONTO-WP-0001 | finished | — | workplans/QONTO-WP-0001-statehub-bootstrap.md |
| workplan | QONTO-WP-0002 | ready | — | workplans/QONTO-WP-0002-policy-kernel-and-rest.md |
| task | QONTO-WP-0001-T01 | done | — | workplans/QONTO-WP-0001-statehub-bootstrap.md |
| task | QONTO-WP-0001-T02 | wait | — | workplans/QONTO-WP-0001-statehub-bootstrap.md |
| task | QONTO-WP-0001-T03 | done | — | workplans/QONTO-WP-0001-statehub-bootstrap.md |
| task | QONTO-WP-0002-T01 | todo | — | workplans/QONTO-WP-0002-policy-kernel-and-rest.md |
| task | QONTO-WP-0002-T02 | todo | — | workplans/QONTO-WP-0002-policy-kernel-and-rest.md |
| task | QONTO-WP-0002-T03 | todo | — | workplans/QONTO-WP-0002-policy-kernel-and-rest.md |
| task | QONTO-WP-0002-T04 | todo | — | workplans/QONTO-WP-0002-policy-kernel-and-rest.md |
| task | QONTO-WP-0002-T05 | todo | — | workplans/QONTO-WP-0002-policy-kernel-and-rest.md |
| task | QONTO-WP-0002-T06 | todo | — | workplans/QONTO-WP-0002-policy-kernel-and-rest.md |
| task | QONTO-WP-0002-T07 | todo | — | workplans/QONTO-WP-0002-policy-kernel-and-rest.md |

View file

@ -0,0 +1,172 @@
# Research: MCP gateways and governed domain assistants for bank access
> Date: 2026-07-21
> Purpose: Capture external and internal research that informed
> `specs/ArchitectureBlueprint.md` and `INTENT.md`.
> Not a product spec — evidence and pattern notes.
---
## 1. Question under investigation
How should multiple coding agents and harnesses interact with Qonto
**conveniently and under one policy**, without wiring vendor MCP into each
client (policy drift) and without relying on Qonto to enforce
read-only / no-spend rules at the API key layer?
---
## 2. Internal facts already established (BINKY-WP-0005)
| Fact | Evidence |
| --- | --- |
| Credentials live at `tenants/binky/qonto-api` | OpenBao; fields `API_KEY`, `API_USER` |
| Thirdparty REST works | First pull 2026-07-21; 122 txs; CostRunRate v2 |
| Auth shape | `Authorization: <API_USER>:<API_KEY>` |
| Self-hosted MCP env map | `API_KEY``QONTO_API_KEY`, `API_USER``QONTO_ORGANIZATION_ID` |
| API keys not scope-limited | Design note in binky-control `integrations/qonto-mcp.md` |
| Hosted OAuth MCP rejected | Corporate access policy: no native connector custody |
| Catalog / CCR | ops-warden `binky-qonto-api` active; CCR-2026-0008 active |
| Founder policy intent | No spend / no volume-cost actions for agents; payments Red lane |
---
## 3. External pattern research (20252026 MCP ecosystem)
### 3.1 MCP gateway / proxy as control plane
Industry deployments place a **gateway between untrusted MCP clients and
tools**:
| Source | Relevant capability |
| --- | --- |
| **Envoy AI Gateway MCPRoute** | Multiplex backends; `toolSelector` include/regex; upstream API-key injection so clients never hold vendor secrets; OAuth on gateway; CEL on `tools/call` params (`request.mcp.tool`, `request.mcp.params`) |
| **Kong AI Gateway MCP Tool ACLs** | Default-deny; per-consumer tool subsets at gateway (solves all-or-nothing MCP) |
| **Portkey / Speakeasy / Permit MCP Gateway** framing | Authn, RBAC, audit every tool call; policy on each call not only at connect time |
| **Harness MCP server docs** | Compatible with MCP gateways: centralized credentials, governance, audit, single endpoint |
**Lesson:** Clients connect to one governed endpoint; the gateway owns
credentials, published tool inventory, and policy.
### 3.2 MCP security best practices
Synthesized from MCP security best-practice docs and industry guides
(confused-deputy / token passthrough / OAuth proxy guidance):
| Practice | Application |
| --- | --- |
| No token passthrough | Clients must not present the bank key; assistant is sole Qonto client |
| Tool-level scopes | “Access to Qonto” too coarse; allow list_transactions, never create_transfer |
| Default deny | Unknown tools fail closed |
| JIT escalation for high privilege | Future spend only with explicit re-auth; never session-global write mode |
| Audit end-to-end | Actor + tool + decision; no secrets in logs |
| Inventory of MCP servers | Fleet must know what is connected (anti shadow-MCP) |
### 3.3 Gateway vs domain assistant
| Approach | Strength | Weakness for bank access |
| --- | --- | --- |
| Generic MCP gateway in front of **vendor** MCP | Fast tool filtering | Vendor catalog still includes write tools; weak **semantic** spend rules; unmaintained self-hosted Qonto MCP |
| **Domain assistant** (own MCP + REST, policy inside) | Safe catalog we design; dual protocol; productizable | Build cost |
| Per-client allow-lists only | Zero new service | Guaranteed policy drift |
**Conclusion adopted in blueprint:** build domain assistant; treat generic
gateways as optional later mesh in front of *our* service, not as sole
control in front of vendor MCP.
### 3.4 Anchoring policy at Qonto (API / MCP)
**Not available** for the chosen custody model:
- Self-hosted API keys: no fine-grained scopes from Qonto.
- Hosted OAuth MCP: write tools + credentials outside OpenBao = rejected
by corporate access policy.
Defense-in-depth still belongs in our choke point even if Qonto later adds
scopes.
---
## 4. Internal platform fit
| Component | Boundary (from INTENT/ADR) | Role for Qonto access |
| --- | --- | --- |
| **agent-harness** | Sole session credential holder / policy shell for *sessions*; not domain logic | Grant profile pointing at qonto-assistant only |
| **llm-connect** | Multi-provider LLM adapters | Models only — not bank tools |
| **flex-auth** | Authorization from identity claims | `finance.qonto.read` (etc.) |
| **OpenBao + ops-warden** | Custody + routing | Existing `binky-qonto-api` lane |
| **email-connect** | Purpose-built connector | Sibling pattern to copy |
| **binky-control** | Company brain / AutonomyPolicy | Policy source + CostRunRate consumer |
| **activity-core** | Schedule, not execute | When Finance Steward runs |
ADR-001 (agent-harness) implies: instances declare profiles; harness
enforces. Domain bank rules must live **below** the harness so interactive
IDE agents and unattended sessions share one policy.
---
## 5. Founder constraints captured in research session
1. Multiple coding agents and harnesses already in use; agent-harness +
llm-connect exist.
2. Desired policy: **no spending**, **no actions with volume cost**.
3. Direct per-harness MCP wiring is suboptimal (policy drift).
4. Preference to explore **qonto-proxy / governed API+MCP service**.
5. Interest in known agentic patterns — research above.
---
## 6. Architectural recommendation (outcome)
See `specs/ArchitectureBlueprint.md` in full. Summary:
```text
Many harnesses
→ authenticate to qonto-assistant (MCP and/or REST)
→ policy kernel (default-deny, no spend / no volume-cost)
→ sole OpenBao consumer of tenants/.../qonto-api
→ Qonto thirdparty API
```
Phased delivery:
0. Custody + first REST pull (done in binky-control)
1. Policy kernel + REST
2. MCP surface for all harnesses
3. flex-auth + rate limits
4. Multi-tenant / productization
---
## 7. Open questions carried into work planning
1. Repo runtime stack (Python FastAPI vs other) — decide in first
implementation workplan, not in research.
2. Whether vendor MCP is ever an internal backend (default: **no**; prefer
direct REST client).
3. AppRole for assistant-only secret fetch vs OIDC role only.
4. IBAN / PII redaction levels for agent channels.
---
## 8. Source index (non-exhaustive)
### Internal paths
- `binky-control/integrations/qonto-mcp.md`
- `binky-control/integrations/qonto-governed-assistant/ArchitectureBlueprint.md` (origin copy)
- `binky-control/finance/CostRunRate.md`, `finance/qonto-first-pull-2026-07-21.json`
- `agent-harness/INTENT.md`, `docs/adr/ADR-001-agent-harness-architecture.md`
- `llm-connect/ARCHITECTURE-LAYERS.md`
- `flex-auth/INTENT.md`
- ops-warden catalog entry `binky-qonto-api`; railiance-platform CCR-2026-0008
### External (retrieved 2026-07-21)
- Envoy AI Gateway MCP documentation (MCPRoute, toolSelector, authorization CEL)
- Kong AI Gateway MCP Tool ACLs product notes
- MCP gateway explainers (Speakeasy, Portkey, Permit, community)
- MCP security best practices (token passthrough, proxy consent, tool-level scopes)
- CSA / industry agentic MCP security maturity notes
- Harness MCP server gateway compatibility notes
- GitHub `qonto/qonto-mcp-server` README (env vars; maintained status warning for local server)

View file

@ -0,0 +1,499 @@
# Architecture Blueprint — Governed Qonto Assistant
> Status: blueprint v0.1 — 2026-07-21
> Repo: **qonto-assistant** (canonical home).
> Context: BINKY-WP-0005 finished (live OpenBao lane + first read-only pull).
> Question: how do multiple coding agents / harnesses interact with Qonto
> **conveniently and under one policy**, without wiring vendor MCP into each
> client and accepting policy drift?
> Related: `INTENT.md`, `research/2026-07-21-mcp-gateway-and-governed-domain-assistant.md`,
> binky-control `integrations/qonto-mcp.md`, AutonomyPolicy, agent-harness
> ADR-001, ops-warden catalog `binky-qonto-api`, CCR-2026-0008.
---
## 1. Problem
We already can:
1. Fetch company Qonto credentials from OpenBao (`tenants/binky/qonto-api`).
2. Call Qontos thirdparty REST API (proven: first pull, CostRunRate v2).
3. Map those credentials into the self-hosted `qonto/qonto-mcp-server` env
shape (`QONTO_API_KEY` / `QONTO_ORGANIZATION_ID`).
What we must **not** do as the long-term pattern:
- Install vendor MCP (or raw API keys) into Claude Desktop, Claude Code,
Codex, Cursor, Grok, agent-harness, executor-worker, etc. **separately**.
- Rely on each harnesss local tool allow-list as the only spend control.
- Hope every client remembers “read-only forever.”
Why that fails:
| Failure mode | Effect |
| --- | --- |
| **Policy drift** | One harness allows a write tool; another forbids it. Same company key, different blast radius. |
| **Credential sprawl** | Keys appear in N configs, Docker args, session env dumps. |
| **Audit fragmentation** | No single log of “which agent did what to the bank.” |
| **Qonto cannot help** | Business API keys are **not** scope-limited server-side (confirmed in design). There is no “read-only API key” or MCP-side spend ACL from Qonto. |
| **Volume-cost tools** | Cards, invoicing, payment requests, transfers, and some “create” operations may incur fees or move money — must be blocked **before** the vendor sees them. |
**Founder policy for now (normative intent):**
> Agents may inspect balances, transactions, and statements for control-plane
> and finance awareness. Agents must **not** initiate spending, transfers,
> card operations, or any action with volume/transaction cost. Payments stay
> Red lane (human in Qonto app).
That is stronger than “read-only MCP tools” in the abstract: it is a **business
policy** that must be enforced at a choke point, not a client preference.
---
## 2. What industry patterns say
Research (20252026 MCP ecosystem) converges on the same shape your proxy
intuition already points at.
### 2.1 MCP gateway / proxy as the control plane
Production patterns treat MCP **clients** (agent harnesses) as untrusted for
tool policy, and put a **gateway** between them and tools:
- **Envoy AI Gateway MCPRoute** — multiplex backends, **toolSelector**
allow/deny, OAuth on the gateway, upstream API-key injection so clients
never hold vendor secrets, CEL rules on `tools/call` params.
- **Kong AI Gateway MCP Tool ACLs** — default-deny, per-consumer tool
subsets at the gateway (solves “all-or-nothing” MCP exposure).
- **Speakeasy / Portkey / Permit MCP Gateway** framing — authn, RBAC, audit
of every tool call, policy evaluated **on each call**, not only at
connect time.
- **Harnesss own MCP guidance** — gateways for centralized credentials,
governance, audit, single endpoint vs N direct MCP connections.
Common lesson: **clients connect to one governed endpoint; the gateway owns
credentials, tool inventory, and policy.**
### 2.2 MCP security best practices (protocol-level)
From MCP security guidance and industry write-ups:
| Practice | Application to Qonto |
| --- | --- |
| **No token passthrough** | Clients must not present the Qonto bank key. The assistant service is the only OAuth/API client to Qonto. |
| **Tool-level scopes, not server-level** | “Access to Qonto MCP” is too coarse; allow `list_transactions`, deny `create_transfer`. |
| **Default deny** | Unknown tools fail closed. |
| **JIT escalation for high privilege** | Any future spend path is Yellow/Orange/Red with explicit re-authorization — never session-global “write mode.” |
| **Confused-deputy / static client ID** | If the service ever OAuth-proxies, consent and audience binding matter; for API-key custody we avoid OAuth to Qonto entirely. |
| **Audit end-to-end** | Actor (harness/agent id) + tool + args hash + decision + latency; no secrets in logs. |
### 2.3 Domain capability service (not “another harness”)
A second pattern, older than MCP: **BFF / capability API** for a domain
(banking, email, CRM). Agents call a **narrow, policy-shaped interface**
(“get runway snapshot”) rather than the full vendor surface. That is how
email-connect already behaves for IMAP: consumer never holds IMAP secrets
in the product path; it goes through custody + a purpose-built reader.
**llm-connect is the wrong home** for this: it is multi-provider **LLM**
abstraction (adapters, routing, cost models). Putting bank tools there
blurs the layer map (ARCHITECTURE-LAYERS.md) and couples finance policy to
model routing.
**agent-harness is the wrong home for Qonto policy itself**, but the right
home for *session* binding: harness ADR-001 already says the harness is the
credential holder and policy enforcement point **for agent sessions**. That
must not mean “each harness reimplements bank policy.” It means the harness
only grants agents a **named tool profile** that talks to **already-governed**
domain services. Domain policy lives **below** the harness.
### 2.4 Synthesis
| Pattern | Use for Qonto? |
| --- | --- |
| Wire vendor MCP into every harness | **No** — policy drift, key sprawl |
| Generic MCP gateway only (Envoy/Kong in front of vendor MCP) | **Partial** — good transport + tool filter; weak on **semantic** spend rules and finance-shaped tools |
| **Governed domain assistant** (our MCP + REST, policy inside) | **Yes — recommended** |
| Flex-auth decisions on every call | **Yes** — who may call; lane/risk attributes |
| OpenBao + ops-warden for bank key | **Yes** — already live; only the assistant fetches |
Your “qonto-proxy governed API & MCP service” is the right **class** of
solution. Name it as a **capability assistant**, not only a dumb reverse
proxy: it should *own* a safe tool surface, not only filter someone elses.
---
## 3. Goals and non-goals
### Goals
1. **One policy** for “no spend / no volume-cost actions” across all agents.
2. **One credential** path (existing OpenBao lane); harnesses never see bank keys.
3. **Convenient agent UX**: natural “ask the finance assistant” via MCP and/or
small REST helpers used by scripts and rhythm sessions.
4. **Auditability**: every call attributable to agent/harness/session.
5. **Fits NetKingdom**: flex-auth, OpenBao, ops-warden, agent-harness
instances, activity-core schedules — no parallel IAM.
6. **Dogfood-ready**: internal use is product evidence (DogfoodPolicy).
### Non-goals (v1)
- Replacing DATEV / StB / DUO accounting.
- Automating payments, SEPA, card issuance, expense submission.
- Hosted Qonto OAuth connector (`mcp.qonto.com`) as corporate custody path.
- Making llm-connect a tool gateway.
- Per-harness custom Qonto integrations.
---
## 4. Recommended architecture
### 4.1 One-liner
> **Build a small “Qonto Governed Assistant” service that is the only
> component allowed to hold the bank API key and speak Qontos protocol.
> Expose a **policy-shaped** MCP server and a thin REST API. All harnesses
> are dumb clients of that service. Policy is code + tests in one place.**
### 4.2 Context diagram
```text
┌──────────────────────────────────────────────────────────────────────────┐
│ Agent clients (many) │
│ Claude Code · Codex · Cursor · Grok · agent-harness sessions · scripts │
└───────────────┬───────────────────────────────┬──────────────────────────┘
│ MCP (streamable-HTTP) │ REST (JSON)
│ tools/list · tools/call │ /v1/snapshot, /v1/txns, …
▼ ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Qonto Governed Assistant («qonto-assistant») │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────────────────────┐ │
│ │ Authn edge │→ │ Policy gate │→ │ Capability tools │ │
│ │ (OIDC/mTLS │ │ (default-deny │ │ (finance-shaped, not 1:1 │ │
│ │ flex-auth) │ │ tool+args+lane) │ │ vendor dump) │ │
│ └─────────────┘ └────────┬─────────┘ └──────────────┬──────────────┘ │
│ │ audit │ │
│ ▼ ▼ │
│ State Hub / logs Qonto client (REST) │
│ (metadata only) Authorization: user:key │
└───────────────────────────────────────────────┬──────────────────────────┘
│ only this process
OpenBao tenants/binky/qonto-api
(via warden / AppRole — short TTL)
Qonto thirdparty API
```
Optional later: put **Envoy AI Gateway** (or similar) **in front of**
`qonto-assistant` for fleet-wide MCP aggregation with other domain assistants
(email, issue-core, …). Do **not** put the gateway *directly* in front of
vendor `qonto-mcp-server` as the sole control — semantic policy is thinner
there.
### 4.3 Why a domain assistant beats “proxy the vendor MCP only”
| Approach | Pros | Cons |
| --- | --- | --- |
| **A. Gateway + vendor MCP** | Fast; toolSelector; industry-standard | Vendor tool names/args still drive the model; hard to encode “no volume cost”; vendor MCP is unmaintained / dual-use with write tools |
| **B. Domain assistant (recommended)** | Safe tool catalog we design; semantic policy; dual MCP+REST; dogfood product surface | Build cost |
| **C. Per-harness allow-lists** | Zero new service | Guaranteed drift; keys leak into clients |
**Hybrid that works:** implement **B**; if we still want vendor MCP for
parity experiments, run it **only as an internal backend** behind the same
policy gate (never published to harnesses). Prefer **direct REST client**
inside the assistant (we already proved REST) — fewer moving parts than
shelling out to unmaintained MCP.
### 4.4 Component responsibilities
| Component | Owns | Does not own |
| --- | --- | --- |
| **qonto-assistant** | Tool catalog, spend/volume policy, Qonto REST client, dual surfaces (MCP+REST), call audit | LLM routing, scheduling, identity issuance |
| **OpenBao + ops-warden** | Custody path, rotation guide, high-risk agent boundary | Business rules about transfers |
| **flex-auth** | “May actor X call capability `finance.qonto.read`?” | Bank credential material |
| **agent-harness** | Session lifecycle; grants profile `finance-qonto-read`; injects *assistant* URL/token only | Bank policy implementation |
| **llm-connect** | Which model answers | Tool execution |
| **activity-core** | When Finance Steward runs | How bank is called |
| **binky-control** | CostRunRate, queues, AutonomyPolicy, dogfood evidence | Runtime of the assistant |
| **Qonto** | Bank of record | Our agent policy |
### 4.5 Surfaces (same policy, two protocols)
**MCP (agent-native)**
- Transport: streamable-HTTP (remote), optional stdio only for local dev
with the **same** binary and policy module.
- `tools/list` returns **only** approved tools (default-deny).
- `tools/call` re-checks policy (tool id + arguments + actor claims) —
connect-time allow-list alone is insufficient.
**REST (script / rhythm / tests)**
- Stable JSON for CostRunRate refresh, CI smoke, non-MCP agents.
- Same policy middleware as MCP (shared library, one decision function).
Example tool catalog (v1 — illustrative):
| Tool / endpoint | Purpose | Policy |
| --- | --- | --- |
| `qonto_org_summary` | Org + account balances | Allow (Green/Blue) |
| `qonto_list_transactions` | Filtered history | Allow; hard caps on page size; no export bulk to chat by default |
| `qonto_cost_run_rate_hints` | Map debits to known CostRunRate rows | Allow |
| `qonto_find_counterparties` | Search labels | Allow |
| *(any transfer / payment / card / invoice create)* | — | **Deny always** (v1) |
| *(vendor MCP write tools)* | — | Not registered |
### 4.6 Policy model (v1)
Encode as **code + declarative YAML**, tested in CI — not wiki prose alone.
```yaml
# policy/qonto-v1.yaml (sketch)
version: 1
default: deny
allow:
- id: org_summary
- id: list_transactions
constraints:
max_per_page: 100
max_pages_per_call: 5
- id: cost_run_rate_hints
deny_classes:
- spend # transfers, payouts, direct debits initiation
- volume_cost # card ops, invoicing sends, paid features that bill per use
- credential_exfil # tools that return raw API keys or full IBANs if avoidable
lanes:
green_blue: allow_set: [org_summary, list_transactions, cost_run_rate_hints]
yellow_plus: same_as_green_blue # no spend even if human is "nearby"
red: human_only_in_qonto_app
```
**Semantic rules (beyond tool name):**
- Inspect arguments: reject if tool is “create_*”, amounts > 0 with side
effects, or known write operation types.
- Prefer **response shaping**: return IBAN last4, not full IBAN, in agent
channels unless a higher assurance mode is granted later.
- **Volume-cost**: deny anything that creates a fee-bearing Qonto operation
(subscription change is also Red — out of band).
Map to AutonomyPolicy:
| Action class | Lane |
| --- | --- |
| Balance / txn read, CostRunRate update drafts | Green / Blue |
| Recommend plan downgrade / keep Qonto | Green (recommend only) |
| Change Qonto plan, create API key, transfer | Red |
### 4.7 Authentication and identity
```text
Harness / human agent
→ authenticates to qonto-assistant (OIDC netkingdom or mTLS workload id)
→ flex-auth: finance.qonto.read (or finer)
→ assistant fetches bank secret with its own AppRole / short-lived token
→ Qonto sees only the company API identity (not the coding agent)
```
- **Never** put `API_KEY` in harness env for general sessions.
- Optional **AppRole** `agent-harness-binky-qonto` mirroring mail lane — but
scoped so only `qonto-assistant` can read the path (not every agent
process). Agents authenticate *to the assistant*, not to OpenBao for
bank secrets.
### 4.8 Placement in the repo map (options)
| Option | Repo | When |
| --- | --- | --- |
| **A (preferred dogfood)** | New small service under Coulomb, e.g. `qonto-assistant` or generalize later to `finance-connect` | Clean boundary, productizable |
| **B** | Module inside a future `domain-connectors` monorepo | If many thin connectors share skeleton |
| **C** | Package under `binky-control` only | Too tenant-specific; resists reuse |
Start as **tenant-backed dogfood** with a clear INTENT that it may graduate
to a multi-tenant “governed bank connector” offer. Keep binky-control as
*consumer + policy source of business truth* (CostRunRate, AutonomyPolicy),
not as the runtime host long-term.
### 4.9 Observability and evidence
Every call records (State Hub progress or dedicated audit log):
```yaml
actor: agt-… / harness session id
capability: finance.qonto.read
tool: list_transactions
decision: allow | deny
deny_reason: volume_cost | unknown_tool | flex_auth | rate_limit
latency_ms: …
qonto_http_status: …
# never: Authorization header, API_KEY, full account numbers if avoidable
```
Finance Steward rhythm writes **metadata** into `finance/` only (same rule
as first pull).
---
## 5. Alternatives considered
### 5.1 “Anchor policy at Qonto”
**Unavailable for our custody model.** Self-hosted MCP + API keys are not
fine-scoped. Hosted OAuth MCP is the corporate **anti-pattern** we already
rejected (credentials outside OpenBao, write tools bundled). Even with
OAuth, spend prevention would still need our gateway for defense in depth.
### 5.2 Generic MCP gateway only
**Good complement, insufficient alone.** Use later for fleet aggregation.
v1 needs **semantic** finance policy and a stable REST face for non-MCP
clients — easier as our service.
### 5.3 agent-harness-only enforcement
Harness tool profiles remain necessary (dont hand agents unrestricted
network). But **bank policy must not live only there**: interactive Claude
on a laptop would bypass harness profiles unless it also only sees the
assistant endpoint. **Network/policy gravity toward the assistant.**
### 5.4 llm-connect as tool hub
Reject for layering reasons (LLM vs domain tools). Route models through
llm-connect; route **bank actions** through qonto-assistant.
---
## 6. Phased delivery
### Phase 0 — Already done (BINKY-WP-0005)
- [x] Custody lane live
- [x] REST pull proven
- [x] CostRunRate TBC resolved for desk + plan
- [x] Catalog + CCR active
### Phase 1 — Policy kernel + REST (minimum useful product)
1. Service skeleton with shared `decide(tool, args, claims) -> Allow|Deny`.
2. REST: `GET /v1/accounts`, `GET /v1/transactions`, `GET /v1/snapshot`.
3. Hard deny list for any write/spend path (even if not implemented).
4. OpenBao fetch only inside service (AppRole or OIDC role).
5. Smoke: CI uses mock Qonto; manual: live read against dogfood account.
6. Script replaces ad-hoc first-pull for CostRunRate refresh.
### Phase 2 — MCP surface for all harnesses
1. Streamable-HTTP MCP on the same decision function.
2. Document **one** client config snippet for Claude/Codex/Cursor/Grok:
URL + OIDC/workload auth — **no bank secrets**.
3. agent-harness tool profile `finance-qonto-read` → assistant only.
4. Audit events to State Hub.
### Phase 3 — Flex-auth + fleet
1. flex-auth resource `finance.qonto.read` (+ later `.export`).
2. Rate limits, concurrency caps, optional response redaction modes.
3. Optional Envoy/gateway in front for multi-assistant mesh.
### Phase 4 — Productization (dogfood → offer)
1. Multi-tenant path layout `tenants/<slug>/qonto-api`.
2. Policy packs per autonomy lane.
3. OfferCatalog entry: “Governed bank assistant for agent fleets.”
---
## 7. Harness integration pattern (the consistency trick)
**Wrong:**
```text
each harness → OpenBao qonto key → vendor MCP (local allow-list hope)
```
**Right:**
```text
each harness → auth to qonto-assistant → policy → Qonto
single tool catalog + single deny rules
```
Checklist for any new harness:
1. Does **not** get `binky-qonto-api` bank secret.
2. Gets only assistant base URL + client credentials for `finance.qonto.read`.
3. Tool profile names assistant tools, not vendor tool names.
4. Offline/dev: point at assistant mock, not a copy of production key.
---
## 8. Risk register (architecture-level)
| Risk | Mitigation |
| --- | --- |
| Assistant is a high-value target | High-risk path already in agent-high-risk-boundary; no raw stream to coding agents; short-lived OpenBao tokens; network policy |
| Model jailbreak asks for transfer | Tool not registered + deny classes; never implement spend in v1 |
| Vendor adds new MCP write tools | We do not auto-mirror vendor catalog; allow-list is ours |
| Bypass via direct thirdparty from laptop | Platform policy + founder discipline; optional egress controls later; secrets not on laptops |
| Over-filtering legitimate finance work | Explicit allow tools + CostRunRate helpers; expand by policy PR with tests |
---
## 9. Decision requests (for later DEC / workplan)
1. **Adopt domain-assistant architecture (B)** for Qonto (this blueprint).
2. **Repo home:** new `qonto-assistant` vs wait for generic `finance-connect`.
3. **v1 policy freeze:** no spend / no volume-cost tools — hard deny.
4. **MCP transport:** remote streamable-HTTP only for production clients.
5. **Whether** to ever run vendor MCP as internal backend (default: **no**).
Suggested workplan slug: `BINKY-WP-0006` or a Coulomb-side
`QONTO-WP-0001` once the repo exists.
---
## 10. References
### Internal
- `integrations/qonto-mcp.md` — variant decision, custody, first-pull results
- `finance/CostRunRate.md` — consumer of read path
- `AutonomyPolicy.md` — Green…Red lanes; spending defaults
- `agent-harness` INTENT + ADR-001 — single runtime; instances declarative
- `llm-connect` ARCHITECTURE-LAYERS — LLM only, not domain tools
- `flex-auth` INTENT — authorization registry
- ops-warden catalog `binky-qonto-api`, CCR-2026-0008
### External patterns (research snapshot 2026-07)
- Envoy AI Gateway MCPRoute: tool filtering, upstream key injection, OAuth,
CEL on MCP params
- Kong AI Gateway MCP Tool ACLs: default-deny per-consumer tool grants
- MCP gateway product literature (Portkey, Speakeasy, Permit): central
policy + audit for multi-client agents
- MCP security guidance: no token passthrough; tool-level scopes; proxy
consent / confused-deputy awareness
- CSA / industry agentic MCP guides: inventory of servers; JIT escalation
for high privilege
---
## 11. Summary recommendation
1. **Do not** wire Qonto MCP into each harness.
2. **Do** build a **Qonto Governed Assistant** with:
- sole possession of bank credentials,
- default-deny tool catalog,
- **no spend / no volume-cost** policy as code,
- MCP + REST for all clients,
- flex-auth + OpenBao + audit.
3. Treat generic MCP gateways as a **later mesh layer**, not a substitute
for domain policy.
4. Align agent-harness so it only ever grants access to that assistant —
preserving ADR-001 without re-implementing bank rules per runtime.
This matches both industry MCP-gateway practice and your existing
platform boundaries (custody, flex-auth, harness instances, dogfood).

View file

@ -0,0 +1,67 @@
---
id: QONTO-WP-0001
type: workplan
title: "Bootstrap State Hub integration"
domain: infotech
repo: qonto-assistant
status: finished
owner: codex
topic_slug: the-custodian
created: "2026-07-21"
updated: "2026-07-21"
state_hub_workstream_id: "b2faddb4-2fdc-4e2a-b2df-9b6dd38faa86"
---
# Bootstrap State Hub integration
Register and prime `qonto-assistant` for fleet agents: identity files, first
workplans, hub consistency.
## Review Generated Integration Files
```task
id: QONTO-WP-0001-T01
status: done
priority: high
state_hub_task_id: "aab19eda-00ff-4e43-91c6-7272bd0422e5"
```
Review `INTENT.md`, `SCOPE.md`, `AGENTS.md`, and `.custodian-brief.md`.
Replace generated placeholders with repo-specific facts where needed.
**Done 2026-07-21:** INTENT authored from governed-assistant design; SCOPE
refined; README points at specs/research; register generated AGENTS.md + brief.
## Verify Local Developer Workflow
```task
id: QONTO-WP-0001-T02
status: wait
priority: medium
state_hub_task_id: "360f399a-36f8-44e6-af8a-55fb0d5732a3"
```
Identify the repo's install, test, lint, build, and run commands. Add or refine
those commands in the agent instructions so future coding sessions can verify
changes confidently.
**Blocked on** runtime stack choice in QONTO-WP-0002-T01. Revisit after that task.
## Seed First Real Workplan
```task
id: QONTO-WP-0001-T03
status: done
priority: medium
state_hub_task_id: "ee9a8125-ff00-4640-80bd-81567722bba6"
```
Create the first implementation workplan for the repository's most important
next change. After workplan file updates, run:
```bash
statehub fix-consistency
```
**Done 2026-07-21:** `workplans/QONTO-WP-0002-policy-kernel-and-rest.md` (Phase 1
from ArchitectureBlueprint).

View file

@ -0,0 +1,145 @@
---
id: QONTO-WP-0002
type: workplan
title: "Phase 1 — policy kernel and read-only REST"
domain: infotech
repo: qonto-assistant
status: ready
owner: codex
topic_slug: the-custodian
created: "2026-07-21"
updated: "2026-07-21"
state_hub_workstream_id: "1540afc2-219e-4d95-96f5-4b45fc6a7aaa"
---
# Phase 1 — policy kernel and read-only REST
Execute **Phase 1** of `specs/ArchitectureBlueprint.md`: a service skeleton
with a shared `decide(tool, args, claims) → Allow|Deny` policy kernel and a
minimal REST surface for org/accounts/transactions/snapshot. No MCP yet (Phase 2).
No spend or volume-cost tools — default deny.
**Depends on:** live OpenBao path `tenants/binky/qonto-api` (BINKY-WP-0005 /
CCR-2026-0008) — already provisioned.
## Task: Choose runtime skeleton and layout
```task
id: QONTO-WP-0002-T01
status: todo
priority: high
state_hub_task_id: "f9e129f3-5bd4-43e1-b7a0-281e4d3dec2a"
```
Pick implementation stack (recommendation: Python 3.12 + FastAPI unless a
fleet standard dictates otherwise). Scaffold package layout, `pyproject.toml`
(or equivalent), Makefile targets (`test`, `lint`, `run`), and document
commands in `AGENTS.md` / complete QONTO-WP-0001-T02.
Done when: `make test` (or documented equivalent) runs an empty/smoke suite;
layout matches blueprint components (policy, qonto client, api, audit).
## Task: Policy kernel — default-deny no-spend / no-volume-cost
```task
id: QONTO-WP-0002-T02
status: todo
priority: high
state_hub_task_id: "552ff651-dc66-4e65-97fe-3ec26652bbdd"
```
Implement declarative policy (YAML or equivalent) + pure decision function:
- default **deny**
- allow only v1 read capability ids (`org_summary`, `list_transactions`,
`cost_run_rate_hints` / snapshot)
- hard deny classes: `spend`, `volume_cost`, `credential_exfil`
- unit tests: allow known reads; deny transfer/card/invoice-shaped tools and
suspicious args even if somehow invoked
Done when: policy tests pass in CI/local; no network required.
## Task: Qonto REST client (credential inject, no secret log)
```task
id: QONTO-WP-0002-T03
status: todo
priority: high
state_hub_task_id: "be3aa7b6-f28c-4436-bd5d-d6940de6c2ce"
```
Implement thirdparty client using `Authorization: login:key` (fields
`API_USER`/`API_KEY` from env or OpenBao fetch helper). Map:
- organization + bank accounts
- paginated transactions with hard caps
- never log Authorization or key material
Support env inject for tests (`QONTO_API_KEY`/`QONTO_ORGANIZATION_ID` or
`API_KEY`/`API_USER`) and document OpenBao fetch for operators.
Done when: unit tests with mocked HTTP; optional live smoke behind a flag.
## Task: REST API surface
```task
id: QONTO-WP-0002-T04
status: todo
priority: high
state_hub_task_id: "678b0b26-15af-4037-849f-d24d320588ac"
```
Expose JSON endpoints that all run through the policy kernel:
| Method | Path | Capability |
| --- | --- | --- |
| GET | `/v1/health` | no bank call |
| GET | `/v1/accounts` | org_summary |
| GET | `/v1/transactions` | list_transactions (capped) |
| GET | `/v1/snapshot` | cost/run-rate oriented summary |
Done when: OpenAPI or documented curl examples; integration test with mock
client; deny paths return 403 with stable error code.
## Task: Audit metadata (no secrets)
```task
id: QONTO-WP-0002-T05
status: todo
priority: medium
state_hub_task_id: "4a42dff1-1281-4cc1-ba6a-24702bce7dc9"
```
Log or emit structured audit events: actor (if present), capability, decision,
deny_reason, latency, upstream HTTP status. Never secret fields. Prefer
stdout JSON + optional State Hub progress for dogfood runs.
Done when: tests assert secrets absent from log lines for a sample allow/deny.
## Task: Operator runbook + CostRunRate refresh path
```task
id: QONTO-WP-0002-T06
status: todo
priority: medium
state_hub_task_id: "7b7ca0f7-f523-473e-b3f6-fe0564f54ed5"
```
Document how binky-control refreshes `finance/CostRunRate.md` via
`GET /v1/snapshot` (or equivalent) with OpenBao-backed service start. Link from
README. Optional thin script under `scripts/`.
Done when: runbook in `docs/` or README section; dry-run instructions without
pasting keys into chat.
## Task: Closure review
```task
id: QONTO-WP-0002-T07
status: todo
priority: low
state_hub_task_id: "9b99ba05-99f2-4624-9044-89bf37055434"
```
Mark workplan finished when T01T06 done; note Phase 2 seed (MCP surface for
all harnesses) in closure. Run `statehub fix-consistency`.