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
28 changes: 28 additions & 0 deletions .github/workflows/wire-shape-contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,24 @@ on:
paths:
- 'src/main/java/**/*.java'
- 'tests/fixtures/wire-shape-baseline.json'
- 'tests/fixtures/audit-binding-allowlist.json'
- 'scripts/wire_shape/**'
# pom.xml governs the Jackson version - exactly what changes the
# Gate 5 probe's view of the wire - so a dependency bump must
# re-run this job.
- 'pom.xml'
- '.github/workflows/wire-shape-contract.yml'
push:
branches: [main]
paths:
- 'src/main/java/**/*.java'
- 'tests/fixtures/wire-shape-baseline.json'
- 'tests/fixtures/audit-binding-allowlist.json'
- 'scripts/wire_shape/**'
# pom.xml governs the Jackson version - exactly what changes the
# Gate 5 probe's view of the wire - so a dependency bump must
# re-run this job.
- 'pom.xml'
- '.github/workflows/wire-shape-contract.yml'

permissions:
Expand Down Expand Up @@ -132,6 +142,24 @@ jobs:
- name: Install PyYAML
run: pip install 'pyyaml>=6,<7'

# Gate 5 (audit-surface binding, #3254) introspects the COMPILED
# classes via Jackson (scripts/wire_shape/AuditWireKeysProbe.java)
# instead of trusting source-regex discovery, which is defeated by
# constant-valued @JsonProperty annotations and by Jackson getter
# auto-detection. The validator FAILS (never skips) if these
# artifacts are missing.
- name: Set up JDK 17 (Gate 5 wire-key introspection)
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'

- name: Compile SDK classes + dependency classpath (Gate 5)
run: |
mvn -q -B compile dependency:build-classpath \
-Dmdep.outputFile=target/wire-shape-cp.txt

- name: Run wire-shape contract validator
env:
AXONFLOW_OPENAPI_SPECS_DIR: ${{ github.workspace }}/axonflow-community/docs/api
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Real wire fields `policy_decision` (`getPolicyDecision()`), `policy_details`
(`getPolicyDetails()`), `response_time_ms` (`getResponseTimeMs()`) on the
audit read model (`AuditLogEntry`), and `action` (`Builder.action(String)`)
on audit search (`AuditSearchRequest`). `policy_decision` is an OPEN string
set (`allowed`/`blocked`/`redacted` named in the server struct, `error`
observed live), not an enum. The pre-existing 19-argument `AuditLogEntry`
constructor is retained and delegates to the new canonical constructor, so
the change is source-compatible for direct constructor callers.
- Wire-shape Gate 5: audit-surface binding. Every wire key the compiled
`AuditLogEntry`, `AuditSearchRequest` and `AuditSearchResponse` classes
actually map (introspected from the built classes via Jackson, so
constant-valued annotations and getter auto-detection are covered) must
exist in the pinned OpenAPI schema of the same name, with unbound fields
allowed only via the curated, note-carrying
`tests/fixtures/audit-binding-allowlist.json`. Unlike Gate 3, this gate has
no refresh path - a baseline that RECORDS drift is how seven never-served
fields shipped in the first place (#3254). An unresolvable binding (class,
schema, or introspection probe missing) fails instead of skipping.

### Deprecated

