Seeded intent and initial workplan
This commit is contained in:
parent
e1ab23f83e
commit
67a5b01e08
3 changed files with 263 additions and 115 deletions
|
|
@ -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 else’s.
|
|||
|
||||
```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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue