[#3254] audit model: additive real-wire fields + deprecations - #204
Merged
Conversation
…ing gate (#3254) Read model (AuditLogEntry): add policy_decision (open string set, not an enum), policy_details (arbitrary-key object) and response_time_ms (nullable Long - absent on pre-9.x servers stays null, never a fake 0). The pre-existing 19-argument constructor is retained and delegates to the new canonical @JsonCreator constructor, so direct constructor callers keep compiling (additive-only surface, proof in the PR body). Deprecations, all in place and still parsing: query_summary, success, blocked, risk_score, latency_ms, policy_violations, metadata on the read model (never populated on the 9.x line) and request_type on the search request (the server does not read it as a filter; a search filtered only by it returns unfiltered results). Java cannot carry @deprecated on constructor parameters, so deprecation rides the getters plus the requestType builder method, with the class Javadoc naming the constructor parameters as equally deprecated. Search request: add action - the filter the 9.x server actually reads, with server-side verdict normalization. Binding gate (the structural fix): wire-shape Gate 5 binds the audit model classes (AuditLogEntry, AuditSearchRequest, AuditSearchResponse) strictly to the pinned spec schemas. Gate 3 is baseline-aware by design, which is exactly how the seven fiction fields shipped - the baseline RECORDED the drift instead of binding the model to the contract. Gate 5 has no refresh path, only the curated note-carrying allowlist in tests/fixtures/audit-binding-allowlist.json (stale entries fail; unresolvable bindings fail instead of skipping). Verified: RED on the pre-fix model naming all seven fiction fields + request_type, RED on a decoy @JsonProperty field, RED on an unresolvable type name, GREEN post-fix. Tests: real captured v9.13.0 payload (verbatim fixture, provenance in the test Javadoc), old-server absence tolerance, fiction+real both-present, old-constructor source-compat proof, action serialization. Runtime-e2e leg runtime-e2e/audit_model_real_wire/ passes against a live community v9.13.0 agent through searchAuditLogs. The per_type_drift baseline entries for the now-modeled fields are removed so the recorded drift stays accurate. Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
…ound-1 items Gate 5 rebuilt at the capability level (R3 items 1+2). The source-regex discovery in lib.py was proven bypassable two ways: a constant-valued annotation (@JsonProperty(SOME_CONSTANT) - the regex needs a quoted string, and the plain-field fallback attributes the JAVA field name, which can even credit the fiction as spec coverage) and Jackson getter auto-detection (an unannotated public getFoo() serializes foo with no annotation anywhere in the source). Gate 5 now asks Jackson itself: scripts/wire_shape/AuditWireKeysProbe.java runs against target/classes plus the resolved dependency classpath and emits, per bound type, the union of the serialization and deserialization bean descriptions - the exact property set the production ObjectMapper can put on or read off the wire. validate.py consumes that instead of the regex view for the bound types (gates 1-4 keep the regex discovery unchanged). Missing java, probe source, target/classes, classpath file, an unloadable class, unparseable probe output, or a bound type reporting zero keys all FAIL the gate - never skip. The wire-shape workflow gains a JDK setup and a compile step so CI provides the artifacts. Proofs executed against the rebuilt gate (outputs in the PR body): both reviewer evasions re-run and RED - the constant-valued annotation is named by its RESOLVED wire key fiction_const_field (not the java name), the unannotated getter is named as fictionFilter; negative control on the unmodified model GREEN; evasion sources reverted. Smaller R3 items: - Two added diff lines carried U+2014 from google-java-format rewraps of pre-existing prose (AuditLogEntry getTransferBasis Javadoc, AuditSearchRequest Builder.decisionId Javadoc): now hyphens. - New hand-modified fixture audit-search-explicit-null.json + test: explicit JSON null on all three new fields normalizes to "" / empty map / null Long through the real mapper, pinning the canonical constructor's null guards (Jackson passes explicit null to creators). - both-present fixture: injected success flipped true -> false so the assertion no longer matches the constructor default and can fail. - Test Javadoc no longer claims the mapper "mirrors" production; it states the configuration is the same and the instance is not. - CHANGELOG historic entries restored byte-for-byte from main; the diff now carries ONLY the Unreleased hunk. Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
BLOCKER fixed by capability removal, per the gate's own principle (unresolvable FAILS, never skips). BeanDescription.findProperties() reports declared bean properties only; three executed review bypasses rode mechanisms outside that view: a @JsonUnwrapped container whose bean-property name is a bound spec key (emits fiction_unwrapped, never the claimed cost), a @JsonAnyGetter map (arbitrary top-level fiction keys at runtime), and a @JsonAlias fiction read key. The probe now scans each bound type's hierarchy (class-level annotations, fields, methods, constructors and their parameters) and exits 2 - refusing to certify - on @JsonUnwrapped, @JsonAnyGetter, @JsonAnySetter, @JsonAlias, @jsonvalue, or class-level @JsonSerialize / @JsonDeserialize / @JsonTypeInfo / @JsonAppend / @JsonNaming. @jsonvalue and the three extra class-level shape rewriters are the same escape class as the named five, so the sweep covers them too. The 'what this probe reports IS what can appear on the wire' claim is replaced with a stated-scope paragraph naming the refused mechanisms in the probe Javadoc and the validate.py docstring (and the PR body): certification now means declared properties are spec-bound AND no shape-escaping mechanism is present. Proofs executed (outputs in the PR body): all three reviewer bypasses re-run RED (probe exit 2 -> gate exit 1, each naming the mechanism and member); negative control GREEN - none of the refused mechanisms exist on the three bound types today. Round-2 low items: - Freshness guard: Gate 5 fails if any bound type's .java is newer than its .class ('the probe would certify stale bytecode. Recompile first: mvn -q compile'). Proven: touch a bound source -> RED, recompile -> GREEN. CI unaffected (compile immediately precedes). - The probe's mapper is now obtained by reflecting the private production factory AxonFlow.createObjectMapper() instead of new ObjectMapper() - identical discovery today, but production gaining a module/introspector/naming strategy can no longer diverge silently. A rename breaks the gate loudly; mirror notes point both ways. - pom.xml added to the wire-shape workflow paths filter: a Jackson version bump is exactly what changes the probe's view and now re-runs the job. Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Additive interim for getaxonflow/axonflow-enterprise#3254: the audit read model gains the three fields a 9.x server actually serves, the search request gains the filter the server actually reads, the seven fiction fields plus
request_typeare deprecated in place, and a new wire-shape binding gate makes this class of drift structurally impossible to reintroduce.Canonical field list (spec pin already at community v9.13.0, untouched by this PR)
Read model (
AuditLogEntry) - ADDED:policy_decisionString getPolicyDecision()via@JsonProperty("policy_decision")allowed/blocked/redactednamed in the server struct,errorobserved live. Documented as a string, NOT an enum. Empty when absent.policy_detailsMap<String,Object> getPolicyDetails()via@JsonProperty("policy_details")response_time_msLong getResponseTimeMs()via@JsonProperty("response_time_ms")nullwhen the server did not send it (pre-9.x / non-LLM planes), never a fake0.Read model - DEPRECATED in place, still parsing, JSON tags kept:
query_summary,success,blocked,risk_score,latency_ms,policy_violations,metadata(never populated on the 9.x line). Wording per the canonical spec, adapted per field (query_summary-> the wire carriesquery/query_hash;risk_score-> no wire equivalent;metadata-> the wire carriespolicy_details/security_metrics). Fields NOT touched (real on the wire):id,request_id,timestamp,user_email,client_id,tenant_id,request_type(read model - it IS served),provider,model,tokens_used,data_residency,transfer_basis.Search request (
AuditSearchRequest) - ADDEDaction(null-omitted via the class's@JsonInclude(NON_NULL)): "Filters by action/request type with verdict normalization on the server side." DEPRECATEDrequest_type(search-request scope only): the 9.x server does not read this filter; a search filtered only by it returns unfiltered results. The SDK keeps sending it (harmless, ignored).Constructor-compatibility decision
AuditLogEntryis final and its public 19-argument creator constructor was the only construction path, so appending parameters would have been source-breaking for direct callers. Decision: the OLD 19-argument constructor survives verbatim and delegates to a new canonical 22-argument@JsonCreatorconstructor (new fields absent -> defaults).@JsonPropertymoved to the new constructor's parameters so Jackson has exactly one creator. Java does not allow@Deprecatedon constructor parameters, so deprecation is carried on the getters (and therequestTypebuilder method), with the class Javadoc naming the constructor parameters as equally deprecated.Compile-level proof,
AuditRealWireModelTest.oldConstructorSignatureStillCompiles(), invokes the exact pre-#3254 19-argument shape and asserts the new fields default. Exported-surface diff (javap, old jar surface vs new; additions only, nothing removed or changed):(Full javap diff contains ONLY these
+lines; no-lines.)Binding gate (wire-shape Gate 5) - the structural fix
Gate 3 (
per_type_drift) is baseline-aware by design: drift recorded at refresh time stays green forever. That is exactly how the seven fiction fields shipped - the baseline RECORDED them undersdk_onlyinstead of binding the model to the contract. Gate 5 inscripts/wire_shape/validate.pybindsAuditLogEntry,AuditSearchRequestandAuditSearchResponsestrictly to the pinned spec schemas.Discovery is capability-level, not source-regex (R3 round 1 items 1+2). Review proved two end-to-end bypasses of the first-cut regex discovery: (a) a constant-valued
@JsonProperty(SOME_CONSTANT)- the regex needs a quoted literal, and the plain-field fallback attributes the JAVA field name, which can even credit the fiction as spec coverage; (b) Jackson getter auto-detection - an unannotated publicgetFoo()serializesfooon every request with no annotation anywhere in the source. Gate 5 therefore asks Jackson itself:scripts/wire_shape/AuditWireKeysProbe.javaruns against the COMPILED classes (target/classes+ resolved dependency classpath, both produced by a new step in the wire-shape workflow) and emits, per bound type, the union of the serialization and deserializationBeanDescriptionproperty names, using a mapper obtained by reflecting the production factoryAxonFlow.createObjectMapper()(a rename breaks the gate loudly; mirror notes point both ways). Constants are resolved at bytecode level; auto-detected getters are first-class properties. Gates 1-4 keep the regex discovery unchanged.Stated scope (R3 round 2).
findProperties()reports declared bean properties only. Mechanisms that add, rename, or replace wire keys outside that view are invisible to it, so the probe REFUSES to certify - exit 2, gate FAILS, never skips - any bound type using:@JsonUnwrapped,@JsonAnyGetter,@JsonAnySetter,@JsonAlias,@JsonValue, or class-level@JsonSerialize/@JsonDeserialize/@JsonTypeInfo/@JsonAppend/@JsonNaming(the last four swept in as the same escape class as the reviewer-named five). Certification therefore means: the declared properties are spec-bound AND no shape-escaping mechanism is present on the audit surface. Gate 5 also carries a freshness guard: a bound type whose.javais newer than its.classfails with a recompile instruction (CI is unaffected - the compile step immediately precedes the validator).Rules: every probed wire key must exist in the same-named pinned schema unless allowlisted in
tests/fixtures/audit-binding-allowlist.jsonwith a note naming a tracking issue; stale allowlist entries FAIL, so the allowlist can only shrink toward empty; spec fields the model misses are informational only. Unresolvable bindings FAIL, never skip: missingjava, missing probe source, missingtarget/classesor classpath file, an unloadable class, unparseable probe output, or a bound type reporting zero keys.Executed proofs (all outputs generated by running the shipped gate; sources reverted after each)
1. Pre-fix RED (rebuilt introspection gate run against the pre-#3254 model compiled from
main, allowlist absent), verbatim Gate 5 lines:2. Reviewer evasion (a) re-run RED - added to
AuditLogEntry:static final String FICTION_WIRE = "fiction_const_field"; @JsonProperty(FICTION_WIRE) private final String plane = null;(java nameplanedeliberately chosen becauseplaneIS a spec property - the exact credit-the-fiction-as-coverage shape). Rebuilt gate names the RESOLVED wire key, exit 1:3. Reviewer evasion (b) re-run RED - added to
AuditSearchRequest:public String getFictionFilter() { return "leaks"; }with no annotation anywhere. Rebuilt gate, exit 1:4. Negative control GREEN - unmodified fixed model, curated allowlist in place: exit 0, Gates 1-4 green (97 class/schema pairs), Gate 5 prints only the informational coverage-gap lines.
5. Decoy self-test (round 0, retained) - a temporary literal
@JsonProperty("decoy_field_never_on_wire")field: caught, exit 1; removed.6. Unresolvable-fails self-test (round 0 behavior retained by the rebuild) - a bogus name in
AUDIT_BINDING_TYPESfails inside the probe (ClassNotFoundExceptionexits 2, the gate FAILS); additionally every missing prerequisite (nojava, notarget/classes, no classpath file, zero-key type) is an explicitSystemExitfailure, never a skip.Live-capture excerpt + provenance
src/test/resources/fixtures/audit-search-live.jsonis the verbatim response captured 2026-08-03 from an isolated community v9.13.0 stack (session 3254; clone of getaxonflow/axonflow tag v9.13.0, df027c788),POST /api/v1/audit/searchthrough the agent proxy. Excerpt:{ "id": "audit_1785794706_23m371y7", "request_type": "tool_call_audit", "query": "Tool: s3254_blocked_probe", "policy_decision": "error", "policy_details": { "caller_name": "unknown", "error_message": "blocked by policy sys_sqli_or_true", "success": false, "tool_name": "s3254_blocked_probe" }, "response_time_ms": 0 }Present on the wire:
policy_decision,policy_details,response_time_ms(plusquery/query_hash/user_role/org_id/cost/... not modeled in this interim). ABSENT: all seven fiction fields. Observed verdict spellingsallowedanderror- proof the set is open, hence string-not-enum. Theaudit-search-old-server.jsonandaudit-search-both-present.jsonfixtures are hand-modified copies of this capture and say so in the test Javadoc.Test evidence
mvn verifylocally green: 1336 unit tests (7 new inAuditRealWireModelTest) + 12 integration, jacoco thresholds met, BUILD SUCCESS.isSuccess()readingtrueon the error-verdict row, which is exactly why it is fiction), old-server absence tolerance (getResponseTimeMs()null-safe, no throw), explicit-JSON-null normalization (hand-modified fixture; null becomes empty string / empty map / null Long through the real mapper, pinning the canonical constructor null guards), fiction+real both-present with non-default injected values (success:falsevs the defaulttrue, so every assertion can fail; no collision), old-constructor compile proof,actionserialization + null-omission, deprecatedrequest_typestill sent.runtime-e2e/audit_model_real_wire/(real JVM + built jar + live community v9.13.0 agent, through the SDK's ownsearchAuditLogs, no mocks):Items not modified in this PR (with justification)
AuditLogEntry(query,query_hash,user_id,user_role,org_id,cost,error_message,response_sample,compliance_flags,security_metrics,redacted_fields,session_id,correlation_id,decision_id,plane) andsession_idonAuditSearchRequest: the canonical #3254 interim scopes the additive set to the three read fields +action; Gate 5 reports these as informational coverage gaps on every run so they stay visible.AuditSearchRequest.equals/hashCode/toStringdo not includeaction: follows the file's existing precedent (decisionId/policyName/overrideIdare likewise excluded); changing equality semantics is not additive.openapi_specs_sha) untouched, per the brief - already at community v9.13.0 (chore(contract): bump OpenAPI spec pin to community v9.13.0 #195).R3 round 1 disposition
All seven items addressed in commit 2 (9733de9): (1+2) Gate 5 rebuilt on compiled-class Jackson introspection, both reviewer evasions re-executed RED against the rebuilt gate, negative control GREEN, unresolvable-fails behavior kept (see proofs above). (3) The two added diff lines carrying U+2014 (google-java-format rewraps of pre-existing prose in
AuditLogEntry.getTransferBasisandAuditSearchRequest.Builder.decisionIdJavadoc) now use hyphens; the full branch diff has zero em/en dashes on added lines. (4) Explicit-null fixture + test added (declared hand-modified). (5) both-present fixture injectssuccess:falseso the assertion is falsifiable. (6) The test Javadoc no longer claims to "mirror" the production mapper; it states the configuration matches and the instance does not. (7) CHANGELOG historic entries restored byte-for-byte frommain; the CHANGELOG diff now contains ONLY the Unreleased hunk (matching the python train; an earlier revision of this branch had mechanically normalized historical em dashes to hyphens, which is hereby declared and is now reverted).R3 round 2 disposition (commit 3, 7bc8973)
BLOCKER - three findProperties() bypasses, fixed by capability removal. The probe now scans each bound type hierarchy (class-level annotations, fields, methods, constructors and their parameters) and exits 2 on any of the ten refused mechanisms above, refusing to certify what introspection cannot see. The "IS what can appear on the wire" claim is replaced by the stated-scope paragraph in the probe Javadoc, the validate.py docstring, and this body. Executed proofs (sources reverted after each; verbatim probe lines):
Bypass (1)
@JsonUnwrappedcontainer whose bean-property name is the bound spec keycost(real emitted keyfiction_unwrapped):Bypass (2)
@JsonAnyGettermap:Bypass (3)
@JsonAlias({"fiction_alias_key"}):Negative control: unmodified bound types (none of the refused mechanisms exist on them today) - exit 0, all gates green.
Low items. (2) Freshness guard added and proven:
touch AuditSearchResponse.java->AuditSearchResponse.java is NEWER than its compiled AuditSearchResponse.class - the probe would certify stale bytecode. Recompile first: mvn -q compile(exit 1); aftermvn -q compile-> exit 0. (3) The probe reflectsAxonFlow.createObjectMapper()for its mapper; identical discovery today, but a future production module/introspector/naming strategy can no longer diverge silently, and a factory rename fails the gate loudly (mirror notes in both files). (4)pom.xmladded to the wire-shape workflowpathsfilter for bothpull_requestandpush- a Jackson version bump is exactly what changes the probe view and now re-runs the job.Local
mvn verifyafter round 2: 1336 unit + 12 integration, BUILD SUCCESS.