Skip to content

fix(platform-sdk): decode nullable open strings as scalars (ALIEN-1310) - #930

Merged
lilienblum merged 1 commit into
mainfrom
dan/alien-1310-staging-kubernetes-remote-operator-registrations-stay
Oct 8, 2026
Merged

lilienblum merged 1 commit into
mainfrom
dan/alien-1310-staging-kubernetes-remote-operator-registrations-stay

Conversation

@lilienblum

@lilienblum lilienblum commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Fixes ALIEN-1310.

What changes for users

Kubernetes Remote Operators report heartbeats again through a hosted manager. Since the Platform client was regenerated in #843, every Kubernetes Operator sync failed in the manager:

Failed to reconcile agent-reported state ... JSON deserialize failed: invalid type: string "container",
expected struct ObservedInventoryBatchResourcesItemResourceTypeHint

The manager then never reconciled with the Platform API. New registrations stayed pending, and existing deployments stopped recording lastHeartbeatAt.

Cause

In the regenerated spec, ObservedResourceSample.resourceTypeHint is anyOf [string, null]. Progenitor generates a struct of flattened optionals for anyOf, and that struct rejects every non-null string. #843 already rewrote closed string enums plus null to oneOf, but not open strings. The Kubernetes observer always sends "container".

Change

normalize_nullable_strings (renamed from normalize_nullable_string_enums) now treats any non-nullable string branch, open or enum, as disjoint from the null branch, so the union becomes oneOf and generates a scalar type. I scanned the generated full client: this was the only flattened scalar union. The filtered client is unaffected.

Verification

  • New tests/observed_inventory.rs (full-api) round-trips an alien_core::ObservedInventoryBatch with resourceTypeHint: "container" and None through the generated type. Before the fix it fails with the error above; after the fix it passes.
  • cargo test --locked -p alien-platform-api and cargo test --locked -p alien-platform-api --features full-api both pass.
  • No generated files change: the SDK generates at build time.

Rollout

Managers built with this client decode the inventory again; no Operator change is needed.

The full Platform client generated ObservedResourceSample.resourceTypeHint
(anyOf [string, null]) as a struct of flattened optional fields, which
rejects any string. The hosted manager therefore failed every Kubernetes
Operator reconcile that carried a resource type hint ("container"), and
those deployments stopped recording heartbeats.

Treat a non-nullable open string and null as disjoint, as was already
done for closed string enums, so the union becomes oneOf and generates a
scalar type.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T17:14:42.788930Z ee1c3a0 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Changes code generation logic for API client types.

The PR appears safe to merge.

What we checked:

  • String and null stay separate: The string branch must have type set to "string" and must not have nullable set to true. The other branch must exactly match the null-only schema. Their accepted values do not overlap.

Summary

The Rust Platform SDK now rewrites nullable open-string unions from anyOf to oneOf, alongside the existing string-enum case. This lets generated types accept scalar strings such as "container".

  • Adds a full-api round-trip test for inventory batches with present and absent resource_type_hint values.
  • Keeps the existing checks that prevent rewriting overlapping branches.
  • No actionable issues found. Tests were not run during this review.

Reviews (1) · Last reviewed commit: "fix(platform-sdk): decode nullable open ..." · Reviewed by Greptile

@lilienblum
lilienblum merged commit ec68b70 into main Oct 8, 2026
24 checks passed
@lilienblum
lilienblum deleted the dan/alien-1310-staging-kubernetes-remote-operator-registrations-stay branch October 8, 2026 03:35
alongubkin added a commit that referenced this pull request Oct 10, 2026
Fixes ALIEN-1384.

## Problem

`alien deployments events` and `alien managers events` fail on every
event with CLI 3.3.29 through 3.3.31:

```
Failed to parse response: invalid type: string "success", expected struct EventListItemResponseState
```

## Root cause

The server is correct: it returns `alien_core::EventState` (`"none"`,
`"started"`, `"success"`, `{"failed": {...}}`). When the Platform spec
was regenerated in #843, `Event.state` changed from `oneOf` to `anyOf`.
Progenitor turns `anyOf` into a struct of flattened optional fields, and
a struct cannot decode a plain string. #930 fixed a similar case (string
plus null), but not this one.

## Fix

`normalize_disjoint_tagged_unions` now also changes an `anyOf` to
`oneOf` when every branch is either a string enum or an object with
exactly one required property, and no tag repeats. When there is more
than one object branch, each must set `additionalProperties: false`, or
one value could match two branches. This is how serde encodes an
externally tagged enum, so at most one branch can match. typify then
generates `enum EventState { Failed { error }, None, Started, Success
}`. In the generated filtered client, the only other union that changes
is `SetupLinkEntryPoint.provider` (two string enums, request only, no
Alien caller). Every flattened struct left in that client has only
object branches.

Alternatives considered: opening `state` to untyped JSON (the CLI only
displays it) would also tolerate future states, but it drops typing for
SDK users and `EventState` has had the same four variants since the
start. Changing the platform spec back to `oneOf` would be undone by the
next regeneration.

## Proof

- New `tests/event_state.rs` decodes the event payload as the API
returns it, with every `alien_core::EventState` variant, through `Event`
(manager events) and `EventListItemResponse` (deployment events), and
checks the state round-trips. On main both tests fail with `invalid
type: string "none", expected struct EventState`; with the fix both
pass.
- A filter test checks that open object branches, repeated tags and
nullable branches stay `anyOf`.
- `cargo test -p alien-platform-api`, with and without `--features
full-api`, and the `alien-cli` event tests pass.
- CLI built from this branch, run against the staging API: `alien
deployments events <dep>` prints the table (`success` / `none` rows).
The released CLI fails on the same deployment with the error above.
Staging has no user-managed manager right now, so `managers events` is
covered by the test (same `Event` type).

## Rollout

CLI-only. Ships with the next CLI release; no server change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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