Skip to content

Admin governance-config round-trip 400s in gateway mode: saving the document the API just served is rejected #29

Description

@cubeorgdev

What happened

In a deployment where the control API has a gateway configured, the admin governance-config
round-trip is broken: PUTting back the exact document GET /admin/governance-config just
returned is rejected with a 400.

{"error":"gateway mode requires capability `gateway_inference_jwt`"}

Mechanism, in services/control-api/src/lib.rs:

  1. update_governance_config overwrites whatever the client sent with the server's own gateway
    policy: next.gateway = configured_gateway_policy(state.config.gateway_kind.as_deref())
    (lib.rs:6174). This is the intended "the server decides the operating mode" invariant, so in a
    gateway-mode deployment next.gateway is always Some(..) no matter what is submitted.
  2. validate_complete_governance then requires gateway_inference_jwt in required_capabilities
    whenever gateway.is_some() (lib.rs:6493-6500).
  3. required_capabilities is taken from the submitted YAML — but the document served by GET
    doesn't carry it (extension_free_yaml serializes GovernanceConfig, where the field is
    skip_serializing_if = "Vec::is_empty").

So any admin write that doesn't manually re-add the capability fails, including a verbatim
save-what-you-loaded. The two specs that do work around it
(gateway-m2m.spec.ts:223, zz-dashboard-filtering.spec.ts:282) each hand-union
gateway_inference_jwt in before saving, which reads as a workaround for this rather than
something a dashboard user could know to do.

Caught by @smoke /direct tears down gateway wiring and offers an agent reload
(tests/e2e/specs/journey.spec.ts:123), which fails on its restore step at the end:

const restoreResponse = await page.request.put(`${control}/admin/governance-config`, {
  data: { base_revision: current.revision, managed_yaml: original.managed_yaml },
});
expect(restoreResponse.status(), await restoreResponse.text()).toBe(200);  // 400

original.managed_yaml is unmodified API output, so no client-side mistake is involved.

I confirmed this is not a regression: the capability gate and both workaround specs all landed
together in #15 (e76b777), and the failure reproduces on main unchanged. It has been invisible
because the e2e workflows have never started — see #25 / #26. It is the first real smoke failure
the repo has ever produced.

I first tried patching the test to union the capability in (like its two siblings do). That moved
the failure from the first PUT to the restore PUT and nothing more, which is what pointed at the
API rather than the test — so I reverted it. Fixing this properly is a product decision I don't
want to guess at, roughly: have the server union the capability it implies when it stamps
gateway, or always serialize required_capabilities, or validate against the client-supplied
gateway rather than the server-forced one.

Steps to reproduce

1. Run a control API with a gateway kind configured (the e2e main stack does this).
2. GET /admin/governance-config  -> note `managed_yaml` and `revision`
3. PUT /admin/governance-config with { base_revision: <revision>, managed_yaml: <that same string> }
4. Observe 400: gateway mode requires capability `gateway_inference_jwt`

Or, in CI: run the smoke e2e tier and watch journey.spec.ts:123 fail all three attempts —
https://github.com/BlocksOrg/blue/actions/runs/34543025885

Blue version

0.1.0 (workspace version at commit 5dcc5cb)

OS / environment

Reproduced on ubuntu-latest GitHub-hosted runners via tests/e2e/run.sh smoke; the control API
is the deploy/Dockerfile image, gateway mode enabled.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions