Skip to content

1.0.0 blocker: unknown enum values fail to decode, contradicting the additive-change guarantee #366

Description

@colbylwilliams

Summary

versioning.md guarantees that additive changes are safe within a MAJOR:

Within a compatible range, additive changes — new optional fields on existing types, new action types, new commands — are introduced in PATCH (or MINOR, while MAJOR is 0) bumps and MUST be ignored by older peers that do not understand them.

The reference clients do not honour that guarantee for enum values. Adding a value to any existing discriminant enum causes a whole-message decode failure on a peer built against an earlier version — not a field-level degrade.

Because SemVer attaches the compatibility promise at 1.0.0, and because a client that pinned 1.x is still broken by a 1.y enum addition, this has to be fixed before the guarantee takes effect. After 1.0 every enum extension effectively becomes a MAJOR change.

Evidence

Deserializing crafted payloads against clients/rust/crates/ahp-types at main (7144635c):

Payload Result
Unknown extra field on an existing shape ✅ tolerated
Unknown action type ✅ tolerated
Unknown object-union variant (ToolCallState, MessageAttachment, …) ✅ tolerated
Unknown enum value on a directly-typed field hard failure
AuthRequiredReason   = "unauthorized" -> unknown variant `unauthorized`, expected `required` or `expired`
TurnState            = "paused"       -> unknown variant `paused`, expected one of `complete` …
ChangesetStatus      = "stale"        -> unknown variant `stale`, expected one of `computing` …
McpAuthRequiredReason= "revoked"      -> unknown variant `revoked`, expected one of `required` …
ResourceChangeType   = "renamed"      -> unknown variant `renamed`, expected one of `added` …
ConfirmationOptionKind = "defer"      -> unknown variant `defer`, expected `approve` or `deny`
MessageOrigin.kind   = "integration"  -> unknown variant `integration`, expected one of `user`, `agent`, `tool`, `systemNotification`

Repro:

// ahp-types from clients/rust
use ahp_types::notifications::AuthRequiredParams;
let json = r#"{"channel":"ahp-root://","resource":"https://example.com","reason":"unauthorized"}"#;
serde_json::from_str::<AuthRequiredParams>(json).unwrap(); // panics

Why this looks like an oversight rather than a decision

The forward-compatibility intent is clearly already there — it just stops one level short:

  • 19 polymorphic unions in ahp-types carry an explicit catch-all: /// Unknown or future variant — preserved as raw JSON for round-trip fidelity.Unknown(serde_json::Value).
  • Even bitsets preserve unknown bits (state.rs: "Unknown/forward-compat bits are preserved across a decode→encode round-trip").
  • But the 40 pure string enums — including the discriminants that select among those very unions — have no equivalent.

So an unknown ToolCallState variant degrades gracefully, while an unknown ToolCallStatus, TurnState, or AuthRequiredReason on a directly-typed field takes the whole message down.

versioning.md also only spells out the ignore-unknown rule for one case — "clients SHOULD silently ignore actions with unrecognized type values" — leaving nested discriminants, enum values on plain fields, and unknown error codes as implied-but-unstated.

Impact

This is not hypothetical for 1.0 — several open proposals extend existing enums, e.g. #362 (MessageAttachmentKind.Collection), #287 (a new CustomizationType), and any additional AuthRequiredReason for auth states that are neither "no token" nor "expired". Under the current clients each of those is a breaking change dressed as an additive one.

It also affects hosts and clients asymmetrically. Hosts are often remote and updated on their own schedule — exactly the case versioning.md calls out — so a host that adopts a newer MINOR can hard-break older clients that were promised compatibility.

Proposed fix (pre-1.0)

  1. Give every discriminant enum an unknown-value escape hatch in every generated client — Rust #[serde(other)] Unknown, and the TypeScript / Kotlin / Swift / Go equivalents — mirroring the treatment the polymorphic unions already get. Preserving the raw value (rather than collapsing to a sentinel) keeps round-trip fidelity for relays and proxies.
  2. State the rule in versioning.md for all discriminated unions, enum-valued fields, and unknown error codes — not just action type. Something like: a peer receiving an unrecognized enum value MUST NOT fail the message; it MUST preserve the value for round-trip and treat the carrier as unknown/no-op.
  3. Add conformance vectors so the behaviour is testable per client rather than incidental.
  4. Decide the companion question explicitly: whether command params and results are guaranteed to tolerate unknown top-level fields. Nothing in schema/ sets additionalProperties: false today, so they do in practice — but that tolerance is currently accidental, and tightening it after 1.0 would itself be breaking.

Happy to help with the Rust side if that's useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions