From e42061224a80c358d5a7ac83781056577001f0fb Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Tue, 26 May 2026 15:29:09 +0200 Subject: [PATCH 1/4] feat(indonesia): add pii-indonesia category, cross-border audit fields Add PolicyCategory.PII_INDONESIA constant ("pii-indonesia") for Indonesian PII detection (NIK, KK, NPWP, BPJS). Add data_residency and transfer_basis optional fields to AuditLogEntry for UU PDP Art. 56 cross-border data transfer logging. Both fields default to None for backward compatibility. Add Indonesia compliance example demonstrating NIK detection, audit log querying, and policy filtering by pii-indonesia category. Signed-off-by: Saurabh Jain Signed-off-by: Saurabh Jain --- CHANGELOG.md | 16 +++++ axonflow/policies.py | 1 + axonflow/types.py | 2 + examples/indonesia_compliance.py | 88 ++++++++++++++++++++++++++ tests/test_indonesia_pii_audit.py | 101 ++++++++++++++++++++++++++++++ 5 files changed, 208 insertions(+) create mode 100644 examples/indonesia_compliance.py create mode 100644 tests/test_indonesia_pii_audit.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e661301..dd903ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and tag v{X.Y.Z}. The release workflow's preflight checks the section header matches the tag. --> +## [Unreleased] + +### Added + +- **`PolicyCategory.PII_INDONESIA` constant** (`"pii-indonesia"`). + Enables filtering and creating policies for Indonesian PII detection + (NIK, KK, NPWP, BPJS) alongside the existing per-jurisdiction categories. +- **`data_residency` and `transfer_basis` fields on `AuditLogEntry`.** + Optional string fields supporting cross-border data transfer logging. + `data_residency` is an ISO 3166-1 alpha-2 country code; + `transfer_basis` is one of `adequacy`, `safeguards`, or `consent`. + Both default to `None` for backward compatibility with older platform versions. +- **Indonesia compliance example** (`examples/indonesia_compliance.py`): + demonstrates NIK detection, audit log querying with cross-border fields, + and policy filtering by the new `pii-indonesia` category. + ## [8.2.0] - 2026-05-23 — `create_hitl_request` for explicit HITL row creation Enables agent-framework plugins (Google ADK, n8n, OpenAI Agents SDK) to diff --git a/axonflow/policies.py b/axonflow/policies.py index 83e6153..d950a68 100644 --- a/axonflow/policies.py +++ b/axonflow/policies.py @@ -29,6 +29,7 @@ class PolicyCategory(str, Enum): PII_EU = "pii-eu" PII_INDIA = "pii-india" PII_SINGAPORE = "pii-singapore" + PII_INDONESIA = "pii-indonesia" # Static policy categories - Code Governance CODE_SECRETS = "code-secrets" diff --git a/axonflow/types.py b/axonflow/types.py index 772f9b5..6bf60c4 100644 --- a/axonflow/types.py +++ b/axonflow/types.py @@ -827,6 +827,8 @@ class AuditLogEntry(BaseModel): latency_ms: int = Field(default=0, ge=0, description="Latency in ms") policy_violations: list[str] = Field(default_factory=list, description="Violated policies") metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata") + data_residency: str | None = Field(default=None, description="ISO 3166-1 alpha-2 country code for data storage location") + transfer_basis: str | None = Field(default=None, description="Legal basis for cross-border transfer: adequacy, safeguards, or consent") class AuditSearchResponse(BaseModel): diff --git a/examples/indonesia_compliance.py b/examples/indonesia_compliance.py new file mode 100644 index 0000000..b138770 --- /dev/null +++ b/examples/indonesia_compliance.py @@ -0,0 +1,88 @@ +"""Indonesia Compliance Example. + +Demonstrates Indonesian PII detection (NIK), audit log querying with +cross-border data transfer fields, and policy filtering by the +pii-indonesia category. + +Requirements: + pip install axonflow + export AXONFLOW_AGENT_URL=http://localhost:8080 + export AXONFLOW_CLIENT_ID=your-client-id + export AXONFLOW_CLIENT_SECRET=your-client-secret +""" + +from __future__ import annotations + +import asyncio +import os + +from axonflow import AxonFlow +from axonflow.policies import PolicyCategory + + +async def main() -> None: + agent_url = os.environ.get("AXONFLOW_AGENT_URL", "http://localhost:8080") + client_id = os.environ.get("AXONFLOW_CLIENT_ID", "") + client_secret = os.environ.get("AXONFLOW_CLIENT_SECRET", "") + + if not client_id or not client_secret: + raise SystemExit("AXONFLOW_CLIENT_ID and AXONFLOW_CLIENT_SECRET must be set") + + client = AxonFlow( + agent_url=agent_url, + client_id=client_id, + client_secret=client_secret, + ) + + print("=== Indonesia Compliance Example ===\n") + + # 1. Verify PII Indonesia category constant + print(f"PII Indonesia category: {PolicyCategory.PII_INDONESIA.value}") + + # 2. Send a request containing an Indonesian NIK + print("\nSending governed request with NIK...") + try: + resp = await client.proxy_llm_call( + user_token="", + query="Customer NIK is 3204110507900003 and their name is Budi Santoso", + request_type="chat", + context={"purpose": "identity_verification"}, + ) + print(f"Response blocked: {resp.blocked}") + if resp.policy_info: + print(f"Policies evaluated: {resp.policy_info.policies_evaluated}") + except Exception as e: + print(f"Request error (expected if no LLM configured): {e}") + + # 3. Query audit logs to demonstrate cross-border fields + print("\nQuerying audit logs...") + try: + audit_resp = await client.search_audit_logs(limit=5) + print(f"Found {len(audit_resp.entries)} audit entries") + for entry in audit_resp.entries: + line = f" [{entry.timestamp}] type={entry.request_type} blocked={entry.blocked}" + if entry.data_residency: + line += f" residency={entry.data_residency}" + if entry.transfer_basis: + line += f" basis={entry.transfer_basis}" + print(line) + except Exception as e: + print(f"Audit search error: {e}") + + # 4. List policies filtered by Indonesia PII category + print("\nListing Indonesia PII policies...") + try: + policies = await client.list_static_policies( + category=PolicyCategory.PII_INDONESIA, + ) + print(f"Found {len(policies)} Indonesia PII policies") + for p in policies: + print(f" {p.name}: {p.description} (severity={p.severity}, action={p.action})") + except Exception as e: + print(f"Policy list error: {e}") + + print("\n=== Done ===") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/test_indonesia_pii_audit.py b/tests/test_indonesia_pii_audit.py new file mode 100644 index 0000000..fa40889 --- /dev/null +++ b/tests/test_indonesia_pii_audit.py @@ -0,0 +1,101 @@ +"""Tests for Indonesia PII category constant and cross-border audit fields.""" + +from __future__ import annotations + +from datetime import datetime, timezone + +from axonflow.policies import PolicyCategory +from axonflow.types import AuditLogEntry + + +class TestPIIIndonesiaCategory: + def test_constant_value(self) -> None: + assert PolicyCategory.PII_INDONESIA.value == "pii-indonesia" + + def test_is_valid_enum_member(self) -> None: + assert PolicyCategory("pii-indonesia") is PolicyCategory.PII_INDONESIA + + def test_string_representation(self) -> None: + assert str(PolicyCategory.PII_INDONESIA) == "PolicyCategory.PII_INDONESIA" + + def test_alongside_other_pii_categories(self) -> None: + pii_categories = [ + PolicyCategory.PII_GLOBAL, + PolicyCategory.PII_US, + PolicyCategory.PII_EU, + PolicyCategory.PII_INDIA, + PolicyCategory.PII_SINGAPORE, + PolicyCategory.PII_INDONESIA, + ] + values = [c.value for c in pii_categories] + assert "pii-indonesia" in values + assert len(set(values)) == len(values) + + +class TestAuditLogEntryCrossBorderFields: + def test_fields_populated(self) -> None: + entry = AuditLogEntry( + id="aud-001", + request_id="req-001", + timestamp=datetime(2026, 5, 26, 10, 0, 0, tzinfo=timezone.utc), + user_email="analyst@bank.co.id", + data_residency="ID", + transfer_basis="adequacy", + ) + assert entry.data_residency == "ID" + assert entry.transfer_basis == "adequacy" + + def test_fields_from_dict(self) -> None: + data = { + "id": "aud-002", + "timestamp": "2026-05-26T10:00:00Z", + "data_residency": "ID", + "transfer_basis": "safeguards", + } + entry = AuditLogEntry.model_validate(data) + assert entry.data_residency == "ID" + assert entry.transfer_basis == "safeguards" + + def test_backward_compat_fields_absent(self) -> None: + data = { + "id": "aud-003", + "timestamp": "2026-05-26T10:00:00Z", + "user_email": "user@company.com", + "success": True, + "blocked": False, + } + entry = AuditLogEntry.model_validate(data) + assert entry.data_residency is None + assert entry.transfer_basis is None + + def test_serialization_omits_none(self) -> None: + entry = AuditLogEntry( + id="aud-004", + timestamp=datetime(2026, 5, 26, 10, 0, 0, tzinfo=timezone.utc), + ) + data = entry.model_dump(exclude_none=True) + assert "data_residency" not in data + assert "transfer_basis" not in data + + def test_serialization_includes_when_set(self) -> None: + entry = AuditLogEntry( + id="aud-005", + timestamp=datetime(2026, 5, 26, 10, 0, 0, tzinfo=timezone.utc), + data_residency="SG", + transfer_basis="consent", + ) + data = entry.model_dump() + assert data["data_residency"] == "SG" + assert data["transfer_basis"] == "consent" + + def test_json_round_trip(self) -> None: + entry = AuditLogEntry( + id="aud-006", + timestamp=datetime(2026, 5, 26, 10, 0, 0, tzinfo=timezone.utc), + data_residency="ID", + transfer_basis="adequacy", + ) + json_str = entry.model_dump_json() + restored = AuditLogEntry.model_validate_json(json_str) + assert restored.data_residency == "ID" + assert restored.transfer_basis == "adequacy" From d3d8baf702bdacc421a7f14feba61ffc463e98a9 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Tue, 26 May 2026 15:32:28 +0200 Subject: [PATCH 2/4] =?UTF-8?q?style:=20fix=20ruff=20lint=20=E2=80=94=20li?= =?UTF-8?q?ne=20length,=20exception=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Saurabh Jain Signed-off-by: Saurabh Jain --- axonflow/types.py | 8 ++++++-- examples/indonesia_compliance.py | 10 ++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/axonflow/types.py b/axonflow/types.py index 6bf60c4..9846cb3 100644 --- a/axonflow/types.py +++ b/axonflow/types.py @@ -827,8 +827,12 @@ class AuditLogEntry(BaseModel): latency_ms: int = Field(default=0, ge=0, description="Latency in ms") policy_violations: list[str] = Field(default_factory=list, description="Violated policies") metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata") - data_residency: str | None = Field(default=None, description="ISO 3166-1 alpha-2 country code for data storage location") - transfer_basis: str | None = Field(default=None, description="Legal basis for cross-border transfer: adequacy, safeguards, or consent") + data_residency: str | None = Field( + default=None, description="ISO 3166-1 alpha-2 data residency code" + ) + transfer_basis: str | None = Field( + default=None, description="Cross-border transfer legal basis" + ) class AuditSearchResponse(BaseModel): diff --git a/examples/indonesia_compliance.py b/examples/indonesia_compliance.py index b138770..1a2c90f 100644 --- a/examples/indonesia_compliance.py +++ b/examples/indonesia_compliance.py @@ -17,6 +17,7 @@ import os from axonflow import AxonFlow +from axonflow.exceptions import AxonFlowError from axonflow.policies import PolicyCategory @@ -25,8 +26,9 @@ async def main() -> None: client_id = os.environ.get("AXONFLOW_CLIENT_ID", "") client_secret = os.environ.get("AXONFLOW_CLIENT_SECRET", "") + msg = "AXONFLOW_CLIENT_ID and AXONFLOW_CLIENT_SECRET must be set" if not client_id or not client_secret: - raise SystemExit("AXONFLOW_CLIENT_ID and AXONFLOW_CLIENT_SECRET must be set") + raise SystemExit(msg) client = AxonFlow( agent_url=agent_url, @@ -51,7 +53,7 @@ async def main() -> None: print(f"Response blocked: {resp.blocked}") if resp.policy_info: print(f"Policies evaluated: {resp.policy_info.policies_evaluated}") - except Exception as e: + except AxonFlowError as e: print(f"Request error (expected if no LLM configured): {e}") # 3. Query audit logs to demonstrate cross-border fields @@ -66,7 +68,7 @@ async def main() -> None: if entry.transfer_basis: line += f" basis={entry.transfer_basis}" print(line) - except Exception as e: + except AxonFlowError as e: print(f"Audit search error: {e}") # 4. List policies filtered by Indonesia PII category @@ -78,7 +80,7 @@ async def main() -> None: print(f"Found {len(policies)} Indonesia PII policies") for p in policies: print(f" {p.name}: {p.description} (severity={p.severity}, action={p.action})") - except Exception as e: + except AxonFlowError as e: print(f"Policy list error: {e}") print("\n=== Done ===") From 835cb47ed8cc1650423f24f8484a3ba38dc7f70e Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Tue, 26 May 2026 15:35:28 +0200 Subject: [PATCH 3/4] chore: update wire-shape baseline for cross-border audit fields Signed-off-by: Saurabh Jain Signed-off-by: Saurabh Jain --- tests/fixtures/wire_shape_baseline.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/fixtures/wire_shape_baseline.json b/tests/fixtures/wire_shape_baseline.json index a877b82..3b6683b 100644 --- a/tests/fixtures/wire_shape_baseline.json +++ b/tests/fixtures/wire_shape_baseline.json @@ -182,9 +182,11 @@ "AuditLogEntry": { "note": "spec-bug-pending: #1745 \u2014 agent-api.yaml AuditLogEntry omits metadata/model/policy_violations the agent emits on every audit-log read.", "sdk_only": [ + "data_residency", "metadata", "model", - "policy_violations" + "policy_violations", + "transfer_basis" ], "spec_only": [] }, From e9ec40f2e50a7c8c088b9f50e0b197dd355e336a Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Tue, 26 May 2026 16:31:35 +0200 Subject: [PATCH 4/4] chore(release): bump to v8.3.0 Signed-off-by: Saurabh Jain Signed-off-by: Saurabh Jain --- CHANGELOG.md | 2 +- axonflow/_version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd903ed..9b1ce9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and tag v{X.Y.Z}. The release workflow's preflight checks the section header matches the tag. --> -## [Unreleased] +## [8.3.0] - 2026-05-26 — Indonesia PII category + cross-border audit fields ### Added diff --git a/axonflow/_version.py b/axonflow/_version.py index c42d601..27ace3a 100644 --- a/axonflow/_version.py +++ b/axonflow/_version.py @@ -1,3 +1,3 @@ """Single source of truth for the AxonFlow SDK version.""" -__version__ = "8.2.0" +__version__ = "8.3.0" diff --git a/pyproject.toml b/pyproject.toml index f724eed..4ecc1b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "axonflow" -version = "8.2.0" +version = "8.3.0" description = "AxonFlow Python SDK - Enterprise AI Governance in 3 Lines of Code" readme = "README.md" license = {text = "MIT"}