Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. -->

## [8.3.0] - 2026-05-26 — Indonesia PII category + cross-border audit fields

### 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
Expand Down
2 changes: 1 addition & 1 deletion axonflow/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Single source of truth for the AxonFlow SDK version."""

__version__ = "8.2.0"
__version__ = "8.3.0"
1 change: 1 addition & 0 deletions axonflow/policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions axonflow/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +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 data residency code"
)
transfer_basis: str | None = Field(
default=None, description="Cross-border transfer legal basis"
)


class AuditSearchResponse(BaseModel):
Expand Down
90 changes: 90 additions & 0 deletions examples/indonesia_compliance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""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.exceptions import AxonFlowError
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", "")

msg = "AXONFLOW_CLIENT_ID and AXONFLOW_CLIENT_SECRET must be set"
if not client_id or not client_secret:
raise SystemExit(msg)

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 AxonFlowError 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 AxonFlowError 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 AxonFlowError as e:
print(f"Policy list error: {e}")

print("\n=== Done ===")


if __name__ == "__main__":
asyncio.run(main())
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
4 changes: 3 additions & 1 deletion tests/fixtures/wire_shape_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
},
Expand Down
101 changes: 101 additions & 0 deletions tests/test_indonesia_pii_audit.py
Original file line number Diff line number Diff line change
@@ -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"
Loading