# 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 `: 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/ -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.