Skip to content

feat(pep): decide -> fulfill -> forward Decision Mode PEP (#2571)#191

Merged
saurabhjain1592 merged 2 commits into
mainfrom
feat/2571-pep-decide-fulfill-obligation
Jun 9, 2026
Merged

feat(pep): decide -> fulfill -> forward Decision Mode PEP (#2571)#191
saurabhjain1592 merged 2 commits into
mainfrom
feat/2571-pep-decide-fulfill-obligation

Conversation

@saurabhjain1592

Copy link
Copy Markdown
Member

Ports the Decision Mode PEP (decide → fulfill → forward) contract into the Java SDK — the SDK analog of platform/shared/pep (ADR-056, epic #2563). Tracking: #2571.

Cross-SDK parity with the canonical Python (#211), Go (#181), and TypeScript (#241) PRs: JSON wire field names are byte-identical (snake_case), and constant VALUES match exactly (redact_pii, request/response, text/plain, allow/deny/needs_approval, paths /api/v1/decide, /api/v1/mcp/check-input, /api/v1/mcp/check-output).

What's added

  • AxonFlow.decide(DecideRequest) (+ decideAsync) — the PDP step. POST /api/v1/decide over the SDK's existing HTTP Basic (org:license) auth; demo/wrong creds → AuthenticationException (401); a deny verdict is returned in the body (HTTP 200), not as an error.
  • AxonFlow.fulfillRequest(DecideResponse, String) — discharges every request-phase redact_pii obligation by round-tripping the statement through the engine's check-input endpoint and returning engine-redacted content (FulfillResult: content + didRedact()). No local redaction anywhere.
  • AxonFlow.decideAndFulfill(DecideRequest) (+ async) — the one-call path (DecideAndFulfillResult); fail-closed by construction.
  • Pep constants + Pep.hasRequestRedaction(List<Obligation>).
  • New types: DecideRequest (fluent builder), DecideResponse, Obligation, ObligationFulfillment, DecisionCallerIdentity, DecisionTarget.
  • ObligationNotFulfillableException — the fail-closed signal (extends AxonFlowException).
  • MCP types: content_type on MCPCheckInputRequest (+ content_type option on mcpCheckInput); redacted / redactedStatement / redactionEvaluated on MCPCheckInputResponse; redactionEvaluated on MCPCheckOutputResponse. Existing source-compat constructors preserved.

Fail-closed (never returns the original statement)

fulfillRequest throws ObligationNotFulfillableException when: no request-phase fulfillment; advertised content_types excludes text/plain; the endpoint is not the request-redaction path (foreign URLs rejected; absolute URLs accepted only when their path matches); the engine call fails / non-200; or redaction_evaluated == false. didRedact reflects whether the engine changed the content.

Verification

  • Unit tests (34 new): decide parse + wire shape; every fail-closed branch (no-fulfillment, response-phase, unadvertised content-type, foreign endpoint, engine-error, redaction_evaluated=false, redaction_evaluated absent); passthrough (no obligation, engine-found-nothing); decideAndFulfill allow/deny/unfulfillable; hasRequestRedaction; absolute-URL acceptance. New-code line coverage 99.4%.

  • Full suite: 1302 tests, 0 failures; JaCoCo BUNDLE gates satisfied (LINE 0.782, BRANCH 0.550).

  • Wire-shape: validator green against the pinned community spec; baseline annotated for the acknowledged SDK-superset fields without bumping the pinned spec SHA (no spec-pin-bump needed).

  • Version: minor bump 8.4.0 → 8.5.0 (pom.xml + example poms + CHANGELOG); Version Alignment script passes.

  • runtime-e2e (runtime-e2e/decide_fulfill_obligation/, NO mocks): run against the live enterprise agent —

    decide -> verdict=allow decision_id=f8ed6956-... obligations=1 evaluated_policies=[sys_pii_credit_card, sys_pii_email]
    PASS step 1: decide returned allow + redact_pii request-phase obligation
    fulfillRequest -> didRedact=true content=Send the receipt to jo****************om and charge card 4**************1
    PASS step 2: fulfillRequest masked email + card via the engine (no local redaction)
    decideAndFulfill -> verdict=allow content=Send the receipt to jo****************om and charge card 4**************1
    PASS step 3: decideAndFulfill returned engine-masked content in one call
    PASS step 4: demo credentials refused -> AuthenticationException (HTTP 401)
    ALL PASS: decide -> fulfill -> forward verified through the SDK against the live agent
    

    Neither john.doe@example.com nor 4111111111111111 survives fulfillment.

Deviations from spec

  • fulfillRequest returns a small FulfillResult (content + didRedact()) and decideAndFulfill returns DecideAndFulfillResult (verdict + content + decision) — idiomatic Java for the spec's multi-value returns; semantics are identical.

Refs #2563

Add the SDK analog of platform/shared/pep (ADR-056, epic #2563): a decide
client that surfaces engine-fulfillable redact_pii obligations, plus a
fulfill helper that discharges them by round-tripping content through the
named engine endpoint (check-input) -- never by redacting locally.

- decide() / fulfillRequest() / decideAndFulfill() (+ async mirrors) and the
  Pep.hasRequestRedaction() helper + constants on the main client
- DecideRequest (fluent builder) / DecideResponse / Obligation /
  ObligationFulfillment / DecisionCallerIdentity / DecisionTarget types
- redacted / redacted_statement / redaction_evaluated on MCPCheckInputResponse;
  redaction_evaluated on MCPCheckOutputResponse; content_type on check-input
- ObligationNotFulfillableException fail-closed signal (no local redaction)
- 34 unit tests (every fail-closed branch + passthrough + decide parse +
  decide_and_fulfill allow/deny/unfulfillable; 99% line cover on new code) +
  runtime-e2e (real enterprise agent: decide -> fulfill -> masked, demo creds
  refused); wire-shape baseline annotated (pinned spec SHA unchanged)

Minor bump 8.4.0 -> 8.5.0 (additive, SDK semver decoupled from platform).

Refs #2563

Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
…571)

R3 parity follow-up: fulfillRequest now throws ObligationNotFulfillableException
on a self-contradictory engine response (redacted=true but null/empty
redacted_statement) instead of forwarding the unredacted original. Adds a
unit test. Brings Java to parity with the same hardening applied to
Python/Go/TS/Rust in this pass.

Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
@saurabhjain1592 saurabhjain1592 merged commit 211750e into main Jun 9, 2026
18 checks passed
@saurabhjain1592 saurabhjain1592 deleted the feat/2571-pep-decide-fulfill-obligation branch June 9, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant