feat: re-check central RTC authorization right before every write (GET /v1/agents/me/authorizations), human labels on the cClassTrib control page, final corpus/scale evidence; 1.2.1

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-25 08:57:45 -03:00
co-authored by Claude Sonnet 5
parent 42dc172763
commit 7063c8c161
39 changed files with 4231 additions and 58 deletions
+1
View File
@@ -21,6 +21,7 @@ class HubClient:
response = httpx.get(f"{self.url}{path}", headers=self._headers(), timeout=30)
response.raise_for_status(); return response.json()
def identity(self): return self.get_json("/v1/agents/me/identity")
def authorizations(self): return self.get_json("/v1/agents/me/authorizations")
def health(self):
response = httpx.get(f"{self.url}/health", timeout=10)
return response.status_code == 200
+1 -1
View File
@@ -9,7 +9,7 @@ import os
import sqlite3
from pathlib import Path
AGENT_VERSION_DEFAULT = "1.2.0"
AGENT_VERSION_DEFAULT = "1.2.1"
REQUIRED_ENV = ("GTI_HUB_URL", "AGENT_TOKEN", "FIREBIRD_DATABASE_PATH", "FB_USER", "FB_PASSWORD")
REQUIRED_CAPABILITIES = ("supports_ncm", "supports_ipi", "supports_cest")
FISCAL_MODES = ("DRY_RUN", "AUTO_APPLY")
+10 -2
View File
@@ -147,10 +147,18 @@ def apply_candidate(conn, candidate: dict, *, company: dict, decision: dict, now
raise
def apply_candidates(conn, candidates: list[dict], *, company: dict, decision: dict, now: str, job_id=None, today: date | None = None) -> dict:
"""Applies each candidate independently; a failure or an abort never stops the rest and never leaves a partial change."""
def apply_candidates(conn, candidates: list[dict], *, company: dict, decision: dict, now: str, job_id=None, today: date | None = None, authorize=None) -> dict:
"""Applies each candidate independently; a failure or an abort never stops the rest and never leaves a partial change.
`authorize(product_code)` (optional) re-asks the Hub, right before each UPDATE, whether the central authorization still permits it: it returns None to proceed or a
reason (AUTHORIZATION_CHANGED / AUTHORIZATION_UNVERIFIABLE) to skip the product with zero writes."""
applied, aborted, reasons = [], [], {}
for candidate in candidates:
if authorize is not None:
denied = authorize(candidate["product_code"])
if denied:
aborted.append({"outcome": "ABORTED", "product_code": candidate["product_code"], "field": "CCLASSTRIB", "reason": denied, "rule_id": candidate["rule_id"], "job_id": job_id, "timestamp": now, "action": "APPLY", "authorization_origin": decision.get("origin")})
reasons[denied] = reasons.get(denied, 0) + 1
continue
try:
applied.append(apply_candidate(conn, candidate, company=company, decision=decision, now=now, job_id=job_id, today=today))
except RtcApplyAborted as stop: