Skip to content

feat(types): wire-shape alignment — WebhookSubscription.secret (security) + Cat B start#138

Merged
saurabhjain1592 merged 6 commits into
mainfrom
fix/wire-shape-alignment-sweep
Apr 25, 2026
Merged

feat(types): wire-shape alignment — WebhookSubscription.secret (security) + Cat B start#138
saurabhjain1592 merged 6 commits into
mainfrom
fix/wire-shape-alignment-sweep

Conversation

@saurabhjain1592

Copy link
Copy Markdown
Member

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

  • `WebhookSubscription.secret` — HMAC-SHA256 signing key surfaced on `createWebhook` response. Required to verify the `X-AxonFlow-Signature` header on inbound webhook deliveries; without it, Java SDK consumers can't validate payload authenticity. The 6-arg constructor is preserved as a source-compat overload that delegates to the new 9-arg variant with nulls for the new fields. `toString()` redacts the secret to avoid log leakage.
  • Also adds `WebhookSubscription.tenantId` and `orgId` (ownership scoping).

Cat B start

  • `BudgetAlert.acknowledged` — alert dismissal flag. Also adds `@JsonProperty` annotations to previously-unannotated fields (`id`, `threshold`, `message`) so the wire-shape gate's discovery sees them. (Jackson's default name-mapping was correct, but the validator walks `@JsonProperty` only.)

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)

  • axonflow-enterprise#1708 — `AISystemRegistry.materiality_classification` (server emits the SDK's name; spec is wrong)
  • axonflow-enterprise#1709 — `DynamicPolicyInfo` schema completely wrong shape (server, all 4 SDKs agree; spec drift)

Verification

  • `mvn compile`: ✅
  • `mvn test`: ✅ 1200 tests pass
  • Wire-shape baseline regenerated: 39 → 37 drift entries
  • Tests cover existing 6-arg `WebhookSubscription` constructor (source-compat preserved)

Test plan

  • `mvn test` passes locally
  • Compile clean
  • Baseline regenerated and committed
  • CI on this PR is green

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.
@saurabhjain1592 saurabhjain1592 merged commit 65cd8aa into main Apr 25, 2026
11 checks passed
@saurabhjain1592 saurabhjain1592 deleted the fix/wire-shape-alignment-sweep branch April 25, 2026 12:04
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