git-subtree-dir: fiscal git-subtree-mainline:d95165fc8fgit-subtree-split:102ba84500
7.6 KiB
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_RUNmakes writes technically impossible (Config.firebird_writes is false,open_write_connectionrefuses, the executor is never called) regardless ofFIREBIRD_WRITES.AUTO_APPLYadditionally 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, incoverage, NCM/IPI/CEST/CFOP/CSOSN/ST/role metrics pluscompany_context(masked CNPJ, UF, CRT, source of truth). - Company context:
python agent/scripts/preflight_company_context.pyfor the resolver alone; rules indocs/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
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
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
# 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
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:
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 noUPDATE. Jobs still complete normally.FIREBIRD_WRITES=1: the Agent also auto-appliesC000025.IPIwhen the proposal is unambiguous (rate_kind=NUMERIC,write_eligible=true). Before opening any write connection, it verifies the configuredFIREBIRD_DATABASE_PATHis exactlyD:\PROJETOS\GTISISTEMAS\Projeto\TESTESOFICIAIS.FDB(seescripts/firebird_write_executor.py::assert_authorized_write_path) and fails closed — no connection opened, no job attempted — if it isn't. SeeCLAUDE.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 viaGET /v1/jobs/{id}. - Local audit (detailed, per product per attempt): the Agent's SQLite
file (
agent/agent_state.sqliteby default, override withAGENT_STATE_DB), tableipi_write_log— one row per product per Job run, with before/after values, write status, and reason code. Tableproduct_state/outboxstill tracks the classify-request dedup from the original manual runner;ipi_write_logis 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.