- `query_summary`/`success`/`blocked`/`risk_score`/`latency_ms`/
`policy_violations`/`metadata` (read model) and `request_type` (search
request) - never served/read on the 9.x line (#3254). Removal rides the
next major. The fields stay in place and keep parsing (they remain at their
defaults against real servers); deprecation is carried on the getters and
the `requestType` builder method because Java does not allow `@Deprecated`
on constructor parameters.

### Security

- **Jackson bumped from 2.17.0 to 2.22.1**, and `jackson-core` is now declared
Expand Down
148 changes: 148 additions & 0 deletions runtime-e2e/audit_model_real_wire/AuditModelRealWireTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* runtime-e2e/audit_model_real_wire/AuditModelRealWireTest.java
*
* Real-stack assertion for the #3254 audit-model interim
* (getaxonflow/axonflow-enterprise#3254): the SDK's audit read model
* carries the fields the server actually serves, and the seven fiction
* fields stay at their defaults against a real agent.
*
* Per runtime-e2e/README.md this runs a real JVM + built SDK jar against
* a real AxonFlow agent - no mocks. It asserts:
*
* 1. searchAuditLogs() through the SDK's public surface returns entries
* whose policyDecision is populated from the wire and whose
* responseTimeMs is present (non-null), while the deprecated
* blocked / success / riskScore fields sit at their defaults -
* the server never sends them.
* 2. The new AuditSearchRequest.action filter is READ by the server:
* action("blocked") returns only non-"allowed" verdict rows.
*
* Env:
* AXONFLOW_ENDPOINT agent URL (default http://127.0.0.1:38080)
* AXONFLOW_CLIENT_ID client identity (default demo-client)
* AXONFLOW_CLIENT_SECRET client secret (default demo-secret)
*
* Run (from the SDK root, against a live community/enterprise agent):
*
* mvn install -DskipTests
* mvn -q dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt
* SDK_JAR=$(ls target/axonflow-sdk-*.jar | grep -v sources | grep -v javadoc | head -1)
* java -cp "$SDK_JAR:$(cat /tmp/cp.txt)" \
* runtime-e2e/audit_model_real_wire/AuditModelRealWireTest.java
*/
import com.getaxonflow.sdk.AxonFlow;
import com.getaxonflow.sdk.AxonFlowConfig;
import com.getaxonflow.sdk.types.AuditLogEntry;
import com.getaxonflow.sdk.types.AuditSearchRequest;
import com.getaxonflow.sdk.types.AuditSearchResponse;

public class AuditModelRealWireTest {

static void fail(String msg) {
System.err.println("FAIL: " + msg);
System.exit(1);
}

static String env(String name, String dflt) {
String v = System.getenv(name);
return (v == null || v.isEmpty()) ? dflt : v;
}

@SuppressWarnings("deprecation")
public static void main(String[] args) {
String endpoint = env("AXONFLOW_ENDPOINT", "http://127.0.0.1:38080");
String clientId = env("AXONFLOW_CLIENT_ID", "demo-client");
String clientSecret = env("AXONFLOW_CLIENT_SECRET", "demo-secret");

AxonFlow client =
AxonFlow.create(
AxonFlowConfig.builder()
.endpoint(endpoint)
.clientId(clientId)
.clientSecret(clientSecret)
.build());

// 1. Unfiltered search: policy_decision / response_time_ms come off
// the real wire; the fiction fields stay at defaults.
AuditSearchResponse all =
client.searchAuditLogs(AuditSearchRequest.builder().limit(50).build());
if (all.getEntries().isEmpty()) {
fail("no audit entries on the stack - write one first (POST /api/v1/audit/tool-call)");
}

int withDecision = 0;
int withResponseTime = 0;
for (AuditLogEntry e : all.getEntries()) {
if (!e.getPolicyDecision().isEmpty()) {
withDecision++;
}
if (e.getResponseTimeMs() != null) {
withResponseTime++;
}
// The deprecated trio must sit at defaults: a real 9.x server never
// sends success/blocked/risk_score, so a non-default value here
// means the model regressed into trusting fiction again.
if (e.isBlocked()) {
fail("entry " + e.getId() + " has blocked=true - the 9.x wire never sends 'blocked'");
}
if (!e.isSuccess()) {
fail("entry " + e.getId() + " has success=false - the 9.x wire never sends 'success'");
}
if (e.getRiskScore() != 0.0) {
fail("entry " + e.getId() + " has risk_score=" + e.getRiskScore()
+ " - the 9.x wire never sends 'risk_score'");
}
}
if (withDecision == 0) {
fail("no entry carried a policy_decision - new field not bound to the wire");
}
if (withResponseTime == 0) {
fail("no entry carried response_time_ms - new field not bound to the wire");
}
AuditLogEntry sample = all.getEntries().get(0);
System.out.println(
"PASS [real-wire-fields] "
+ all.getEntries().size()
+ " entries; "
+ withDecision
+ " with policy_decision, "
+ withResponseTime
+ " with response_time_ms. Sample: id="
+ sample.getId()
+ " policyDecision="
+ sample.getPolicyDecision()
+ " responseTimeMs="
+ sample.getResponseTimeMs()
+ " policyDetailsKeys="
+ sample.getPolicyDetails().keySet()
+ " | deprecated defaults held: blocked="
+ sample.isBlocked()
+ " success="
+ sample.isSuccess()
+ " riskScore="
+ sample.getRiskScore());

// 2. The action filter is read server-side (request_type is not).
AuditSearchResponse blocked =
client.searchAuditLogs(
AuditSearchRequest.builder().action("blocked").limit(50).build());
for (AuditLogEntry e : blocked.getEntries()) {
if (e.getPolicyDecision().isEmpty() || "allowed".equals(e.getPolicyDecision())) {
fail(
"action=\"blocked\" returned entry "
+ e.getId()
+ " with policy_decision="
+ e.getPolicyDecision()
+ " - the server did not apply the filter");
}
}
System.out.println(
"PASS [action-filter] action=\"blocked\" returned "
+ blocked.getEntries().size()
+ " of "
+ all.getEntries().size()
+ " entries, none with an allowed/empty verdict");

System.out.println("ALL PASS");
}
}
51 changes: 51 additions & 0 deletions runtime-e2e/audit_model_real_wire/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# audit_model_real_wire (audit model real-wire fields, #3254)

Real-stack proof for the getaxonflow/axonflow-enterprise#3254 additive
interim: the SDK's audit read model now carries the fields a 9.x server
actually serves, and the seven never-served fields stay at their
defaults against a live agent.

Background: `AuditLogEntry` modeled `query_summary`, `success`,
`blocked`, `risk_score`, `latency_ms`, `policy_violations` and
`metadata` - none of which any 9.x server has ever sent. Consumers
reading `isBlocked()` on a genuinely blocked request saw `false`
(the default), because the wire carries the verdict in
`policy_decision`, the context in `policy_details` and the latency in
`response_time_ms`. Similarly, `AuditSearchRequest.request_type` is a
silent server-side no-op; the real filter is `action`.

This test asserts, through the SDK's real public surface
(`searchAuditLogs`), against a real running agent with NO mocks:

1. **Real fields are bound.** At least one returned entry carries a
populated `policyDecision` and a present (non-null)
`responseTimeMs`, while `isBlocked()` / `isSuccess()` /
`getRiskScore()` sit at their documented defaults on every entry.
2. **`action` is read server-side.** `action("blocked")` returns only
entries whose verdict is not `allowed`/empty.

## Run

```bash
# from the SDK root, against a live agent
export AXONFLOW_ENDPOINT=http://127.0.0.1:38080 # default
export AXONFLOW_CLIENT_ID=demo-client # default
export AXONFLOW_CLIENT_SECRET=demo-secret # default

mvn install -DskipTests
mvn -q dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt
SDK_JAR=$(ls target/axonflow-sdk-*.jar | grep -v sources | grep -v javadoc | head -1)
java -cp "$SDK_JAR:$(cat /tmp/cp.txt)" \
runtime-e2e/audit_model_real_wire/AuditModelRealWireTest.java
```

The stack must hold at least one audit row; write one via
`POST /api/v1/audit/tool-call` through the agent proxy if empty.

Expected output shape:

```
PASS [real-wire-fields] N entries; N with policy_decision, N with response_time_ms. Sample: ...
PASS [action-filter] action="blocked" returned M of N entries, none with an allowed/empty verdict
ALL PASS
```
Loading
Loading