Skip to content

[#3254] audit model: additive real-wire fields + deprecations - #223

Merged
saurabhjain1592 merged 3 commits into
mainfrom
feat/3254-audit-real-wire-fields
Aug 4, 2026
Merged

[#3254] audit model: additive real-wire fields + deprecations#223
saurabhjain1592 merged 3 commits into
mainfrom
feat/3254-audit-real-wire-fields

Conversation

@saurabhjain1592

@saurabhjain1592 saurabhjain1592 commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Additive interim for getaxonflow/axonflow-enterprise#3254: the Python SDK's audit read model was built to spec fiction - platform/orchestrator/audit_logger.go AuditEntry has never served query_summary/success/blocked/risk_score/latency_ms/policy_violations/metadata on the 9.x line, so every consumer reading audit entries gets silent zero-values today. This PR adds the real wire fields alongside the fiction fields (no removal, no rename - the coordinated removal rides the next major per the operator decision on #3254).

Canonical field list (per the #3254 cross-SDK spec)

Read model AuditLogEntry, three fields ADDED (all defaulted, absence-tolerant):

Wire JSON tag Type on wire Python
policy_decision string, OPEN set: allowed/blocked/redacted observed in code, error observed live - documented as string, NOT an enum policy_decision: str = ""
policy_details object (arbitrary keys) policy_details: dict[str, Any] = {} (default_factory)
response_time_ms integer response_time_ms: int = 0 (ge=0)

Read model, seven fiction fields DEPRECATED (kept, keep parsing, keep JSON tags): query_summary, success, blocked, risk_score, latency_ms, policy_violations, metadata.

Search request AuditSearchRequest: ADDED action (optional, the filter the 9.x server actually reads, audit_read_handlers.go; doc: "Filters by action/request type with verdict normalization on the server side"). DEPRECATED request_type on the search request only (the 9.x server does not read it; a search filtered only by it returns unfiltered results). It is still sent when set (harmless, ignored) - _build_audit_search_body keeps emitting it until the next major removes the field.

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.

Deprecation idiom chosen: docstring "Deprecated:" notes on each attribute line (full canonical wording) plus "Deprecated:" prefixes on the Field(description=...) strings - the SDK's established idiom (see ToolContext.tool_type, axonflow/types.py, "Deprecated: use caller_name instead"). No runtime DeprecationWarning on field access: pydantic v2's Field(deprecated=...) warns on every attribute read, which would fire inside the SDK's own serialization paths and on every consumer render loop; a documented deprecation is the interim contract, removal warnings ride the next major.

Live capture (provenance: captured 2026-08-03 from an isolated community v9.13.0 stack, session 3254)

Raw POST /api/v1/audit/search response through the agent proxy (excerpt, entry 1 of 2):

{
  "id": "audit_1785794706_23m371y7",
  "timestamp": "2026-08-03T22:05:06.947296Z",
  "client_id": "community",
  "tenant_id": "community",
  "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,
  "error_message": "blocked by policy sys_sqli_or_true"
}

ABSENT from the wire: blocked, success, risk_score, latency_ms, query_summary, policy_violations, metadata - the seven fiction fields. Verdict spellings observed: allowed, error (and blocked in the live probe below) - the set is open, so the SDK documents the field as a plain string.

The unmodified capture is committed as tests/fixtures/audit_search_live_v9130.json with the provenance comment at the fixture load site in tests/test_audit.py.

Additive-only public API proof

Diff of the exported audit-model surface (sorted model_fields with requiredness), main vs this branch - additions only, no removal, no optionality or type change on any existing field:

7a8,9
> AuditLogEntry.policy_decision: optional
> AuditLogEntry.policy_details: optional
12a15
> AuditLogEntry.response_time_ms: optional
19a23
> AuditSearchRequest.action: optional

Wire-shape gate: red run before the curated baseline entries

Main's spec pin (0bd92562, pre-v9) predates the real fields, so the new SDK fields read as sdk-only drift. Gate run against the pinned specs BEFORE adding the curated baseline entries (evidence of what the gate catches):

NEW wire-shape drift detected (not covered by baseline):

  AuditLogEntry:
    NEW, only in SDK model:  ['policy_decision', 'policy_details', 'response_time_ms']
    (baseline, only in SDK):  ['data_residency', 'metadata', 'model', 'policy_violations', 'transfer_basis']
  AuditSearchRequest:
    NEW, only in SDK model:  ['action']
    (baseline, only in SDK):  ['decision_id', 'offset', 'override_id', 'policy_name']

FAILED tests/test_wire_shape.py::test_no_new_sdk_vs_spec_drift
1 failed, 6 passed

Curated per_model_drift entries added for exactly those four fields, notes naming #3254 and the PR #214 pin bump that clears them (note mechanism per scripts/refresh_wire_shape_baseline.py: notes are carried forward verbatim on regen). Gate re-run with the entries: 7/7 passed. openapi_specs_sha is UNCHANGED in this PR - no spec-pin-bump label needed; the v9.13.0 pin refresh is stacked separately as #214.

Test evidence

  • tests/test_audit.py::TestAuditRealWireShape (new):
    • real capture deserializes, new fields populated, seven fiction fields at defaults, open verdict set (error) parses, no throw
    • old-server tolerance: hand-modified capture (says so) with the three new fields removed parses, new fields default
    • both-present: hand-modified capture (says so) with fiction fields injected alongside real ones, both parse, no collision
    • null-tolerance: hand-modified capture with policy_details/metadata/policy_violations set to JSON null parses (see the runtime finding below)
    • action sent on the wire, request_type still sent when set
  • Full local suite: 1066 passed, 29 skipped (coverage 82.87 percent, gate 75)
  • ruff check + format: clean; falsey-clobber lint: clean (baseline refresh is line-number shifts only from a comment insertion in client.py, finding set unchanged at 51)
  • Live-stack leg against the session-3254 community v9.13.0 stack via the SDK's own search_audit_logs: 2/2 entries with policy_decision populated (blocked, allowed) while blocked=False/success=True/risk_score=0.0 stay at defaults on every entry, and AuditSearchRequest(action="blocked") returns exactly the blocked-verdict entry (server reads the filter; a request_type-only filter is a server-side no-op per the session-3254 capture audit-search-reqtype-noop.json)

Runtime E2E (real stack, new suite in this PR)

runtime-e2e/audit_real_wire_fields/test.py drives the real SDK against a real running community v9.13.0 agent (session-3254 stack): writes a success-shaped and a failure-shaped row via client.audit_tool_call, polls the real POST /api/v1/audit/search, and asserts on the TYPED model. Run output:

wrote rows: ok=audit_1785795797_z0585v3i fail=audit_1785795797_gwnbgznd
typed parse OK: ok.policy_decision='allowed' fail.policy_decision='error' fiction fields at defaults on both rows
action filter OK: 3 entries, all verdict 'error'
request_type no-op confirmed: nonsense filter still returned 11 rows including this run's
PASS: audit_real_wire_fields

Runtime finding the mock-based tests could not catch: the first live run FAILED with pydantic ValidationError: policy_details Input should be a valid dictionary [input_value=None] - the orchestrator marshals a nil Go map as JSON null on some real rows, so the new field must be NULL-tolerant, not merely absence-tolerant. Fixed with a mode="before" validator coercing null to {} on policy_details/metadata (and null to [] on policy_violations, same class), pinned by the null-tolerance unit test and re-proven by the passing runtime run above.

CHANGELOG

Unreleased section only, no version bump: Added policy_decision/policy_details/response_time_ms (read model) and action (search); Deprecated the seven fiction fields (read model) and request_type (search request) - removal rides the next major.

R3 round 2 fix (third commit)

  • _specs_dir(): set-but-EMPTY AXONFLOW_OPENAPI_SPECS_DIR now FAILS instead of reading as unset. An empty (or whitespace-only) value fell through the round-1 fix and still produced 7 silent skips + exit 0 - a CI consumer wiring the variable from an expression that evaluates empty would get a green gate, the exact class the fix targets. Choice: fail on set-but-empty (stated in the test comment); unset keeps the designed local-dev skip. Also pins the set-to-a-FILE direction (behaviorally covered by is_dir(), previously unpinned). Proven end-to-end: empty env = 7 errors, exit 1; unset = 7 skipped; valid dir = 12 passed. Suite now 1071 passed, 29 skipped.

R3 round 1 fixes (second commit)

  • CHANGELOG.md restored byte-for-byte from main except the Unreleased hunk. A global editor hook had rewritten 129 em/en dashes across ~250 lines of already-published release entries (9.0.0, 8.5.1, ...) - retroactive mutation of dated release records. The diff vs main is now exactly the 13-line Unreleased addition (13 insertions, 0 deletions, verified with git diff main --numstat).
  • tests/test_wire_shape.py _specs_dir(): a set-but-missing specs dir now FAILS instead of silently skipping. Previously AXONFLOW_OPENAPI_SPECS_DIR set to a nonexistent path produced 7 silent skips and exit 0 - a broken CI specs checkout read as a green gate. Now: env unset keeps the designed local-dev skip (proven: 7 skipped); env set + missing dir fails loudly naming the variable (proven: 7 errors, exit 1); env set + valid dir runs the gate (proven: 10 passed). Three guard tests pin all directions and run in the regular suite as well.

Items not modified in this PR (with justification)

  • No runtime DeprecationWarning on field access (see idiom note above): documented deprecation chosen per the #3254 interim contract.
  • query/query_hash (served on the wire) are not modeled: out of the #3254 canonical interim scope; noted in the query_summary deprecation text.
  • OpenAPI spec pin (openapi_specs_sha) untouched: the v9.13.0 pin refresh re-lands as chore(contract): bump OpenAPI spec pin to community v9.13.0 #214, stacked on top of this PR.

Refs getaxonflow/axonflow-enterprise#3254.

saurabhjain1592 added a commit that referenced this pull request Aug 3, 2026
Refreshes tests/fixtures/wire_shape_baseline.json via
scripts/refresh_wire_shape_baseline.py against a checkout of
getaxonflow/axonflow tag v9.13.0 (df027c788) docs/api. Stacked on the
#3254 additive interim (PR #223): the audit fiction fields and the
request_type search filter stay as consciously noted
deprecated-pending-removal drift entries naming #3254 and the
coordinated next-major burn-down.

Regen result: 12 per-model drift entries, MCPCheckInputRequest drops
out (spec now declares content_type + tool), 14 newly registered
models, spec-bug-pending count 0.

Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
…onse_time_ms, deprecate the seven fiction fields (#3254)

Additive interim per the operator decision on
getaxonflow/axonflow-enterprise#3254: the orchestrator has never served
query_summary/success/blocked/risk_score/latency_ms/policy_violations/
metadata on the 9.x line; the real wire carries policy_decision,
policy_details and response_time_ms. AuditSearchRequest gains action
(the filter the 9.x server actually reads); request_type is deprecated
on the search request (silently ignored server-side).

- AuditLogEntry: three new defaulted fields; docstring deprecation
  notes on the seven fiction fields (kept, still parse, removal rides
  the next major); policy_details/metadata/policy_violations are
  null-tolerant (the orchestrator marshals a nil Go map/slice as JSON
  null - observed live, caught by the runtime-e2e suite)
- AuditSearchRequest.action wired into _build_audit_search_body;
  request_type still sent when set (harmless, ignored)
- tests: real v9.13.0 capture fixture (session 3254) + old-server
  tolerance + both-present + null-tolerance, action filter on the wire
- runtime-e2e/audit_real_wire_fields: real-stack proof (typed parse of
  fresh rows, action filter read server-side, request_type no-op)
- wire_shape_baseline: curated sdk_only entries for the new fields
  against the pre-v9 spec pin, notes name #3254 and the PR #214 pin
  bump that clears them
- falsey_clobber baseline: line-number refresh only (comment insertion
  in client.py shifted 15 entries; finding set unchanged at 51)

Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
@saurabhjain1592
saurabhjain1592 force-pushed the feat/3254-audit-real-wire-fields branch from 97be544 to 5d99c68 Compare August 3, 2026 22:25
saurabhjain1592 added a commit that referenced this pull request Aug 3, 2026
Refreshes tests/fixtures/wire_shape_baseline.json via
scripts/refresh_wire_shape_baseline.py against a checkout of
getaxonflow/axonflow tag v9.13.0 (df027c788) docs/api. Stacked on the
#3254 additive interim (PR #223): the audit fiction fields and the
request_type search filter stay as consciously noted
deprecated-pending-removal drift entries naming #3254 and the
coordinated next-major burn-down.

Regen result: 12 per-model drift entries, MCPCheckInputRequest drops
out (spec now declares content_type + tool), 14 newly registered
models, spec-bug-pending count 0.

Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
…oudly on a set-but-missing specs dir

R3 round 1 follow-ups on #223:

- CHANGELOG.md: a global editor hook had rewritten 129 em/en dashes
  across ~250 lines of already-published release entries (9.0.0, 8.5.1,
  ...) - retroactive mutation of dated release records. Restored from
  main byte-for-byte; the diff vs main is now exactly the 13-line
  Unreleased hunk (13 insertions, 0 deletions).

- tests/test_wire_shape.py _specs_dir(): when
  AXONFLOW_OPENAPI_SPECS_DIR was SET but pointed at a missing
  directory, all 7 gate tests skipped silently with exit 0 - a broken
  CI specs checkout read as a green wire-shape gate. That case now
  pytest.fail()s with a clear message (7 errors, exit 1); an UNSET
  variable keeps the designed local-dev skip. Three guard tests pin
  all directions (unset skips, set+missing fails naming the variable,
  set+valid resolves); they run in the regular suite too.

Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
saurabhjain1592 added a commit that referenced this pull request Aug 3, 2026
Refreshes tests/fixtures/wire_shape_baseline.json via
scripts/refresh_wire_shape_baseline.py against a checkout of
getaxonflow/axonflow tag v9.13.0 (df027c788) docs/api. Stacked on the
#3254 additive interim (PR #223): the audit fiction fields and the
request_type search filter stay as consciously noted
deprecated-pending-removal drift entries naming #3254 and the
coordinated next-major burn-down.

Regen result: 12 per-model drift entries, MCPCheckInputRequest drops
out (spec now declares content_type + tool), 14 newly registered
models, spec-bug-pending count 0.

Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
…in the set-to-a-file direction

R3 round 2 on #223: an EMPTY (or whitespace-only) value read as unset
and produced the same 7 silent skips + exit 0 the round-1 fix targeted -
a CI consumer wiring the variable from an expression that evaluates
empty would get a green gate. Choice: FAIL on set-but-empty (rather
than pinning empty-as-unset), stated in the test comment; unset keeps
the designed local-dev skip. Also pins the set-to-a-FILE direction
(behaviorally covered by is_dir(), previously unpinned) so a refactor
to exists() cannot reopen the class. Proven end-to-end: empty env = 7
errors exit 1; unset = 7 skips; valid dir = 12 passed.

Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
saurabhjain1592 added a commit that referenced this pull request Aug 3, 2026
Refreshes tests/fixtures/wire_shape_baseline.json via
scripts/refresh_wire_shape_baseline.py against a checkout of
getaxonflow/axonflow tag v9.13.0 (df027c788) docs/api. Stacked on the
#3254 additive interim (PR #223): the audit fiction fields and the
request_type search filter stay as consciously noted
deprecated-pending-removal drift entries naming #3254 and the
coordinated next-major burn-down.

Regen result: 12 per-model drift entries, MCPCheckInputRequest drops
out (spec now declares content_type + tool), 14 newly registered
models, spec-bug-pending count 0.

Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
@saurabhjain1592
saurabhjain1592 merged commit 0fb80ac into main Aug 4, 2026
18 checks passed
@saurabhjain1592
saurabhjain1592 deleted the feat/3254-audit-real-wire-fields branch August 4, 2026 07:28
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