Arodonata v2
Arodonata v2 (2.0.0) is an async-first Python 3.13+ library, declarative Policy-as-Code (CPCRUD) engine, auditable change report generator, and Streamable-HTTP Model Context Protocol (MCP) server for Check Point Security Management and Multi-Domain Security Management (MDSM).
Looking for Arodonata v1 (
1.16.0)? Legacy v1 remains frozen in this repository on branchmaster_v1and tagv1.16.0. All new projects and downstream integrations should target Arodonata v2.
Quickstart
import asyncio
from arodonata import ArodonataClient
async def main() -> None:
# Reads ARODONATA_* / MGMT_* environment variables and initializes SQLite/PostgreSQL cache
async with ArodonataClient() as client:
domains = await client.get_domains()
print(f"Discovered {len(domains)} domains: {[d.name for d in domains]}")
# Smart cache-first read across hosts in Domain4
hosts = await client.get_hosts(domain="Domain4", cache_mode="smart")
print(f"Domain4 has {len(hosts)} cached hosts")
# Multi-term search with recursive parent group resolution
search = await client.search_objects("10.1.10.50, *.internal.corp", max_depth=2)
for obj in search.objects:
print(f"Matched {obj.type} {obj.name} ({obj.uid}) in {obj.domain_name}")
asyncio.run(main())
Core Capability Matrix
| Capability | Primary Entry Point | Description |
|---|---|---|
| Unified Async Facade | ArodonataClient |
Single composition root wiring transport, SQL cache, distributed locks, rulebase numbering, CPCRUD, and change reports |
| Database-Backed Cache | DatabaseManager, SqlCacheAdapter |
15 arodonata_*-prefixed SQLModel tables on PostgreSQL (asyncpg) or SQLite (aiosqlite) with SHA-256 auto-migration and cross-process locks |
| Smart & Incremental Sync | refresh_objects, stream_refresh_* |
Four cache modes (cache, smart, smart-fast, force) with show-changes incremental diffs and parallel per-MDS-member execution |
| Search & Aggregation | search_objects, query_objects |
IP/CIDR/range/wildcard/exact search with recursive group membership traversal and SQL-free cross-domain aggregation (list, count, describe) |
| Rulebase Introspection | get_package_rulebase, filter_rulebase |
Normalized 6-table rulebase cache with exact SmartConsole hierarchical numbering (1, 2.1, 3), reverse UID lookup (locate_rules), and local filtering |
| Session Isolation & Overlay | client.write_session(...) |
Shared read-only arodonata_sid_cache sessions vs isolated WriteSession with contextvars-scoped SessionOverlay (read-your-own-writes before publish) |
| Declarative CPCRUD | client.cpcrud (CPCRUDService) |
Offline JSON Schema validation, HybridStateReader, two-axis conflict policies (on_name_conflict × on_ip_conflict), staleness guard (DomainStamp), and rollback (inverse) |
| Auditable Change Reports | build_change_report |
Collects object and rule diffs across sessions or date ranges (SessionScope, RangeScope) and renders self-contained HTML, Markdown, and JSON |
| Consolidated MCP Server | arodonata-mcp, create_mcp_server |
Streamable-HTTP MCP server exposing 13 cache-aligned core tools + 5 tamper-proof HMAC-signed CPCRUD tools for AI agents and FastAPI host apps |
| Observability & Security | @traced, redact_sid, TrustPolicy |
Provider-agnostic OpenTelemetry spans (arlogi[otel]), TOFU/pinned TLS fingerprint verification, and automatic sid/secret redaction everywhere |
Documentation Map
- Getting Started: Installation & Overview, Your First Script, and CPCRUD Quickstart.
- Downstream Integration Contract: Capabilities & Integration Guide (
CAPABILITIES.md) — mandatory reading for host applications (e.g., FPCR v2). - Architecture Deep Dives:
- User Guides & MCP:
- Configuration: Environment Variables & Settings, Multi-Server Topology, and TLS Verification (
pinned/tofu/lab-memory). - Examples (
01..12): Runnable Examples Catalog. - Development: Testing & Quality Gates (
./pytest.sh) and Changelog.
Arodonata v2: Capabilities & Downstream Integration Guide
Version: 2.0.0 · Status: Canonical Architecture Specification · Date: October 2026
1. Executive Summary & Architectural Role
Arodonata v2 is an async-first Python 3.13+ platform library, hexagonal SDK, Policy-as-Code (CPCRUD) engine, auditable change report generator, and Streamable-HTTP MCP server for Check Point Security Management and Multi-Domain Security Management (MDSM).
In the enterprise automation ecosystem (such as FPCR v2, web portals, CLIs, and AI agent platforms), Arodonata acts as the single authoritative engine for all Check Point interactions:
- Native
aiohttptransport, post-handshake SHA-256 certificate verification (pinned,tofu,lab-memory), per-MDS-member rate limiting, and70slogin throttle recovery (LoginGate). - Database-backed caching across 15
arodonata_*-prefixed SQLModel tables (PostgreSQLasyncpgand SQLiteaiosqlite) with SHA-256 schema hash auto-migration, naive UTC timestamp uniformity (tzinfo=None), and distributed locks (arodonata_distributed_locks). - Incremental (
show-changes) and full synchronization across domains, objects, services, assets (gateways/clusters/VSX), and rulebases with typed streaming progress events (SyncProgressEvent). - Isolated write sessions (
WriteSession) withcontextvars-boundSessionOverlayfor read-your-own-writes isolation prior topublish. - Declarative Policy-as-Code (
CPCRUDService): offline JSON Schema validation,HybridStateReader, two-axis conflict policies (NameConflictPolicy×IpConflictPolicy), pre-publish staleness verification (DomainStamp), staged session handoff (take-over-session), and compensating rollback generation (inverse). - Normalized 6-table rulebase cache with exact SmartConsole hierarchical rule numbering (
1,2.1,3), reverse UID lookup (locate_rules), and local rulebase filtering (filter_rulebase, includingoutput_format="cpcrud"). - Auditable change evidence collection and offline multi-format rendering (
HTML,Markdown,JSON). - Consolidated Model Context Protocol (
arodonata.mcp) server with 13 cache-aligned core tools and 5 tamper-proof HMAC-signed CPCRUD tools.
Core Architectural Principle for Downstream Consumers
"Delegate, Never Duplicate" Downstream applications MUST NOT implement custom Check Point HTTP callers, bespoke SQL cache tables for Check Point objects/rules, manual
sidsession pools, custom rulebase numbering parsers, or ad-hoc object naming algorithms. Consumer applications act strictly as business workflow orchestrators, state machines, user interfaces, and authorization/audit coordinators.
2. Capability Catalog & Consumer Contracts
| Capability Area | Canonical Arodonata v2 Module | Primary Classes / Functions | Configuration Source |
|---|---|---|---|
| Unified Facade | arodonata / arodonata.client |
ArodonataClient, ArodonataSettings |
ARODONATA_* / MGMT_* env vars |
| Storage & Locks | arodonata.adapters.storage |
DatabaseManager, SqlCacheAdapter, SqlLockAdapter, ARODONATA_TABLE_NAMES |
ARODONATA_DATABASE_URL |
| Transport & Sessions | arodonata.adapters.checkpoint |
AMgmtClient, TrustPolicy, RateLimiter, LoginGate, TaskWaiter |
ARODONATA_TLS_TRUST, ARODONATA_CONCURRENT_LIMIT |
| Contexts & Overlays | arodonata.core.context |
WriteSession, SessionOverlay, AuthContext, UserContext |
Python contextvars |
| Sync, Search & Query | arodonata.core.services |
DomainSyncCoordinator, ObjectService, ServiceCacheService, AssetRefreshService, SearchService |
CacheMode (cache, smart, smart-fast, force) |
| Rulebase & Numbering | arodonata.rulebase |
CachedRulebaseSource, RulebaseRefreshService, fetch_paged_rulebase, rule_to_cpcrud_operation |
RuleFormat (markdown, model_friendly, raw, cpcrud) |
| Declarative CPCRUD | arodonata.cpcrud |
CPCRUDService, Plan, ApplyReport, NamingPrefixes, StagedSessionInfo |
checkpoint_ops_schema.json |
| Evidence & Reports | arodonata.reports |
collect_change_report, render_change_report, build_change_report |
SessionScope, RangeScope, RenderOptions |
| MCP Integration | arodonata.mcp |
create_mcp_server, create_asgi_app, ArodonataMCPSettings |
ARODONATA_MCP_* env vars |
3. Deep-Dive: Host Application Integration Patterns
3.1 Shared AsyncEngine & Namespace Coexistence
Host applications (such as FastAPI backends using SQLAlchemy/SQLModel) can pass their existing AsyncEngine directly to ArodonataClient(engine=host_engine).
- Non-Destructive Ownership (
owns_engine=False): When an externalAsyncEngineis passed,await client.close()closes Arodonata's HTTP transport and background tasks without disposing the host application'sAsyncEngine. - Strict Table Namespace Isolation (
arodonata_*): All 15 Arodonata tables are prefixed witharodonata_(ARODONATA_TABLE_NAMES).DatabaseManager.initialize()inspects and migrates onlyarodonata_*tables, completely ignoring any host application tables registered inSQLModel.metadata(even if the host app has tables namedassets,domains, ordistributed_locks). - Naive UTC Timestamp Uniformity: All Arodonata models and database columns use naive UTC (
DateTime(timezone=False),tzinfo=Noneviato_naive_utc()andutc_now()), eliminatingasyncpgoffset-naive vs offset-awareDataErrorcrashes on PostgreSQL.
from sqlalchemy.ext.asyncio import create_async_engine
from arodonata import ArodonataClient, ArodonataSettings
engine = create_async_engine("postgresql+asyncpg://app:secret@db:5432/fpcr")
settings = ArodonataSettings()
client = ArodonataClient(settings, engine=engine)
await client.initialize()
# ... serve requests ...
await client.close() # Leaves `engine` open for the host application
3.2 Streaming Synchronization to Web UIs (SSE)
Every batch refresh method (refresh_objects, refresh_services, refresh_assets, refresh_rulebases) and CPCRUD execution (cpcrud.apply) has a paired async generator (stream_refresh_*, cpcrud.stream_apply, stream_publish_staged_session) yielding typed domain events (SyncProgressEvent, CPCRUDEvent).
To stream progress over HTTP Server-Sent Events (SSE), convert domain events at the HTTP boundary using arodonata.adapters.sse.to_sse_event:
from arodonata.adapters.sse import to_sse_event
async for event in client.stream_refresh_objects(domains=["Domain4"], mode="smart-fast"):
sse = to_sse_event(event)
yield sse.encode() # "event: ...\ndata: {...}\n\n"
3.3 Exact Search, Human-Readable Composite IDs, Cross-Domain Structured Queries, and Rule-to-CPCRUD Export
- Human-Readable Composite Identifiers (
object_id,service_id,asset_id) & Dual Topology Links:- Every cached entity carries both a collision-safe UID primary key (
id = "{mgmt_name}:{domain_name}:{uid}") and an indexed human-readable composite identifier (object_id,service_id, orasset_id = "{mgmt_name}:{domain_name}:{name}"). - Gateway, cluster, and VSX assets (
Asset/arodonata_assets) expose both UID and human-readable parent/child links (parent_uid,parent_name,parent_id,parent_asset_id,member_uids,member_asset_ids,virtual_system_uids,virtual_system_asset_ids), automatically resolving cross-domain VSX-to-Virtual-System relationships across domains on the same management server. find_object(),get_group_members(),search_objects(), andquery_objects()accept raw UIDs, object names, or composite identifiers (e.g.,"mds:Domain4:hWebHost"or"mds:VSX:VSX-Cluster-01").
- Every cached entity carries both a collision-safe UID primary key (
- Exact vs Substring Search: Pass
exact_match=Truetoawait client.search_objects(".example.com", exact_match=True)when validating whether an exact FQDN or object name exists without matching subdomains (sub.example.com). EachGroupMembershipNodeinSearchResult.membershipsincludesid,object_id,parent_uids, andparent_object_ids. - Cross-Domain Structured Queries (
query_objects): Useawait client.query_objects(types=["host", "network"], group_by=["domain_name", "type"], aggregate="count")to build dashboard summaries or filter across domains (scope_origin="local" | "global" | "all", plusobject_id,service_id,asset_id,parent_id,parent_asset_id) without writing raw SQL against cache tables. - Exporting Existing Rules to CPCRUD (
output_format="cpcrud"): Useawait client.filter_rulebase(mgmt, domain, layer="Standard Network", filter_query="10.1.10.50", output_format="cpcrud")orrule_to_cpcrud_operation(rule)to convert cached rules directly into schema-valid CPCRUD rule dicts for modification or migration.
3.4 Staged CPCRUD Sessions for Approval Workflows
When a workflow requires peer review before publishing to Check Point:
# Phase 1: Stage changes in a dedicated Check Point session without publishing
plan = await client.cpcrud.plan(template)
report = await client.cpcrud.apply(plan, no_publish=True, keep_session=True)
staged_uid = report.staged_sessions[0].session_uid
# Phase 2a: Collect a rich HTML/Markdown ChangeReport from the unpublished session!
evidence = await client.build_change_report(
[{"mgmt_name": mgmt, "domain": "Domain4", "session_uids": [staged_uid]}],
formats=["html", "markdown"],
)
# Phase 2b: After human approval, adopt via take-over-session and publish (or discard)
await client.publish_staged_session(mgmt, "Domain4", session_uid=staged_uid)
# Or on rejection:
# await client.discard_staged_session(mgmt, "Domain4", session_uid=staged_uid)
Database Schema Reference (15 arodonata_* Tables)
Arodonata v2 persists all cached topology, shared sessions, distributed locks, objects, services, gateway/VSX assets, and normalized policy rulebases across 15 SQLModel tables defined in src/arodonata/adapters/storage/tables.py (ARODONATA_TABLE_NAMES).
Both PostgreSQL (postgresql+asyncpg://) and SQLite (sqlite+aiosqlite://) are first-class supported backends.
1. Host Application Coexistence & Auto-Migration Design
Arodonata v2 is engineered to share a single database and SQLModel.metadata registry safely with host applications (such as FPCR v2):
- Strict
arodonata_Table Namespace (ARODONATA_TABLE_NAMES):- Every table owned by Arodonata is prefixed with
arodonata_(includingarodonata_assets,arodonata_domains,arodonata_distributed_locks,arodonata_sid_cache,arodonata_domain_sync_state). DatabaseManager.initialize()inspects and migrates only tables inARODONATA_TABLE_NAMES. Any host application tables in the same database orSQLModel.metadata(such as a host app's ownassetsordomainstable) are never touched, altered, or dropped.
- Every table owned by Arodonata is prefixed with
- SHA-256 Schema Hash Auto-Migration (
arodonata_schema_version):DatabaseManagercomputes a deterministic SHA-256 digest (compute_schema_hash()) over the table names, column names, SQL types, primary keys, nullability, and indexes of all 15arodonata_*tables.- If the current
schema_hashis already recorded inarodonata_schema_versionand all 15 tables exist, startup skips column inspection and completes in< 2ms. - When the schema evolves,
DatabaseManageradds missing columns/indexes via savepoint-protected (begin_nested())ALTER TABLEstatements—or drops and recreates ephemeralarodonata_*cache tables if primary keys orNOT NULLconstraints changed—and records the newschema_hash.
- Cross-Worker Migration Serialization:
- Multi-worker Uvicorn/Gunicorn startups are serialized using
SELECT pg_advisory_xact_lock(...)on PostgreSQL andBEGIN IMMEDIATEon SQLite, with a double-check ofarodonata_schema_versionafter acquiring the lock.
- Multi-worker Uvicorn/Gunicorn startups are serialized using
- Legacy v1 Table Fingerprint Cleanup:
- If upgrading an existing database from Arodonata v1,
DatabaseManagerdrops legacy unprefixeddomainsorsid_cachetables only if they contain positive v1 column fingerprints (mdm_dmnormgmt_dmn_key), guaranteeing zero risk to unprefixed host tables.
- If upgrading an existing database from Arodonata v1,
- Naive UTC Timestamp Uniformity (
DateTime(timezone=False)):- Every timestamp column in Arodonata uses
Column(DateTime(timezone=False))normalized viato_naive_utc()(tzinfo=Nonein UTC), preventingasyncpgoffset-naive vs offset-aware errors on PostgreSQL.
- Every timestamp column in Arodonata uses
- UID Primary Keys (
id) + Human-Readable Composite Identifiers (object_id,service_id,asset_id):- Check Point permits distinct objects in the same domain (or visible from
Global) to share the samenameacross different object classes (e.g., built-in objects namedemail) while having distinctuidvalues, and allows existing objects to be renamed across publishes. - Therefore,
arodonata_objects,arodonata_services, andarodonata_assetsretain a UID-based primary keyid = "{mgmt_name}:{domain_name}:{uid}"(preventing primary-key collisions and orphaned rows on rename duringsmart-fastincremental sync) while simultaneously exposing an indexed, non-unique human-readable composite identifier:CPObject.object_id(arodonata_objects.object_id):"{mgmt_name}:{domain_name}:{name}"CPService.service_id(arodonata_services.service_id):"{mgmt_name}:{domain_name}:{name}"Asset.asset_id(arodonata_assets.asset_id):"{mgmt_name}:{domain_name}:{name}"
- In addition,
arodonata_assetsmaintains dual UID and human-readable parent/child topology links (parent_uid,parent_name,parent_id,parent_asset_id,member_uids,member_asset_ids,virtual_system_uids, andvirtual_system_asset_ids), supporting both intra-domain clusters and cross-domain VSX topology (where a physical VSX gateway/cluster resides in an infrastructure domain likeVSXand its Virtual Systems reside in tenant domains likeDomain4).
- Check Point permits distinct objects in the same domain (or visible from
2. Entity-Relationship Diagram (15 Tables)
3. Detailed Table Catalog
3.1 Infrastructure, Sessions & Synchronization Tables
1. arodonata_schema_version (SchemaVersionRecord)
Tracks applied SQLModel metadata SHA-256 hashes so workers skip redundant table migration inspections.
| Column | Type | Key / Index | Description |
|---|---|---|---|
schema_hash |
str |
PK |
64-char SHA-256 hex digest of the 15 arodonata_* table schemas |
applied_at |
datetime |
NOT NULL |
Naive UTC timestamp when the schema version was applied |
2. arodonata_distributed_locks (DistributedLockRecord)
Coordinates cross-process critical sections (per-domain cache refreshes, login coordination, rate-limit slots).
| Column | Type | Key / Index | Description |
|---|---|---|---|
lock_key |
str |
PK |
Resource identifier (e.g., sync:mdsm-prod:Domain4:objects) |
owner_id |
str |
INDEX |
Unique holder token (uuid4) verified on renew() and release() |
acquired_at |
datetime |
NOT NULL |
Naive UTC acquisition timestamp |
expires_at |
datetime |
INDEX, NOT NULL |
Naive UTC expiration timestamp (periodically extended by background heartbeat) |
3. arodonata_sid_cache (SIDCacheRecord)
Persistent shared read-only Check Point session store. Strictly omits sid from __repr__ and __str__.
| Column | Type | Key / Index | Description |
|---|---|---|---|
id |
str |
PK |
Composite key "{mgmt_name}:{domain_name}:{auth_identity_hash}" |
mgmt_name |
str |
INDEX |
Management server logical name |
domain_name |
str |
INDEX |
Normalized domain name ("default" for SMS / MDS system domain) |
sid |
str |
repr=False |
Check Point session token (redacted in all representations) |
server_ip |
str |
NOT NULL |
Active CMA or MDS IP where this session resides |
session_uid |
str | None |
— | Check Point session UID from login response |
auth_identity |
str | None |
— | Non-secret HMAC-SHA256 fingerprint of the credential that opened the session |
created_at |
datetime |
NOT NULL |
Naive UTC creation timestamp |
last_used_at |
datetime |
NOT NULL |
Naive UTC last API call timestamp |
expires_at |
datetime | None |
— | Naive UTC local expiry (last_used_at + session_expire_seconds) |
4. arodonata_domain_sync_state (DomainSyncStateRecord)
Unified synchronization state tracker per (mgmt_name, domain_name, scope, item_type). Updated atomically inside the same database transaction as cache data writes.
| Column | Type | Key / Index | Description |
|---|---|---|---|
id |
str |
PK |
Composite key "{mgmt_name}:{domain_name}:{scope}:{item_type}" |
mgmt_name |
str |
INDEX |
Management server logical name |
domain_name |
str |
INDEX |
Domain name |
scope |
str |
INDEX |
"domains", "objects", "services", "assets", or "rulebase" |
item_type |
str |
INDEX |
Specific type (e.g., "host", "service-tcp") or "*" for whole-scope syncs |
last_published_at |
datetime | None |
— | Domain's show-last-published-session timestamp (None if never published) |
last_session_uid |
str | None |
— | Last published Check Point session UID (used for show-changes from-session and HybridStateReader) |
synced_at |
datetime | None |
— | Naive UTC timestamp of the last successful sync |
status |
str |
— | "idle", "syncing", "ok", or "failed" |
object_count |
int |
— | Count of cached rows for this (scope, item_type) slice |
last_error |
str | None |
— | Sanitized (redact_sid) error message if the last sync failed |
3.2 Topology, Objects, Services & Assets Tables
5. arodonata_domains (DomainRecord)
Cached MDSM/SMS domain topology discovered via show-domains (plus synthesized Global domain on MDSM).
| Column | Type | Key / Index | Description |
|---|---|---|---|
id |
str |
PK |
"{mgmt_name}:{name}" |
mgmt_name |
str |
INDEX |
Management server logical name |
uid |
str |
INDEX |
Check Point domain UID |
name |
str |
INDEX |
Domain name (e.g., "Domain4", "Global", "default") |
domain_type |
str |
— | "domain", "global domain", or "smc-user" |
active_ip |
str | None |
— | Active CMA IPv4/IPv6 address |
active_mds_ip |
str | None |
INDEX |
Hosting MDS member IP (used to group per-MDS-member sync concurrency) |
standby_ips |
list[str] (JSON) |
NOT NULL |
Standby CMA IPs for automatic failover |
servers |
list[dict] (JSON) |
NOT NULL |
Full CMA server entries from Check Point API |
is_global |
bool |
INDEX |
True for the MDSM Global domain |
raw_data |
dict (JSON) |
NOT NULL |
Full sanitized API payload |
updated_at |
datetime |
NOT NULL |
Naive UTC update timestamp |
6. arodonata_objects (CPObjectRecord)
Cached network objects (host, network, address-range, group, dns-domain, etc.).
| Column | Type | Key / Index | Description |
|---|---|---|---|
id |
str |
PK |
"{mgmt_name}:{domain_name}:{uid}" |
object_id |
str |
INDEX |
Human-readable composite key "{mgmt_name}:{domain_name}:{name}" |
uid |
str |
INDEX |
Check Point object UID |
name |
str |
INDEX |
Object name |
type |
str |
INDEX |
Canonical object type ("host", "network", "address-range", "group", "dns-domain") |
mgmt_name, domain_name |
str |
INDEX |
Server and domain context |
original_domain |
str | None |
INDEX |
Owning domain reported by Check Point (domain.name inside raw_data, distinguishing local vs Global objects) |
ipv4_address, ipv6_address |
str | None |
INDEX (ipv4) |
Host IPv4 / IPv6 address |
subnet4, mask_length4, subnet_mask |
str / int |
INDEX (subnet4) |
Network CIDR subnet and mask |
ipv4_address_first, ipv4_address_last |
str | None |
— | Address range bounds |
fqdn, is_sub_domain |
str / bool |
INDEX (fqdn) |
DNS domain FQDN and subdomain flag |
members |
list[str] (JSON) |
NOT NULL |
Member UIDs for group objects |
comments, color, tags, nat_settings |
str / JSON |
— | Metadata, tag names, and automatic NAT settings |
raw_data, updated_at |
JSON / datetime |
NOT NULL |
Full API payload and naive UTC timestamp |
7. arodonata_services (CPServiceRecord)
Dedicated cache table for service objects (service-tcp, service-udp, service-icmp, service-group, and on-demand service-other, service-sctp, service-dce-rpc, service-rpc, service-icmp6, service-citrix-tcp, service-compound-tcp).
| Column | Type | Key / Index | Description |
|---|---|---|---|
id |
str |
PK |
"{mgmt_name}:{domain_name}:{uid}" |
service_id |
str |
INDEX |
Human-readable composite key "{mgmt_name}:{domain_name}:{name}" |
uid, name, type |
str |
INDEX |
Service UID, name, and canonical type |
mgmt_name, domain_name, original_domain |
str |
INDEX |
Server, domain, and owning domain |
port, protocol |
str | None |
INDEX |
Port/range string (e.g., "443", "8080-8090") and protocol |
icmp_type, icmp_code |
int | None |
— | ICMP type and code |
members |
list[str] (JSON) |
NOT NULL |
Member service UIDs for service-group |
session_timeout, match_for_any |
int / bool |
— | Service session timeout and match-for-any flag |
comments, color, tags, raw_data, updated_at |
str / JSON / datetime |
— | Metadata, raw payload, and naive UTC timestamp |
8. arodonata_assets (AssetRecord)
Cached gateways, clusters, cluster members, VSX gateways/clusters/virtual systems, and management servers, enriched with parent/child topology links by ClusterRelationshipManager and VSXRelationshipManager.
| Column | Type | Key / Index | Description |
|---|---|---|---|
id |
str |
PK |
"{mgmt_name}:{domain_name}:{uid}" |
asset_id |
str |
INDEX |
Human-readable composite key "{mgmt_name}:{domain_name}:{name}" |
uid, name, type |
str |
INDEX |
Asset UID, name, and type (simple-gateway, CpmiGatewayCluster, CpmiClusterMember, vsx-gateway, vsx-cluster-netobj, virtual-system, etc.) |
mgmt_name, domain_name |
str |
INDEX |
Server and domain context |
ipv4_address, ipv6_address |
str | None |
INDEX (ipv4) |
Management IPv4 / IPv6 address |
version, os_name, hardware, sic_status |
str | None |
— | Check Point version (e.g. "R82"), Gaia OS, appliance model, and SIC trust status |
parent_uid, parent_name |
str | None |
INDEX (parent_uid) |
Parent cluster or VSX container UID and name |
parent_id, parent_asset_id |
str | None |
INDEX |
Parent composite UID key ("{mgmt_name}:{parent_domain}:{parent_uid}") and human-readable key ("{mgmt_name}:{parent_domain}:{parent_name}") |
member_uids, member_asset_ids |
list[str] (JSON) |
NOT NULL |
Child cluster member UIDs and human-readable asset_id values |
virtual_system_uids, virtual_system_asset_ids |
list[str] (JSON) |
NOT NULL |
Child VSX virtual system UIDs and human-readable asset_id values |
interfaces |
list[dict] (JSON) |
NOT NULL |
Normalized network interfaces (name, ipv4-address, ipv4-network-mask, topology) |
network_security_blades |
dict (JSON) |
NOT NULL |
Enabled software blades (firewall, ips, url-filtering, anti-bot, etc.) |
comments, color, raw_data, updated_at |
str / JSON / datetime |
— | Metadata, raw payload, and naive UTC timestamp |
3.3 Normalized Rulebase Cache Tables (SmartConsole-in-SQL)
Arodonata v2 normalizes policy packages, layers, section headers, and rules across 7 dedicated tables so SmartConsole hierarchical rule numbers (1, 2.1, 3), inline layers, and Global domain placeholders can be queried and filtered locally in milliseconds without hitting the Check Point API.
9. arodonata_rulebase_package_layer (RulebasePackageLayerRecord)
Links each policy package (package_uid, package_name) to its ordered access, HTTPS, threat, and NAT layers (layer_uid, layer_name, rulebase_type, position).
10. arodonata_rulebase_layer (RulebaseLayerRecord)
Stores every access, HTTPS, and threat layer (uid, name, rulebase_type), including inline layers (parent_layer_uid, parent_rule_uid), is_global, shared, and implicit_cleanup_action ("Drop" or "Accept").
11. arodonata_rulebase_section (RulebaseSectionRecord)
Stores rulebase section headers (layer_uid, uid, name, position, from_rule, to_rule, and ordered rule_uids list) stitched cleanly across 100-rule API pagination boundaries.
12. arodonata_rulebase_access (AccessRuleRecord)
Stores access-control rules (layer_uid, section_uid, uid, name, position, rule_number, enabled, action, inline_layer_uid, inline_layer_name, placeholder_domain_layer_uid, source, destination, service, source_negate, destination_negate, service_negate, track, install_on, comments, raw_data).
13. arodonata_rulebase_nat (NatRuleRecord)
Stores NAT rules (package_uid, package_name, section_uid, uid, name, position, rule_number, enabled, method, auto_generated, original_source, original_destination, original_service, translated_source, translated_destination, translated_service, install_on, comments, raw_data).
14. arodonata_rulebase_https (HttpsRuleRecord)
Stores HTTPS inspection rules (layer_uid, section_uid, uid, name, position, rule_number, enabled, action, inline_layer_uid, source, destination, service, site_category, certificate, track, install_on, comments, raw_data).
15. arodonata_rulebase_threat (ThreatRuleRecord)
Stores Threat Prevention rules (layer_uid, section_uid, uid, name, position, rule_number, enabled, action, protected_scope, source, destination, service, track, install_on, comments, raw_data).
Model Context Protocol (MCP) Server & Consolidated Tool Surface
Arodonata v2 includes a production-hardened Streamable-HTTP Model Context Protocol (MCP) server (arodonata[mcp]) that can run either as a standalone CLI (arodonata-mcp) or embedded as an ASGI sub-application (create_mcp_server, create_asgi_app) inside a FastAPI or Starlette host.
1. Consolidated Tool Surface Optimization (v2 Architecture)
In Arodonata v2, the MCP tool surface was redesigned and consolidated into 13 cache-aligned core tools (plus 5 opt-in tamper-proof CPCRUD tools when ARODONATA_MCP_CPCRUD=true).
Why We Consolidated the MCP Tools
Early MCP servers often expose dozens of 1-to-1 wrappers over raw Check Point API endpoints (show-hosts, show-networks, show-access-layers, show-packages, etc.). That causes three major problems for LLM agents:
- Tool Selection Ambiguity & Context Bloat: Too many overlapping tool schemas consume thousands of prompt tokens and cause models to pick the wrong endpoint.
- Unbounded Output Overflow: Raw Check Point API calls or unanchored substring searches on large enterprise domains return thousands of objects and blow up the LLM context window.
- N+1 API Round-Trips: Answering questions like "How many hosts and networks exist per domain?" or "What packages and layers exist in Domain4?" previously required dozens of sequential tool calls.
Arodonata v2 solves all three problems with progressive disclosure, multi-table unified lookup, SQL-free cached aggregation, and hybrid cache/live execution:
| Tool Category | Tool Name | Optimization & Key Capabilities |
|---|---|---|
| Collection Listing | list_domains |
Lists discovered domains (arodonata_domains) with active_ip, active_mds_ip, standby_ips, is_global, source, and cache_age_seconds |
| Collection Listing | list_objects |
Lists cached network objects (host, network, address-range, group, dns-domain) with id, object_id, pagination (from, to, total), and type normalization |
| Collection Listing | list_services |
Lists cached services (arodonata_services with id and service_id), accepting both short ("tcp", "udp", "icmp", "group") and canonical ("service-tcp", etc.) types filters |
| Collection Listing | list_assets |
Lists cached gateways, clusters, cluster members, VSX gateways/virtual systems, and management servers (arodonata_assets with id, asset_id, parent_id, parent_asset_id, member_asset_ids, virtual_system_asset_ids), with optional parent UID/name/asset_id filtering |
| Multi-Table Entity Lookup | show_object |
Searches across arodonata_objects, arodonata_services, arodonata_assets (including gateway interfaces, network_security_blades, member_uids, member_asset_ids, virtual_system_asset_ids), and arodonata_rulebase_layer / package_layer by UID, exact name, or composite ID (object_id/service_id/asset_id) using 3-tier matching (exact, strict wildcard via fnmatchcase, and paginated substring_candidates fallback) |
| Structured Analytics | query_objects |
Executes cross-domain filtering (name_pattern, ip_subnet, port, protocol, tags, scope_origin="all"|"local"|"global", object_id, service_id, asset_id, parent_id, parent_asset_id), group_by, and aggregation (aggregate="list"|"count"|"describe") over cached SQL tables in a single tool call |
| Cache Management | cache_control |
Unified cache inspection (action="status", returning per-domain/per-type freshness from arodonata_domain_sync_state) and scoped refresh (action="refresh", scope="domains"|"objects"|"services"|"assets"|"rulebase") |
| Search & Membership | search_objects |
Multi-term IP/CIDR/range/wildcard/UID/composite-ID/name search with exact_match: bool = False and recursive parent group resolution (max_depth, including object_id and parent_object_ids) |
| Search & Membership | get_group_members |
Expands direct or recursive members of a network group, service-group, gateway cluster (member_uids / member_asset_ids), or VSX container (virtual_system_uids / virtual_system_asset_ids, including cross-domain Virtual Systems) by UID, name, or composite ID |
| Policy & Rulebase | show_rulebase |
Catalog + Rulebase Hybrid: when package and layer are omitted (or unmatched), returns the domain's cached packages and layers catalog! When specified, evaluates filter locally against cached rules/objects/services (markdown, model_friendly, raw, cpcrud) or switches to live API when show_hits / cache_mode="live" is used |
| Policy & Rulebase | locate_rules |
Resolves rule UIDs or layer UIDs into exact SmartConsole hierarchical rule numbers (1, 2.1) and package/layer paths across a domain |
| Audit & Compliance | change_report |
Collects and renders auditable session change reports (SessionScope or RangeScope) in markdown, json, or html with full sid redaction |
| Escape Hatch | api_call |
Executes direct Check Point Web API commands (where-used, show-sessions, verify-policy). Enforces a strict read-only command allowlist when ARODONATA_MCP_ALLOW_WRITE_API=false, and isolates allowed write calls in a dedicated session |
| Opt-in CPCRUD | cpcrud_validate |
Offline JSON Schema validation of declarative CPCRUD templates (ARODONATA_MCP_CPCRUD=true) |
| Opt-in CPCRUD | cpcrud_plan |
Generates a deterministic CPCRUD Plan signed with an in-memory HMAC-SHA256 key (plan_signature + plan_id) |
| Opt-in CPCRUD | cpcrud_apply |
Verifies the plan_id or HMAC plan_signature (rejecting model-forged or modified plans), executes inside an isolated WriteSession, and supports staging (no_publish=True, keep_session=True) |
| Opt-in CPCRUD | cpcrud_inverse |
Generates a compensating rollback template from a verified Plan and ApplyReport |
| Opt-in CPCRUD | cpcrud_staged_session |
Adopts a staged session (session_uid) via take-over-session and either publishes (action="publish", invalidating domain cache) or discards (action="discard") cleanly |
2. Deep-Dive into Key Tool Behaviors
2.1 Three-Tier Matching in show_object
When an LLM calls show_object(uid_or_name=..., mgmt_name=..., domain=..., obj_type=None, limit=20, offset=0):
- Tier 1 — Exact Match (
match_type="exact"):- If
uid_or_namecontains no*or?wildcards and matches an exact UID or case-insensitive name inarodonata_objects,arodonata_services,arodonata_assets, orarodonata_rulebase_layer/arodonata_rulebase_package_layer,show_objectreturns that single entity with full details (including gatewayinterfaces,network_security_blades,member_uids, andvirtual_system_uids).
- If
- Tier 2 — Strict Explicit Wildcard (
match_type="wildcard"):- If
uid_or_namecontains*or?(e.g."web-*-prod"or"*.example.com"), it evaluates anchoredfnmatchcasestrictly against the pattern without adding implicit leading/trailing wildcards. Iftotal == 1, it returns the full object; iftotal > 1, it returns a paginated candidate slice (limitdefault20, max500).
- If
- Tier 3 — Paginated Substring Candidate Fallback (
match_type="substring_candidates"):- If
uid_or_namehas no wildcard and no exact match exists in cache, instead of failing with a bare"not found"error or dumping thousands of objects,show_objectsearches for case-insensitive substring matches across cached names and returns up tolimit(20) compact candidate summaries (uid,name,type,domain_name,summary) withfrom,to, andtotalso the LLM can pick the exact object on the next turn.
- If
2.2 Progressive Disclosure via query_objects & show_rulebase
- Schema & Inventory Discovery (
query_objects(aggregate="describe")):- Calling
query_objects(mgmt_name="mdsm-prod", aggregate="describe")returns all available domains, per-type object/service/asset counts, and the exact list of filterable and groupable fields (domain_name,type,original_domain,color,protocol,port, etc.).
- Calling
- Cross-Domain Counting (
query_objects(aggregate="count", group_by=["domain_name", "type"])):- Counts objects and services across all domains in a single SQL query, supporting
scope_origin="local"(domain-owned only) vs"global"(original_domain == "Global").
- Counts objects and services across all domains in a single SQL query, supporting
- Guided Rulebase Discovery (
show_rulebase):- Calling
show_rulebase(mgmt_name="mdsm-prod", domain="Domain4")withoutpackageorlayer(or with a misspelled layer name) automatically returns"mode": "catalog"containing all cachedpackages(arodonata_rulebase_package_layer) andlayers(arodonata_rulebase_layer, including inline layers andimplicit_cleanup_action).
- Calling
2.3 Pre-Login Domain Validation & Actionable ToolFailure
Every domain-scoped MCP tool validates domain against the cached arodonata_domains table (triggering a single domain-list refresh on cache miss) before attempting any Check Point login. If an invalid domain name is passed, the tool returns an actionable error listing the valid domain names on that server instead of triggering unnecessary failed logins.
3. Running & Embedding the MCP Server
3.1 Standalone CLI (arodonata-mcp)
# Start with static bearer token authentication (default)
export ARODONATA_MCP_AUTH_MODE=static
export ARODONATA_MCP_TOKEN_VARS=TEAM_ALPHA_MCP_TOKEN
export TEAM_ALPHA_MCP_TOKEN="a-very-long-random-secret-token-at-least-32-chars"
uv run arodonata-mcp --host 127.0.0.1 --port 8000 --path /mcp
3.2 Embedding in FastAPI / Starlette (create_mcp_server & create_asgi_app)
See examples/12_mcp_embedded.py for a complete runnable example mounting Arodonata's MCP server inside a FastAPI application with custom host-level tools:
from contextlib import asynccontextmanager
from fastapi import FastAPI
from arodonata import ArodonataClient, ArodonataMCPSettings, create_asgi_app, create_mcp_server
client = ArodonataClient()
mcp_settings = ArodonataMCPSettings(auth_mode="host", path="/mcp")
mcp_server = create_mcp_server(client, mcp_settings=mcp_settings)
mcp_asgi = create_asgi_app(client, mcp_settings=mcp_settings, server=mcp_server)
@asynccontextmanager
async def lifespan(app: FastAPI):
async with client, mcp_asgi.lifespan(app):
yield
app = FastAPI(lifespan=lifespan)
app.mount("/mcp", mcp_asgi)
4. MCP Configuration & Security Reference (ArodonataMCPSettings)
| Env Variable | Type | Default | Description |
|---|---|---|---|
ARODONATA_MCP_HOST |
str |
"127.0.0.1" |
Bind address for standalone arodonata-mcp |
ARODONATA_MCP_PORT |
int |
8000 |
Bind TCP port (1..65535) |
ARODONATA_MCP_PATH |
str |
"/mcp" |
Streamable-HTTP mount path |
ARODONATA_MCP_AUTH_MODE |
"static" | "host" | "jwt" | "none" |
"static" |
Authentication mode ("host" and "jwt" are for embedded ASGI mode only; standalone CLI exits with code 2 if used) |
ARODONATA_MCP_TOKEN_VARS |
list[str] (CSV) |
[] |
Env var names holding bearer tokens (>= 32 chars); each var name becomes client_id in UserContext |
ARODONATA_MCP_TOKENS |
list[str] (CSV) |
[] |
Direct "client_id=secret", "client_id:secret", or bare base64 token secrets (>= 32 chars; safe with trailing = base64 padding) |
ARODONATA_MCP_ENABLE_DNS_REBINDING_PROTECTION |
bool |
True |
Enforces Host (HTTP 421) and Origin (HTTP 403) header allowlists |
ARODONATA_MCP_ALLOWED_HOSTS |
list[str] (CSV) |
127.0.0.1:*,localhost:*,[::1]:* |
Permitted Host headers (validates numeric ports on wildcard :port patterns) |
ARODONATA_MCP_ALLOWED_ORIGINS |
list[str] (CSV) |
http(s)://localhost:*, 127.0.0.1:* |
Permitted browser Origin headers |
ARODONATA_MCP_ALLOW_WRITE_API |
bool |
False |
When False, api_call permits only the explicit read-only allowlist (show-*, where-used, verify-policy); when True, mutating calls run in an isolated dedicated session |
ARODONATA_MCP_CPCRUD |
bool |
False |
Registers the 5 tamper-proof HMAC-signed CPCRUD tools (cpcrud_validate, cpcrud_plan, cpcrud_apply, cpcrud_inverse, cpcrud_staged_session) |
ARODONATA_MCP_JSON_RESPONSE |
bool |
False |
Return JSON responses instead of SSE streams on Streamable-HTTP POST |
ARODONATA_MCP_STATELESS_HTTP |
bool |
False |
Enable stateless Streamable-HTTP mode per request |
ARODONATA_MCP_SERVER_NAME |
str |
"arodonata-mcp" |
Server identifier reported in MCP initialization |
Getting Started
Arodonata v2 requires Python 3.13+ and uses asynchronous I/O (asyncio, aiohttp, SQLAlchemy 2.0+ async engine) across all operations.
Installation
Install arodonata using uv (recommended) or pip:
# Core SDK (includes SQLite cache via aiosqlite, CPCRUD, rulebase, and change reports)
uv add arodonata
# Recommended: install with all optional extras (PostgreSQL asyncpg, MCP server, OpenTelemetry)
uv add "arodonata[all]"
Optional Extras
You can install individual extras depending on your deployment environment:
| Extra | Command | Installed Packages | Purpose |
|---|---|---|---|
postgres |
uv add "arodonata[postgres]" |
asyncpg>=0.30.0 |
PostgreSQL database cache & distributed locks |
mcp |
uv add "arodonata[mcp]" |
mcp>=1.2.0, starlette, uvicorn |
Standalone arodonata-mcp CLI and embeddable ASGI MCP server |
otel |
uv add "arodonata[otel]" |
arlogi[otel]>=0.1.0 |
OpenTelemetry span export and structured trace correlation |
all |
uv add "arodonata[all]" |
All of the above | Full production feature set |
Minimal Environment Configuration
Arodonata never loads .env files implicitly on import; your application (or CLI) loads environment variables explicitly (for example via python-dotenv or container orchestration) or passes typed arguments to ArodonataSettings.
Create a .env file for a single management server or MDSM server:
# Management server topology
ARODONATA_MGMT_NAMES=mdsm-prod
ARODONATA_MGMT_SERVERS=192.0.2.10:443
ARODONATA_API_KEYS=your-checkpoint-api-key
# Persistent cache (SQLite file or PostgreSQL URL)
ARODONATA_DATABASE_URL=sqlite+aiosqlite:///./arodonata_cache.db
# Or PostgreSQL:
# ARODONATA_DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/arodonata
# TLS certificate verification mode: pinned | tofu | lab-memory
ARODONATA_TLS_TRUST=tofu
Next Steps
- Your First Script — Initialize
ArodonataClient, query cached domains/objects/gateways, and useWriteSessionwithSessionOverlay. - CPCRUD Quickstart — Validate, plan, apply, and roll back declarative YAML/JSON policy templates.
- MCP Server Guide — Run
arodonata-mcpor mount the consolidated MCP tool surface inside FastAPI.
Your First Script
Every interaction in Arodonata v2 goes through ArodonataClient. It manages the database schema, connection pools, session lifecycle, rate limiting, and domain cache synchronization.
1. Read-Only Queries & Smart Caching
When you enter async with ArodonataClient() as client:, Arodonata automatically:
- Verifies and migrates the 15
arodonata_*tables in your SQLite or PostgreSQL database (DatabaseManager.initialize()). - Schedules non-blocking cleanup of any orphaned idle Arodonata read sessions (
changes == 0andlocks == 0). - Disposes owned connections cleanly on exit (preserving persistent
arodonata_sid_cacherows across CLI runs when using a file-backed SQLite or PostgreSQL database).
import asyncio
from dotenv import load_dotenv
from arodonata import ArodonataClient
async def main() -> None:
load_dotenv(".env")
async with ArodonataClient() as client:
# 1. Discover domains on all configured management servers
domains = await client.get_domains(include_global=True)
for dom in domains:
print(f"{dom.mgmt_name} / {dom.name} (active IP: {dom.active_ip}, global: {dom.is_global})")
# 2. Fetch hosts, networks, and services using smart cache synchronization
hosts = await client.get_hosts(domain="Domain4", cache_mode="smart")
networks = await client.get_networks(domain="Domain4", cache_mode="smart")
services = await client.get_services(domain="Domain4", types=["tcp", "udp"], cache_mode="smart")
print(f"Loaded {len(hosts)} hosts, {len(networks)} networks, {len(services)} services")
# 3. Inspect gateways, clusters, and VSX topology (with human-readable asset_id & parent_asset_id)
gateways = await client.get_gateways(domain="Domain4", cache_mode="smart")
for gw in gateways:
print(
f"Asset: {gw.asset_id} ({gw.type}) IP={gw.ipv4_address} "
f"parent={gw.parent_asset_id} members={gw.member_asset_ids}"
)
if __name__ == "__main__":
asyncio.run(main())
2. Understanding cache_mode
All read and refresh methods accept a CacheMode:
"cache": Read strictly from the local SQL database (0network calls)."smart"(default): Probesshow-last-published-sessionwhen the domain TTL (cache_ttl, default300s) expires; reloads from the API only if a newer publish occurred."smart-fast": Like"smart", but fetches only incremental deltas viashow-changes(preferringfrom-sessionover UTCfrom-date), falling back to a full type reload if changes exceedmax_incremental_changes(500)."force": Unconditionally reloads the requested scope from the Check Point API and atomically updates the SQL cache.
3. Isolated Writes & Read-Your-Own-Writes (SessionOverlay)
Never mutate objects on the shared read-only session. Instead, open an isolated client.write_session(mgmt_name, domain) or call high-level mutation helpers (add_object, set_object, delete_object):
async with ArodonataClient() as client:
mgmt = client.settings.mgmt_names[0]
# Opens a dedicated Check Point session outside arodonata_sid_cache
async with client.write_session(mgmt, "Domain4", publish_on_exit=False) as ws:
await client.add_object(
mgmt,
"host",
{"name": "h-temp-app", "ipv4-address": "198.51.100.42"},
domain="Domain4",
)
# SessionOverlay automatically merges uncommitted creates/updates/deletes into cache reads!
found = await client.find_object(mgmt, "Domain4", "h-temp-app")
assert found is not None and found.ipv4_address == "198.51.100.42"
# Explicitly discard so nothing is published to Domain4
await client.api_call(mgmt, "discard", {}, domain="Domain4", sid=ws.sid, server_ip=ws.server_ip)
ws.discarded = True
CPCRUD Quickstart
client.cpcrud (CPCRUDService) is Arodonata's declarative Policy-as-Code engine. It takes a YAML or JSON template describing desired objects, services, groups, sections, and rules, computes an idempotent execution plan against cached + live state, applies changes in an isolated session per domain, and can generate a compensating rollback template (inverse).
1. Define a Declarative Template
version: 1
mgmt_name: "mdsm-prod"
domain: "Domain4"
on_name_conflict: "update"
on_ip_conflict: "reuse"
objects:
- type: "host"
name: "web-srv-01"
ipv4-address: "198.51.100.10"
comments: "Managed by Arodonata CPCRUD"
color: "dark green"
services:
- type: "service-tcp"
name: "tcp-8443"
port: "8443"
groups:
- type: "group"
name: "grp-web-servers"
members:
- "web-srv-01"
rules:
- type: "access-rule"
layer: "Standard Network"
position: "bottom"
name: "allow-web-8443"
source: ["Any"]
destination: ["grp-web-servers"]
service: ["tcp-8443"]
rule_action: "Accept"
track: "Log"
2. Validate, Plan, Apply, and Roll Back
import asyncio
from pathlib import Path
from arodonata import ArodonataClient
async def main() -> None:
template_text = Path("examples/crud_example.yaml").read_text(encoding="utf-8")
async with ArodonataClient() as client:
# Step 1: Offline JSON Schema validation (zero network or DB calls)
errors = client.cpcrud.validate(template_text)
if errors:
raise ValueError(f"Invalid template: {errors}")
# Step 2: Plan (captures DomainStamp and resolves diffs via HybridStateReader)
plan = await client.cpcrud.plan(template_text)
for action in plan.actions:
print(f"[{action.outcome}] {action.object_type} {action.name} -> {action.command}")
# Step 3: Apply (verifies DomainStamp hasn't drifted, executes in isolated WriteSession, publishes)
report = await client.cpcrud.apply(plan)
print(f"Applied: success={report.success}, published={report.published}")
# Step 4: Generate a compensating rollback template from (plan, report)
rollback_template = client.cpcrud.inverse(plan, report)
rollback_plan = await client.cpcrud.plan(rollback_template)
rollback_report = await client.cpcrud.apply(rollback_plan)
print(f"Rolled back: success={rollback_report.success}")
if __name__ == "__main__":
asyncio.run(main())
3. Staged Sessions (Human-in-the-Loop Approval)
When integrating into approval workflows (or MCP agent workflows), pass no_publish=True, keep_session=True to apply(). This leaves the Check Point session open and returns StagedSessionInfo (session_uid) in report.staged_sessions.
Later, once approved or rejected, finalize the session by its session_uid:
# Approve and publish (adopts via take-over-session, publishes, logs out, and invalidates domain cache)
await client.publish_staged_session("mdsm-prod", "Domain4", session_uid=staged.session_uid)
# Or reject and discard cleanly
await client.discard_staged_session("mdsm-prod", "Domain4", session_uid=staged.session_uid)
Architecture Overview
Arodonata v2 is built on a strict Hexagonal (Ports & Adapters) architecture designed for high-concurrency asynchronous execution, multi-worker database safety, and zero-leak security hygiene.
System Context & Component Diagram
Key Architectural Invariants
- Pure Core Domain (
arodonata.core,arodonata.rulebase,arodonata.cpcrud,arodonata.reports):- Business logic depends exclusively on typed Python
Protocolinterfaces (ApiPort,CachePort,LockPort,AuthProvider). - Core modules never import
aiohttp,asyncpg,aiosqlite, or HTTP transport primitives directly.
- Business logic depends exclusively on typed Python
- Database-First Synchronization & Multi-Worker Coordination:
- All cached state lives in 15
arodonata_*SQLModel tables (Database Schema Reference). - Schema initialization is serialized across worker processes (
pg_advisory_xact_lockon PostgreSQL,BEGIN IMMEDIATEon SQLite) and tracked by SHA-256 metadata digest inarodonata_schema_version. - Domain synchronization acquires database-backed distributed locks (
arodonata_distributed_locks) with automatic background TTL heartbeat renewal.
- All cached state lives in 15
- Strict Read vs Write Session Separation:
- Read operations share a persistent, credential-bound read-only session in
arodonata_sid_cache. Mutating commands (add-*,set-*,delete-*,publish,discard) are strictly rejected on the shared session. - Write operations open an isolated
WriteSession(create_dedicated_session) with an in-memorySessionOverlaybound viacontextvarsso callers immediately read their own uncommitted writes beforepublish.
- Read operations share a persistent, credential-bound read-only session in
- Defense-in-Depth Security Hygiene:
- Session tokens (
sid), API keys, and passwords are redacted ("***REDACTED***") in allrepr(), logs, OpenTelemetry span attributes, exception messages, and MCP tool outputs (redact_sid(..., known_sids=...)). - TLS certificates are verified post-handshake and pre-request against SHA-256 DER fingerprints (
pinned,tofu, orlab-memory).
- Session tokens (
- Provider-Agnostic Observability:
- Functions decorated with
@traced(arodonata.core.telemetry) emit OpenTelemetry spans viaopentelemetry-api(arlogi.otel.traced) without ever owning or overriding the host application's globalTracerProvider.
- Functions decorated with
Hexagonal Ports & Adapters
Arodonata v2 isolates all Check Point Management API and SQL database details behind four core Protocol interfaces defined in src/arodonata/core/ports/__init__.py.
Core Protocols (arodonata.core.ports)
1. ApiPort
Defines the asynchronous contract for communicating with Check Point Management and MDSM servers:
api_call(mgmt_name, command, payload, *, domain=None, sid=None, server_ip=None, wait_for_task=True, timeout=None) -> ApiCallResultapi_query(mgmt_name, command, *, domain=None, details_level="standard", payload=None, sid=None, server_ip=None, limit=500) -> ApiQueryResultcreate_dedicated_session(mgmt_name, domain=None, *, auth=None, session_name=None, session_description=None) -> tuple[str, str]logout_sid(mgmt_name, sid, *, server_ip=None) -> boolclose(*, logout=False) -> None
Default Adapter: AMgmtClient (src/arodonata/adapters/checkpoint/client.py), backed by:
ApiTransport: Nativeaiohttpclient withallow_redirects=False,trust_env=False, post-handshake SHA-256 certificate verification, and phase-taggedTransportError(phase="connect"vs"post-send"so mutating calls are never re-sent after bytes leave the socket).VerifiedConnectionPool: Reuses verified keep-alive connections (up to10sidle) strictly for idempotentshow-*commands.RateLimiter&LoginGate: Enforcesconcurrent_limitper MDS member IP and a70scooldown window on login throttling (err_too_many_requests).TaskWaiter: Polls asynchronous Check Pointtask-idresponses with exponential backoff (2s..10s) up totask_timeout.
2. CachePort
Defines the contract for reading and persisting cached Check Point topology, objects, services, assets, rulebases, sessions, and sync states:
- Domain topology:
get_domains(),replace_domains() - Shared sessions:
get_cached_sid(),upsert_cached_sid(),delete_cached_sid(),list_cached_sids() - Sync state:
get_sync_state(),list_sync_states(),upsert_sync_state() - Objects & Services:
get_objects(),get_object_by_uid(),replace_domain_objects(),apply_incremental_objects(),get_services(),get_service_by_uid(),replace_domain_services(),apply_incremental_services() - Assets:
get_assets(),replace_domain_assets() - Rulebase snapshots:
get_domain_rulebase(),replace_domain_rulebase() - Structured aggregation:
describe_objects(),query_objects()
Default Adapter: SqlCacheAdapter (src/arodonata/adapters/storage/cache_adapter.py), which executes atomic transactions and dialect-specific ON CONFLICT DO UPDATE upserts on PostgreSQL and SQLite.
3. LockPort
Coordinates distributed cross-process critical sections (per-domain cache refreshes, login serialization, and cross-worker rate-limit slots):
acquire(lock_key, *, ttl_seconds=60.0, timeout_seconds=30.0, poll_interval=0.25) -> str(returnsowner_id)renew(lock_key, owner_id, *, ttl_seconds=60.0) -> boolrelease(lock_key, owner_id) -> boollock(lock_key, *, ttl_seconds=60.0, timeout_seconds=30.0) -> AsyncIterator[str]
Default Adapter: SqlLockAdapter (src/arodonata/adapters/storage/lock_adapter.py) backed by the arodonata_distributed_locks table, evaluating expiration against the database server's UTC clock.
4. AuthProvider
Optional pluggable protocol for resolving credentials dynamically per (mgmt_name, domain) when auth_mode="dynamic" is used without an explicit auth_context.
Dependency Injection in Tests & Custom Hosts
Because ArodonataClient accepts custom api: ApiPort, cache: CachePort, and lock_port: LockPort instances in its constructor, unit tests and downstream applications can inject in-memory fakes or custom adapters without monkeypatching internal classes:
client = ArodonataClient(
settings,
api=custom_api_port,
cache=custom_cache_port,
lock_port=custom_lock_port,
)
Caching, Incremental Synchronization & Search
Arodonata v2 coordinates multi-domain synchronization through DomainSyncCoordinator (src/arodonata/core/services/sync_coordinator.py) and exposes four cache freshness modes (CacheMode).
1. The Four CacheMode Policies
| Mode | Staleness Probe (show-last-published-session) |
Data Fetch Strategy | Typical Use Case |
|---|---|---|---|
"cache" |
Never | Local SQLite/PostgreSQL query only (0 API calls) |
High-speed rulebase filtering, offline analysis, MCP query_objects |
"smart" (default) |
Only when synced_at is older than cache_ttl (or after invalidate_domain) |
Full atomic type-slice reload if (last_published_at, last_session_uid) changed |
Default SDK reads (get_hosts, get_services, get_gateways) |
"smart-fast" |
Same as "smart" |
Queries show-changes (from-session or UTC from-date) and applies dialect ON CONFLICT upserts/deletes; falls back to full reload if changes > max_incremental_changes (500) |
Rapid refresh after small publishes across large domains |
"force" |
Always probes current publish head | Unconditional atomic reload of all requested types | Initial cache build (04_smart_refresh.py) or forced recovery |
2. Synchronization Pipeline & Safeguards
Critical Invariants
- Dirty Shared Session Refusal (
DirtySessionError): Before syncing any domain into the shared cache,DomainSyncCoordinatorverifies viashow-sessionthat the session haschanges == 0. It never caches uncommitted objects into the shared SQL tables. - Per-MDS-Member Concurrency: Domains hosted on the same
active_mds_ipare refreshed concurrently viaasyncio.TaskGroupup tomax(1, concurrent_limit - 1)at a time, leaving at least one rate-limiter slot free for interactive API calls. - Lock Contention vs Genuine Failure: If another worker holds the per-domain lock (
LockAcquisitionError), the coordinator skips redundant work without markingarodonata_domain_sync_state.status = "failed". Genuine API or credential errors record a sanitized (redact_sid) error inlast_errorwhile preserving previously cached rows.
3. Search & Recursive Group Membership (SearchService)
await client.search_objects(query, ..., exact_match=False, max_depth=2) classifies each comma-separated term in query and searches across arodonata_objects (plus any active WriteSession overlay):
- IPv4 Address (
10.1.10.50): Matches hosts with that IPv4 address, networks whose CIDR subnet contains the IP, and address ranges whose[first..last]bounds include the IP. - CIDR Subnet (
10.1.10.0/24): Matches networks with that subnet/mask and hosts/subnets contained within it. - IPv4 Range (
10.1.10.1-10.1.10.20): Matches overlappingaddress-rangeobjects and contained hosts. - Wildcard Pattern (
*.corp.example,web-*): Evaluates case-insensitive anchoredfnmatchcaseacross object names andfqdnfields. - Composite Identifier (
mds:Domain4:hWebHostormds:Domain4:<uid>): MatchesCPObject.object_idorCPObject.iddirectly (case-insensitively onobject_id). - Exact or Substring Name (
exact_match=TruevsFalse):- When
exact_match=False(default), matches case-insensitive substrings inname,fqdn,object_id, andcomments. - When
exact_match=True, matches only exact case-insensitivename,fqdn,object_id, orid(e.g., searching".example.com"withexact_match=Truematches.example.comwithout matchingsub.example.com).
- When
- Recursive Group Resolution (
max_depth): Automatically traversesgroup.membersupward up tomax_depthlevels, returning bothSearchResult.objectsandSearchResult.memberships(object_uid -> list[GroupMembershipNode], where eachGroupMembershipNodecarriesuid,name,type,domain,id,object_id,depth,parent_uids, andparent_object_ids).
4. Cross-Domain Asset Topology & Composite Identifiers
During refresh_assets / stream_refresh_assets, AssetRefreshService runs ClusterRelationshipManager and VSXRelationshipManager across cached domain assets:
- Intra-Domain Cluster Members: Links each cluster (
CpmiGatewayCluster,vsx-cluster-netobj) and its members (CpmiClusterMember,vsx-cluster-member) with both UID references (parent_uid,member_uids) and composite identifiers (parent_id,parent_asset_id,member_asset_ids). - Cross-Domain VSX Virtual Systems: On Multi-Domain Security Management (MDSM), a physical VSX gateway or cluster typically lives in an infrastructure domain (e.g.,
"VSX") while its Virtual Systems (virtual-system) reside in tenant domains (e.g.,"Domain4").AssetRefreshServicereconciles VSX relationships across all domains on the samemgmt_name, populatingparent_id("mds:VSX:<vsx-uid>"),parent_asset_id("mds:VSX:<vsx-name>"),virtual_system_uids, andvirtual_system_asset_ids(["mds:Domain4:<vs-name>"]).
Sessions, MDSM & SessionOverlay
Check Point Management servers tie every uncommitted change and object lock to a session identifier (sid). Sharing a single sid across concurrent tasks or mutating on a read session leads to accidental publishes of another user's changes or deadlocked objects.
Arodonata v2 enforces a strict architectural separation between shared read-only sessions and isolated write sessions.
1. Shared Read-Only Sessions (arodonata_sid_cache)
- All read commands (
show-*,where-used,verify-policy) use a pooled session cached in thearodonata_sid_cachetable per(mgmt_name, domain_name, auth_identity_hash). - Hard Read-Only Guard:
AMgmtClientchecksis_readonly_command(command)before using a cachedsid. Attempting to runadd-*,set-*,delete-*,publish, ordiscardon the shared session raisesSessionStateErrorimmediately. - Persistent Reuse Across CLI Invocations: When
ARODONATA_DATABASE_URLpoints to a file-backed SQLite or PostgreSQL database,await client.close()leaves validarodonata_sid_cachesessions alive so subsequent CLI commands or worker processes reuse the existing login without triggering Check Point's login rate limiter. - Startup
SessionCleaner: On initialization,SessionCleanerinspects open sessions created by Arodonata (application="arodonata") and logs out only orphaned idle sessions that havechanges == 0andlocks == 0.
2. Isolated WriteSession & SessionOverlay
Whenever you open async with client.write_session(mgmt_name, domain) as ws:, Arodonata:
- Calls
create_dedicated_session()to open a fresh Check Point session outsidearodonata_sid_cache. - Binds the
WriteSessionand itsSessionOverlayto Python'scontextvarsfor the current async task tree. - Intercepts every
add-<type>,set-<type>, anddelete-<type>call inside thatwithblock and records the created, modified, or deleted object/service inws.overlay. - Automatically merges
ws.overlayinto all cached read helpers (get_hosts,get_objects,get_services,find_object,search_objects,get_group_members) called within the same async context! - On normal block exit (
publish_on_exit=True), executespublish, waits for the task to complete, clears the overlay, invalidates the domain cache so the next read fetches the new published revision, and logs out the dedicatedsid. - On exception, automatically executes
discard(shielded viaasyncio.shield) and logs out cleanly so no locks remain orphaned on the Check Point server.
3. Login Throttling Recovery (LoginGate)
Check Point R81.20 / R82 Management servers enforce a strict login rate limit; exceeding it returns HTTP 400 / 429 (err_too_many_requests / "Too many login attempts").
LoginGate(src/arodonata/adapters/checkpoint/rate_limiter.py) serializes login attempts per MDS member IP with inter-login spacing and, upon detecting a throttle response, opens alogin_throttle_window(70.0sdefault) cooldown gate across all waiting tasks up tologin_max_wait(900.0s).- Genuine authentication failures (
InvalidCredentialsError, including Check Point R82"Authentication to server failed."/err_login_failed) fail fast immediately without waiting or retrying.
CPCRUD Engine Architecture
arodonata.cpcrud (src/arodonata/cpcrud/) implements a deterministic, two-phase (plan → apply) Policy-as-Code engine for Check Point Security Management and MDSM.
Architecture & Data Flow
Core Components
1. Offline Schema Validator (validator.py)
Validates YAML strings or Python dictionaries against src/arodonata/cpcrud/checkpoint_ops_schema.json (JSON Schema Draft-07) with zero network or database calls. Checks structural requirements, object/service/group/section/rule types, conflict policies, and strict (object_type, operation) command allowlisting.
2. DomainStamp & HybridStateReader (state_reader.py)
Before inspecting any objects or rules, Planner queries show-last-published-session for each target domain and records a DomainStamp(mgmt_name, domain, last_session_uid, last_published_at).
HybridStateReader then serves object and service lookups using a two-tier strategy:
- Cache Hit (Fast Path): If
arodonata_domain_sync_statefor(mgmt_name, domain, scope, item_type)matches the planner'sDomainStamp(last_published_atandlast_session_uid), lookups (host,network,address-range,group,dns-domain,service-tcp,service-udp, etc.) are resolved directly fromarodonata_objectsandarodonata_servicesin local SQL. - Live API Fallback: If the domain cache is absent or older than the
DomainStamp,HybridStateReaderfalls back transparently to live Check Point API queries. - Intra-Plan Virtual State Overlay: Each
plan()call instantiates a freshHybridStateReaderwhose virtual overlay tracks objects, services, and groups planned earlier in the same template so downstream groups and rules see newly planned members immediately. - Fail-Closed Lookup Errors: Transport or API errors during state lookup raise
StateLookupErrorand produceOutcome.ERROR(plusOutcome.SKIPPED_DEPENDENCYon dependent groups/rules) rather than falsely assuming an object is missing and planning a duplicateCREATE.
3. Two-Axis Conflict Resolution (differ.py & planner.py)
When planning an object or service:
on_name_conflict: "update" | "error":- If an object with the same
nameexists with identical desired fields →Outcome.UNCHANGED(no-op). - If fields differ and
on_name_conflict="update"→Outcome.UPDATEwith field-levelFieldChangediffs. - If fields differ and
on_name_conflict="error"→Outcome.ERROR.
- If an object with the same
on_ip_conflict: "reuse" | "create_new" | "error":- Evaluated when no object has the requested
name, but an existing object in the domain already owns the same IPv4 address, CIDR subnet, IP range, or TCP/UDP port: "reuse"(default): PlansOutcome.REUSEand recordsrequested_name -> existing_nameinPlan.name_resolutions, automatically rewriting references in subsequent groups and rules."create_new": Generates a canonical convention name viaNamingPrefixes(Host_<ip>,Net_<subnet>_<mask>,IPR_<first>-<last>,TCP_<port>,UDP_<port>) and plansOutcome.CREATE(or reuses if that canonical name already exists)."error": PlansOutcome.ERRORand skips dependent actions.
- Evaluated when no object has the requested
4. Rule Identity & Cleanup-Aware Positioning (rule_identity.py & position_helper.py)
- Access, HTTPS, and Threat Rules: Matched by
uid,name, or traffic tuple (source,destination,service,action). - NAT Rules: Matched by
uid,name, or original/translated traffic tuple, normalizing Check Point's built-in"Original"(NAT_ORIGINAL_OBJECT_UID) and"Any"(NAT_ANY_OBJECT_UID) sentinel UIDs so re-planning an applied NAT rule is 100% idempotent (Outcome.UNCHANGED). - Cleanup Rule Anchoring: When
position: "bottom"is requested on an access layer whose bottom rule is an explicit Check Point cleanup rule (name="Cleanup rule"oraction="Drop"withAny/Any/Any),position_helperautomatically anchors the new rule{"above": "<cleanup_rule_uid>"}so traffic rules are never placed below the cleanup drop rule.
5. Dedicated-Session Executor & Staleness Guard (executor.py)
When await client.cpcrud.apply(plan, force=False) runs:
- Pre-Session Staleness Check: Re-queries
show-last-published-sessionand compares againstplan.domain_stamps. If another admin published changes afterplan()was created andforce=False, execution aborts withOutcome.PLAN_STALEbefore opening a write session. - Pre-Publish Drift Re-Verification: Before calling
publishat the end of a domain's action batch,Executorre-checksshow-last-published-sessiona second time; if concurrent changes were published while actions were running andforce=False, it discards the session and reportsOutcome.PLAN_STALE. - Compensating Rollback (
inverse.py):client.cpcrud.inverse(plan, report)inspects only actions that actually succeeded (Outcome.CREATED,Outcome.UPDATED,Outcome.DELETED) inApplyReportand generates a schema-valid reverse template (deleteforCREATED,setwithbeforevalues forUPDATED,addwithbefore_snapshotforDELETED) in reverse dependency order (rules → sections → groups → services/objects).
CPCRUD — Declarative Policy-as-Code Engine (client.cpcrud)
client.cpcrud (CPCRUDService) provides a deterministic, two-phase (plan -> apply) Policy-as-Code CRUD engine for Check Point Security Management and MDSM servers with offline JSON Schema validation, two-axis conflict resolution, staleness guards, and compensating rollback templates (inverse).
Quick Start
ARODONATA_LAB=home uv run examples/08_cpcrud_operations.py
ARODONATA_LAB=home uv run examples/09_cpcrud_inverse.py
08_cpcrud_operations.pyvalidatesexamples/crud_example.yamloffline, plans and applies it twice inDomain4, and verifies that Run 2 producesunchangedoutcomes with zerocreateactions.09_cpcrud_inverse.pyplans and applies a template, generates a compensating rollback template viaclient.cpcrud.inverse(plan, report), and applies the rollback.
Template Structure
Templates are validated offline against src/arodonata/cpcrud/checkpoint_ops_schema.json using client.cpcrud.validate(template) (zero network or database calls):
version: 1
mgmt_name: "10.192.15.140"
domain: "Domain4"
on_name_conflict: "update"
on_ip_conflict: "reuse"
objects:
- type: "host"
name: "cpcrud-example-host-1"
ipv4-address: "198.51.100.10"
comments: "CPCRUD example host"
color: "dark green"
- type: "network"
name: "cpcrud-example-net-1"
subnet4: "198.51.100.0"
mask-length4: 24
services:
- type: "service-tcp"
name: "cpcrud-example-tcp-8443"
port: "8443"
groups:
- type: "group"
name: "cpcrud-example-group"
members:
- "cpcrud-example-host-1"
- "cpcrud-example-net-1"
rules:
- type: "access-rule"
layer: "FPCR_UAT_Active Network"
position: "bottom"
name: "cpcrud-example-access-rule"
source: ["cpcrud-example-host-1"]
destination: ["Any"]
service: ["cpcrud-example-tcp-8443"]
rule_action: "Drop"
Multi-domain templates can also group operations under targets: (or domains:), where each item specifies its own mgmt_name, domain, and objects / services / groups / rules / operations.
Two-Axis Conflict Policies
on_name_conflict
Controls what happens when an object with the requested name already exists with different attributes:
| Value | Behavior |
|---|---|
update (default) |
Plans Outcome.UPDATE (set-<type>) with a field-level diff |
error |
Plans Outcome.ERROR and skips dependent groups/rules (Outcome.SKIPPED_DEPENDENCY) |
on_ip_conflict
Controls what happens when no object has the requested name, but another object already owns the same IPv4/subnet/range or service port:
| Value | Behavior |
|---|---|
reuse (default) |
Plans Outcome.REUSE and maps requested_name -> resolved_name in Plan.name_resolutions so downstream groups and rules reference the existing object |
create_new |
Plans Outcome.CREATE using convention-compliant NamingPrefixes (Host_<ip>, Net_<subnet>_<mask>, IPR_<first>-<last>, TCP_<port>, UDP_<port>) |
error |
Plans Outcome.ERROR and skips dependent actions |
Staleness Protection & Execution Options
- Pre-Lookup
DomainStamp:client.cpcrud.plan(template)capturesshow-last-published-session(last_session_uid,last_published_at) for every target domain BEFORE reading state. - Staleness Guard on
apply():await client.cpcrud.apply(plan, force=False)re-verifies each domain's published head before opening a write session. If another administrator published changes sinceplan(),apply(force=False)aborts that domain withOutcome.PLAN_STALE. Passforce=Trueto override. - Staged Sessions (
no_publish=True, keep_session=True): Leaves the dedicated write session open on the server and returnsStagedSessionInfoinApplyReport.staged_sessions(with.sidredacted inrepr()and logs). - Automatic Retries (
retry_remaining=N): Retries failed or locked actions up toNadditional passes.
Compensating Rollback (inverse)
Always pass the original Plan (captured before apply) and the resulting ApplyReport to client.cpcrud.inverse(plan, report):
plan = await client.cpcrud.plan(template)
report = await client.cpcrud.apply(plan)
inverse_template = client.cpcrud.inverse(plan, report)
inv_plan = await client.cpcrud.plan(inverse_template)
inv_report = await client.cpcrud.apply(inv_plan)
Auditable Change Reports (arodonata.reports)
Arodonata v2 provides a read-only change evidence collector and offline multi-format renderer (HTML, Markdown, JSON) in src/arodonata/reports/ and on ArodonataClient.
It is designed for change-management tickets, compliance audits, and human-in-the-loop review of both unpublished staged sessions and historical published sessions.
1. Scopes: SessionScope vs RangeScope
You can collect change reports across one or more management servers and domains in a single call by passing SessionScope or RangeScope items:
from datetime import datetime, UTC
from arodonata import ArodonataClient, RangeScope, RenderOptions, SessionScope
async with ArodonataClient() as client:
# Option A: Inspect specific session UIDs (published or currently open)
scope_sessions = SessionScope(
mgmt_name="mdsm-prod",
domain="Domain4",
session_uids=["4a8b9c10-..."],
)
# Option B: Inspect all published sessions within a UTC time window or session range
scope_range = RangeScope(
mgmt_name="mdsm-prod",
domain="Domain4",
from_date=datetime(2026, 10, 25, 0, 0, tzinfo=UTC),
to_date=datetime(2026, 10, 26, 0, 0, tzinfo=UTC),
)
bundle = await client.build_change_report(
[scope_sessions, scope_range],
formats=["html", "markdown", "json"],
options=RenderOptions(
title="Firewall Change Evidence — Ticket CHG-10492",
include_object_changes=True,
include_field_diffs=True,
include_warnings=True,
),
)
# Save rendered outputs
Path("report.html").write_bytes(bundle.html)
Path("report.md").write_text(bundle.markdown, encoding="utf-8")
Path("report.json").write_text(bundle.json_text, encoding="utf-8")
2. Unpublished Live Numbering vs Published Cached Numbering
A key capability in Arodonata v2 is accurate SmartConsole rule numbering (1, 2.1, 3) even for uncommitted rules inside an open WriteSession or staged CPCRUD session:
- Live Unpublished Session (
OwnedSession):- When you pass
owned_sessions=[OwnedSession(mgmt_name=..., domain=..., session_uid=..., sid=ws.sid, server_ip=ws.server_ip)]inRenderOptions, the collector reads the rulebase directly through that unpublishedsid(strictly read-onlyshow-*calls) so newly added or moved rules receive their exact post-change SmartConsole rule numbers beforepublish! - The
OwnedSession.sidis never stored inChangeReportand is redacted in__repr__and all rendered outputs.
- When you pass
- Published Session Fallback:
- For already-published sessions (or when no
OwnedSessionis provided), the collector resolves rule numbers and layer locations from the cached rulebase snapshot (arodonata_rulebase_*), complemented by bounded (MAX_UID_LOOKUPS = 200) liveshow-objectUID resolution for referenced objects.
- For already-published sessions (or when no
Configuration Overview
Arodonata v2 uses ArodonataSettings (src/arodonata/core/config.py) built on pydantic-settings for type-safe configuration resolution.
Core Design Principles
- No Implicit
.envLoading:ArodonataSettingsreads from explicit constructor keyword arguments andos.environ(env_file=None). Host applications and CLIs control when.envfiles are loaded. - Automatic Secret Redaction:
api_key,api_keys,password, anddatabase_urlpasswords are automatically masked ("***REDACTED***") in__repr__(),__str__(),model_dump(), andmodel_dump_json(). - Dual Alias Support: Every setting supports both prefixed
ARODONATA_*environment variables and legacyMGMT_*/ unprefixed aliases.
ArodonataSettings Reference
Server Topology & Authentication
| Field | Env Variable(s) | Type | Default | Description |
|---|---|---|---|---|
mgmt_names |
ARODONATA_MGMT_NAMES, MGMT_NAMES |
list[str] (CSV/JSON) |
[] |
Logical names of management servers (positional match with mgmt_servers) |
mgmt_servers |
ARODONATA_MGMT_SERVERS, MGMT_SERVERS |
list[str] (CSV/JSON) |
[] |
Target host, host:port, or [ipv6]:port addresses |
api_keys |
ARODONATA_API_KEYS, API_KEYS |
list[str] (CSV/JSON) |
[] |
Positional API keys for each server in mgmt_servers |
api_key_vars |
ARODONATA_API_KEY_VARS, API_KEY_VARS |
list[str] (CSV/JSON) |
[] |
Names of environment variables holding each server's API key |
mgmt_name |
ARODONATA_MGMT_NAME, MGMT_NAME |
str | None |
None |
Single-server logical name convenience fallback |
mgmt_ip |
ARODONATA_MGMT_IP, MGMT_IP |
str | None |
None |
Single-server IP/hostname convenience fallback |
mgmt_port |
ARODONATA_MGMT_PORT, MGMT_PORT |
int |
443 |
Default HTTPS port when omitted in mgmt_servers |
api_key |
ARODONATA_API_KEY, MGMT_API_KEY |
str | None |
None |
Single-server API key fallback |
username |
ARODONATA_USERNAME, MGMT_USER, MGMT_USERNAME |
str | None |
None |
Administrator username for "credential" auth mode |
password |
ARODONATA_PASSWORD, MGMT_PASSWORD |
str | None |
None |
Administrator password for "credential" auth mode |
auth_mode |
ARODONATA_AUTH_MODE |
"api_key" | "credential" | "dynamic" |
Auto-inferred | Inferred from configured keys/credentials (dynamic when neither is static) |
Timeouts, Concurrency & Rate Limiting
| Field | Env Variable | Type | Default | Description |
|---|---|---|---|---|
connect_timeout |
ARODONATA_CONNECT_TIMEOUT |
float |
30.0 |
TCP/TLS handshake timeout in seconds |
api_timeout |
ARODONATA_API_TIMEOUT |
float |
120.0 |
Per-request Management API timeout budget in seconds |
login_timeout |
ARODONATA_LOGIN_TIMEOUT |
float |
120.0 |
Timeout budget for login requests in seconds |
task_timeout |
ARODONATA_TASK_TIMEOUT |
float |
900.0 |
Maximum polling duration (TaskWaiter) for async tasks (publish, show-changes, revert-to-revision) |
rate_limit_slot_timeout |
ARODONATA_RATE_LIMIT_SLOT_TIMEOUT |
float |
90.0 |
Maximum wait time to acquire a per-MDS-member concurrency slot |
session_timeout |
ARODONATA_SESSION_TIMEOUT |
int |
600 |
Check Point server-side session-timeout requested on login (seconds) |
session_expire_seconds |
ARODONATA_SESSION_EXPIRE_SECONDS |
int |
540 |
Local TTL in arodonata_sid_cache before proactive keepalive/re-login |
concurrent_limit |
ARODONATA_CONCURRENT_LIMIT |
int (1..20) |
4 |
Max concurrent API requests per MDS member IP (domain_sync_concurrency = max(1, concurrent_limit - 1)) |
login_throttle_window |
ARODONATA_LOGIN_THROTTLE_WINDOW |
float |
70.0 |
Cooldown window (seconds) enforced by LoginGate when Check Point throttles logins |
login_max_wait |
ARODONATA_LOGIN_MAX_WAIT |
float |
900.0 |
Maximum total wait time across throttled login retries |
Storage, Cache & Telemetry
| Field | Env Variable | Type | Default | Description |
|---|---|---|---|---|
database_url |
ARODONATA_DATABASE_URL |
str |
"sqlite+aiosqlite:///:memory:" |
SQLAlchemy async database URL (sqlite+aiosqlite:///... or postgresql+asyncpg://...) |
sqlite_busy_timeout_ms |
ARODONATA_SQLITE_BUSY_TIMEOUT_MS |
int |
30000 |
SQLite PRAGMA busy_timeout in milliseconds (WAL mode enabled automatically on file DBs) |
cache_ttl |
ARODONATA_CACHE_TTL |
int |
300 |
Freshness window (seconds) before "smart" / "smart-fast" probes show-last-published-session |
domain_list_ttl |
ARODONATA_DOMAIN_LIST_TTL |
int |
3600 |
TTL (seconds) for cached MDSM show-domains topology (arodonata_domains) |
max_incremental_changes |
ARODONATA_MAX_INCREMENTAL_CHANGES |
int |
500 |
Threshold of show-changes entries above which "smart-fast" falls back to full type reload |
tls_trust |
ARODONATA_TLS_TRUST |
"pinned" | "tofu" | "lab-memory" |
"tofu" |
TLS peer certificate verification mode |
tls_known_hosts_path |
ARODONATA_TLS_KNOWN_HOSTS_PATH |
str |
~/.config/arodonata/tls_known_hosts.json |
Path to persistent 0600 JSON trust store |
tls_fingerprints |
ARODONATA_TLS_FINGERPRINTS |
str |
"" |
Comma-separated pinned SHA-256 certificate fingerprints (host:port=SHA256:... or bare hex) |
cpcrud_schema_path |
ARODONATA_CPCRUD_SCHEMA_PATH |
str | None |
None |
Optional path override for checkpoint_ops_schema.json |
trace_modules |
ARODONATA_TRACE_MODULES |
str |
"" |
OpenTelemetry module span gating rules (e.g. "+arodonata.core,-arodonata.adapters.storage") |
Multi-Server Topology & Authentication Modes
Arodonata v2 natively manages multiple Check Point Security Management Servers (SMS) and Multi-Domain Security Management (MDSM) clusters within a single ArodonataClient instance.
1. Positional Multi-Server Configuration
Configure multiple servers by providing comma-separated lists (or JSON arrays) in ARODONATA_MGMT_NAMES and ARODONATA_MGMT_SERVERS:
ARODONATA_MGMT_NAMES=mdsm-eu,mdsm-us,sms-dmz
ARODONATA_MGMT_SERVERS=10.10.1.10:443,10.20.1.10:443,[2001:db8::10]:443
ARODONATA_API_KEY_VARS=MDSM_EU_KEY,MDSM_US_KEY,SMS_DMZ_KEY
MDSM_EU_KEY=secret-api-key-eu
MDSM_US_KEY=secret-api-key-us
SMS_DMZ_KEY=secret-api-key-dmz
Indirect Secret References (ARODONATA_API_KEY_VARS)
Using ARODONATA_API_KEY_VARS is recommended for production deployments so each server's secret key resides in its own dedicated secret environment variable rather than a combined comma-separated string.
2. Authentication Modes (AuthMode)
ArodonataSettings automatically determines auth_mode using strict precedence:
"api_key"Mode: Active whenapi_keys,api_key, orapi_key_varsare provided. Counts ofmgmt_names,mgmt_servers, andapi_keysmust match exactly."credential"Mode: Active whenusernameandpasswordare provided (and no explicitapi_keysare set)."dynamic"Mode: Active when no static credentials or API keys are configured inArodonataSettings. Callers supply per-request credentials at runtime viawith client.auth_context(username=..., password=...):or an injectedAuthProviderport.
3. Runtime Credential & Attribution Contexts
In multi-tenant web backends (such as FPCR v2), a single long-lived ArodonataClient can execute operations under the logged-in administrator's identity using Python contextvars:
with (
client.auth_context(api_key=user_api_key),
client.user_context(
user_id="u-1042",
username="alice@corp.example",
session_name="fpcr-ticket-9812",
session_description="Automated firewall rule provisioning",
),
):
async with client.write_session("mdsm-eu", "Domain4") as ws:
await client.add_object("mdsm-eu", "host", {"name": "h-srv-99", "ipv4-address": "10.10.99.1"}, domain="Domain4")
- Credential Isolation in
arodonata_sid_cache: Shared read-only session keys incorporate an HMAC-SHA256 digest of the active credential (auth_identity), ensuring two different dynamic credentials never share a cachedsid.
TLS Certificate Verification (TrustPolicy & TrustStore)
Check Point Management and MDSM servers frequently use self-signed or internal CA certificates, and MDSM standby/member CMA IPs often present a certificate issued to the primary MDS hostname. Standard hostname-based TLS verification breaks on CMA IP routing, while disabling TLS verification (verify_ssl=False) exposes automation to man-in-the-middle attacks.
Arodonata v2 solves this with post-handshake, pre-request SHA-256 DER certificate fingerprint verification (src/arodonata/adapters/checkpoint/tls.py).
How Verification Works
- Before sending any HTTP request headers or Check Point credentials (
api-key/password/X-chkp-sid) over anaiohttpconnection,ApiTransportinspects the peer's raw DER certificate fromtransport.get_extra_info("ssl_object").getpeercert(binary_form=True). - It computes the uppercase SHA-256 hex digest and validates it against
TrustPolicybefore a single byte of HTTP payload leaves the socket. - If the certificate does not match, the connection is aborted immediately with
CertificateMismatchErrororUnknownServerCertificateError.
Trust Modes (ARODONATA_TLS_TRUST)
| Mode | Behavior | Recommended Use |
|---|---|---|
"pinned" |
Strictly requires the server's SHA-256 fingerprint to exist in ARODONATA_TLS_FINGERPRINTS or tls_known_hosts.json. Rejects unknown certificates with UnknownServerCertificateError. |
Production |
"tofu" (default) |
Trust On First Use: pins the certificate fingerprint to ~/.config/arodonata/tls_known_hosts.json (with 0600 permissions and fcntl.flock cross-process locking) on first contact, and raises CertificateMismatchError if it ever changes. |
Staging / CLI / Standard Deployments |
"lab-memory" |
Stores TOFU pins in memory for the lifetime of the process without writing to disk. Refuses to activate unless ARODONATA_LAB is set or explicitly configured for tests. |
CI / Ephemeral Lab Tests |
Cross-IP MDSM Member Recognition
On Multi-Domain servers, show-domains discovers domain CMA IPs and active MDS member IPs (active_mds_ip) that share the same physical server certificate as the primary mgmt_ip.
When TrustPolicy connects to an MDSM member IP whose certificate SHA-256 fingerprint matches an already-trusted entry in the same management cluster, it automatically recognizes and records that (host, port) endpoint without failing.
Configuring Pinned Fingerprints
You can supply pinned fingerprints via environment variable:
ARODONATA_TLS_TRUST=pinned
# Host-scoped or global comma-separated SHA-256 fingerprints:
ARODONATA_TLS_FINGERPRINTS="10.10.1.10:443=SHA256:A1B2C3...,10.20.1.10:443=SHA256:D4E5F6..."
Testing & Quality Gates
Arodonata v2 enforces strict offline unit test coverage, static typing, linting, OpenSpec specification validation, and a 7-bucket live Check Point MDSM integration suite.
1. Unified Test Runner (./pytest.sh)
All test execution goes through ./pytest.sh:
# Fast offline unit tests (mandatory pre-PR quality gate; zero network access)
./pytest.sh
./pytest.sh unit
# Pass extra pytest arguments (e.g. -k or specific test file)
./pytest.sh unit tests/unit/test_mcp_server.py
Live Check Point MDSM/SMS Integration Buckets (b1..b7)
Live integration tests require .env.test, .env.secrets, and an optional lab overlay .env.lab.<ARODONATA_LAB> (e.g., ARODONATA_LAB=home). They acquire a non-blocking process-wide lock (tests/integration/run_lock.py), probe server readiness (lab_ready.py), and automatically snapshot and restore mutated domains via revert-to-revision (cp_revision.py).
| Command | Bucket | Scope |
|---|---|---|
./pytest.sh int-1 |
bucket_1 |
Authentication, session lifecycle, rate limiter, cached reads, search, rulebase reads, change reports |
./pytest.sh int-2 |
bucket_2 |
Cache build, streaming sync events (SyncProgressEvent), live CPCRUD (plan / apply / inverse) |
./pytest.sh int-3 |
bucket_3 |
WriteSession + SessionOverlay, publish, discard, revert-to-revision, and 70s login throttle recovery |
./pytest.sh int-4 |
bucket_4 |
Multi-domain isolation (Domain4 vs Domain5 vs Global) and CacheMode matrix (cache, smart, smart-fast, force) |
./pytest.sh int-5 |
bucket_5 |
Full rebuilds, 120-rule layer pagination stitching, publish matrix, and session change reports |
./pytest.sh int-6 |
bucket_6 |
Bounded soak loop (repeated publish → smart-fast → revert stability cycles) |
./pytest.sh int-7 |
bucket_7 |
Live MCP server and consolidated tool surface verification |
./pytest.sh int-full |
b1..b7 |
Full sequential integration suite |
2. Mandatory Pre-Commit / Pre-PR Quality Gates
# 1. Python lint and formatting
uv run ruff check .
uv run ruff format --check .
# 2. Strict MyPy type checking (Python 3.13)
uv run mypy src
# 3. Offline unit test suite
./pytest.sh unit
# 4. Markdown linting
npx markdownlint-cli "**/*.md" --ignore node_modules --ignore .venv --fix
# 5. OpenSpec specification validation
openspec validate --specs --no-interactive
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Added
- Human-readable composite identifiers (
object_id,service_id,asset_id) and dual UID/human-readable topology links (arodonata.core.models,arodonata.adapters.storage,arodonata.core.services,arodonata.client,arodonata.mcp): non-unique indexedobject_id = "{mgmt_name}:{domain_name}:{name}"onCPObject/CPObjectRecord(arodonata_objects),service_id = "{mgmt_name}:{domain_name}:{name}"onCPService/CPServiceRecord(arodonata_services), andasset_id = "{mgmt_name}:{domain_name}:{name}"onAsset/AssetRecord(arodonata_assets) alongside UID primary keysid = "{mgmt_name}:{domain_name}:{uid}";id,object_id, andparent_object_idsonGroupMembershipNodeinSearchResult.memberships;parent_id,parent_asset_id,member_asset_ids, andvirtual_system_asset_idsonAsset/AssetRecordwith cross-domain VSX gateway/cluster to Virtual System reconciliation across domains on the same management server; and composite identifier support acrossfind_object(),get_group_members(),search_objects(),query_objects(), and MCP tools (show_object,list_assets,get_group_members,query_objects). - Bilingual (English and Russian) documentation architecture (
docs/,mkdocs.yml,.github/workflows/docs.yml) and NotebookLM monolithic knowledge bundles (docs-notebooklm/arodonata_v2_bundle_Part1..Part3.md).
v2.0.0 (2026-10-26)
Feat
- mcp: consolidate MCP tool surface into cache-aligned catalog (#9)
- sdk: live integration tests (b1..b7), SDK examples (01..12), and host-app integration hardening (#8)
- mcp: implement Streamable-HTTP MCP server, CLI, security guards, and tool surface (Milestone 6) (#7)
- cpcrud: implement declarative Policy-as-Code CPCRUD engine (#6)
- rulebase,reports: implement rulebase introspection, numbering, and change reports (#5)
- sync: implement core sync/search services and ArodonataClient facade (#4)
- checkpoint: implement native aiohttp transport, TLS verification, and session lifecycle (#3)
- core,storage: implement milestone 1 core foundation and sql storage adapters (#2)
Security
- Comprehensive security and specification conformance remediation (
audit-remediation-261025acrossarodonata.adapters,arodonata.core,arodonata.cpcrud,arodonata.rulebase, andarodonata.mcp): strict Web API command-name validation (^[a-z0-9]+(-[a-z0-9]+)*$), disabled HTTP redirects (allow_redirects=False) and ambient proxies (trust_env=False), and phase-classifiedTransportError(connectvspost-send) preventing post-write request resends on failover; cross-processfcntl.flocklocking,0600permission enforcement, and IPv6[host]:portkey normalization inTrustStore; full secret anddatabase_urlpassword masking acrossArodonataSettings/ArodonataMCPSettingsand recursive secret/SID redaction across nested collections and exception chains;arodonata_prefix on all 15 SQLModel tables with positive v1 column fingerprint checks (mdm_dmn,mgmt_dmn_key), cross-worker serialized migration locks (pg_advisory_xact_lock/BEGIN IMMEDIATE), atomic single-transaction cache + sync-state commits, dialectON CONFLICTupserts, and owner-verified distributed lock renewal heartbeats; HMAC-SHA256 credential-boundarodonata_sid_cachekeys,LoginGateself-deadlock elimination and inter-login spacing, Arodonata-onlySessionCleanerfiltering (changes == 0andlocks == 0), and strict prohibition of mutating commands on the shared read-onlyarodonata_sid_cachesession; CPCRUD(object_type, operation)command allowlisting, per-planHybridStateReaderisolation, pre-publish head drift re-verification, and report-verifiedinverse()generation; and MCP read-only command allowlisting, HMAC-SHA256 signedcpcrud_planverification incpcrud_apply/cpcrud_inverse,cpcrud_staged_sessiontool, base64-safe 32-char minimum static token parsing with SHA-256hmac.compare_digest, and fail-closedjwtauthentication.
Added
- Consolidated cache-aligned MCP tool surface (
arodonata.mcp,arodonata.core.services,arodonata.client): 13 base tools (list_domains,list_objects,list_services,list_assets,show_objectwith 3-tier exact/anchored-wildcard/substring matching across objects, services, assets, and rulebase layers,search_objects,get_group_membersexpanding network groups, service groups, and cluster/VSX child assets,query_objectswithaggregate="list" | "count" | "describe",show_rulebase,locate_rules,change_report,cache_control, andapi_call) plus 5 opt-in CPCRUD tools (cpcrud_validate,cpcrud_plan,cpcrud_apply,cpcrud_inverse,cpcrud_staged_session) and multi-tableArodonataClient.find_object. - Host application integration hardening (
arodonata.adapters.storage,arodonata.core.services,arodonata.cpcrud,arodonata.rulebase):to_naive_utc()helper and uniform naive UTC timestamps (DateTime(timezone=False),tzinfo=None) across all database tables, domain models, and Check Point timestamp parsers;ARODONATA_TABLE_NAMESnamespace isolation andarodonata_assetstable rename (preventing collision with host applicationassetstables and ignoring non-Arodonata tables inSQLModel.metadata); savepoint-wrapped (begin_nested()) multi-worker schema evolution with automaticDROP+CREATErecovery for incompatible legacy v1 cache tables (domains,sid_cache,arodonata_schema_version);exact_match: bool = FalseinSearchService.search_objects(),ArodonataClient.search_objects(), and MCPsearch_objects; staged session finalization (publish_staged_session,stream_publish_staged_session,discard_staged_session) onCPCRUDServiceandArodonataClientviatake-over-session(uid=session_uid); andrule_to_cpcrud_operation()withoutput_format="cpcrud"support infilter_rulebase()for exporting cached rules into schema-valid CPCRUD operations. - Live Check Point MDSM/SMS integration test harness (
tests/integration/,openspec/specs/integration-testing-and-examples/spec.md),./pytest.shtest runner (unit,int-1..int-7,int-fullwith-venabled by default), and 12 progressive runnable SDK examples (examples/01..12,examples/_session_guard.py,examples/crud_example.yaml,examples/README.md,examples/README_CRUD.md): layered lab environment loader (tests/integration/lab_env.pysupportingARODONATA_LAB=<profile>), non-blocking process-widefcntl.flockrun lock (tests/integration/run_lock.py), live API readiness probe (tests/integration/lab_ready.py), crash-safe domain revision snapshot andrevert-to-revisionbaseline recovery (tests/integration/cp_revision.pyandrestore_baseline.py), standalone diagnostic utilities (probe_lab_domains.py,probe_login_throttle.py,lab_build.py), 60 live integration tests across 7 buckets (b1auth/sessions/reads/search/rate-limiting,b2cache build & CPCRUD live operations,b3publish/revert & 70s login throttle recovery,b4cache mode matrix & multi-domain isolation,b5full rebuild/120-rule pagination/rulebase session refresh/publish matrix/change reports,b6bounded soak loop,b7MCP server & tools), top-level re-exports (Asset,CPObject,CPService,DomainInfo,RefreshOutcome,SearchResult),ArodonataClient.cleanup_server_sessions(), andoteloptional dependency group (arodonata[otel]). - Streamable-HTTP MCP server and standalone CLI (
arodonata.mcp,arodonata[mcp],arodonata-mcp):ArodonataMCPSettings(ARODONATA_MCP_*configuration with secret-safe__repr__and standalone startup validation refusingauth_mode="host"or"jwt"and missingpinnedTLS trust material with exit code2),create_mcp_serverandcreate_asgi_app(ArodonataASGIAppfor Starlette/FastAPI embedding),StaticTokenVerifierandMCPBearerAuthMiddleware(constant-timehmac.compare_digestverification acrossARODONATA_MCP_TOKEN_VARSmapping variable names toclient_idin logs andUserContextwithout logging token values),MCPOriginHostGuardMiddleware(DNS rebinding protection returning HTTP421for unlistedHostand HTTP403for unlistedOrigin), pre-logindomainvalidation against the cachedarodonata_domainstable (with single domain-list refresh on miss) andInvalidCredentialsErrortranslation into actionableToolFailuremessages listing known domains, and MCP prompt templates (audit_session_changes,locate_ip_in_policy,plan_declarative_change). - Declarative Policy-as-Code CPCRUD engine (
arodonata.cpcrud) wired intoArodonataClient(client.cpcrud): offline JSON Schema Draft-07 template validator (CPCRUDService.validate/validate_templateovercheckpoint_ops_schema.jsonwith zero network or database calls),HybridStateReader(isolated per-plan state reader serving objects fromarodonata_objectsand services fromarodonata_servicesstrictly whenarodonata_domain_sync_statefor(scope, item_type)matches the planner'sDomainStamplast_published_atandlast_session_uid, with transparent live API fallback and intra-plan virtual state overlay),PlannerandDiffer(pre-lookupDomainStampcapture, two-axis conflict resolutionNameConflictPolicyxIpConflictPolicywithNamingPrefixesauto-naming,Outcome.ERRORandOutcome.SKIPPED_DEPENDENCYon lookup failures instead of falseCREATE, rule identity matching by key or traffic tuple with Check Point NAT"Original"/"Any"sentinel normalization, and cleanup-rule-aware"bottom"placement anchoring{"above": "<cleanup_rule_uid>"}), dedicated-sessionExecutor(applyandstream_applywithOutcome.PLAN_STALEsafeguard, per-domainWriteSession+SessionOverlayupdates,no_publish=True, keep_session=Truestaging withStagedSessionInfo/StagedSessionEventand SID redaction, post-publishinvalidate_domain, andretry_remainingfolding), and compensating rollback generator (CPCRUDService.inverse/build_inverse_template). - Rulebase introspection, SmartConsole hierarchical numbering (
arodonata.rulebase), and auditable session change reports (arodonata.reports) wired intoArodonataClient: section-stitching rulebase pager (fetch_paged_rulebasemerging splitrulebase-sectionheaders across 100-rule pages and recovering empty trailing sections),RulebaseRefreshService(refresh_rulebases/stream_refresh_rulebaseswith dirty-session refusal, full package/NAT/access/HTTPS/threat/inline/Global placeholder traversal, and atomicreplace_domain_rulebasereplacement),CachedRulebaseSource(get_policy_packages,get_package_rulebase,get_layer_rulebase,locate_rules, automatic domain resolution withAmbiguousLayerName, and localfilter_rulebaseevaluation over cached rules/objects/services), and change report pipeline (collect_change_reportwithSessionScope/RangeScopeUTC validation, deduplication,ReportWarningpartial-failure tracking, bounded 200-UIDshow-objectlookup, read-only liveOwnedSessionrule numbering with full SID redaction and cached snapshot fallback, and offlinerender_change_report/build_change_reportfor self-contained HTML, Markdown, and versioned JSON). - Core synchronization, search services (
arodonata.core.services), and unifiedArodonataClientfacade (arodonata.client):DomainServiceandDomainListRefreshTracker(MDSMshow-domainsdiscovery,Globaldomain synthesis,include_globalfiltering, active/standby failover IP re-resolution, and 1-hour TTL),DomainSyncCoordinator(parallel domain traversal peractive_mds_ipatmax(1, concurrent_limit - 1), per-domain distributed locks, dirty-session checks viashow-session,(last_published_at, last_session_uid)staleness comparison inarodonata_domain_sync_state, and probeInvalidCredentialsErrorfailure handling),ObjectService(pairedrefresh_objects/stream_refresh_objects,CacheMode"cache" | "smart" | "smart-fast" | "force","smart-fast"incremental diff viashow-changeswith fallback on> max_incremental_changes, default("host", "network", "address-range", "group", "dns-domain"), and type-scoped replacement),ServiceCacheService(arodonata_serviceswith default("service-tcp", "service-udp", "service-icmp", "service-group")and selective on-demandtypes=[...]),AssetRefreshService(ClusterRelationshipManagerandVSXRelationshipManagervia encapsulatedgeneric-objectqueries),SearchService(search_objectswith IP/CIDR/range/wildcard/UID/name classification and recursive group traversal up tomax_depth,describe_objects,query_objects, andget_cache_status), andArodonataClient(typed getters,WriteSessionoverlay merging,write_session,auth_context,add_object,set_object,delete_object,find_object,get_group_members,start_object_cache_warmup, andinvalidate_domain). - Check Point native
asynciotransport and session adapter (arodonata.adapters.checkpoint):TrustStoreandTrustPolicy(pinned,tofu,lab-memorywith atomic0600tls_known_hosts.jsonpersistence and cross-IP MDS member certificate recognition),ApiTransportoveraiohttp(post-handshake pre-request SHA-256 fingerprint verification, separateconnect_timeout/read_timeoutApiTimeoutErrorphases, no automatic re-send after socket write, andVerifiedConnectionPoolreusing keep-alive sockets up to 10s idle strictly forshow-*commands),TaskWaiter(exponential backoff2s..10spolling undertask_timeouttolerating up to 5 transient poll errors), continuity-verified object pager (pager.pywith±10boundary swap window recovery and single restart on discontinuity), per-MDS-memberRateLimiter(database slot locks + FIFO waiter queue + coalesced keepalive sweeps) andLoginGate(70sthrottle recovery and fast-failInvalidCredentialsError),LoginCoordinator,SessionCleaner, context-boundWriteSession(SessionOverlayupdates, auto-publish/discard), andAMgmtClientimplementingApiPort. - Core foundation (
arodonata.core) and SQL storage adapter (arodonata.adapters.storage):ArodonataSettings, exception hierarchy with SID/secret redaction,@tracedandspan_attrsinstrumentation, typed domain progress events (SyncProgressEvent,CPCRUDEvent),contextvars-boundAuthContext,UserContext, andWriteSessionwithSessionOverlay, Pydantic v2 domain models, hexagonal protocols (ApiPort,CachePort,LockPort,AuthProvider),DatabaseManager(SQLite + PostgreSQL with SHA-256 schema hash auto-migration viaarodonata_schema_version),SqlLockAdapter,SqlCacheAdapter, and SSE boundary adapter (arodonata.adapters.sse).
Changed
- Revised all OpenSpec capability specifications for Arodonata v2 clean-slate hexagonal architecture: native
aiohttptransport (replacingcpapi),SqlCacheAdapter(replacingPostgresCacheAdapter/CacheRepository), canonicalCacheModenames (cache,smart,smart-fast,force), paired coroutine andstream_*domain event APIs (removingSSEEventfrom core), and unifiedArodonataClientfacade (replacingarodonata.helpers).
Fixed
- Check Point R82 live compatibility and persistent cache/session reuse fixes:
ArodonataClientandAMgmtClientnow honorsettings.database_urlwhenengine=None, auto-create parent directories for file-backed SQLite URLs, preservearodonata_sid_cacheentries onclose()for persistent databases, and skip redundant startupSessionCleanerlogins when a valid system-domain SID is already cached;LoginCoordinatorclassifies generic R82"Authentication to server failed."(err_login_failed) as non-retryableInvalidCredentialsError;DomainSyncCoordinatorandObjectServiceextract"iso-8601"from dict-formattedpublish-timeand unwrap task-basedshow-changesoperationsarrays;Collectorpassesview-published-sessions=Truefor published session discovery; andPlanner._find_existing_nat_ruleresolves inline object/service names to UIDs for idempotent NAT rule re-planning.
Arodonata v2 Examples
Progressive runnable scripts demonstrating ArodonataClient usage from basic cache-backed queries to advanced multi-user session overlays, declarative Policy-as-Code (CPCRUD), auditable change reports, OpenTelemetry tracing, and embedded MCP servers.
Catalog Overview
| Script | Doc Page | Topic | Mutates Lab? |
|---|---|---|---|
01_basic_queries.py |
01 — Basic Queries | Discovers domains (get_domains) and queries cached hosts, networks, and services (get_hosts, get_networks, get_services) |
No |
02_search_and_query.py |
02 — Search & Structured Query | Multi-term object search with recursive group memberships (search_objects), progressive disclosure (describe_objects), structured cross-domain aggregation (query_objects), and cache freshness (get_cache_status) |
No |
03_gateway_relationships.py |
03 — Gateway & VSX Topology | Gateway, cluster, and VSX asset topology (get_gateways) linking cluster members and virtual systems via parent_uid and member_uids |
No |
04_smart_refresh.py |
04 — Smart & Streaming Refresh | Paired streaming (stream_refresh_*) and batch (refresh_*) cache synchronization across assets, objects, services, and rulebases — run this first to warm a persistent DB |
No |
05_rulebase_queries.py |
05 — Rulebase Queries | Policy package discovery (get_policy_packages), hierarchical SmartConsole numbering (get_package_rulebase), reverse UID lookup (locate_rules), and local rule filtering (filter_rulebase) |
No |
06_session_basics.py |
06 — Session Lifecycle Basics | Low-level session lifecycle: capture baseline (show-last-published-session), create_dedicated_session, add-host, publish, and revert-to-revision |
Yes (auto-reverted via _session_guard.py) |
07_context_and_overlay.py |
07 — Contexts & Session Overlay | Dynamic auth_context, user_context, and write_session with SessionOverlay read-your-own-writes isolation (discards on exit without publishing) |
No (discards uncommitted session) |
08_cpcrud_operations.py |
08 — CPCRUD Operations | Declarative Policy-as-Code (client.cpcrud.validate, plan, apply) applying crud_example.yaml twice to prove idempotency — see CPCRUD User Guide |
Yes (auto-reverted via _session_guard.py) |
09_cpcrud_inverse.py |
09 — CPCRUD Inverse Rollback | Compensating rollback workflow: plan -> apply -> inverse(plan, report) -> plan -> apply |
Yes (auto-reverted via _session_guard.py) |
10_change_report_evidence.py |
10 — Change Report Evidence | Auditable ChangeReport evidence: live unpublished numbering via OwnedSession vs published cached numbering rendered to HTML, Markdown, and JSON |
Yes (auto-reverted via _session_guard.py) |
11_otel_tracing.py |
11 — OpenTelemetry Tracing | Provider-agnostic OpenTelemetry instrumentation (arlogi.otel.setup_tracing) with a per-span timing breakdown from exported JSONL traces |
No |
12_mcp_embedded.py |
12 — Embedded FastAPI MCP | Embedding Arodonata's MCP server (create_mcp_server, create_asgi_app) inside a FastAPI application with custom tools |
No |
_session_guard.py |
— | Crash-safe baseline snapshot/restore context manager (guarded_session) and standalone CLI tool for manual testing |
Helper / CLI |
Setup & Execution
# 1. Install dependencies (including dev/example dependencies)
uv sync --all-extras
# 2. Configure environment files (.env.lib / .env.test / .env.secrets)
cp .env.example .env.lib
# 3. Warm the cache first (when using a persistent SQLite or PostgreSQL DATABASE_URL)
uv run examples/04_smart_refresh.py
# 4. Run read-only query, search, topology, rulebase, and overlay examples
uv run examples/01_basic_queries.py
uv run examples/02_search_and_query.py
uv run examples/03_gateway_relationships.py
uv run examples/05_rulebase_queries.py
uv run examples/07_context_and_overlay.py
uv run examples/11_otel_tracing.py
# 5. Run mutating lab examples (automatically snapshot and revert Domain4/Domain5 baseline)
ARODONATA_LAB=home uv run examples/06_session_basics.py
ARODONATA_LAB=home uv run examples/08_cpcrud_operations.py
ARODONATA_LAB=home uv run examples/09_cpcrud_inverse.py
ARODONATA_LAB=home uv run examples/10_change_report_evidence.py
# 6. Run the embedded FastAPI + MCP server on http://127.0.0.1:8000/mcp
uv run examples/12_mcp_embedded.py
Crash-Safe Manual Testing with _session_guard.py
When experimenting manually in SmartConsole or with ad-hoc scripts against a lab domain:
# 1st invocation: snapshots and saves the current last-published session to examples/_tmp/session_guard_state.json
ARODONATA_LAB=home uv run examples/_session_guard.py 192.168.5.140 Domain4
# ... perform manual changes/publishes in Domain4 ...
# 2nd invocation: discards open sessions, reverts Domain4 to the saved baseline, and removes the state file
ARODONATA_LAB=home uv run examples/_session_guard.py 192.168.5.140 Domain4
01 — Basic Queries (01_basic_queries.py)
Demonstrates discovering domains (client.get_domains()) and querying cached hosts, networks, and services (client.get_hosts(), client.get_networks(), client.get_services()) using ArodonataClient.
Source Code (examples/01_basic_queries.py)
"""Example 01 — Basic typed queries (`get_domains`, `get_hosts`, `get_networks`, `get_services`).
Run:
uv run examples/01_basic_queries.py
Reads configuration from `.env.lib` and `.env.secrets` (or `.env`).
Demonstrates how `ArodonataClient` discovers domains and queries cached network and service
objects with `cache_mode="smart"`.
"""
from __future__ import annotations
import asyncio
import os
import time
from dotenv import load_dotenv
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
from arodonata import ArodonataClient, ArodonataSettings # noqa: E402
async def main() -> None:
"""Query domains, hosts, networks, and services across configured management servers."""
settings = ArodonataSettings()
print(f"Using database: {settings.database_url or 'sqlite+aiosqlite:///:memory:'}")
print(f"Management servers: {settings.mgmt_names}")
t0 = time.perf_counter()
async with ArodonataClient(settings=settings) as client:
print("\n[Step 1/4] Discovering MDS domains (get_domains)...", flush=True)
s0 = time.perf_counter()
domains = await client.get_domains(include_global=True)
print(f"Discovered domains ({len(domains)}) in {time.perf_counter() - s0:.2f}s:")
for dom in domains[:10]:
print(f" - {dom.name} (mgmt={dom.mgmt_name}, active_ip={dom.active_ip}, global={dom.is_global})")
target_domain = os.getenv("EXAMPLE_DOMAIN") or (domains[0].name if domains else None)
if not target_domain:
print("No domains found.")
return
print(f"\n[Step 2/4] Querying hosts in domain {target_domain!r} (get_hosts, cache_mode='smart')...", flush=True)
s0 = time.perf_counter()
hosts = await client.get_hosts(domain=target_domain, cache_mode="smart")
print(f"Hosts ({len(hosts)}) in {time.perf_counter() - s0:.2f}s:")
for host in hosts[:5]:
print(f" - {host.name}: {host.ipv4_address} (uid={host.uid})")
print(
f"\n[Step 3/4] Querying networks in domain {target_domain!r} (get_networks, cache_mode='smart')...",
flush=True,
)
s0 = time.perf_counter()
networks = await client.get_networks(domain=target_domain, cache_mode="smart")
print(f"Networks ({len(networks)}) in {time.perf_counter() - s0:.2f}s:")
for net in networks[:5]:
print(f" - {net.name}: {net.subnet4}/{net.mask_length4}")
print(
f"\n[Step 4/4] Querying services in domain {target_domain!r} (get_services, cache_mode='smart')...",
flush=True,
)
s0 = time.perf_counter()
services = await client.get_services(domain=target_domain, cache_mode="smart")
print(f"Services ({len(services)}) in {time.perf_counter() - s0:.2f}s:")
for svc in services[:5]:
print(f" - {svc.name} ({svc.type}): port={svc.port} proto={svc.protocol}")
elapsed = time.perf_counter() - t0
print(f"\nCompleted in {elapsed:.2f}s")
if __name__ == "__main__":
asyncio.run(main())
02 — Search & Structured Query (02_search_and_query.py)
Demonstrates multi-term object search with recursive parent group memberships (client.search_objects(), including exact_match=True), progressive-disclosure schema discovery (client.describe_objects()), SQL-free cross-domain aggregation (client.query_objects()), and cache freshness inspection (client.get_cache_status()).
Source Code (examples/02_search_and_query.py)
"""Example 02 — Object search, group membership resolution, and structured `query_objects`.
Run:
uv run examples/02_search_and_query.py
Demonstrates:
1. `client.search_objects(query, max_depth=2)` — classifies comma-separated search terms
(IPv4, CIDR, range, wildcard, UID, name) and resolves parent group memberships recursively.
2. `client.describe_objects()` — progressive-disclosure schema and type counts across domains.
3. `client.query_objects()` — cross-domain filtering, grouping (`group_by`), and aggregation
(`aggregate="count" | "list"`), including `scope_origin="local" | "global"`.
4. `client.get_cache_status()` — per-scope cache freshness and `cache_age_seconds`.
"""
from __future__ import annotations
import asyncio
import os
import time
from dotenv import load_dotenv
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
from arodonata import ArodonataClient, ArodonataSettings # noqa: E402
async def main() -> None:
"""Run search_objects, describe_objects, query_objects, and get_cache_status."""
settings = ArodonataSettings()
t0 = time.perf_counter()
async with ArodonataClient(settings=settings) as client:
print("[Step 1/4] Resolving target domain (get_domains)...", flush=True)
domains = await client.get_domains()
target_domain = os.getenv("EXAMPLE_DOMAIN") or (domains[0].name if domains else None)
# 1. Search objects by wildcard and IPv4/CIDR with recursive parent group resolution
query_str = os.getenv("EXAMPLE_SEARCH_QUERY", "*host*, 10.0.0.0/8")
print(
f"\n[Step 2/4] Searching objects across all 5 types in {target_domain!r} "
f"(search_objects={query_str!r}, max_depth=2, cache_mode='smart')...",
flush=True,
)
s0 = time.perf_counter()
search_res = await client.search_objects(
query_str,
domains=[target_domain] if target_domain else None,
max_depth=2,
cache_mode="smart",
)
print(
f"Search query {query_str!r} matched {len(search_res.matches)} object(s) in {time.perf_counter() - s0:.2f}s:"
)
for match in search_res.matches[:5]:
groups = search_res.memberships.get(match.uid, [])
group_names = [f"{g.name} (depth={g.depth})" for g in groups]
print(f" - {match.name} ({match.type}, domain={match.domain_name}) -> groups={group_names}")
# 2. Progressive-disclosure metadata via describe_objects
print(
"\n[Step 3/4] Inspecting cached schema & aggregating objects (describe_objects / query_objects)...",
flush=True,
)
desc = await client.describe_objects(domain_name=target_domain)
print(f"Cached summary: {desc['total_objects']} objects, {desc['total_services']} services")
print(f" Object types: {desc['object_types']}")
print(f" Groupable fields: {desc['groupable_fields']}")
# 3. Structured cross-domain aggregation via query_objects
grouped = await client.query_objects(
domain_name=target_domain,
group_by=["domain_name", "type"],
aggregate="count",
)
print(f"\nGrouped counts (total={grouped['total']}):")
for row in grouped.get("groups", [])[:10]:
print(f" - {row['domain_name']} / {row['type']}: {row['count']}")
# 4. Inspect domain_sync_state freshness via get_cache_status
print("\n[Step 4/4] Checking cache freshness (get_cache_status)...", flush=True)
status = await client.get_cache_status(domain_name=target_domain)
print(f"Cache status entries ({status['total_entries']}):")
for entry in status["entries"][:5]:
print(
f" - [{entry['scope']}:{entry['item_type']}] {entry['mgmt_name']}:{entry['domain_name']} "
f"status={entry['status']} count={entry['object_count']} age={entry['cache_age_seconds']}s"
)
print(f"\nCompleted in {time.perf_counter() - t0:.2f}s")
if __name__ == "__main__":
asyncio.run(main())
03 — Gateway & VSX Topology (03_gateway_relationships.py)
Demonstrates querying cached gateways, clusters, cluster members, and VSX virtual systems (client.get_gateways()) and traversing both UID (id, parent_uid, member_uids, virtual_system_uids) and human-readable composite identifiers (asset_id, parent_id, parent_asset_id, member_asset_ids, virtual_system_asset_ids, including cross-domain VSX links), along with interfaces and network_security_blades.
Source Code (examples/03_gateway_relationships.py)
"""Example 03 — Gateway, cluster, and VSX asset topology (`get_gateways`, `refresh_assets`).
Run:
uv run examples/03_gateway_relationships.py
Demonstrates how `AssetRefreshService` links cluster members and VSX virtual systems to their
parent cluster or VSX gateway via `parent_uid`, `parent_name`, `member_uids`, and
`virtual_system_uids`.
"""
from __future__ import annotations
import asyncio
import os
import time
from collections import defaultdict
from dotenv import load_dotenv
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
from arodonata import ArodonataClient, ArodonataSettings, Asset # noqa: E402
async def main() -> None:
"""Inspect standalone gateways, clusters, cluster members, and VSX systems."""
settings = ArodonataSettings()
target_domain = os.getenv("EXAMPLE_DOMAIN")
t0 = time.perf_counter()
async with ArodonataClient(settings=settings) as client:
print(
f"[Step 1/2] Fetching gateway/cluster/VSX assets for domain={target_domain!r} (get_gateways)...", flush=True
)
gateways = await client.get_gateways(domain=target_domain, cache_mode="smart")
print("[Step 2/2] Resolving parent-child cluster & VSX topology...", flush=True)
by_uid: dict[str, Asset] = {gw.uid: gw for gw in gateways}
members_by_parent: dict[str, list[Asset]] = defaultdict(list)
standalone: list[Asset] = []
for gw in gateways:
if gw.parent_uid:
members_by_parent[gw.parent_uid].append(gw)
elif not gw.member_uids and not gw.virtual_system_uids:
standalone.append(gw)
print(f"Total cached gateway/server assets: {len(gateways)}")
print(f"Standalone gateways/servers: {len(standalone)}")
for gw in standalone[:5]:
print(f" - {gw.name} ({gw.type}, ip={gw.ipv4_address}, domain={gw.domain_name})")
print(f"\nClusters / VSX parents with linked members: {len(members_by_parent)}")
for parent_uid, members in list(members_by_parent.items())[:5]:
parent = by_uid.get(parent_uid)
parent_label = f"{parent.name} ({parent.type})" if parent else parent_uid
member_names = ", ".join(f"{m.name} ({m.ipv4_address or 'no-ip'})" for m in members)
print(f" - {parent_label}: {member_names}")
print(f"\nCompleted in {time.perf_counter() - t0:.2f}s")
if __name__ == "__main__":
asyncio.run(main())
04 — Smart & Streaming Refresh (04_smart_refresh.py)
Demonstrates paired streaming (stream_refresh_*) and batch (refresh_*) cache synchronization across assets, objects, services, and rulebases, including SSE conversion via arodonata.adapters.sse.to_sse_event.
Source Code (examples/04_smart_refresh.py)
"""Example 04 — Paired batch & streaming cache synchronization (`refresh_*` and `stream_refresh_*`).
Run:
uv run examples/04_smart_refresh.py
Run this script first against a persistent SQLite or PostgreSQL database (`DATABASE_URL`) to
warm the cache across all discovered domains. Demonstrates both streaming (`stream_refresh_*`)
and awaitable (`refresh_*`) synchronization methods and optional SSE wire formatting.
"""
from __future__ import annotations
import asyncio
import os
import time
from dotenv import load_dotenv
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
from arodonata import ArodonataClient, ArodonataSettings # noqa: E402
from arodonata.adapters.sse import to_sse_event # noqa: E402
async def main() -> None:
"""Stream progress events while refreshing assets, objects, services, and rulebases."""
settings = ArodonataSettings()
target_domains = [os.environ["EXAMPLE_DOMAIN"]] if os.getenv("EXAMPLE_DOMAIN") else None
t0 = time.perf_counter()
async with ArodonataClient(settings=settings) as client:
print("[Step 1/4] Streaming asset synchronization (gateways, clusters, VSX)...", flush=True)
async for ev in client.stream_refresh_assets(domains=target_domains, mode="smart"):
if ev.phase in ("started", "completed", "skipped", "failed"):
print(f" [{ev.phase}] {ev.message or f'{ev.mgmt_name}:{ev.domain} ({ev.item_type})'}")
print("\n[Step 2/4] Streaming network object synchronization (hosts, networks, ranges, groups)...", flush=True)
async for ev in client.stream_refresh_objects(domains=target_domains, mode="smart"):
if ev.phase in ("started", "completed", "skipped", "failed"):
print(f" [{ev.phase}] {ev.message or f'{ev.mgmt_name}:{ev.domain} ({ev.item_type})'}")
# Example of converting a domain event to an SSE frame for web endpoints:
_sse_frame = to_sse_event(ev).encode()
print("\n[Step 3/4] Batch refreshing services...", flush=True)
svc_outcome = await client.refresh_services(domains=target_domains, mode="smart")
print(
f" Services sync: refreshed={len(svc_outcome.refreshed_domains)}, "
f"skipped={len(svc_outcome.skipped_domains)}, items={svc_outcome.total_items}"
)
print("\n[Step 4/4] Batch refreshing policy rulebases...", flush=True)
rb_outcome = await client.refresh_rulebases(domains=target_domains, mode="smart")
print(
f" Rulebases sync: refreshed={len(rb_outcome.refreshed_domains)}, "
f"skipped={len(rb_outcome.skipped_domains)}, items={rb_outcome.total_items}"
)
print(f"\nCache warm-up completed in {time.perf_counter() - t0:.2f}s")
if __name__ == "__main__":
asyncio.run(main())
05 — Rulebase Queries (05_rulebase_queries.py)
Demonstrates policy package discovery (client.get_policy_packages()), hierarchical SmartConsole numbering (client.get_package_rulebase(), client.get_layer_rulebase()), reverse UID lookup (client.locate_rules()), and local rulebase filtering (client.filter_rulebase(), including output_format="cpcrud").
Source Code (examples/05_rulebase_queries.py)
"""Example 05 — Rulebase introspection, hierarchical SmartConsole numbering, and local filtering.
Run:
uv run examples/05_rulebase_queries.py
Demonstrates:
1. `client.get_policy_packages(mgmt_name, domain)` — list cached policy packages and layers.
2. `client.get_package_rulebase(mgmt_name, domain, package)` — hierarchical SmartConsole rule
numbers (`1`, `2.1`, `2.2.1`) across ordered Global/Domain layers and inline sub-layers.
3. `client.locate_rules(mgmt_name, domain, rule_uids=[...])` — reverse lookup from rule UID to
SmartConsole rule numbers across packages.
4. `client.filter_rulebase(...)` — local rulebase query evaluation (`src:`, `dst:`, `svc:`,
`action:`, free text, CIDR containment) rendered in `markdown` or `model_friendly` format.
"""
from __future__ import annotations
import asyncio
import os
import time
from dotenv import load_dotenv
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
from arodonata import ArodonataClient, ArodonataSettings # noqa: E402
async def main() -> None:
"""Inspect policy packages, numbered rulebases, rule locations, and local rule filtering."""
settings = ArodonataSettings()
t0 = time.perf_counter()
async with ArodonataClient(settings=settings) as client:
print("[Step 1/5] Resolving target domain (get_domains)...", flush=True)
domains = await client.get_domains()
if not domains:
print("No domains discovered.")
return
dom = next((d for d in domains if d.name == os.getenv("EXAMPLE_DOMAIN")), domains[0])
mgmt_name = dom.mgmt_name
domain_name = dom.name
print(f"[Step 2/5] Ensuring rulebase cache for {mgmt_name}:{domain_name} (refresh_rulebases)...", flush=True)
await client.refresh_rulebases(mgmt_name=mgmt_name, domains=[domain_name], mode="smart")
print(f"[Step 3/5] Listing policy packages in {domain_name} (get_policy_packages)...", flush=True)
packages = await client.get_policy_packages(mgmt_name, domain_name, cache_mode="cache")
if not packages:
print(f"No policy packages cached in {domain_name}.")
return
pkg = packages[0]
print(f"\nPolicy Package: {pkg.name} (uid={pkg.uid})")
print(f" Access layers: {[lay.layer_name for lay in pkg.access_layers]}")
print(
f"\n[Step 4/5] Computing hierarchical SmartConsole rule numbers for {pkg.name!r} (get_package_rulebase)...",
flush=True,
)
pkg_rb = await client.get_package_rulebase(
mgmt_name,
domain_name,
pkg.name,
rulebase_type="access",
cache_mode="cache",
)
for layer in pkg_rb.layers:
print(f"\nLayer {layer.layer_name!r} (prefix={layer.prefix!r}, rules={len(layer.rules)}):")
for nr in layer.rules[:5]:
print(f" #{nr.rule_number:<6} {nr.name or '(unnamed)':<28} action={nr.action} enabled={nr.enabled}")
access_rules = await client.get_access_rules(mgmt_name, domain_name)
if access_rules:
sample = access_rules[0]
locs = await client.locate_rules(
mgmt_name,
domain_name,
rule_uids=[sample.uid],
cache_mode="cache",
)
print(f"\nLocated rule UID {sample.uid}: {locs.rule_locations.get(sample.uid)}")
print("\n[Step 5/5] Filtering rulebase locally (filter_rulebase)...", flush=True)
filtered = await client.filter_rulebase(
mgmt_name,
domain_name,
package=pkg.name,
rulebase_type="access",
filter_query=os.getenv("EXAMPLE_RULE_FILTER", "action:Accept"),
output_format="markdown",
cache_mode="cache",
)
print(f"\nFiltered rulebase ({filtered.matched_count}/{filtered.total_rules} matching rules):")
print("\n".join(filtered.rendered_text.splitlines()[:15]))
print(f"\nCompleted in {time.perf_counter() - t0:.2f}s")
if __name__ == "__main__":
asyncio.run(main())
06 — Session Lifecycle Basics (06_session_basics.py)
Demonstrates low-level Check Point session lifecycle operations (create_dedicated_session, add-host, publish, and crash-safe revert-to-revision via _session_guard.py).
Source Code (examples/06_session_basics.py)
"""Example 06 — Check Point session lifecycle basics: baseline, dedicated session, publish, and revert.
Run:
uv run examples/06_session_basics.py
Demonstrates:
1. Reading the domain's current baseline revision (`show-last-published-session`).
2. Opening an isolated write session via `client.create_dedicated_session(mgmt_name, domain)`.
3. Creating a test host (`add-host`) and publishing (`publish` with `wait_for_task=True`).
4. Verifying that `show-last-published-session` moved to the newly published session UID.
5. Reverting the domain back to the saved baseline revision (`revert-to-revision`) via
`_session_guard.guarded_session`.
"""
from __future__ import annotations
import asyncio
import os
import sys
import uuid
from pathlib import Path
from typing import Any
from dotenv import load_dotenv
load_dotenv(".env.test", override=True)
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
sys.path.insert(0, str(Path(__file__).parent))
from _session_guard import guarded_session # noqa: E402
from arodonata import ArodonataClient, ArodonataSettings # noqa: E402
def _extract_iso_time(time_field: Any) -> str:
if isinstance(time_field, dict):
return str(time_field.get("iso-8601") or time_field)
return str(time_field or "unknown")
async def main() -> None:
"""Run the baseline -> create_dedicated_session -> add-host -> publish -> revert cycle."""
settings = ArodonataSettings()
mgmt_name = settings.mgmt_names[0]
domain = os.getenv("EXAMPLE_DOMAIN") or os.getenv("TEST_DOMAIN_A", "Domain4")
host_name = f"example-session-host-{uuid.uuid4().hex[:6]}"
async with ArodonataClient(settings=settings) as client:
async with guarded_session(client, mgmt_name, domains=[domain]) as baseline:
base_rev = baseline[domain]
print(f"Step 1: Captured baseline on {mgmt_name}:{domain}:")
print(f" UID: {base_rev['uid']}")
print(f" Name: {base_rev['name']}")
print(f" Time: {_extract_iso_time(base_rev['publish_time'])}")
print(f"\nStep 2: Opening dedicated write session and creating host {host_name!r}...")
sid, server_ip = await client.create_dedicated_session(
mgmt_name,
domain,
session_name=f"example-06-{host_name[-6:]}",
session_description="Example 06 dedicated write session",
)
try:
add_res = await client.api_call(
mgmt_name,
"add-host",
{
"name": host_name,
"ipv4-address": "198.51.100.240",
"comments": "Created by examples/06_session_basics.py",
},
domain=domain,
sid=sid,
server_ip=server_ip,
)
assert add_res.success, f"add-host failed: {add_res.error_message}"
print(f" Created host {host_name} (uid={add_res.data.get('uid')})")
print("\nStep 3: Publishing session...")
pub_res = await client.api_call(
mgmt_name,
"publish",
{},
domain=domain,
sid=sid,
server_ip=server_ip,
wait_for_task=True,
)
assert pub_res.success, f"publish failed: {pub_res.error_message}"
print(" Published successfully.")
finally:
await client.logout_sid(mgmt_name, sid, server_ip=server_ip)
print("\nStep 4: Verifying show-last-published-session advanced...")
after_res = await client.api_call(mgmt_name, "show-last-published-session", {}, domain=domain)
assert after_res.success
new_uid = after_res.data.get("uid")
print(f" Previous UID: {base_rev['uid']}")
print(f" Current UID: {new_uid}")
assert new_uid != base_rev["uid"]
print("\nStep 5: Exiting guarded_session will automatically revert domain to baseline...")
final_res = await client.api_call(mgmt_name, "show-last-published-session", {}, domain=domain)
print(f"Step 6: Verified post-revert UID: {final_res.data.get('uid')}")
if __name__ == "__main__":
asyncio.run(main())
07 — Contexts & Session Overlay (07_context_and_overlay.py)
Demonstrates runtime auth_context, user_context, and isolated write_session with SessionOverlay read-your-own-writes isolation (verifying that uncommitted objects appear immediately in get_hosts(), find_object(), and search_objects() before discarding cleanly on exit).
Source Code (examples/07_context_and_overlay.py)
"""Example 07 — Dynamic `auth_context`, `user_context`, and `write_session` with `SessionOverlay`.
Run:
uv run examples/07_context_and_overlay.py
Demonstrates Arodonata v2's context-aware multi-user and transactional features:
1. `with client.auth_context(username=..., password=...)` — dynamically overrides credentials
per asyncio task without mutating client settings.
2. `with client.user_context(username=..., session_name=..., session_description=...)` —
propagates operator attribution and Check Point session naming into `write_session` and logs.
3. `async with client.write_session(mgmt_name, domain, publish_on_exit=False)` — opens an
isolated `WriteSession` backed by `SessionOverlay` so uncommitted `add_object` / `set_object` /
`delete_object` mutations are immediately visible to `get_hosts`, `find_object`, and
`search_objects` inside the context ("read-your-own-writes") while remaining invisible to
other readers and the shared SQLite cache until published.
"""
from __future__ import annotations
import asyncio
import os
import uuid
from dotenv import load_dotenv
load_dotenv(".env.test", override=True)
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
from arodonata import ArodonataClient, ArodonataSettings # noqa: E402
async def main() -> None:
"""Demonstrate auth_context, user_context, and WriteSession SessionOverlay isolation."""
settings = ArodonataSettings()
mgmt_name = settings.mgmt_names[0]
domain = os.getenv("EXAMPLE_DOMAIN") or os.getenv("TEST_DOMAIN_A", "Domain4")
tag = uuid.uuid4().hex[:6]
temp_host = f"overlay-demo-host-{tag}"
async with ArodonataClient(settings=settings) as client:
# Warm the host cache for the target domain
await client.refresh_objects(mgmt_name=mgmt_name, domains=[domain], types=["host"], mode="smart")
eng_password = os.getenv("USER_eng1") or os.getenv("USER_Eng1") or os.getenv("USER_admin", "") # noqa: SIM112
eng_user = "eng1" if (os.getenv("USER_eng1") or os.getenv("USER_Eng1")) else "admin" # noqa: SIM112
print(f"1. Entering auth_context(username={eng_user!r}) + user_context(session_name=' Ticket-700 ')...")
with (
client.auth_context(username=eng_user, password=eng_password),
client.user_context(
user_id=eng_user,
username=eng_user,
session_name=f"CHG-Overlay-{tag}",
session_description="Read-your-own-writes SessionOverlay demo",
),
):
# Use publish_on_exit=False and discard explicitly so the lab is never mutated
async with client.write_session(
mgmt_name,
domain,
publish_on_exit=False,
) as ws:
print(f"2. Opened WriteSession (session_name={ws.session_name!r}, server_ip={ws.server_ip})")
add_res = await client.add_object(
mgmt_name,
"host",
{
"name": temp_host,
"ipv4-address": "198.51.100.241",
"comments": "Uncommitted host in SessionOverlay",
},
domain=domain,
)
assert add_res.success, add_res.error_message
print(f"3. Added uncommitted host {temp_host!r} inside WriteSession.")
# Read-your-own-writes: find_object and search_objects see the uncommitted host!
found = await client.find_object(mgmt_name, domain, temp_host, obj_type="host", cache_mode="cache")
assert found is not None
print(f"4. Inside WriteSession, find_object({temp_host!r}) -> {found.name} ({found.ipv4_address})")
search_res = await client.search_objects(
"198.51.100.241",
mgmt_name=mgmt_name,
domains=[domain],
cache_mode="cache",
)
print(
f"5. Inside WriteSession, search_objects('198.51.100.241') matched: {[m.name for m in search_res.matches]}"
)
# Discard uncommitted changes before exiting so nothing is published
disc = await client.api_call(
mgmt_name,
"discard",
{},
domain=domain,
sid=ws.sid,
server_ip=ws.server_ip,
)
assert disc.success
ws.discarded = True
print("6. Discarded WriteSession cleanly without publishing.")
# Outside the WriteSession, the discarded host is not in the cache
outside = await client.find_object(mgmt_name, domain, temp_host, obj_type="host", cache_mode="cache")
print(f"7. Outside WriteSession, find_object({temp_host!r}) -> {outside}")
if __name__ == "__main__":
asyncio.run(main())
08 — CPCRUD Operations (08_cpcrud_operations.py)
Demonstrates offline JSON Schema validation (client.cpcrud.validate()), deterministic planning (client.cpcrud.plan()), and idempotent execution (client.cpcrud.apply()) using examples/crud_example.yaml.
Declarative Template (examples/crud_example.yaml)
# CPCRUD v2 example template: hosts, networks, TCP service, group, access rule, and NAT rule.
#
# Validated offline against `checkpoint_ops_schema.json` via `client.cpcrud.validate()`.
# Adjust `mgmt_name`, `domain`, `layer`, and `package` to match your target environment.
version: 1
mgmt_name: "mdsH5b"
domain: "Domain4"
on_name_conflict: "update"
on_ip_conflict: "reuse"
objects:
- type: "host"
name: "cpcrud-example-host-1"
ipv4-address: "198.51.100.10"
comments: "CPCRUD example host"
color: "dark green"
- type: "network"
name: "cpcrud-example-net-1"
subnet4: "198.51.100.0"
mask-length4: 24
comments: "CPCRUD example network"
color: "blue"
services:
- type: "service-tcp"
name: "cpcrud-example-tcp-8443"
port: "8443"
comments: "CPCRUD example custom HTTPS port"
groups:
- type: "group"
name: "cpcrud-example-group"
members:
- "cpcrud-example-host-1"
- "cpcrud-example-net-1"
comments: "CPCRUD example group"
rules:
- type: "access-rule"
layer: "FPCR_UAT_Active Network"
position: "bottom"
name: "cpcrud-example-access-rule"
source:
- "cpcrud-example-host-1"
destination:
- "Any"
service:
- "cpcrud-example-tcp-8443"
rule_action: "Drop"
comments: "CPCRUD example access rule"
- type: "nat-rule"
package: "FPCR_UAT_Active"
position: "top"
name: "cpcrud-example-nat-rule"
method: "hide"
original-source: "cpcrud-example-host-1"
original-destination: "Any"
original-service: "Any"
translated-source: "cpcrud-example-host-1"
translated-destination: "Original"
translated-service: "Original"
comments: "CPCRUD example NAT rule"
Source Code (examples/08_cpcrud_operations.py)
"""Example 08 — Declarative CPCRUD operations (`validate`, `plan`, `apply`, and idempotency).
Run:
uv run examples/08_cpcrud_operations.py
Demonstrates:
1. Loading `examples/crud_example.yaml` and validating it offline (`client.cpcrud.validate`).
2. Planning (`await client.cpcrud.plan`) and applying (`await client.cpcrud.apply`) the template.
3. Re-planning and re-applying the exact same template to prove idempotency (Pass 2 produces
`unchanged` / `reuse` outcomes and zero `create` actions).
4. Wrapping execution in `_session_guard.guarded_session` so the lab domain is automatically
reverted to its pre-run baseline revision on exit.
"""
from __future__ import annotations
import asyncio
import logging
import os
import sys
from pathlib import Path
import yaml
from dotenv import load_dotenv
load_dotenv(".env.test", override=True)
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
logging.basicConfig(level=logging.WARNING)
sys.path.insert(0, str(Path(__file__).parent))
from _session_guard import guarded_session # noqa: E402
from arodonata import ArodonataClient, ArodonataSettings # noqa: E402
YAML_FILE = Path(__file__).parent / "crud_example.yaml"
async def main() -> None:
"""Validate, plan, and apply `examples/crud_example.yaml` twice to demonstrate idempotency."""
settings = ArodonataSettings()
mgmt_name = settings.mgmt_names[0]
domain = os.getenv("EXAMPLE_DOMAIN") or os.getenv("TEST_DOMAIN_A", "Domain4")
raw_template = yaml.safe_load(YAML_FILE.read_text(encoding="utf-8"))
raw_template["mgmt_name"] = mgmt_name
raw_template["domain"] = domain
async with ArodonataClient(settings=settings) as client:
# 1. Offline JSON Schema validation (zero network calls)
print("[Step 1/4] Validating template against offline CPCRUD JSON Schema...", flush=True)
errors = client.cpcrud.validate(raw_template)
assert not errors, f"Template validation failed: {errors}"
print("Template passed offline JSON Schema validation.")
print(f"[Step 2/4] Capturing baseline revision on {mgmt_name}:{domain}...", flush=True)
async with guarded_session(client, mgmt_name, domains=[domain]):
for run in (1, 2):
step_num = run + 1
print(
f"[Step {step_num}/4] Planning and applying CPCRUD template (Run {run}/2)...",
flush=True,
)
plan = await client.cpcrud.plan(raw_template)
print(f"\n--- Run {run}: Planned {len(plan.actions)} action(s) ---")
for act in plan.actions:
print(
f" plan: [{act.mgmt_name}:{act.domain_name}] {act.outcome.value:9} {act.object_type} {act.name}"
)
report = await client.cpcrud.apply(
plan,
session_name=f"cpcrud-example-run-{run}",
)
print(
f"Run {run} apply report: success={report.success} "
f"created={report.created_count} updated={report.updated_count} "
f"unchanged={report.unchanged_count} failed={report.failed_count}"
)
for res in report.results:
print(
f" apply: [{res.mgmt_name}:{res.domain_name}] {res.outcome.value:9} {res.object_type} {res.name}"
)
print(f"\nReverting {mgmt_name}:{domain} to baseline revision...", flush=True)
if __name__ == "__main__":
asyncio.run(main())
09 — CPCRUD Inverse Rollback (09_cpcrud_inverse.py)
Demonstrates generating a compensating rollback template via client.cpcrud.inverse(plan, report) and applying it to restore the domain state.
Source Code (examples/09_cpcrud_inverse.py)
"""Example 09 — CPCRUD compensating rollback template (`plan` -> `apply` -> `inverse` -> `apply`).
Run:
uv run examples/09_cpcrud_inverse.py
Demonstrates how `client.cpcrud.inverse(plan, report)` builds a schema-valid compensating
template from the original `Plan` and `ApplyReport`:
- Created objects/services/groups/rules become `delete` operations keyed by `uid`.
- Updated objects/rules roll back to their captured `prior_state`.
- Deleted objects/rules are re-created from their captured `prior_state`.
"""
from __future__ import annotations
import asyncio
import logging
import os
import sys
from pathlib import Path
from typing import Any
from dotenv import load_dotenv
load_dotenv(".env.test", override=True)
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
logging.basicConfig(level=logging.WARNING)
sys.path.insert(0, str(Path(__file__).parent))
from _session_guard import guarded_session # noqa: E402
from arodonata import ArodonataClient, ArodonataSettings # noqa: E402
async def main() -> None:
"""Plan, apply, generate an inverse rollback template, and apply the rollback."""
settings = ArodonataSettings()
mgmt_name = settings.mgmt_names[0]
domain = os.getenv("EXAMPLE_DOMAIN") or os.getenv("TEST_DOMAIN_A", "Domain4")
template: dict[str, Any] = {
"version": 1,
"mgmt_name": mgmt_name,
"domain": domain,
"objects": [
{
"type": "host",
"name": "cpcrud-example-inverse-host",
"ipv4-address": "198.51.100.19",
"comments": "Created for CPCRUD inverse rollback demo",
}
],
}
print(f"[Step 1/4] Capturing baseline revision on {mgmt_name}:{domain}...", flush=True)
async with ArodonataClient(settings=settings) as client, guarded_session(client, mgmt_name, domains=[domain]):
# 1. Plan ONCE and keep the Plan instance for inverse()
print("[Step 2/4] Planning and applying forward CPCRUD template...", flush=True)
plan = await client.cpcrud.plan(template)
report = await client.cpcrud.apply(plan, session_name="cpcrud-inverse-forward")
print(f"Forward apply: success={report.success} created={report.created_count}")
for r in report.results:
print(f" forward: [{r.mgmt_name}:{r.domain_name}] {r.outcome.value:9} {r.object_type} {r.name}")
# 2. Generate compensating inverse template from (plan, report)
print("\n[Step 3/4] Generating and validating compensating inverse template...", flush=True)
inverse_template = client.cpcrud.inverse(plan, report)
val_errors = client.cpcrud.validate(inverse_template)
assert not val_errors, f"Inverse template failed schema validation: {val_errors}"
# 3. Plan and apply the compensating template
print("[Step 4/4] Planning and applying inverse rollback template...", flush=True)
inv_plan = await client.cpcrud.plan(inverse_template)
inv_report = await client.cpcrud.apply(inv_plan, session_name="cpcrud-inverse-rollback")
print(f"\nInverse rollback: success={inv_report.success} deleted={inv_report.deleted_count}")
for r in inv_report.results:
print(f" inverse: [{r.mgmt_name}:{r.domain_name}] {r.outcome.value:9} {r.object_type} {r.name}")
print(f"\nVerifying/reverting {mgmt_name}:{domain} baseline on exit...", flush=True)
if __name__ == "__main__":
asyncio.run(main())
10 — Change Report Evidence (10_change_report_evidence.py)
Demonstrates collecting and rendering auditable ChangeReport evidence (HTML, Markdown, JSON), comparing live unpublished rulebase numbering (OwnedSession) against published cached numbering.
Source Code (examples/10_change_report_evidence.py)
"""Example 10 — Auditable change report evidence (`OwnedSession` pending vs published HTML/Markdown/JSON).
Run:
ARODONATA_LAB=home uv run examples/10_change_report_evidence.py
Demonstrates:
1. Creating a baseline setup session and a change session in a lab domain (`Domain5` by default).
2. Collecting and rendering a `ChangeReport` while the session is still unpublished using
`OwnedSession(sid=..., server_ip=...)` (`numbering_source == "owned_session"`).
3. Publishing the session and rendering the final auditable bundle (`html`, `markdown`, `json`)
to `examples/_tmp/`.
4. Reverting the domain to its original baseline via `_session_guard.guarded_session`.
"""
from __future__ import annotations
import asyncio
import logging
import os
import sys
import uuid
from pathlib import Path
from dotenv import load_dotenv
load_dotenv(".env.test", override=True)
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
logging.basicConfig(level=logging.WARNING)
sys.path.insert(0, str(Path(__file__).parent))
from _session_guard import guarded_session # noqa: E402
from arodonata import ArodonataClient, ArodonataSettings # noqa: E402
from arodonata.reports import ( # noqa: E402
OwnedSession,
RenderedReportBundle,
RenderOptions,
SessionScope,
)
OUT_DIR = Path(__file__).parent / "_tmp"
def _write_bundle(bundle: RenderedReportBundle, prefix: str) -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
if bundle.html is not None:
(OUT_DIR / f"{prefix}.html").write_bytes(bundle.html)
if bundle.markdown is not None:
(OUT_DIR / f"{prefix}.md").write_text(bundle.markdown, encoding="utf-8")
if bundle.json_text is not None:
(OUT_DIR / f"{prefix}.json").write_text(bundle.json_text, encoding="utf-8")
for sess in bundle.report.sessions:
print(
f"{prefix}: session={sess.session_name!r} state={sess.state} "
f"numbering_source={sess.numbering_source} "
f"rules={len(sess.rule_changes)} objects={len(sess.object_changes)}"
)
print(f" -> wrote {OUT_DIR / prefix}.{{html,md,json}}")
async def main() -> None:
"""Create a sample firewall change, generate pending and published evidence reports, and revert."""
settings = ArodonataSettings()
mgmt_name = settings.mgmt_names[0]
domain = os.getenv("TEST_DOMAIN_B", "Domain5")
tag = uuid.uuid4().hex[:6]
options = RenderOptions(
title="Evidence: **RITM-EXAMPLE** — *Firewall Policy Change*",
header_fields={"RITM": "RITM-EXAMPLE", "Requester": "secops"},
)
print(f"[Step 1/5] Capturing baseline revision on {mgmt_name}:{domain}...", flush=True)
async with ArodonataClient(settings=settings) as client, guarded_session(client, mgmt_name, domains=[domain]):
print(f"[Step 2/5] Refreshing rulebase cache for {mgmt_name}:{domain}...", flush=True)
await client.refresh_rulebases(mgmt_name=mgmt_name, domains=[domain], mode="force")
packages = await client.get_policy_packages(mgmt_name, domain, cache_mode="cache")
if not packages or not packages[0].access_layers:
print(f"No policy package/access layer found in {domain}.")
return
layer_name = next(
(lay.layer_name for lay in packages[0].access_layers if lay.layer_name != "Global"),
packages[0].access_layers[0].layer_name,
)
print(
f"[Step 3/5] Opening dedicated session on {mgmt_name}:{domain} and staging host + rule in {layer_name!r}...",
flush=True,
)
sid, server_ip = await client.create_dedicated_session(
mgmt_name,
domain,
session_name=f"ritm-example-{tag}",
session_description="Change report evidence example",
)
try:
s_info = await client.api_call(
mgmt_name,
"show-session",
{},
domain=domain,
sid=sid,
server_ip=server_ip,
)
session_uid = str(s_info.data["uid"])
host_name = f"cr-web-{tag}"
rule_name = f"cr-allow-web-{tag}"
await client.api_call(
mgmt_name,
"add-host",
{"name": host_name, "ipv4-address": "198.51.100.245", "comments": "DMZ web server"},
domain=domain,
sid=sid,
server_ip=server_ip,
)
await client.api_call(
mgmt_name,
"add-access-rule",
{
"layer": layer_name,
"position": "top",
"name": rule_name,
"source": "Any",
"destination": host_name,
"service": "https",
"action": "Accept",
},
domain=domain,
sid=sid,
server_ip=server_ip,
)
# 1. Pending session evidence numbered live via OwnedSession
print("[Step 4/5] Building pending change report via OwnedSession...", flush=True)
owned = OwnedSession(
sid=sid,
server_ip=server_ip,
session_uid=session_uid,
mgmt_name=mgmt_name,
domain=domain,
)
pending_scope = SessionScope(
mgmt_name=mgmt_name,
domain=domain,
session_uids=[session_uid],
owned_sessions=owned,
)
pending_bundle = await client.build_change_report(
[pending_scope],
formats=("html", "markdown", "json"),
options=options,
)
_write_bundle(pending_bundle, "evidence-1-pending")
# 2. Publish the session and refresh rulebase cache
print("[Step 5/5] Publishing session and building published change report...", flush=True)
await client.api_call(
mgmt_name,
"publish",
{},
domain=domain,
sid=sid,
server_ip=server_ip,
wait_for_task=True,
)
finally:
await client.logout_sid(mgmt_name, sid, server_ip=server_ip)
await client.refresh_rulebases(mgmt_name=mgmt_name, domains=[domain], mode="force")
pub_scope = SessionScope(
mgmt_name=mgmt_name,
domain=domain,
session_uids=[session_uid],
)
pub_bundle = await client.build_change_report(
[pub_scope],
formats=("html", "markdown", "json"),
options=options,
)
_write_bundle(pub_bundle, "evidence-2-published")
print(f"Reverting {mgmt_name}:{domain} to baseline revision...", flush=True)
if __name__ == "__main__":
asyncio.run(main())
11 — OpenTelemetry Tracing (11_otel_tracing.py)
Demonstrates provider-agnostic OpenTelemetry span instrumentation (arlogi.otel.setup_tracing + ARODONATA_TRACE_MODULES) and per-span latency analysis from exported JSONL traces.
Source Code (examples/11_otel_tracing.py)
"""Example 11 — Provider-agnostic OpenTelemetry tracing with per-span timing breakdown.
Run:
uv run examples/11_otel_tracing.py
Arodonata v2 is a provider-agnostic OpenTelemetry span producer (`opentelemetry-api` +
`arlogi.otel.traced`) and never owns or configures a global `TracerProvider`. When an application
or script configures tracing via `arlogi.otel.setup_tracing()`, all `@traced` methods across
`ArodonataClient`, `DomainSyncCoordinator`, `ObjectService`, `AMgmtClient`, and `ApiTransport`
automatically emit nested spans with sanitized attributes (never leaking SIDs or secrets).
"""
from __future__ import annotations
import asyncio
import json
import os
import shutil
import time
from collections.abc import Iterator
from pathlib import Path
from typing import Any
from dotenv import load_dotenv
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
from arlogi.otel import setup_tracing, shutdown_tracing # noqa: E402
from arodonata import ArodonataClient, ArodonataSettings # noqa: E402
from arodonata.core.telemetry import set_trace_modules # noqa: E402
TRACE_DIR = Path(__file__).parent / "_tmp" / "otel_traces"
def _iter_spans(trace_dir: Path) -> Iterator[dict[str, Any]]:
"""Yield every exported span dict across rotated JSONL trace files."""
for file_path in sorted(trace_dir.glob("*.jsonl")):
for line in file_path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
payload = json.loads(line)
for resource_span in payload.get("resourceSpans", []):
for scope_span in resource_span.get("scopeSpans", []):
yield from scope_span.get("spans", [])
def _print_span_summary(trace_dir: Path) -> None:
durations_ms: dict[str, list[float]] = {}
for span in _iter_spans(trace_dir):
start = int(span.get("startTimeUnixNano", 0))
end = int(span.get("endTimeUnixNano", 0))
durations_ms.setdefault(span.get("name", "?"), []).append(max(end - start, 0) / 1e6)
if not durations_ms:
print(" (no spans exported in TRACE_DIR)")
return
print(f"\n{'Span Name':<65} {'Count':>6} {'Total ms':>10} {'Avg ms':>10}")
print("-" * 95)
for name, values in sorted(durations_ms.items(), key=lambda kv: -sum(kv[1])):
print(f" {name:<63} {len(values):>6} {sum(values):>10.1f} {sum(values) / len(values):>10.1f}")
async def main() -> None:
"""Run asset and object refresh with OpenTelemetry JSONL trace export and summarize spans."""
shutil.rmtree(TRACE_DIR, ignore_errors=True)
TRACE_DIR.mkdir(parents=True, exist_ok=True)
setup_tracing(service_name="arodonata-example-11", file_dir=TRACE_DIR)
set_trace_modules({"arodonata": True})
settings = ArodonataSettings(trace_modules="arodonata:on")
target_domains = [os.environ["EXAMPLE_DOMAIN"]] if os.getenv("EXAMPLE_DOMAIN") else None
t0 = time.perf_counter()
try:
async with ArodonataClient(settings=settings) as client:
print("[Step 1/2] Refreshing assets with OpenTelemetry tracing active...", flush=True)
await client.refresh_assets(domains=target_domains, mode="smart")
print("[Step 2/2] Refreshing network objects with OpenTelemetry tracing active...", flush=True)
await client.refresh_objects(domains=target_domains, types=["host", "network"], mode="smart")
finally:
shutdown_tracing()
print(f"\nTotal wall time: {time.perf_counter() - t0:.2f}s")
_print_span_summary(TRACE_DIR)
if __name__ == "__main__":
asyncio.run(main())
12 — Embedded FastAPI MCP Server (12_mcp_embedded.py)
Demonstrates embedding Arodonata's Streamable-HTTP MCP server (create_mcp_server, create_asgi_app) inside a FastAPI application alongside custom host-defined MCP tools.
Source Code (examples/12_mcp_embedded.py)
"""Example 12 — Embedding Arodonata's MCP server inside a FastAPI application (`create_asgi_app`).
Run:
uv run examples/12_mcp_embedded.py
Then connect an MCP client to `http://127.0.0.1:8000/mcp`.
Demonstrates:
1. Creating an `ArodonataClient` and wiring it into `create_mcp_server(client, auth_mode="host")`.
2. Registering custom application-specific tools alongside Arodonata's built-in Check Point tools.
3. Wrapping the MCP server in `create_asgi_app` and mounting it inside a FastAPI lifespan.
"""
from __future__ import annotations
import os
from collections.abc import AsyncIterator, Awaitable, Callable
from contextlib import asynccontextmanager
from dotenv import load_dotenv
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
import uvicorn # noqa: E402
from fastapi import FastAPI, Request, Response # noqa: E402
from fastapi.responses import JSONResponse # noqa: E402
from arodonata import ArodonataClient, ArodonataSettings, user_context # noqa: E402
from arodonata.mcp import ArodonataMCPSettings, create_asgi_app, create_mcp_server # noqa: E402
settings = ArodonataSettings()
client = ArodonataClient(settings=settings)
mcp_settings = ArodonataMCPSettings(
server_name="acme-firewall-assistant",
auth_mode="host",
port=8000,
path="/mcp",
live_compat=True,
cpcrud=True,
)
mcp_server = create_mcp_server(client, mcp_settings=mcp_settings)
@mcp_server.tool(
name="acme_change_ticket",
description="Look up the ACME change ticket that introduced a firewall rule (demo stub).",
)
async def acme_change_ticket(rule_uid: str) -> dict[str, str]:
"""Return a demo change ticket reference for `rule_uid`."""
return {"rule_uid": rule_uid, "ticket": "CHG-2026-0001"}
mcp_app = create_asgi_app(client, mcp_server=mcp_server, mcp_settings=mcp_settings)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
"""Initialize ArodonataClient and the Streamable-HTTP MCP router lifespan."""
async with client, mcp_app.router.lifespan_context(mcp_app.starlette_app):
yield
app = FastAPI(title="ACME Firewall Assistant (Arodonata v2 MCP)", lifespan=lifespan)
EXPECTED_HOST_API_KEY = os.environ.get("ACME_HOST_API_KEY", "acme-host-demo-token-0123456789abcdef")
@app.middleware("http")
async def host_authentication_middleware(
request: Request,
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
"""Enclosing ASGI host authentication middleware required when `auth_mode='host'` (`I18`)."""
if request.url.path == "/healthz":
return await call_next(request)
auth_header = request.headers.get("Authorization", "")
if auth_header != f"Bearer {EXPECTED_HOST_API_KEY}":
return JSONResponse({"detail": "Unauthorized"}, status_code=401)
caller_id = request.headers.get("X-Client-ID", "acme-portal-user")
with user_context(user_id=caller_id, client_id=caller_id):
return await call_next(request)
@app.get("/healthz")
async def healthz() -> dict[str, str]:
"""Health check endpoint."""
return {"status": "ok"}
app.mount("/", mcp_app)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)