Seeded intent and initial workplan

This commit is contained in:
tegwick 2026-07-22 00:31:50 +02:00
parent e1ab23f83e
commit 67a5b01e08
3 changed files with 263 additions and 115 deletions

View file

@ -85,7 +85,7 @@ them with operational bank visibility.
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
5. **Platform fit** — flex-auth, OpenBao, ops-warden, State Hub evidence; no
parallel IAM.
6. **Autonomy lanes** — map to binky-control AutonomyPolicy: reads Green/Blue;
plan change / transfers / API key minting remain Red.

View file

@ -1,6 +1,6 @@
# Architecture Blueprint — Governed Qonto Assistant
> Status: blueprint v0.1 — 2026-07-21
> Status: blueprint v0.2 — 2026-07-21 (revised after repo review)
> 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
@ -159,31 +159,42 @@ proxy: it should *own* a safe tool surface, not only filter someone elses.
```text
┌──────────────────────────────────────────────────────────────────────────┐
Agent clients (many)
Claude Code · Codex · Cursor · Grok · agent-harness sessions · scripts │
Agent clients
│ 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
│ Qonto Governed Assistant («qonto-assistant») │
│ │
│ Authn/Authz edge │
│ OIDC / workload id / mTLS + optional flex-auth │
│ │ │
│ ▼ │
│ Protocol adapters │
│ MCP adapter REST adapter │
│ │ │ │
│ └──────────┬────────┘ │
│ ▼ │
│ Capability service core │
│ canonical capability ids + response contracts │
│ │ │
│ ▼ │
│ Policy engine │
│ default-deny, tenant scope, arg constraints, data class rules │
│ │ │
│ ┌─────┴──────────┐ │
│ ▼ ▼ │
│ Qonto adapter Audit emitter │
│ REST client structured events │
│ │ │ │
└──────┼────────────────┼──────────────────────────────────────────────────┘
│ │
▼ ▼
OpenBao tenants/<tenant>/qonto-api stdout / log sink / metrics
Qonto thirdparty API
```
Optional later: put **Envoy AI Gateway** (or similar) **in front of**
@ -219,65 +230,102 @@ shelling out to unmaintained MCP.
| **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)
### 4.5 Protocol adapters over one service core
**MCP (agent-native)**
The critical implementation rule is:
- 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 routes and MCP tools are only transport adapters. They must translate
> into the same internal `CapabilityRequest` model before policy runs.
**REST (script / rhythm / tests)**
That avoids the most likely drift bug in this repo: REST enforcing one set of
rules while MCP grows a slightly different set later.
- Stable JSON for CostRunRate refresh, CI smoke, non-MCP agents.
- Same policy middleware as MCP (shared library, one decision function).
Canonical request shape:
Example tool catalog (v1 — illustrative):
```text
CapabilityRequest
capability_id
tenant_id
actor_claims
resource_scope
request_args
protocol # rest | mcp
```
| 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 |
Recommended v1 capability map:
| Internal capability id | REST surface | MCP surface | Notes |
| --- | --- | --- | --- |
| `org_summary` | `GET /v1/accounts` | `qonto_org_summary` | Core balances/org read |
| `list_transactions` | `GET /v1/transactions` | `qonto_list_transactions` | Capped, filtered history |
| `cost_run_rate_hints` | used by `GET /v1/snapshot` | `qonto_cost_run_rate_hints` (Phase 2) | Normalized finance hints, not raw export |
| `snapshot_bundle` | `GET /v1/snapshot` | optional later | Composite read; not a separate privilege if it only orchestrates allowed reads |
| `find_counterparties` | defer | defer | Candidate future read capability, not Phase 1 MVP |
Rules:
- `snapshot_bundle` is an orchestrator over already-allowed reads, not a way
to bypass policy with a "bigger" internal export.
- REST and MCP names may differ, but both must map to the same
`capability_id`, deny reasons, and response redaction rules.
- Vendor payloads should be normalized into repo-owned response schemas; do
not stream raw Qonto JSON into agent contexts by default.
### 4.6 Policy model (v1)
Encode as **code + declarative YAML**, tested in CI — not wiki prose alone.
Policy should evaluate this tuple:
```text
(capability_id, actor_claims, tenant_id, resource_scope, request_args, response_class)
```
Sketch:
```yaml
# policy/qonto-v1.yaml (sketch)
version: 1
default: deny
allow:
- id: org_summary
- id: list_transactions
capabilities:
org_summary:
lanes: [green, blue]
list_transactions:
lanes: [green, blue]
constraints:
max_per_page: 100
max_pages_per_call: 5
- id: cost_run_rate_hints
max_window_days: 93
cost_run_rate_hints:
lanes: [green, blue]
snapshot_bundle:
compose_only: [org_summary, 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
spend:
match_prefixes: [create_, issue_, transfer_, payout_]
volume_cost:
match_tags: [card_operation, invoice_send, payment_link, subscription_change]
credential_exfil:
response_fields: [api_key, authorization_header, full_iban]
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
green_blue:
allow_set: [org_summary, list_transactions, cost_run_rate_hints, snapshot_bundle]
yellow_plus:
same_as: green_blue
red:
human_only_in_qonto_app: true
```
**Semantic rules (beyond tool name):**
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).
- Reject unknown capability ids before touching Qonto.
- Enforce tenant binding before resource lookup; callers do not choose an
arbitrary OpenBao path or tenant id.
- Inspect arguments for suspicious write semantics even if a route/tool is
mis-wired later.
- Treat response shaping as policy, not presentation polish. Redaction rules
should be versioned and testable like allow/deny rules.
- Emit stable deny reasons such as `unknown_capability`, `tenant_scope`,
`arg_constraint`, `volume_cost`, `credential_exfil`, and `authz_denied`.
Map to AutonomyPolicy:
@ -289,21 +337,117 @@ Map to AutonomyPolicy:
### 4.7 Authentication and identity
Separate three identities clearly:
1. **Caller identity**: human or workload calling REST/MCP.
2. **Assistant service identity**: the only identity allowed to read
`tenants/<tenant>/qonto-api`.
3. **Qonto upstream identity**: the company credential presented to Qonto.
```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)
Caller
-> authenticates to qonto-assistant (OIDC workload identity, mTLS, or similar)
-> assistant checks claims + optional flex-auth grant (finance.qonto.read)
-> assistant resolves tenant from claims/policy
-> assistant fetches secret using its own runtime role
-> Qonto sees only the tenant's API identity
```
- **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.
Rules:
### 4.8 Placement in the repo map (options)
- **Never** put bank credentials in harness env for general sessions.
- Do **not** reuse an `agent-harness-*` OpenBao role for assistant secret
fetch. Use a dedicated assistant runtime identity such as
`qonto-assistant-runtime`.
- Even in single-tenant dogfood, keep `tenant_id` explicit in claims and audit
events so productization does not require reworking the core contract.
### 4.8 Secret and upstream session lifecycle
The blueprint needs an explicit secret lifecycle because this service is the
sole credential choke point.
- OpenBao read happens inside the service only.
- Secrets may be cached **in memory only** with a short TTL to avoid fetching
on every request.
- Cache invalidation should occur on startup, TTL expiry, and upstream 401/403.
- No secret material is written to disk, progress logs, traces, or chat output.
- Local developer mode may use env inject for mocks/tests, but production path
remains OpenBao-first.
This keeps rotation and incident response tractable without coupling every
request to a secret store round-trip.
### 4.9 Data handling and response shaping
The current blueprint mentions redaction, but this needs to be a first-class
contract because the main exfil path is not only "the key" but over-sharing
financial data into agent chat contexts.
| Data class | Examples | Default handling |
| --- | --- | --- |
| `secret` | API key, Authorization header, OpenBao token | Never return, never log |
| `sensitive_financial` | full IBAN, account owner identifiers, raw vendor refs | Redact or truncate by default |
| `operational_summary` | balances, normalized counterparties, capped txn summaries | Return when capability allows |
Guidelines:
- Prefer normalized summary DTOs over raw vendor objects.
- Default to IBAN last4 or account alias, not full identifiers.
- Bulk export, statement download, and raw vendor dumps are out of scope for v1.
- If a future higher-assurance mode needs fuller data, it should be a separate
capability with separate policy and audit semantics, not a flag hidden inside
an existing read.
### 4.10 Observability and audit
Per-request audit should not depend on State Hub progress writes. State Hub is
appropriate for work/progress records, not as the hot-path sink for every bank
read.
Recommended split:
- **Hot path**: structured stdout/file/OTLP audit events for every request.
- **Cold path**: State Hub progress notes or operator summaries for notable
sessions, incidents, or rollout evidence.
Suggested audit envelope:
```yaml
request_id: req-...
timestamp: ...
actor: agt-... / workload id
tenant_id: binky
capability: list_transactions
protocol: rest | mcp
decision: allow | deny
deny_reason: volume_cost | unknown_capability | tenant_scope | authz_denied
policy_version: 1
latency_ms: ...
qonto_http_status: ...
result_count: ...
# never: Authorization header, API_KEY, OpenBao token, full IBAN by default
```
### 4.11 Operational guardrails (v1, not "later if we remember")
Basic safety and reliability controls belong in Phase 1 because one noisy
agent session can otherwise degrade the single bank integration for everyone.
| Guardrail | Minimum requirement |
| --- | --- |
| Upstream timeout | bounded per request; fail fast and explicitly |
| Pagination | hard caps in policy, not caller preference |
| Concurrency | bounded per actor/tenant to avoid Qonto burst abuse |
| Rate limiting | basic per-actor or per-token limit from first deploy |
| Retry policy | conservative; no blind retries on ambiguous write-like failures |
| Persistence | no raw Qonto payload store in v1; ephemeral in-memory only |
| Failure mode | fail closed on policy/authn/authz uncertainty |
These controls move from "nice operational follow-up" to "architecture
requirement" because the repo's entire point is governed centralization.
### 4.12 Placement in the repo map (options)
| Option | Repo | When |
| --- | --- | --- |
@ -316,24 +460,6 @@ 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
@ -376,25 +502,31 @@ llm-connect; route **bank actions** through qonto-assistant.
### 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`.
1. Service skeleton with a protocol-neutral capability core and shared
`decide(request, claims) -> Allow|Deny`.
2. REST adapters for `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.
4. OpenBao fetch only inside service using a dedicated assistant runtime role.
5. Baseline guardrails ship in Phase 1: bounded pagination, timeouts, basic
rate limiting, bounded concurrency, and redaction tests.
6. Smoke: CI uses mock Qonto; manual live read stays explicitly opt-in.
7. 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.
1. Streamable-HTTP MCP adapter on the same capability core and policy engine.
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.
4. MCP and REST emit the same structured audit schema; State Hub receives
operator-level progress notes, not per-call hot-path events.
### Phase 3 — Flex-auth + fleet
1. flex-auth resource `finance.qonto.read` (+ later `.export`).
2. Rate limits, concurrency caps, optional response redaction modes.
2. Graduated quotas, differentiated response redaction profiles, and finer
capability scopes such as `.export` if ever approved.
3. Optional Envoy/gateway in front for multi-assistant mesh.
### Phase 4 — Productization (dogfood → offer)
@ -439,6 +571,8 @@ Checklist for any new harness:
| 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 |
| Cross-tenant data mix-up later | Tenant id explicit in claims, policy, audit, and OpenBao path resolution from v1 |
| Audit/log sink leaks sensitive fields | Redaction-by-class policy + tests; State Hub not used as raw per-request event store |
---
@ -447,8 +581,9 @@ Checklist for any new harness:
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**).
4. **Service identity:** dedicated assistant runtime role; no harness role reuse.
5. **MCP transport:** remote streamable-HTTP only for production clients.
6. **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.
@ -486,8 +621,10 @@ Suggested workplan slug: `BINKY-WP-0006` or a Coulomb-side
1. **Do not** wire Qonto MCP into each harness.
2. **Do** build a **Qonto Governed Assistant** with:
- sole possession of bank credentials,
- a protocol-neutral capability core behind REST and MCP adapters,
- default-deny tool catalog,
- **no spend / no volume-cost** policy as code,
- data classification/redaction rules,
- MCP + REST for all clients,
- flex-auth + OpenBao + audit.
3. Treat generic MCP gateways as a **later mesh layer**, not a substitute

View file

@ -4,7 +4,7 @@ type: workplan
title: "Phase 1 — policy kernel and read-only REST"
domain: infotech
repo: qonto-assistant
status: ready
status: active
owner: codex
topic_slug: the-custodian
created: "2026-07-21"
@ -15,9 +15,12 @@ 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.
with a protocol-neutral capability core and shared
`decide(request, claims) → Allow|Deny` policy kernel plus a minimal REST
surface for org/accounts/transactions/snapshot. No MCP yet (Phase 2). No
spend or volume-cost tools — default deny. Baseline guardrails ship in this
phase: bounded pagination/timeouts, basic rate limiting, bounded concurrency,
and redaction tests.
**Depends on:** live OpenBao path `tenants/binky/qonto-api` (BINKY-WP-0005 /
CCR-2026-0008) — already provisioned.
@ -26,7 +29,7 @@ CCR-2026-0008) — already provisioned.
```task
id: QONTO-WP-0002-T01
status: todo
status: progress
priority: high
state_hub_task_id: "f9e129f3-5bd4-43e1-b7a0-281e4d3dec2a"
```
@ -37,13 +40,14 @@ fleet standard dictates otherwise). Scaffold package layout, `pyproject.toml`
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).
layout matches blueprint components (protocol adapters, capability core,
policy, qonto client, api, audit).
## Task: Policy kernel — default-deny no-spend / no-volume-cost
```task
id: QONTO-WP-0002-T02
status: todo
status: progress
priority: high
state_hub_task_id: "552ff651-dc66-4e65-97fe-3ec26652bbdd"
```
@ -52,8 +56,10 @@ 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)
`cost_run_rate_hints`, `snapshot_bundle`)
- hard deny classes: `spend`, `volume_cost`, `credential_exfil`
- stable deny reasons for authz, tenant scope, argument constraint, and
credential exfil cases
- unit tests: allow known reads; deny transfer/card/invoice-shaped tools and
suspicious args even if somehow invoked
@ -63,7 +69,7 @@ Done when: policy tests pass in CI/local; no network required.
```task
id: QONTO-WP-0002-T03
status: todo
status: progress
priority: high
state_hub_task_id: "be3aa7b6-f28c-4436-bd5d-d6940de6c2ce"
```
@ -73,10 +79,12 @@ Implement thirdparty client using `Authorization: login:key` (fields
- organization + bank accounts
- paginated transactions with hard caps
- bounded timeouts and conservative retry behavior
- 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.
`API_KEY`/`API_USER`) and document OpenBao fetch for operators. Production
path fetches through a dedicated assistant runtime role only.
Done when: unit tests with mocked HTTP; optional live smoke behind a flag.
@ -84,7 +92,7 @@ Done when: unit tests with mocked HTTP; optional live smoke behind a flag.
```task
id: QONTO-WP-0002-T04
status: todo
status: progress
priority: high
state_hub_task_id: "678b0b26-15af-4037-849f-d24d320588ac"
```
@ -96,23 +104,26 @@ Expose JSON endpoints that all run through the policy kernel:
| 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 |
| GET | `/v1/snapshot` | snapshot_bundle (composed allowed reads) |
Done when: OpenAPI or documented curl examples; integration test with mock
client; deny paths return 403 with stable error code.
client; deny paths return 403 with stable error code; route-to-capability
mapping is explicit and shared with future MCP.
## Task: Audit metadata (no secrets)
```task
id: QONTO-WP-0002-T05
status: todo
status: progress
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.
deny_reason, latency, upstream HTTP status, and policy version. Never secret
fields. Prefer structured stdout JSON (or equivalent sink) on the hot path;
State Hub progress stays optional roll-up evidence for dogfood runs, not
per-call audit storage.
Done when: tests assert secrets absent from log lines for a sample allow/deny.
@ -120,7 +131,7 @@ Done when: tests assert secrets absent from log lines for a sample allow/deny.
```task
id: QONTO-WP-0002-T06
status: todo
status: progress
priority: medium
state_hub_task_id: "7b7ca0f7-f523-473e-b3f6-fe0564f54ed5"
```