Complete Phase 1: policy kernel, REST service, and local smoke tooling

Implements QONTO-WP-0002 (policy-gated Qonto REST service with audit
logging, rate limiting, and credential handling) and the ADHOC-2026-07-21
follow-up (fixture-backed local smoke mode, repo classification metadata).
Marks QONTO-WP-0001/0002 and the ad-hoc workplan finished, and regenerates
WORK-RECORDS.md and the ADHOC workplan's state_hub_workstream_id via
fix-consistency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-22 21:21:05 +02:00
parent eef408bb19
commit ca12843013
33 changed files with 2533 additions and 30 deletions

View file

@ -0,0 +1,74 @@
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
def _repo_root() -> Path:
return Path(__file__).resolve().parents[2]
@dataclass(frozen=True, slots=True)
class Settings:
service_name: str
default_tenant_id: str
default_actor_lane: str
required_scope: str
enforce_scope: bool
policy_file: Path
qonto_base_url: str
qonto_fixture_dir: Path | None
qonto_auth_mode: str
qonto_organization_path: str
qonto_transactions_path: str
qonto_timeout_seconds: float
qonto_max_retries: int
qonto_secret_ttl_seconds: int
rate_limit_requests: int
rate_limit_window_seconds: int
max_concurrency: int
credential_source: str
openbao_path: str
openbao_command: str
openbao_timeout_seconds: float
host: str
port: int
@classmethod
def from_env(cls) -> "Settings":
policy_file = Path(
os.getenv(
"QONTO_ASSISTANT_POLICY_FILE",
str(_repo_root() / "src" / "qonto_assistant" / "policy" / "qonto-v1.yaml"),
)
)
return cls(
service_name=os.getenv("QONTO_ASSISTANT_SERVICE_NAME", "qonto-assistant"),
default_tenant_id=os.getenv("QONTO_ASSISTANT_DEFAULT_TENANT", "binky"),
default_actor_lane=os.getenv("QONTO_ASSISTANT_DEFAULT_LANE", "green"),
required_scope=os.getenv("QONTO_ASSISTANT_REQUIRED_SCOPE", "finance.qonto.read"),
enforce_scope=os.getenv("QONTO_ASSISTANT_ENFORCE_SCOPE", "false").lower() == "true",
policy_file=policy_file,
qonto_base_url=os.getenv("QONTO_BASE_URL", "https://thirdparty.qonto.com"),
qonto_fixture_dir=(
Path(os.environ["QONTO_FIXTURE_DIR"]).resolve()
if os.getenv("QONTO_FIXTURE_DIR")
else None
),
qonto_auth_mode=os.getenv("QONTO_AUTH_MODE", "legacy_api_key"),
qonto_organization_path=os.getenv("QONTO_ORGANIZATION_PATH", "/v2/organization"),
qonto_transactions_path=os.getenv("QONTO_TRANSACTIONS_PATH", "/v2/transactions"),
qonto_timeout_seconds=float(os.getenv("QONTO_TIMEOUT_SECONDS", "10")),
qonto_max_retries=int(os.getenv("QONTO_MAX_RETRIES", "1")),
qonto_secret_ttl_seconds=int(os.getenv("QONTO_SECRET_TTL_SECONDS", "300")),
rate_limit_requests=int(os.getenv("QONTO_RATE_LIMIT_REQUESTS", "20")),
rate_limit_window_seconds=int(os.getenv("QONTO_RATE_LIMIT_WINDOW_SECONDS", "60")),
max_concurrency=int(os.getenv("QONTO_MAX_CONCURRENCY", "4")),
credential_source=os.getenv("QONTO_CREDENTIAL_SOURCE", "env"),
openbao_path=os.getenv("QONTO_OPENBAO_PATH", "tenants/binky/qonto-api"),
openbao_command=os.getenv("QONTO_OPENBAO_COMMAND", "bao"),
openbao_timeout_seconds=float(os.getenv("QONTO_OPENBAO_TIMEOUT_SECONDS", "5")),
host=os.getenv("QONTO_ASSISTANT_HOST", "127.0.0.1"),
port=int(os.getenv("QONTO_ASSISTANT_PORT", "8080")),
)