Skip to content

Commit f7d8d61

Browse files
feat(release): v8.4.0 — decision request context + pasal_56b_dpa transfer basis (#240)
Targets AxonFlow platform v8.5.0 (epic #2508). - DecisionSummary + DecisionExplanation gain a context?: Record<string, string> surfacing the sanitized request context a PEP attaches to a Decision Mode call (platform #2509). listDecisions() returns the platform-truncated 5-key summary; explainDecision() returns the full map plus a contextTruncated?: boolean flag. The parseDecisionSummary / parseDecisionExplanation decoders map the new wire fields through (snake_case -> camelCase passthrough). - New exported TransferBasis union type ('adequacy' | 'safeguards' | 'pasal_56b_dpa' | 'consent'); AuditLogEntry.transferBasis is now typed TransferBasis. The union is compile-time only — the decoder surfaces the value verbatim, so existing 'safeguards' consumers are unaffected. - version 8.3.0 -> 8.4.0 (package.json + package-lock + src/version.ts), CHANGELOG entry, unit tests, and a runtime-e2e driver that creates a decision via the PEP path and reads context back through the SDK. Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
1 parent 039c40b commit f7d8d61

13 files changed

Lines changed: 339 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,34 @@ All notable changes to the AxonFlow TypeScript SDK will be documented in this fi
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [8.4.0] - 2026-05-30 — Decision request context + Pasal 56(b) transfer basis
9+
10+
Targets AxonFlow platform **v8.5.0**.
11+
12+
### Added
13+
14+
- **`context` field on `DecisionSummary` and `DecisionExplanation`**
15+
`Record<string, string>`. Surfaces the sanitized request context a PEP attaches
16+
to a Decision Mode call (canonical `lower_snake_case` keys such as `x_ai_agent`,
17+
`x_session_id`, `x_leader_identity`, and `x-bukuwarung-*`), persisted by the
18+
platform at the audit row's `policy_details->'context'`. `listDecisions()`
19+
returns the platform-truncated summary (5 keys); `explainDecision()` returns the
20+
full map. The `parseDecisionSummary` / `parseDecisionExplanation` decoders map
21+
the new wire fields through. Absent for pre-v8.4.0 audit rows.
22+
- **`contextTruncated` field on `DecisionExplanation`**`boolean`. True when the
23+
agent dropped surplus context keys at write time.
24+
- **`TransferBasis` union type** (`'adequacy' | 'safeguards' | 'pasal_56b_dpa' |
25+
'consent'`), exported from the package root. Names the Indonesia UU PDP Pasal 56
26+
legal bases.
27+
28+
### Changed
29+
30+
- **`AuditLogEntry.transferBasis`** is now typed `TransferBasis` (was `string`) and
31+
accepts `pasal_56b_dpa` (Pasal 56(b) explicit DPA tag) alongside `adequacy`,
32+
`safeguards`, and `consent`. The union is a compile-time hint only — the runtime
33+
decoder passes the value through verbatim, so existing `safeguards` consumers are
34+
unaffected and an unknown future value still parses.
35+
836
## [8.3.0] - 2026-05-27 — Indonesia PII category + cross-border audit fields
937

1038
### Added

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@axonflow/sdk",
3-
"version": "8.3.0",
3+
"version": "8.4.0",
44
"description": "AxonFlow SDK - Add invisible AI governance to your applications in 3 lines of code",
55
"main": "dist/cjs/index.js",
66
"module": "dist/esm/index.js",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# decision_context_transfer_basis (v8.4.0)
2+
3+
Real-stack proof for the v8.4.0 SDK surface (platform epic #2508):
4+
5+
- **`DecisionSummary.context` / `DecisionExplanation.context` (+ `contextTruncated`)**
6+
the sanitized request context a PEP attaches to a Decision Mode call is surfaced
7+
back through `listDecisions` and `explainDecision`. The `parseDecisionSummary` /
8+
`parseDecisionExplanation` decoders map the new wire fields through.
9+
- **`AuditLogEntry.transferBasis = 'pasal_56b_dpa'`** — the Pasal 56(b) explicit DPA
10+
tag (a member of the new `TransferBasis` union) round-trips verbatim.
11+
12+
The driver builds the local SDK, acts as the PEP (raw `POST /api/v1/decide` — that
13+
endpoint is not SDK-wrapped per ADR-056), then reads the decision back through the
14+
SDK against a real running agent.
15+
16+
## Run
17+
18+
```
19+
export AXONFLOW_AGENT_URL=http://localhost:8080
20+
export AXONFLOW_TENANT_ID=buku-e-ts-e2e
21+
export AXONFLOW_TENANT_SECRET=buku-e-secret
22+
./test.sh
23+
```
24+
25+
Exits non-zero if the SDK does not surface the new fields.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env bash
2+
# Runtime proof — TypeScript SDK v8.4.0 surfaces the Decision Mode request
3+
# context (platform #2509, epic #2508) and the pasal_56b_dpa transfer basis.
4+
#
5+
# Builds the LOCAL SDK (dist/) and runs a Node program that:
6+
# 1. acts as the PEP via a raw POST /api/v1/decide (that endpoint is not
7+
# SDK-wrapped per ADR-056), forwarding request context in the body;
8+
# 2. reads the decision back through the SDK's listDecisions + explainDecision
9+
# and asserts DecisionSummary.context / DecisionExplanation.context are
10+
# populated with the forwarded keys;
11+
# 3. JSON round-trips an AuditLogEntry carrying transferBasis='pasal_56b_dpa'
12+
# to confirm the value is surfaced verbatim.
13+
#
14+
# The npm registry is blocked, so we use the LOCAL build via file:../.. (never
15+
# `npm install @axonflow/sdk` — that would fail on the publish boundary).
16+
#
17+
# Usage:
18+
# AXONFLOW_AGENT_URL=http://localhost:8080 ./test.sh
19+
20+
set -uo pipefail
21+
22+
AGENT_URL=${AXONFLOW_AGENT_URL:-http://localhost:8080}
23+
CLIENT_ID=${AXONFLOW_TENANT_ID:-buku-e-ts-e2e}
24+
SECRET=${AXONFLOW_TENANT_SECRET:-buku-e-secret}
25+
SDK_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
26+
RUN_TAG=$(date -u +%s)
27+
28+
red() { printf '\033[31m%s\033[0m\n' "$*"; }
29+
green() { printf '\033[32m%s\033[0m\n' "$*"; }
30+
31+
if [ ! -f "$SDK_ROOT/dist/esm/index.js" ]; then
32+
echo "Building local SDK..."
33+
(cd "$SDK_ROOT" && npm run build) || { red "FAIL: SDK build failed"; exit 1; }
34+
fi
35+
36+
WORK=$(mktemp -d)
37+
trap 'rm -rf "$WORK"' EXIT
38+
39+
cat > "$WORK/package.json" <<EOF
40+
{
41+
"name": "decision-ctx-rt-${RUN_TAG}",
42+
"version": "1.0.0",
43+
"type": "module",
44+
"private": true,
45+
"dependencies": { "@axonflow/sdk": "file:${SDK_ROOT}" }
46+
}
47+
EOF
48+
49+
cat > "$WORK/main.mjs" <<EOF
50+
import { AxonFlow } from '@axonflow/sdk';
51+
52+
const AGENT_URL = process.env.AXONFLOW_AGENT_URL || 'http://localhost:8080';
53+
const CLIENT_ID = process.env.AXONFLOW_TENANT_ID || 'buku-e-ts-e2e';
54+
const SECRET = process.env.AXONFLOW_TENANT_SECRET || 'buku-e-secret';
55+
const want = { x_ai_agent: 'refund-bot', x_session_id: 'sess-buku-42', x_leader_identity: 'ops-lead' };
56+
57+
const fail = (m) => { console.error('FAIL: ' + m); process.exit(1); };
58+
const sameCtx = (got) => got && want.x_ai_agent === got.x_ai_agent
59+
&& want.x_session_id === got.x_session_id && want.x_leader_identity === got.x_leader_identity;
60+
61+
// 1. PEP: create a decision carrying request context (body 'context' map).
62+
const auth = Buffer.from(CLIENT_ID + ':' + SECRET).toString('base64');
63+
const decideResp = await fetch(AGENT_URL + '/api/v1/decide', {
64+
method: 'POST',
65+
headers: { 'Content-Type': 'application/json', 'X-Client-ID': CLIENT_ID, Authorization: 'Basic ' + auth },
66+
body: JSON.stringify({
67+
stage: 'llm',
68+
query: 'summarize this support ticket',
69+
target: { type: 'llm', model: 'gpt-4', provider: 'openai' },
70+
context: { 'x-ai-agent': 'refund-bot', 'x-session-id': 'sess-buku-42', 'x-leader-identity': 'ops-lead' },
71+
}),
72+
});
73+
const decideText = await decideResp.text();
74+
if (!decideResp.ok) fail('decide HTTP ' + decideResp.status + ': ' + decideText);
75+
console.log('server /decide response: ' + decideText);
76+
const decisionId = JSON.parse(decideText).decision_id;
77+
if (!decisionId) fail('no decision_id: ' + decideText);
78+
console.log('PEP decide -> decision_id=' + decisionId);
79+
80+
// 2. Read it back through the SDK.
81+
const client = new AxonFlow({ endpoint: AGENT_URL, clientId: CLIENT_ID, clientSecret: SECRET, mode: 'production' });
82+
83+
const rows = await client.listDecisions({ limit: 5 });
84+
const found = rows.find((r) => r.decisionId === decisionId);
85+
if (!found) fail('listDecisions did not return ' + decisionId + ' (got ' + rows.length + ' rows)');
86+
console.log('SDK listDecisions -> ' + JSON.stringify(found));
87+
if (!sameCtx(found.context)) fail('listDecisions context = ' + JSON.stringify(found.context));
88+
console.log('PASS: listDecisions DecisionSummary.context populated with ' + Object.keys(found.context).length + ' PEP-forwarded keys');
89+
90+
const exp = await client.explainDecision(decisionId);
91+
console.log('SDK explainDecision -> context=' + JSON.stringify(exp.context) + ' contextTruncated=' + exp.contextTruncated);
92+
if (!sameCtx(exp.context)) fail('explainDecision context = ' + JSON.stringify(exp.context));
93+
console.log('PASS: explainDecision returned full context (contextTruncated=' + exp.contextTruncated + ')');
94+
95+
// 3. transfer_basis = pasal_56b_dpa is JSON-preserved. NOTE: TypeScript types are
96+
// erased at runtime, so this only asserts the value survives a JSON round-trip.
97+
// The SDK's real snake_case->camelCase decoder (parseAuditLogEntry) is covered
98+
// by tests/audit.test.ts ('should parse the pasal_56b_dpa transfer basis ...').
99+
const entry = { id: 'e2e-audit', dataResidency: 'ID', transferBasis: 'pasal_56b_dpa' };
100+
const back = JSON.parse(JSON.stringify(entry));
101+
if (back.transferBasis !== 'pasal_56b_dpa') fail('transferBasis round-trip = ' + back.transferBasis);
102+
console.log('PASS: transferBasis "' + back.transferBasis + '" JSON-preserved (SDK decoder path covered by tests/audit.test.ts)');
103+
104+
console.log('ALL PASS: v8.4.0 context + pasal_56b_dpa verified through SDK runtime');
105+
EOF
106+
107+
echo "Run tag: $RUN_TAG Agent: $AGENT_URL"
108+
(
109+
cd "$WORK"
110+
npm install --silent --no-audit --no-fund 2>&1 | tail -2
111+
AXONFLOW_AGENT_URL="$AGENT_URL" AXONFLOW_TENANT_ID="$CLIENT_ID" AXONFLOW_TENANT_SECRET="$SECRET" \
112+
AXONFLOW_TELEMETRY=off node main.mjs 2>&1
113+
)
114+
RC=$?
115+
[ $RC -eq 0 ] && green "runtime-e2e PASS" || { red "runtime-e2e FAIL (rc=$RC)"; exit $RC; }

src/client.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
ListDecisionsOptions,
4141
AuditQueryOptions,
4242
AuditLogEntry,
43+
TransferBasis,
4344
AuditSearchResponse,
4445
ExecuteQueryOptions,
4546
ExecuteQueryResponse,
@@ -278,6 +279,9 @@ function parseDecisionSummary(raw: Record<string, unknown>): DecisionSummary {
278279
decision: (raw.decision as string) ?? '',
279280
policyId: raw.policy_id as string | undefined,
280281
toolSignature: raw.tool_signature as string | undefined,
282+
// v8.4.0 (platform #2509): the PEP-forwarded request context. Keys arrive
283+
// already canonicalized to lower_snake_case; pass the map through verbatim.
284+
context: raw.context as Record<string, string> | undefined,
281285
};
282286
}
283287

@@ -2839,6 +2843,10 @@ export class AxonFlow {
28392843
historicalHitCountSession: (r.historical_hit_count_session as number) ?? 0,
28402844
policySourceLink: r.policy_source_link as string | undefined,
28412845
toolSignature: r.tool_signature as string | undefined,
2846+
// v8.4.0 (platform #2509): full PEP-forwarded context + truncation flag.
2847+
// Keys arrive already canonicalized to lower_snake_case.
2848+
context: r.context as Record<string, string> | undefined,
2849+
contextTruncated: r.context_truncated as boolean | undefined,
28422850
};
28432851
}
28442852

@@ -2972,7 +2980,9 @@ export class AxonFlow {
29722980
policyViolations: (data.policy_violations as string[]) ?? [],
29732981
metadata: (data.metadata as Record<string, unknown>) ?? {},
29742982
...(data.data_residency != null && { dataResidency: data.data_residency as string }),
2975-
...(data.transfer_basis != null && { transferBasis: data.transfer_basis as string }),
2983+
// Cast is compile-time only — the value is surfaced verbatim, so an
2984+
// unknown future transfer-basis value still passes through (v8.4.0).
2985+
...(data.transfer_basis != null && { transferBasis: data.transfer_basis as TransferBasis }),
29762986
};
29772987
}
29782988

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ export type {
120120
AuditQueryOptions,
121121
AuditLogEntry,
122122
AuditSearchResponse,
123+
// Cross-border transfer basis (UU PDP Pasal 56)
124+
TransferBasis,
123125
// Audit Tool Call types
124126
AuditToolCallRequest,
125127
AuditToolCallResponse,

src/types/decisions.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ export interface DecisionExplanation {
4747
historicalHitCountSession: number;
4848
policySourceLink?: string;
4949
toolSignature?: string;
50+
/**
51+
* The FULL sanitized request context the PEP attached to the decision
52+
* (canonical lower_snake_case keys, string values), read from the audit
53+
* row's `policy_details->'context'`. Unlike {@link DecisionSummary} (which
54+
* truncates to 5 keys), explain returns every persisted key up to the
55+
* platform's 10-key cap (e.g. `x_ai_agent`, `x_session_id`,
56+
* `x_leader_identity`, `x-bukuwarung-*`). Absent for pre-v8.4.0 audit rows.
57+
* (platform #2509 / epic #2508)
58+
*/
59+
context?: Record<string, string>;
60+
/** True when the agent dropped surplus context keys at write time. */
61+
contextTruncated?: boolean;
5062
}
5163

5264
/**
@@ -69,6 +81,15 @@ export interface DecisionSummary {
6981
decision: string;
7082
policyId?: string;
7183
toolSignature?: string;
84+
/**
85+
* The sanitized request context the PEP attached to the decision (canonical
86+
* lower_snake_case keys, string values), surfaced from the audit row's
87+
* `policy_details->'context'`. The list summary is truncated by the platform
88+
* to the 5 most-correlated keys; the full map is available via
89+
* {@link AxonFlowClient.explainDecision}. Absent for pre-v8.4.0 audit rows or
90+
* decisions with no context. (platform #2509 / epic #2508)
91+
*/
92+
context?: Record<string, string>;
7293
}
7394

7495
/**

src/types/gateway.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,19 @@ export interface AuditQueryOptions {
258258
offset?: number;
259259
}
260260

261+
/**
262+
* Cross-border transfer-basis values recognized under Indonesia UU PDP Pasal 56:
263+
*
264+
* - `adequacy` — Pasal 56(a): destination with adequate protection
265+
* - `safeguards` — Pasal 56(b): binding legal instrument (generic label)
266+
* - `pasal_56b_dpa` — Pasal 56(b): binding legal instrument, explicit DPA tag
267+
* - `consent` — Pasal 56(c): explicit data-subject consent
268+
*
269+
* `safeguards` and `pasal_56b_dpa` are semantic equivalents; the platform
270+
* surfaces whichever was recorded at decision time, verbatim. (platform #2513)
271+
*/
272+
export type TransferBasis = 'adequacy' | 'safeguards' | 'pasal_56b_dpa' | 'consent';
273+
261274
/**
262275
* A single audit log entry representing an audited request or event.
263276
*/
@@ -298,8 +311,11 @@ export interface AuditLogEntry {
298311
metadata: Record<string, unknown>;
299312
/** ISO 3166-1 alpha-2 country code for data residency (cross-border transfer logging). */
300313
dataResidency?: string;
301-
/** Legal basis for cross-border data transfer: "adequacy", "safeguards", or "consent". */
302-
transferBasis?: string;
314+
/**
315+
* Legal basis for cross-border data transfer under Indonesia UU PDP Pasal 56.
316+
* See {@link TransferBasis}. Surfaced verbatim — never auto-translated.
317+
*/
318+
transferBasis?: TransferBasis;
303319
}
304320

305321
/**

src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
* this value matches package.json.
99
*/
1010
// AUTO-GENERATED — do not edit. Run `npm run stamp-version` to update.
11-
export const VERSION = '8.3.0';
11+
export const VERSION = '8.4.0';

0 commit comments

Comments
 (0)