chore: establish project baseline

This commit is contained in:
OpenCode
2026-09-21 15:51:47 -03:00
commit 9dcc263217
459 changed files with 447890 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
"""GTI Fiscal Agent installation preflight (READ-ONLY): python agent/scripts/preflight.py [--env-file FILE] [--json]
Prints PREFLIGHT=PASS|FAIL and the blockers; exit code 0 on PASS, 2 on FAIL. Secrets are never printed.
"""
import argparse
import json
import os
import sys
from pathlib import Path
AGENT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(AGENT_ROOT / "src"))
from gti_fiscal_agent.preflight import run_preflight # noqa: E402
def load_env(path):
if path and Path(path).exists():
for line in Path(path).read_text(encoding="utf-8").splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, _, v = line.partition("=")
os.environ.setdefault(k.strip(), v.strip())
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("--env-file")
ap.add_argument("--json", action="store_true")
a = ap.parse_args(argv)
load_env(a.env_file or os.environ.get("GTI_AGENT_ENV_FILE") or AGENT_ROOT.parent / ".env")
os.environ.setdefault("AGENT_STATE_DB", str(AGENT_ROOT / "agent_state.sqlite"))
os.environ.setdefault("AGENT_LOG_PATH", str(AGENT_ROOT / "logs" / "agent.log"))
res = run_preflight(os.environ)
if a.json:
print(json.dumps(res, ensure_ascii=False, indent=1, default=str))
else:
for c in res["checks"]:
print(f" [{c['status']:<4}] {c['name']:<24} {c['detail']}")
print(f"PREFLIGHT={res['PREFLIGHT']}")
for b in res["blockers"]:
print(f"BLOCKER: {b}")
raise SystemExit(0 if res["PREFLIGHT"] == "PASS" else 2)
if __name__ == "__main__":
main()
@@ -0,0 +1,40 @@
"""Agent preflight: COMPANY_CONTEXT=PASS/FAIL for one Firebird database (READ-ONLY).
python agent/scripts/preflight_company_context.py [--database <FDB>]
Configuration (environment or the local ignored .env): COMPANY_CODE / COMPANY_CNPJ (company selection when C000004 is ambiguous),
COMPANY_CRT_SOURCE = EMISSOR | C000004 (explicit choice when the two CRT sources disagree). The full CNPJ is never printed.
"""
import argparse
import json
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "agent" / "src"))
from gti_fiscal_agent.company_context import preflight_company_context, preflight_report # noqa: E402
def load_dotenv(path):
if path.exists():
for line in path.read_text(encoding="utf-8").splitlines():
if "=" in line and not line.startswith("#"):
k, _, v = line.partition("=")
os.environ.setdefault(k.strip(), v.strip())
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("--database")
a = ap.parse_args(argv)
load_dotenv(ROOT / ".env")
if a.database:
os.environ["FIREBIRD_DATABASE_PATH"] = a.database
report = preflight_report(preflight_company_context(os.environ))
print(json.dumps(report, ensure_ascii=False, indent=1))
raise SystemExit(0 if report["COMPANY_CONTEXT"] == "PASS" else 2)
if __name__ == "__main__":
main()
+34
View File
@@ -0,0 +1,34 @@
"""Run the agent against a sanitized sample only after an explicit fingerprint gate."""
import argparse, json, os, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from gti_fiscal_agent.client import HubClient
from gti_fiscal_agent.runner import run_sample
from gti_fiscal_agent.state import AgentState
FIELDS = "CODIGO,PRODUTO,NCM,CLASSIFICACAO_FISCAL,CFOP,CFOP_ENTRADA,CST,CST_ENTRADA,ALIQUOTA,CEST,CSOSN,COD_ENTRADA_COFINS,COD_SAIDA_COFINS,ALIQ_COFINS,COD_ENTRADA_PIS,COD_SAIDA_PIS,ALIQ_PIS,ALIQ_IPI,CST_IPI,ORIGEM,IAT,INCIDENCIA_PISCOFINS,IPPT,ULTIMA_ALTERACAO"
def read_firebird_products(codes):
import fdb
connection = fdb.connect(database=os.environ["FIREBIRD_DATABASE_PATH"], user=os.environ["FB_USER"], password=os.environ["FB_PASSWORD"], fb_library_name=os.environ.get("FIREBIRD_CLIENT_LIBRARY"), isolation_level=fdb.ISOLATION_LEVEL_READ_COMMITED_RO)
try:
cursor = connection.cursor(); cursor.execute(f"SELECT {FIELDS} FROM C000025 WHERE CODIGO IN ({','.join('?' for _ in codes)}) ORDER BY CODIGO", tuple(codes))
names = [column[0].strip() for column in cursor.description]
return [dict(zip(names, row)) for row in cursor.fetchall()]
finally:
connection.close()
def main():
parser = argparse.ArgumentParser(); parser.add_argument("--sample", required=True); parser.add_argument("--output", required=True); parser.add_argument("--schema-fingerprint", default="")
args = parser.parse_args(); sample = json.loads(Path(args.sample).read_text(encoding="utf-8"))
codes = [item["CODIGO"] for item in sample.get("sample_without_ncm", [])[:20]]
products = read_firebird_products(codes)
result = run_sample(products, HubClient(os.getenv("HUB_URL", "http://localhost:8000"), os.getenv("AGENT_DEV_TOKEN", "")), AgentState(Path(args.output).with_suffix(".sqlite")), args.schema_fingerprint)
result["products_requested"] = len(products); result["firebird_products_read"] = len(products); result["firebird_access"] = "READ_ONLY_SELECT"
output = Path(args.output)
if result["results"]:
output.write_text(json.dumps(result, indent=2, default=str), encoding="utf-8")
elif not output.exists():
output.write_text(json.dumps(result, indent=2, default=str), encoding="utf-8")
print(json.dumps(result, indent=2))
if __name__ == "__main__": main()
+104
View File
@@ -0,0 +1,104 @@
"""Integrated Agent flow: Firebird read -> Hub /v1/classify/batch -> IPI dry-run
-> guarded auto-apply write.
FIREBIRD_WRITES=0 (default): classifies every product and logs the decision
(and what write *would* happen) to the local SQLite audit log; never writes.
FIREBIRD_WRITES=1: also auto-applies every write_eligible C000025.IPI proposal
where rate_kind=NUMERIC, reusing the same schema/path/allowlist/compare-before-write
guards as the manual executor. Fails closed before touching the connection unless
the configured database is exactly the authorized disposable test database.
Auto-apply is scoped to table=C000025, field=IPI, rate_kind=NUMERIC only. NT/
non-numeric markers, blank rates, ambiguous EX candidates, legacy/invalid NCM,
CEST and EX_TIPI are never auto-applied here.
"""
import argparse
import importlib.util
import json
import os
import sys
from pathlib import Path
AGENT_ROOT = Path(__file__).resolve().parents[1]
PROJECT_ROOT = AGENT_ROOT.parent
sys.path.insert(0, str(AGENT_ROOT / "src"))
from gti_fiscal_agent.client import HubClient # noqa: E402
from gti_fiscal_agent.firebird_reader import read_products # noqa: E402
from gti_fiscal_agent.pipeline import run_pipeline # noqa: E402
from gti_fiscal_agent.runner import REFERENCE_SCHEMA_V1, to_hub_product # noqa: E402
from gti_fiscal_agent.state import AgentState # noqa: E402
def load_dotenv(path: Path) -> None:
if not path.exists():
return
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
os.environ.setdefault(key.strip(), value.strip())
def load_executor():
spec = importlib.util.spec_from_file_location("firebird_write_executor", PROJECT_ROOT / "scripts" / "firebird_write_executor.py")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--hub-url", default=os.getenv("HUB_URL", "http://localhost:8000"))
parser.add_argument("--token", default=os.getenv("AGENT_DEV_TOKEN", ""))
parser.add_argument("--state-db", default=str(AGENT_ROOT / "agent_state.sqlite"))
parser.add_argument("--schema-fingerprint", default=REFERENCE_SCHEMA_V1)
args = parser.parse_args()
load_dotenv(PROJECT_ROOT / ".env")
if not args.token:
args.token = os.environ.get("AGENT_DEV_TOKEN", "")
firebird_writes = os.environ.get("FIREBIRD_WRITES", "0") == "1"
executor = load_executor()
database_path = os.environ["FIREBIRD_DATABASE_PATH"]
if firebird_writes:
# Fail closed before any product is read or any connection is opened for writing.
executor.assert_authorized_write_path(database_path)
if args.schema_fingerprint != REFERENCE_SCHEMA_V1:
print(json.dumps({"status": "SCHEMA_UNKNOWN", "processed": 0}))
return
products = read_products()
client = HubClient(args.hub_url, args.token)
decisions = client.classify_batch([to_hub_product(p) for p in products])
state = AgentState(args.state_db)
write_connection = None
if firebird_writes:
import fdb
write_connection = fdb.connect(
database=database_path,
user=os.environ["FB_USER"],
password=os.environ["FB_PASSWORD"],
fb_library_name=os.environ.get("FIREBIRD_CLIENT_LIBRARY"),
)
try:
summary = run_pipeline(products, decisions, state, firebird_writes, write_connection, executor, database_path)
finally:
if write_connection is not None:
write_connection.close()
summary = {"status": "OK", "firebird_writes_enabled": firebird_writes, **summary}
print(json.dumps(summary, indent=2))
if __name__ == "__main__":
main()
+297
View File
@@ -0,0 +1,297 @@
"""Long-running Agent service: heartbeat + poll Hub jobs + execute pipeline.
startup -> load config -> validate Hub -> validate Firebird -> heartbeat
-> poll job -> claim -> execute -> report result -> back to polling.
No job available: sleep POLL_INTERVAL_SECONDS.
Hub unavailable: retry with simple capped backoff; the Agent stays alive.
Job fails internally (bad Firebird read, classify error, write refused):
the job is reported FAILED with a sanitized error and the Agent keeps polling.
SIGINT/SIGTERM: finish the current job (if any) and stop the loop cleanly.
Written to be runnable as-is today, and easy to later wrap as a Windows
Service (a service wrapper would just call main()); no installer here.
"""
import argparse
import importlib.util
import json
import logging
import logging.handlers
import os
import signal
import sys
import time
from pathlib import Path
AGENT_ROOT = Path(__file__).resolve().parents[1]
PROJECT_ROOT = AGENT_ROOT.parent
sys.path.insert(0, str(AGENT_ROOT / "src"))
sys.path.insert(0, str(PROJECT_ROOT)) # ships central/app engine modules read-only (central.app.*)
from gti_fiscal_agent.client import HubClient # noqa: E402
from gti_fiscal_agent.firebird_reader import check_connection, read_products # noqa: E402
from gti_fiscal_agent.pipeline import run_pipeline # noqa: E402
from gti_fiscal_agent.runner import REFERENCE_SCHEMA_V1, to_hub_product # noqa: E402
from gti_fiscal_agent.state import AgentState # noqa: E402
_MAX_BACKOFF_SECONDS = 60
logger = logging.getLogger("gti_fiscal_agent")
def setup_logging(log_path):
"""Rotating local log file. Never logs FB_PASSWORD or AGENT_TOKEN — every
log call in this module only references non-secret identifiers/counts."""
logger.setLevel(logging.INFO)
if logger.handlers:
return logger
Path(log_path).parent.mkdir(parents=True, exist_ok=True)
handler = logging.handlers.RotatingFileHandler(log_path, maxBytes=5_000_000, backupCount=5, encoding="utf-8")
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(handler)
logger.addHandler(logging.StreamHandler())
return logger
def load_dotenv(path: Path) -> None:
if not path.exists():
return
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
os.environ.setdefault(key.strip(), value.strip())
def load_executor():
spec = importlib.util.spec_from_file_location("firebird_write_executor", PROJECT_ROOT / "scripts" / "firebird_write_executor.py")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class Config:
def __init__(self, env):
self.hub_url = env.get("GTI_HUB_URL") or env.get("HUB_URL") or "http://localhost:8000"
self.agent_id = env.get("AGENT_ID", "")
self.agent_token = env.get("AGENT_TOKEN") or env.get("AGENT_DEV_TOKEN", "")
self.company_id = env.get("COMPANY_ID", "")
self.company_uf = env.get("COMPANY_UF", "")
self.database_path = env["FIREBIRD_DATABASE_PATH"]
# FISCAL_MODE gates writes technically: DRY_RUN can never write, whatever FIREBIRD_WRITES says. Unset = legacy
# behaviour (FIREBIRD_WRITES decides). `firebird_writes` below is the EFFECTIVE value used by every write path.
requested = env.get("FIREBIRD_WRITES", "0") == "1"
self.fiscal_mode = (env.get("FISCAL_MODE") or ("AUTO_APPLY" if requested else "DRY_RUN")).strip().upper()
self.requested_writes = requested
self.firebird_writes = requested and self.fiscal_mode == "AUTO_APPLY"
self.poll_interval_seconds = float(env.get("POLL_INTERVAL_SECONDS", "5"))
self.agent_version = env.get("AGENT_VERSION", "1.0.0-rc1")
self.schema_fingerprint = env.get("SCHEMA_FINGERPRINT", REFERENCE_SCHEMA_V1)
self.state_db = env.get("AGENT_STATE_DB", str(AGENT_ROOT / "agent_state.sqlite"))
# Pilot write authorization: both must be explicitly set by the installer for
# a real client database. Left unset, both default to the dev-only values,
# so a real client path/schema is never silently authorized (see
# scripts/firebird_write_executor.py's assert_authorized_write_path).
self.authorized_write_database_path = env.get("AUTHORIZED_WRITE_DATABASE_PATH")
self.expected_schema_fingerprint = env.get("EXPECTED_SCHEMA_FINGERPRINT", REFERENCE_SCHEMA_V1)
self.log_path = env.get("AGENT_LOG_PATH", str(AGENT_ROOT / "logs" / "agent.log"))
class GracefulStop:
def __init__(self):
self.stop = False
signal.signal(signal.SIGINT, self._handle)
signal.signal(signal.SIGTERM, self._handle)
def _handle(self, signum, frame):
self.stop = True
def build_job_coverage(env, client, products, decisions):
"""Company-context-aware coverage over the job's products (read-only). Fails closed when the company context is unresolved."""
from gti_fiscal_agent.company_context import mask_cnpj, preflight_company_context
from gti_fiscal_agent.coverage import build_refs, compute_coverage
from gti_fiscal_agent.firebird_reader import read_evidence
ctx = preflight_company_context(env)
if ctx["status"] != "PASS":
raise RuntimeError("COMPANY_CONTEXT=FAIL: " + "; ".join(ctx.get("reasons") or ["unresolved"]))
evidence = read_evidence(env, ctx["company_code"])
refs = build_refs(client.get_json, products, ctx["company_uf"])
report = compute_coverage(products, decisions, {"uf": ctx["company_uf"], "crt": ctx["company_crt"]}, evidence.get("company_default_csosn"), refs, evidence)
report["company_context"] = {"COMPANY_CONTEXT_STATUS": "PASS", "classification": ctx["classification"], "company_code": ctx["company_code"], "company_cnpj": ctx["company_cnpj"], "company_uf": ctx["company_uf"],
"company_crt": ctx["company_crt"], "source_of_truth": ctx["source_of_truth"], "confidence": ctx["confidence"], "warnings": ctx["warnings"]}
return report
def open_write_connection(env, database_path, firebird_writes=True):
if not firebird_writes: # technical guarantee: no write-capable connection exists in DRY_RUN / FIREBIRD_WRITES=0
raise RuntimeError("write connection refused: writes are disabled (FISCAL_MODE=DRY_RUN or FIREBIRD_WRITES=0)")
import fdb
return fdb.connect(
database=database_path,
user=env["FB_USER"],
password=env["FB_PASSWORD"],
fb_library_name=env.get("FIREBIRD_CLIENT_LIBRARY"),
)
def execute_job(job, config, executor, state, env):
"""Run the pipeline for one claimed job. Never raises for job-domain
failures (bad Firebird read, classify error) — returns (result_summary, error)."""
kind = job["kind"]
payload = job.get("payload") or {}
try:
if config.firebird_writes:
# Layered, per-installation authorization: the path must match this
# installation's own explicitly configured authorized_write_database_path
# (defaults to the dev-only constant, so a real client path is refused
# unless the installer deliberately set it), AND the live schema
# fingerprint must match what this installation was configured to expect.
executor.assert_authorized_write_path(config.database_path, authorized_path=config.authorized_write_database_path)
executor.assert_schema_fingerprint_authorized(config.schema_fingerprint, config.expected_schema_fingerprint)
codigos = payload.get("codigos") if kind == "REPROCESS_PRODUCTS" else None
products = read_products(env, codigos=codigos)
logger.info("job %s: read %d product(s) from Firebird", job.get("job_id"), len(products))
client = HubClient(config.hub_url, config.agent_token)
decisions = client.classify_batch([to_hub_product(p) for p in products])
write_connection = None
if config.firebird_writes:
write_connection = open_write_connection(env, config.database_path)
try:
summary = run_pipeline(products, decisions, state, config.firebird_writes, write_connection, executor, config.database_path, authorized_path=config.authorized_write_database_path)
finally:
if write_connection is not None:
write_connection.close()
coverage = build_job_coverage(env, client, products, decisions) if kind == "REPROCESS_ALL" else None
result_summary = {
"coverage": coverage,
"fiscal_mode": getattr(config, "fiscal_mode", None),
"release_version": env.get("AGENT_VERSION") or "1.0.0-rc1",
"products_processed": summary["processed"],
"writes_attempted": summary["writes_attempted"],
"writes_succeeded": summary["writes_succeeded"],
"writes_failed": summary["writes_failed"],
"no_ops": summary["no_ops"],
"firebird_writes_enabled": config.firebird_writes,
"agent_id": job["agent_id"],
"schema_fingerprint": config.schema_fingerprint,
}
logger.info(
"job %s finished: processed=%d writes_attempted=%d writes_succeeded=%d writes_failed=%d no_ops=%d",
job.get("job_id"), summary["processed"], summary["writes_attempted"], summary["writes_succeeded"], summary["writes_failed"], summary["no_ops"],
)
if summary["writes_failed"] > 0:
return result_summary, f"{summary['writes_failed']} write(s) failed guard checks"
return result_summary, None
except executor.WriteRefused as error:
logger.warning("job %s refused: %s", job.get("job_id"), error)
return None, f"write refused: {error}"
except Exception as error: # noqa: BLE001 — job-domain isolation boundary
logger.warning("job %s failed internally: %s: %s", job.get("job_id"), type(error).__name__, error)
return None, f"{type(error).__name__}: {error}"
def run(config, env, *, max_iterations=None, stopper=None):
"""`stopper` defaults to a signal-based GracefulStop (SIGINT/SIGTERM), which
only works from the main thread. A host that manages its own lifecycle
(e.g. the Windows Service wrapper, which stops the loop via the Service
Control Manager instead of OS signals) should pass its own stop object —
anything with a `.stop` boolean attribute — and flip it from any thread."""
executor = load_executor()
state = AgentState(config.state_db)
client = HubClient(config.hub_url, config.agent_token)
stopper = stopper if stopper is not None else GracefulStop()
backoff = 1
was_down = False
iterations = 0
last_job_id = None
while not stopper.stop:
if max_iterations is not None and iterations >= max_iterations:
break
iterations += 1
try:
client.heartbeat("IDLE", schema_fingerprint=config.schema_fingerprint, agent_version=config.agent_version, last_job_id=last_job_id)
if was_down:
logger.info("reconnected to Hub after %ss backoff", backoff)
was_down = False
backoff = 1
except Exception as error:
logger.warning("heartbeat failed, Hub unreachable: %s (retrying in %ss)", error, backoff)
was_down = True
time.sleep(backoff)
backoff = min(backoff * 2, _MAX_BACKOFF_SECONDS)
continue
try:
job = client.next_job()
except Exception as error:
logger.warning("poll failed, Hub unreachable: %s (retrying in %ss)", error, backoff)
was_down = True
time.sleep(backoff)
backoff = min(backoff * 2, _MAX_BACKOFF_SECONDS)
continue
if job is None:
time.sleep(config.poll_interval_seconds)
continue
last_job_id = job.get("job_id")
logger.info("job %s received: kind=%s", job.get("job_id"), job["kind"])
try:
client.heartbeat("BUSY", schema_fingerprint=config.schema_fingerprint, agent_version=config.agent_version, last_job_id=last_job_id)
except Exception:
pass # best-effort; the job itself still proceeds
result_summary, error = execute_job(job, config, executor, state, env)
try:
if error is None:
client.complete_job(job.get("job_id"), result_summary)
logger.info("job %s reported COMPLETED", job.get("job_id"))
else:
client.fail_job(job.get("job_id"), error)
logger.warning("job %s reported FAILED: %s", job.get("job_id"), error)
except Exception as report_error:
logger.warning("could not report job %s result to Hub: %s", job.get("job_id"), report_error)
def enforce_startup_preflight(env):
"""Full read-only preflight at startup (Hub, auth, Firebird, capabilities, company context, guards). Fail closed."""
from gti_fiscal_agent.preflight import run_preflight
report = run_preflight(env, agent_version=env.get("AGENT_VERSION") or "1.0.0-rc1")
logger.info("startup preflight: %s", json.dumps({k: report[k] for k in ("PREFLIGHT", "blockers", "fiscal_mode", "effective_firebird_writes")}, ensure_ascii=False))
print(json.dumps({"PREFLIGHT": report["PREFLIGHT"], "blockers": report["blockers"]}, ensure_ascii=False))
if report["PREFLIGHT"] != "PASS":
logger.error("startup aborted: preflight failed (fail closed): %s", report["blockers"])
raise SystemExit(2)
return report
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--max-iterations", type=int, default=None, help="stop after N poll cycles (used by tests); omit to run forever")
args = parser.parse_args()
load_dotenv(Path(os.environ.get("GTI_AGENT_ENV_FILE") or PROJECT_ROOT / ".env"))
config = Config(os.environ)
setup_logging(config.log_path)
logger.info("startup: agent_id=%s company_id=%s agent_version=%s firebird_writes=%s", config.agent_id, config.company_id, config.agent_version, config.firebird_writes)
hub_ok = HubClient(config.hub_url, config.agent_token).health()
firebird_ok = check_connection(os.environ)
logger.info("startup checks: hub_ok=%s firebird_ok=%s", hub_ok, firebird_ok)
enforce_startup_preflight(os.environ)
run(config, os.environ, max_iterations=args.max_iterations)
logger.info("shutdown: stopped cleanly")
if __name__ == "__main__":
main()