feat(types): wire-shape alignment — WebhookSubscription.secret (security) + Cat B start#138
Merged
Merged
Conversation
Adds a CI gate that fails any PR introducing drift between Java @JsonProperty annotations and the OpenAPI specs pinned at tests/fixtures/wire-shape-baseline.json::openapi_specs_sha. Four gates: 1. Cross-spec schema divergence — same schema name declared with different shapes across spec files. 2. Intra-file duplicates — same schema name declared twice in one spec file (PolicyMatch in orchestrator-api.yaml is the existing example, baselined for now). 3. Per-type SDK-vs-spec drift — wire field names diff between Java @JsonProperty and the spec, baseline-aware. 4. Registered-type rename-escape — types in the baseline that disappear from either side fail the gate. The pinned spec SHA is itself guarded by a `spec-pin-bump` PR label so a single PR can't both move the SHA and silence drift. Source discovery walks brace depth on string- and comment-stripped text so annotations are attributed to the innermost enclosing class/record/interface/enum rather than the file's outer class. Without this, types like WorkflowTypes.CreateWorkflowRequest and WorkflowTypes.RetryContext (10+ wire types nested inside the WorkflowTypes namespace class) would silently escape coverage. Initial baseline at SHA bf1ca22: - 70 registered Java<->OpenAPI type pairs - 39 per-type drift entries (burndown follow-ups) - 8 cross-spec divergences (platform-side reconciliation tracked separately) - 1 intra-file duplicate (PolicyMatch) Mirrors the Python, Go, and TypeScript wire-shape gates. Re-baseline: python3 scripts/wire_shape/refresh.py /path/to/community/docs/api
- load_baseline raises SystemExit with regen hint when the JSON file
is malformed instead of dumping an opaque traceback.
- write_baseline cleans up its tmp sidecar on any exception so a
crashed run can't poison the next refresh from the same PID.
- The validator now exits 1 (not 0) when AXONFLOW_OPENAPI_SPECS_DIR
is set but doesn't point at a directory. A misconfigured CI step
silently disabling the gate produces a green check on a non-running
validator, which we refuse to do. An unset env still skips with 0.
- Workflow's SHA-bump guard distinguishes "baseline missing on base
branch" (genuine first-pin introduction) from "baseline present
but unparseable" (bypass attempt). A malformed baseline on the
base branch can no longer route a labelless PR through the
first-pin path.
- Java source parser:
* Recognises Java 15+ text blocks (`"""..."""`). Previously the
first `"""` looked like quote-empty-quote and the body parsed
as live source — fake `@JsonProperty(...)` and `class X {`
inside a text block silently corrupted attribution.
* Filters annotation matches whose position falls inside a
blanked range (string, comment, text block) so the wire-name
regex on raw content can't grab annotations the structural
pass already nullified.
* Attributes record-parameter annotations to the record. Pending
decls are now tracked between their token and the body `{`,
so `record Foo(@JsonProperty("x") int x) {}` no longer drops
the wire name. The SDK has no records today; this is forward
defence for the next type-class refactor.
- Documents the remaining anonymous-inner-class limitation (no
`class` keyword, so annotations would leak to the enclosing type).
The SDK does not put @JsonProperty inside anonymous inners.
Synthetic-fixture tests cover record params, text blocks with fake
annotations, line and block comments, nested classes, and enum
values. Real Java SDK baseline unchanged: 70 registered types, 39
drift, 8 cross-spec, 1 intra-file.
Gate 1 (cross-spec divergence) only iterated currently observed schemas. A baselined divergence that the platform has since reconciled silently lingered in the baseline forever; the same old incompatible shape could be reintroduced and pass the gate because its fingerprint matched a stale entry that should have been deleted. Adds the reverse pass that Gate 2 already does for intra-file duplicates: any baselined cross-spec name that is no longer observed in the current specs fails the run with a pointer at the specific baseline key to delete. Verified locally: - Positive run on clean baseline still exits 0. - Adding a phantom 'PhantomDivergence' entry to baseline.cross_spec_ duplicates causes the validator to exit 1 with a clear "remove from baseline" message naming the stale key.
Audit-driven sweep against the wire-shape contract gate. Initial PR focuses on the security-critical addition; the larger Cat B work across other types is tracked in a follow-up issue (Java SDK has the largest wire-shape gaps of the four SDKs and parts of the sweep need the validator-discovery fix below before they can ship without false-positive baseline noise). Changes: - WebhookSubscription.secret (security): HMAC-SHA256 signing key surfaced on the createWebhook response. Required to verify the X-AxonFlow-Signature header on inbound deliveries; without it, callers can't validate payload authenticity. Also adds tenantId and orgId (ownership scoping). The 6-arg constructor is preserved as a source-compat overload that delegates to the 9-arg variant with nulls for the new fields. toString() redacts the secret to avoid log leakage. - BudgetAlert.acknowledged: alert dismissal flag. Also adds @JsonProperty annotations to previously-unannotated fields (id, threshold, message) so the wire-shape validator's discovery sees them; Jackson's default name-mapping was correct in those cases, but the validator currently walks @JsonProperty only. The validator's missing field-name fallback for unannotated fields is documented in a follow-up issue. Validator findings: The audit surfaced 39 drift entries (37 after this PR). Most remaining entries fall into one of: a) SDK types missing many wire fields (StaticPolicy +13, CreateStaticPolicyRequest +11, UsageRecord +11, Budget +6, etc.) — this is real coverage gap, not measurement error. b) SDK types where most fields exist but lack @JsonProperty — the validator under-counts these. Filed for validator fix. c) RENAME_SAFE / orphan-read entries similar to the TS+Go sweep (DynamicPolicyMatch.reason, ExfiltrationCheckInfo.within_limits, PolicyOverride.active, etc.). Filed alongside (no SDK change needed; spec is wrong): - axonflow-enterprise#1708 — AISystemRegistry.materiality_classification - axonflow-enterprise#1709 — DynamicPolicyInfo schema wrong shape Tests: 1200 pass.
The Java validator's source-discovery only saw fields with @JsonProperty(...) annotations. Many Java SDK POJOs declare fields without the annotation and rely on Jackson's default name-mapping (which works correctly when the Java field name matches the wire key — `id`, `threshold`, `message`, `name`, etc.). The validator under-counted SDK fields in those cases. Three changes to lib.py: - _FIELD_DECL_RE matches plain field declarations: modifier-prefix + type-expression + field-name + `;`/`=`. Captures the modifier sequence so the next change can post-filter. - Filter out declarations whose modifier sequence contains `static` (those are class-level constants — never wire-serialized). - _extract_types_from_java now collects both annotated (@JsonProperty) AND plain field decls. Each plain field's "claim" on a preceding @JsonProperty within a 250-char lookback window is consumed — whichever annotation/field pair is closest. Annotation wins on the wire-name when a field is annotated; field name is used directly when the field is unannotated. Synthetic tests cover: - Mixed annotated + unannotated POJO (BudgetAlert pattern) - `private static final String CONST = "..."` excluded - Methods (have `(`) excluded - Generics like `Map<String, Object>` and `List<String>` - Record params with `@JsonProperty` - Class with both constants and fields Effect on Java SDK baseline: - registered_types: 70 → 73 (3 types newly visible; previously had no @JsonProperty annotations so the validator skipped them) - per_type_drift: 37 → 39 (newly visible types surfaced 2 more drift entries; previously hidden coverage gaps) The drift increase is the validator working correctly for the first time — the previous count was an under-measurement, not a real "no drift" signal.
Earlier in this PR I added @JsonProperty("secret"), @JsonProperty("tenant_id"), and @JsonProperty("org_id") to WebhookSubscription's @JsonCreator constructor and folded them into equals()/hashCode()/toString(). The field additions are additive, but the equality contract change is not — and the user review caught it. Concrete impact: WebhookSubscription localView = new WebhookSubscription( "wh-1", "https://example.com", List.of("e"), true, "2026-01-01", "2026-01-01"); // 6-arg ctor WebhookSubscription serverView = client.getWebhook("wh-1"); // includes secret etc. // Pre-PR: localView.equals(serverView) → true // After the field-addition step: localView.equals(serverView) → false // localView.hashCode() != serverView.hashCode() // → Set membership broken, Map keying broken, identity caches split. Fix in this commit: equals() and hashCode() now use only `id`. A WebhookSubscription is an entity, not a value object — two instances with the same id represent the same subscription, regardless of which fields are populated in this particular view. Callers needing content-equality (e.g. detecting a rotated secret) should compare getters directly. This restores the additive claim of the PR (no observable behavior change for callers comparing the same logical webhook), while preserving the value-add of exposing secret + scoping fields for HMAC verification. toString() is unchanged (still shows full state with secret redacted as '***'). Tests: 1200 pass.
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.
Summary
Initial wire-shape alignment PR for the Java SDK. Focuses on security-critical adds + a small Cat B start. Mirrors the broader sweep on the TS (axonflow-sdk-typescript#185) and Go (axonflow-sdk-go#133) SDKs.
The Java SDK has the largest wire-shape gaps of the four SDKs (39 drift entries pre-fix vs TS 41, Go 43, Python 0). Many entries surface coverage gaps where Java is missing 10+ wire fields per type (e.g. `StaticPolicy` is missing 13 of the spec's fields). Tracking the full burndown in a follow-up issue rather than ballooning this PR — both because of volume and because the validator has a measurement gap (see below) that should be fixed first to avoid false-positive baseline churn.
What's in this PR
Security-critical
Cat B start
Validator measurement gap (filed for follow-up)
The Java validator's source-discovery (`scripts/wire_shape/lib.py`) only sees fields with `@JsonProperty("...")` annotations. Many Java SDK POJOs declare fields without the annotation and rely on Jackson's default name-mapping (which works correctly when Java field names match wire keys). The validator under-counts SDK fields in those cases and reports false drift.
This affects ~half of the 39 drift entries. The fix is to extend `_extract_types_from_java` in `scripts/wire_shape/lib.py` to also recognize plain field declarations (`private String foo;`) and use the field name as the wire name. Filing as a follow-up so we can land the safe security fix first.
Filed alongside (no SDK change needed; spec is wrong)
Verification
Test plan