Missão 4 / Bloco E.1-E.2, on feature/cfop-ippt-write only (never main):
- Migration 0033_cfop_ippt_write_mode: Installation.cfop_ippt_write_mode
(DISABLED|DRY_RUN|APPROVAL_REQUIRED|ENABLED, DEFAULT 'DISABLED' for every
installation, existing and new) + append-only cfop_ippt_write_audits table.
- central/app/cfop_ippt_audit.py mirrors rtc_audit.py for this narrower
capability: job_authorizations(installation) (delivered inside the
authenticated job/heartbeat authorizations, never a local Agent switch) and
record_cfop_ippt_write_audits (persists applied/aborted cells; only CFOP/
IPPT are ever accepted, everything else silently ignored).
- central/app/web.py: POST /agents/{id}/cfop-ippt-write - ADMIN only (SSO
Gerenciador), CSRF, typing GRAVAR required for any mode other than
DISABLED, every change audited (CFOP_IPPT_WRITE_MODE). Small toggle UI in
agent_detail.html next to Contexto Fiscal.
- Wired into the existing job/heartbeat authorization payload and job
completion handling in central/app/api.py, alongside (never mixed with)
the RTC wiring.
- 18 new tests (central/tests/test_cfop_ippt_write_plumbing.py): default
DISABLED everywhere (job payload AND /v1/agents/me/authorizations); a
forged completion payload can never change the mode (read fresh from the
Installation row, never from Agent-supplied JSON); PADRAO role gets 403
with no audit row; ADMIN + wrong/missing CSRF is refused; ADMIN without
typing GRAVAR is refused (422); disabling never requires GRAVAR; invalid
mode is refused; the audit recorder only accepts CFOP/IPPT, silently
drops any other field (e.g. a stray CCLASSTRIB row).
- Two existing RTC tests updated for the additive authorizations-dict key
(cfop_ippt_write_mode) they now also carry.
Still NOT wired into agent/scripts/run_agent_service.py::execute_job - no
Agent code calls select_candidates/apply_candidates yet, so nothing writes
to Firebird from this branch. That wiring, an Agent version bump, a new
installer and Windows Sandbox validation are the next step, deliberately
left for after review (see docs/CFOP_IPPT_WRITE_CAPABILITY_V1.md) - this
mission explicitly asked not to publish this as an active release yet.
FULL_SUITE on this branch: 1 known, expected failure -
test_manifest_records_the_real_migration_head - because this branch adds
migration 0033 that was never merged to master/deployed; RELEASE_MANIFEST.json
intentionally still describes master/production (head 0032). Every other
test passes, including all CFOP/IPPT and RTC-adjacent tests.
REAL_PRODUCT_FISCAL_WRITES=0 (no write path is reachable from any Agent
code on any branch).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
52 lines
2.8 KiB
Python
52 lines
2.8 KiB
Python
"""Persists the Agent's CFOP/IPPT write audit records (append-only). Mirrors central.app.rtc_audit's
|
|
safety model for a separate, narrower capability - no candidate preview table (candidates are
|
|
recomputed fresh from field_contract every job, never stored server-side), no rollback queue yet
|
|
(rollback is operator-triggered later, out of scope for this pass)."""
|
|
from __future__ import annotations
|
|
|
|
from .events import record_event
|
|
from .models import CfopIpptWriteAudit
|
|
|
|
MAX_AUDITS_PER_JOB = 2000
|
|
WRITE_MODES = ("DISABLED", "DRY_RUN", "APPROVAL_REQUIRED", "ENABLED")
|
|
ALLOWED_FIELDS = ("CFOP", "IPPT")
|
|
|
|
|
|
def _clip(value, size):
|
|
return None if value is None else str(value)[:size]
|
|
|
|
|
|
def job_authorizations(installation) -> dict:
|
|
"""What the authenticated job carries to the Agent. Decided centrally per installation; the Agent
|
|
has no local switch. DISABLED unless an installation row explicitly says otherwise (server_default)."""
|
|
mode = installation.cfop_ippt_write_mode if installation.cfop_ippt_write_mode in WRITE_MODES else "DISABLED"
|
|
return {"cfop_ippt_write_mode": mode}
|
|
|
|
|
|
def record_cfop_ippt_write_audits(session, *, tenant_id: int, installation, agent, job_id: int | None, cfop_ippt_apply: dict | None) -> int:
|
|
"""Stores every applied/aborted write of the job. Never trusts the Agent for identity: tenant/installation/agent come from the authenticated context."""
|
|
if not isinstance(cfop_ippt_apply, dict):
|
|
return 0
|
|
records = list(cfop_ippt_apply.get("applied") or []) + list(cfop_ippt_apply.get("aborted") or [])
|
|
stored = applied = aborted = 0
|
|
for item in records[:MAX_AUDITS_PER_JOB]:
|
|
if not isinstance(item, dict) or item.get("field") not in ALLOWED_FIELDS:
|
|
continue
|
|
outcome = str(item.get("outcome", ""))[:16]
|
|
session.add(CfopIpptWriteAudit(
|
|
tenant_id=tenant_id, installation_id=installation.id, agent_id=agent.id, job_id=job_id,
|
|
product_code=_clip(item.get("product_code"), 64) or "", field=item["field"], outcome=outcome,
|
|
reason=_clip(item.get("reason") or item.get("reason_code"), 96), old_value=_clip(item.get("old_value"), 16), new_value=_clip(item.get("new_value"), 16),
|
|
action="APPLY", written_at=_clip(item.get("timestamp"), 40)))
|
|
stored += 1
|
|
if outcome == "APPLIED":
|
|
applied += 1
|
|
else:
|
|
aborted += 1
|
|
if stored:
|
|
record_event(session, tenant_id=tenant_id, installation_id=installation.id, agent_id=agent.id, severity="WARNING" if applied else "INFO", event_type="CFOP_IPPT_AUTO_APPLY", job_id=job_id,
|
|
message=f"CFOP/IPPT: {applied} aplicado(s), {aborted} abortado(s)",
|
|
context={"applied": applied, "aborted": aborted, "reasons": cfop_ippt_apply.get("aborted_reasons") or {}, "mode": cfop_ippt_apply.get("mode")})
|
|
session.commit()
|
|
return stored
|