Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'

git-subtree-dir: fiscal
git-subtree-mainline: d95165fc8f
git-subtree-split: 102ba84500
This commit is contained in:
gti
2026-09-29 14:43:23 -03:00
868 changed files with 566394 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
# GTI Fiscal Hub + Agent — Operations Guide
Living document. Update it when the architecture or operational flow changes;
keep `docs/AI_PROJECT_STATE.md` as the short factual snapshot, this file as
the "how do I actually run it" reference.
## RC1 operational notes (1.0.0-rc1)
- **Versions:** `release/VERSION`, `central/app/version.py` (`GTI_FISCAL_VERSION`), `GET /version`, `release/RELEASE_MANIFEST.json` (`python scripts/release_manifest.py <token>`: release, agent, engine/decision versions, ACTIVE dataset hashes, migrations head/applied). A release bump never changes fiscal semantics.
- **Modes:** `FISCAL_MODE=DRY_RUN` makes writes technically impossible (Config.firebird_writes is false, `open_write_connection` refuses, the executor is never called) regardless of `FIREBIRD_WRITES`. `AUTO_APPLY` additionally needs the two pilot authorization layers (`AUTHORIZED_WRITE_DATABASE_PATH`, `EXPECTED_SCHEMA_FINGERPRINT`) and is NOT part of RC1 pilots.
- **REPROCESS_ALL result** (stored by the Hub in the job's `result_summary`): counts of products and, in `coverage`, NCM/IPI/CEST/CFOP/CSOSN/ST/role metrics plus `company_context` (masked CNPJ, UF, CRT, source of truth).
- **Company context:** `python agent/scripts/preflight_company_context.py` for the resolver alone; rules in `docs/AGENT_COMPANY_CONTEXT_RULES_V1.md`.
- **Known follow-up (not RC1):** `dataset_versions()` runs ~11 queries per classification request and dominates the ~15 ms cache-hit latency; it can be memoized/cached later.
## Architecture
```
Firebird (client ERP, TESTESOFICIAIS.FDB in dev)
│ read-only SELECT (always)
▼
Agent (agent/) Hub (central/) ── PostgreSQL
- reads C000025 - classify NCM/CEST/TIPI/BA_ST_MVA
- polls Hub for Jobs ────HTTP──▶ - owns Job queue + Agent registry
- runs dry-run + guarded - never touches Firebird, never
IPI write locally stores Firebird credentials
- writes only to the same
Firebird it read from (only
when FIREBIRD_WRITES=1 and
the path is the authorized
disposable test database)
```
The Hub never has Firebird credentials and never connects to any ERP
database. All Firebird reads/writes happen inside the Agent process, on the
machine where the ERP database lives.
## Bring up Hub + Postgres
```bash
docker compose up -d postgres fiscal-hub
curl http://localhost:8000/health
```
The `fiscal-hub` container runs `alembic upgrade head` on startup, so schema
migrations apply automatically. Check `docker compose logs fiscal-hub` if it
doesn't come up healthy.
## Agent configuration
Copy `.env.example` to `.env` locally (never commit it — already gitignored)
and set:
| Variable | Meaning |
|---|---|
| `GTI_HUB_URL` | Hub base URL, e.g. `http://localhost:8000` (falls back to `HUB_URL` for backward compatibility) |
| `AGENT_TOKEN` | Bearer token identifying this Agent (falls back to `AGENT_DEV_TOKEN` in dev) |
| `AGENT_ID` | Informational identifier for this Agent instance (the Hub derives the real identity from the token) |
| `COMPANY_ID` / `COMPANY_UF` | Which client/company this Agent instance serves (informational, for logs/heartbeat) |
| `FIREBIRD_DATABASE_PATH` | Absolute path to the client's `.FDB` file |
| `FB_USER` / `FB_PASSWORD` | Firebird credentials — **local only, never sent to the Hub** |
| `FIREBIRD_WRITES` | `0` (default, required) or `1`. See "Write safety" below. |
| `POLL_INTERVAL_SECONDS` | How long to sleep when there's no pending Job (default `5`) |
| `AGENT_VERSION` | Reported in heartbeats/job results |
## Registering an Agent
There's no separate "register" endpoint yet — an Agent is created the first
time its bearer token is used against the Hub (`bootstrap` in
`central/app/auth.py`), the same mechanism `/v1/classify` already used. Set
`AGENT_TOKEN`/`AGENT_DEV_TOKEN` to a value only this Agent knows and the Hub
will create the Agent record on first contact.
## Starting the Agent
```bash
cd agent
python scripts/run_agent_service.py
```
On startup it prints one line confirming Hub and Firebird connectivity, then
loops forever: heartbeat → poll for a Job → claim → execute → report →
sleep or poll again. `Ctrl+C` (SIGINT) or SIGTERM stops it after the current
iteration finishes.
For a bounded run (useful for scripting/tests), pass `--max-iterations N`.
## Creating work
```bash
# Reprocess everything this Agent can see in C000025
curl -X POST $GTI_HUB_URL/v1/jobs \
-H "Authorization: Bearer $AGENT_TOKEN" -H "Content-Type: application/json" \
-d '{"kind": "REPROCESS_ALL"}'
# Reprocess a specific list of CODIGOs
curl -X POST $GTI_HUB_URL/v1/jobs \
-H "Authorization: Bearer $AGENT_TOKEN" -H "Content-Type: application/json" \
-d '{"kind": "REPROCESS_PRODUCTS", "payload": {"codigos": ["000026", "000027"]}}'
```
Both return the Job in `PENDING` status. The Agent that owns that token is
the only one that will ever see or claim it — `GET /v1/jobs/next` only
returns `PENDING` jobs whose `agent_id` matches the caller.
## Checking status
```bash
curl $GTI_HUB_URL/v1/jobs/<job_id> -H "Authorization: Bearer $AGENT_TOKEN"
```
`status` is one of `PENDING`, `RUNNING`, `COMPLETED`, `FAILED`. A completed
job's `result_summary` has `products_processed`, `writes_attempted`,
`writes_succeeded`, `writes_failed`, `no_ops`, plus `agent_id` and
`schema_fingerprint`. A failed job's `error` is a sanitized (truncated,
no-secrets) message; the Agent keeps polling afterwards.
## Heartbeat
Every poll cycle the Agent calls `POST /v1/agents/heartbeat` with its
status (`IDLE`/`BUSY`), `schema_fingerprint`, and `agent_version`. The Hub
updates `agents.last_seen_at`, `last_status`, `schema_fingerprint`,
`agent_version`. Check it's alive with:
```sql
SELECT id, name, last_seen_at, last_status, agent_version FROM agents;
```
## Write safety: FIREBIRD_WRITES
- `FIREBIRD_WRITES=0` (the required default): the Agent classifies every
product, builds the dry-run decision, and logs what it *would* write —
but issues no `UPDATE`. Jobs still complete normally.
- `FIREBIRD_WRITES=1`: the Agent also auto-applies `C000025.IPI` when the
proposal is unambiguous (`rate_kind=NUMERIC`, `write_eligible=true`).
Before opening any write connection, it verifies the configured
`FIREBIRD_DATABASE_PATH` is **exactly**
`D:\PROJETOS\GTISISTEMAS\Projeto\TESTESOFICIAIS.FDB` (see
`scripts/firebird_write_executor.py::assert_authorized_write_path`) and
fails closed — no connection opened, no job attempted — if it isn't.
See `CLAUDE.md`'s "Firebird write policy" for the full policy; this is
the only database ever authorized for writes.
- Auto-apply never touches CEST, EX_TIPI, CFOP, CST/CSOSN, PIS/COFINS, or
MVA. Those remain measurement-only.
## Where logs/audits live
- **Central audit** (coarse, per Job): Postgres `jobs.result_summary` —
counts only, queryable via `GET /v1/jobs/{id}`.
- **Local audit** (detailed, per product per attempt): the Agent's SQLite
file (`agent/agent_state.sqlite` by default, override with
`AGENT_STATE_DB`), table `ipi_write_log` — one row per product per Job
run, with before/after values, write status, and reason code. Table
`product_state`/`outbox` still tracks the classify-request dedup from the
original manual runner; `ipi_write_log` is independent of it.
## Running as a Windows Service (future)
`run_agent_service.py`'s `main()` is a plain, blocking, signal-aware loop —
suitable to wrap with `pywin32`'s `win32serviceutil.ServiceFramework` (call
`main()` or `run()` from `SvcDoRun`, use `SvcStop` to flip the same
`GracefulStop` flag). No installer is built yet; this is a follow-up task,
not part of this MVP.