--- id: QONTO-WP-0002 type: workplan title: "Phase 1 — policy kernel and read-only REST" 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: "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 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. ## Task: Choose runtime skeleton and layout ```task id: QONTO-WP-0002-T01 status: done 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 (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: done 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_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 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: done 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 - 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. Production path fetches through a dedicated assistant runtime role only. Done when: unit tests with mocked HTTP; optional live smoke behind a flag. ## Task: REST API surface ```task id: QONTO-WP-0002-T04 status: done 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` | 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; route-to-capability mapping is explicit and shared with future MCP. ## Task: Audit metadata (no secrets) ```task id: QONTO-WP-0002-T05 status: done 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, 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. ## Task: Operator runbook + CostRunRate refresh path ```task id: QONTO-WP-0002-T06 status: done 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: done priority: low state_hub_task_id: "9b99ba05-99f2-4624-9044-89bf37055434" ``` **Done 2026-07-21:** Phase 1 landed in `src/qonto_assistant/` with a FastAPI-based REST service, policy YAML, Qonto client, audit layer, rate limits, concurrency bounds, and operator runbook. Verified with `PYTHONPATH=src ../state-hub/.venv/bin/python -m pytest` (`12 passed`) plus `python3 -m compileall src tests`. Phase 2 seed remains the MCP surface on the same capability core and policy engine. Run `statehub fix-consistency`.