Skip to content

Commit 3158bea

Browse files
feat(indonesia): pii-indonesia + cross-border audit fields [skip-runtime-e2e] (#207)
* 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 <dev@getaxonflow.com> Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com> * style: fix ruff lint — line length, exception types Signed-off-by: Saurabh Jain <dev@getaxonflow.com> Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com> * chore: update wire-shape baseline for cross-border audit fields Signed-off-by: Saurabh Jain <dev@getaxonflow.com> Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com> * chore(release): bump to v8.3.0 Signed-off-by: Saurabh Jain <dev@getaxonflow.com> Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com> --------- Signed-off-by: Saurabh Jain <dev@getaxonflow.com> Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
1 parent 703eea5 commit 3158bea

8 files changed

Lines changed: 219 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
and tag v{X.Y.Z}. The release workflow's preflight checks the section
1010
header matches the tag. -->
1111

12+
## [8.3.0] - 2026-05-26 — Indonesia PII category + cross-border audit fields
13+
14+
### Added
15+
16+
- **`PolicyCategory.PII_INDONESIA` constant** (`"pii-indonesia"`).
17+
Enables filtering and creating policies for Indonesian PII detection
18+
(NIK, KK, NPWP, BPJS) alongside the existing per-jurisdiction categories.
19+
- **`data_residency` and `transfer_basis` fields on `AuditLogEntry`.**
20+
Optional string fields supporting cross-border data transfer logging.
21+
`data_residency` is an ISO 3166-1 alpha-2 country code;
22+
`transfer_basis` is one of `adequacy`, `safeguards`, or `consent`.
23+
Both default to `None` for backward compatibility with older platform versions.
24+
- **Indonesia compliance example** (`examples/indonesia_compliance.py`):
25+
demonstrates NIK detection, audit log querying with cross-border fields,
26+
and policy filtering by the new `pii-indonesia` category.
27+
1228
## [8.2.0] - 2026-05-23 — `create_hitl_request` for explicit HITL row creation
1329

1430
Enables agent-framework plugins (Google ADK, n8n, OpenAI Agents SDK) to

axonflow/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Single source of truth for the AxonFlow SDK version."""
22

3-
__version__ = "8.2.0"
3+
__version__ = "8.3.0"

axonflow/policies.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class PolicyCategory(str, Enum):
2929
PII_EU = "pii-eu"
3030
PII_INDIA = "pii-india"
3131
PII_SINGAPORE = "pii-singapore"
32+
PII_INDONESIA = "pii-indonesia"
3233

3334
# Static policy categories - Code Governance
3435
CODE_SECRETS = "code-secrets"

axonflow/types.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,12 @@ class AuditLogEntry(BaseModel):
827827
latency_ms: int = Field(default=0, ge=0, description="Latency in ms")
828828
policy_violations: list[str] = Field(default_factory=list, description="Violated policies")
829829
metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
830+
data_residency: str | None = Field(
831+
default=None, description="ISO 3166-1 alpha-2 data residency code"
832+
)
833+
transfer_basis: str | None = Field(
834+
default=None, description="Cross-border transfer legal basis"
835+
)
830836

831837

832838
class AuditSearchResponse(BaseModel):

examples/indonesia_compliance.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Indonesia Compliance Example.
2+
3+
Demonstrates Indonesian PII detection (NIK), audit log querying with
4+
cross-border data transfer fields, and policy filtering by the
5+
pii-indonesia category.
6+
7+
Requirements:
8+
pip install axonflow
9+
export AXONFLOW_AGENT_URL=http://localhost:8080
10+
export AXONFLOW_CLIENT_ID=your-client-id
11+
export AXONFLOW_CLIENT_SECRET=your-client-secret
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import asyncio
17+
import os
18+
19+
from axonflow import AxonFlow
20+
from axonflow.exceptions import AxonFlowError
21+
from axonflow.policies import PolicyCategory
22+
23+
24+
async def main() -> None:
25+
agent_url = os.environ.get("AXONFLOW_AGENT_URL", "http://localhost:8080")
26+
client_id = os.environ.get("AXONFLOW_CLIENT_ID", "")
27+
client_secret = os.environ.get("AXONFLOW_CLIENT_SECRET", "")
28+
29+
msg = "AXONFLOW_CLIENT_ID and AXONFLOW_CLIENT_SECRET must be set"
30+
if not client_id or not client_secret:
31+
raise SystemExit(msg)
32+
33+
client = AxonFlow(
34+
agent_url=agent_url,
35+
client_id=client_id,
36+
client_secret=client_secret,
37+
)
38+
39+
print("=== Indonesia Compliance Example ===\n")
40+
41+
# 1. Verify PII Indonesia category constant
42+
print(f"PII Indonesia category: {PolicyCategory.PII_INDONESIA.value}")
43+
44+
# 2. Send a request containing an Indonesian NIK
45+
print("\nSending governed request with NIK...")
46+
try:
47+
resp = await client.proxy_llm_call(
48+
user_token="",
49+
query="Customer NIK is 3204110507900003 and their name is Budi Santoso",
50+
request_type="chat",
51+
context={"purpose": "identity_verification"},
52+
)
53+
print(f"Response blocked: {resp.blocked}")
54+
if resp.policy_info:
55+
print(f"Policies evaluated: {resp.policy_info.policies_evaluated}")
56+
except AxonFlowError as e:
57+
print(f"Request error (expected if no LLM configured): {e}")
58+
59+
# 3. Query audit logs to demonstrate cross-border fields
60+
print("\nQuerying audit logs...")
61+
try:
62+
audit_resp = await client.search_audit_logs(limit=5)
63+
print(f"Found {len(audit_resp.entries)} audit entries")
64+
for entry in audit_resp.entries:
65+
line = f" [{entry.timestamp}] type={entry.request_type} blocked={entry.blocked}"
66+
if entry.data_residency:
67+
line += f" residency={entry.data_residency}"
68+
if entry.transfer_basis:
69+
line += f" basis={entry.transfer_basis}"
70+
print(line)
71+
except AxonFlowError as e:
72+
print(f"Audit search error: {e}")
73+
74+
# 4. List policies filtered by Indonesia PII category
75+
print("\nListing Indonesia PII policies...")
76+
try:
77+
policies = await client.list_static_policies(
78+
category=PolicyCategory.PII_INDONESIA,
79+
)
80+
print(f"Found {len(policies)} Indonesia PII policies")
81+
for p in policies:
82+
print(f" {p.name}: {p.description} (severity={p.severity}, action={p.action})")
83+
except AxonFlowError as e:
84+
print(f"Policy list error: {e}")
85+
86+
print("\n=== Done ===")
87+
88+
89+
if __name__ == "__main__":
90+
asyncio.run(main())

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "axonflow"
7-
version = "8.2.0"
7+
version = "8.3.0"
88
description = "AxonFlow Python SDK - Enterprise AI Governance in 3 Lines of Code"
99
readme = "README.md"
1010
license = {text = "MIT"}

tests/fixtures/wire_shape_baseline.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,11 @@
182182
"AuditLogEntry": {
183183
"note": "spec-bug-pending: #1745 \u2014 agent-api.yaml AuditLogEntry omits metadata/model/policy_violations the agent emits on every audit-log read.",
184184
"sdk_only": [
185+
"data_residency",
185186
"metadata",
186187
"model",
187-
"policy_violations"
188+
"policy_violations",
189+
"transfer_basis"
188190
],
189191
"spec_only": []
190192
},

tests/test_indonesia_pii_audit.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Tests for Indonesia PII category constant and cross-border audit fields."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import datetime, timezone
6+
7+
from axonflow.policies import PolicyCategory
8+
from axonflow.types import AuditLogEntry
9+
10+
11+
class TestPIIIndonesiaCategory:
12+
def test_constant_value(self) -> None:
13+
assert PolicyCategory.PII_INDONESIA.value == "pii-indonesia"
14+
15+
def test_is_valid_enum_member(self) -> None:
16+
assert PolicyCategory("pii-indonesia") is PolicyCategory.PII_INDONESIA
17+
18+
def test_string_representation(self) -> None:
19+
assert str(PolicyCategory.PII_INDONESIA) == "PolicyCategory.PII_INDONESIA"
20+
21+
def test_alongside_other_pii_categories(self) -> None:
22+
pii_categories = [
23+
PolicyCategory.PII_GLOBAL,
24+
PolicyCategory.PII_US,
25+
PolicyCategory.PII_EU,
26+
PolicyCategory.PII_INDIA,
27+
PolicyCategory.PII_SINGAPORE,
28+
PolicyCategory.PII_INDONESIA,
29+
]
30+
values = [c.value for c in pii_categories]
31+
assert "pii-indonesia" in values
32+
assert len(set(values)) == len(values)
33+
34+
35+
class TestAuditLogEntryCrossBorderFields:
36+
def test_fields_populated(self) -> None:
37+
entry = AuditLogEntry(
38+
id="aud-001",
39+
request_id="req-001",
40+
timestamp=datetime(2026, 5, 26, 10, 0, 0, tzinfo=timezone.utc),
41+
user_email="analyst@bank.co.id",
42+
data_residency="ID",
43+
transfer_basis="adequacy",
44+
)
45+
assert entry.data_residency == "ID"
46+
assert entry.transfer_basis == "adequacy"
47+
48+
def test_fields_from_dict(self) -> None:
49+
data = {
50+
"id": "aud-002",
51+
"timestamp": "2026-05-26T10:00:00Z",
52+
"data_residency": "ID",
53+
"transfer_basis": "safeguards",
54+
}
55+
entry = AuditLogEntry.model_validate(data)
56+
assert entry.data_residency == "ID"
57+
assert entry.transfer_basis == "safeguards"
58+
59+
def test_backward_compat_fields_absent(self) -> None:
60+
data = {
61+
"id": "aud-003",
62+
"timestamp": "2026-05-26T10:00:00Z",
63+
"user_email": "user@company.com",
64+
"success": True,
65+
"blocked": False,
66+
}
67+
entry = AuditLogEntry.model_validate(data)
68+
assert entry.data_residency is None
69+
assert entry.transfer_basis is None
70+
71+
def test_serialization_omits_none(self) -> None:
72+
entry = AuditLogEntry(
73+
id="aud-004",
74+
timestamp=datetime(2026, 5, 26, 10, 0, 0, tzinfo=timezone.utc),
75+
)
76+
data = entry.model_dump(exclude_none=True)
77+
assert "data_residency" not in data
78+
assert "transfer_basis" not in data
79+
80+
def test_serialization_includes_when_set(self) -> None:
81+
entry = AuditLogEntry(
82+
id="aud-005",
83+
timestamp=datetime(2026, 5, 26, 10, 0, 0, tzinfo=timezone.utc),
84+
data_residency="SG",
85+
transfer_basis="consent",
86+
)
87+
data = entry.model_dump()
88+
assert data["data_residency"] == "SG"
89+
assert data["transfer_basis"] == "consent"
90+
91+
def test_json_round_trip(self) -> None:
92+
entry = AuditLogEntry(
93+
id="aud-006",
94+
timestamp=datetime(2026, 5, 26, 10, 0, 0, tzinfo=timezone.utc),
95+
data_residency="ID",
96+
transfer_basis="adequacy",
97+
)
98+
json_str = entry.model_dump_json()
99+
restored = AuditLogEntry.model_validate_json(json_str)
100+
assert restored.data_residency == "ID"
101+
assert restored.transfer_basis == "adequacy"

0 commit comments

Comments
 (0)