Skip to content

RFC: report compute providers and release stage per Namespace - #867

Draft
rossnelson wants to merge 4 commits into
mainfrom
propose-compute-provider-status
Draft

RFC: report compute providers and release stage per Namespace#867
rossnelson wants to merge 4 commits into
mainfrom
propose-compute-provider-status

Conversation

@rossnelson

@rossnelson rossnelson commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

RFC — not for merge as-is

Proposes one source of truth for which compute providers a Namespace can use, whether each is enabled, and what release stage each is in — replacing the pattern of adding a boolean to Capabilities per provider.

Revised after review. The first draft put this on GetSystemInfoResponse. @Quinn-Klassen pointed out that will not work, because Cloud serves that endpoint as a fixed response at the reverse proxy layer, and suggested a Namespace-scoped surface. That is what this now does, and the objection turned out to be right on the mechanism and on the correct scope. History is kept so the argument is reviewable.

The AgentCore work this came out of does not depend on this and is not blocked by it.

The problem with a boolean per provider

server_scaled_provider_cloud_run = 13 (#799) works, but it only answers "is this one provider usable, Service-wide". Three costs follow.

A new provider needs a full release train before a client can even gate on it. Proto field → api release → server release. Cloud Run is the worked example: the UI shipped the option in temporalio/ui#3518 on 2026-06-12, the capability field landed 2026-07-06, and the UI could not consume it until an api bump on 2026-08-13. Two months, for a boolean.

It carries no release stage, so every client hardcodes its own. temporalio/ui has this table today:

export const defaultReleaseStage = {
  lambda: 'public-preview',
  agentcore: 'pre-release',
  'cloud-run': 'pre-release',
};

Moving Lambda to GA is a UI release. cloud-ui keeps a parallel list, the CLI keeps one for scaler pairing, and temporal-auto-scaled-workers — the actual authority — keeps the real one.

Service scope cannot express what is actually true. Provider availability varies per Namespace: a provider is tied to the cloud its Namespace runs in. cloud-ui already encodes this, deriving the list from the Namespace's RegionID_CloudProvider — an AWS Namespace offers Lambda and AgentCore, a GCP Namespace offers Cloud Run. A single Service-wide boolean has no way to say that.

All three are live right now:

Proposed shape

message ComputeProviderStatus {
    // Matches ComputeProvider.type — the provider's identity, not a display name.
    string type = 1;
    // Whether a ComputeConfig naming this provider will be accepted here.
    bool enabled = 2;
    temporal.api.enums.v1.ComputeProviderReleaseStage release_stage = 3;
}

on NamespaceInfo, alongside the existing capabilities and limits:

repeated temporal.api.compute.v1.ComputeProviderStatus compute_providers = 9;

A Service adds a provider without a proto change. A provider moves from pre-release to GA without a client release. Availability can differ per Namespace. And clients drop their hardcoded tables.

Why NamespaceInfo, concretely

Both halves of Quinn's point check out in the client code:

  • GetSystemInfo does not reach Cloud. temporalio/ui's fetchSystemInfo returns {} outright when isCloud, and cloud-ui synthesizes the entire systemInfo object client-side from account feature flags rather than calling the endpoint. A per-provider boolean there is not a reachable signal.
  • NamespaceInfo does. cloud-ui already reads namespace.namespaceInfo?.capabilities in standalone-nexus-guard.svelte. NamespaceInfo.Capabilities is a live, actively growing surface — 17 fields, most recently standalone_activity_operator_commands.

Cloud already has a working model for this, and it is two-key

Worth spelling out, because it decides whether the server populating this is
useful or academic.

NamespaceInfo.capabilities is not synthesized by cloud-ui the way
systemInfo is. cloud-ui fetches the real namespace from the data plane and
reads the capabilities off the wire (src/lib/services/settings.ts):

`${webUrl}/api/v1/namespaces/${namespace?.namespace}`
 dataplaneNamespace?.namespaceInfo?.capabilities

It then overlays account state on top, with two distinct idioms:

capabilities: {
  ...dataplaneNamespace?.namespaceInfo?.capabilities,
  workerHeartbeats: dataplane...?.workerHeartbeats ?? true,
  // TODO: Update these to use the dataplaneNamespace value when feature flag is removed
  standaloneActivityStartDelay: standaloneActivitiesGAEnabled,
  // dataplaneNamespace?.namespaceInfo?.capabilities?.standaloneActivityStartDelay ?? true,
}

?? true for capabilities Cloud knows are universally on, so an older data
plane that does not report one is treated as capable. Feature-flag
substitution for a capability still rolling out, with the real read commented
out beneath it and a TODO to swap at GA.

And consumers AND the two keys rather than choosing between them
(standalone-nexus-guard.svelte):

namespace.namespaceInfo?.capabilities?.standaloneNexusOperation &&
  $CurrentUser.hasFeatureFlag('enable_standalone_nexus_operations')

The namespace capability answers is this possible here; the account flag
answers is this account permitted. That is the shape a provider list wants
too, and it already works.

The standaloneActivityStartDelay lines are also, by hand, exactly the
lifecycle this proposal describes: flag first, server-reported value later,
with the swap tracked in a comment. release_stage is what lets the server say
"this is generally available now" instead of a client releasing to delete a
TODO.

Prior art: this package started typed and moved away from it

api#704 introduced compute.v1 on the serverless branch in February, and
#752 reshaped it when that branch merged to master. Two things did not survive,
and both bear on this proposal.

Per-provider config was typed, then deliberately untyped. #704 carried a
ProviderDetailAWSLambda message holding the function ARN and an optional role
ARN. Today ComputeProvider.details is an opaque Payload. So a reviewer
should ask why this proposes a typed message after the package moved the other
way.

The answer is that those two things are different in kind. Provider config is
open-ended and genuinely provider-specific — a Lambda ARN, a Cloud Run worker
pool, an AgentCore endpoint ARN share no shape, and every new provider would
otherwise cost a proto message. Provider status is uniform: every provider has
an identity, an enabled bit, and a release stage, and no provider needs a field
the others do not. Untyping config was right for the same reason typing status
is: the shape either varies per provider or it does not.

ComputeConfig.task_queues was removed. #704 declared task queues as
name-and-type tuples; main keeps only task_queue_types. That is why a
serverless Version has no task queue until something teaches matching one: the
first invoke starts a Worker, it polls with versioning, and that registration
is the association. Nothing in the current API can state it up front. Not this
proposal's problem, but it is the reason create-version takes no task queue,
which surprises everyone who meets it.

Two other deliberate choices

It reports disabled providers too, rather than omitting them. That lets a client distinguish "this Namespace has never heard of AgentCore" from "it knows AgentCore but it is off here" and say so, instead of silently dropping the option. It is the difference between a "Coming Soon" badge and a provider that appears not to exist.

A repeated message rather than more booleans in NamespaceInfo.Capabilities. Booleans there would inherit the same two problems — a proto field per provider, and no release stage.

server_scaled_provider_cloud_run is kept for wire compatibility and marked superseded, with a note recording why no further per-provider booleans belong on that response.

Open questions for reviewers

  1. NamespaceInfo, or a dedicated RPC? NamespaceInfo is where namespace capability data already lives and already reaches Cloud, so it is the cheap and consistent choice. A ListComputeProviders would be more discoverable and could carry more per-provider detail later.

  2. Should release_stage live in the api at all? It is arguably product metadata, not a capability. The counter-argument is that it is already in every client, just duplicated and going stale.

  3. Where does the value come from? temporal-auto-scaled-workers has the real registry — iface.ComputeProviderType and RegisterComputeProvider. The server should derive this from it rather than maintaining a third list. Worth confirming that is exposable.

  4. Does Cloud populate this per Namespace, or keep overlaying account flags in cloud-ui? Resolved: neither is a lift. The data plane populates NamespaceInfo.capabilities today and Cloud passes it through; saas-control-plane writes no namespace capabilities at all, so nothing new is asked of the control plane. cloud-ui keeps its account-flag overlay during rollout and drops it at GA, which is what it already does for standaloneActivities. See "Cloud already has a working model" above.

  5. Should the enabled bit be per Namespace, or is per Service enough in practice? Availability varies per Namespace because of the cloud a Namespace runs in, which is static. If nothing varies it dynamically, a Service-level list plus the Namespace's cloud provider would also work and be cheaper to populate.

Verification

  • make http-api-docs run; openapi/openapiv2.json and openapi/openapiv3.yaml regenerated and committed.
  • make buf-lint clean.
  • make api-linter clean.
  • protoc -I. --descriptor_set_out=/dev/null $(find temporal -name '*.proto') clean across the whole tree.
  • make buf-breaking reports 18 errors, all pre-existingExecutionType in enums/v1/common.proto, CallbackInfo.request_id, and several Nexus fields. Clean main reports the same 18. None involve compute/v1 or namespace/v1; this change adds no breaking change.

Nothing else already does this

Checked before proposing, on current main of each repository:

  • NamespaceInfo has no compute, provider, agentcore, or serverless field, and no capability has been added to it since this RFC opened.
  • The server's namespace_handler.go sets five worker-related capabilities and nothing provider-related.
  • saas-proto mentions serverless only as infrastructure plumbing — an isServerless flag on internal service-account provisioning, and serverless_worker as a Key Vault cert purpose. Cloud has built certs and identity for server-scaled Workers; it has not built a surface for which providers a Namespace may use.

What is not done

  • No server implementation. Nothing populates compute_providers yet. This PR is the contract argument only.

Context

RFC, not for merge as-is. Proposes reporting compute providers as a
repeated message on GetSystemInfoResponse instead of adding one boolean to
Capabilities per provider.

- enums/v1/compute.proto: ComputeProviderReleaseStage
- compute/v1/status.proto: ComputeProviderStatus (type, enabled, release_stage)
- GetSystemInfoResponse.compute_providers = 3
- Marks server_scaled_provider_cloud_run as superseded, kept for wire
  compatibility, and closes the door on further per-provider booleans

The boolean pattern answers only "is this one provider usable". Every new
provider costs a proto field, an api release, and a server release before a
client can gate on it, and it carries no release stage, so each client
hardcodes its own table. Both problems are live: Cloud Run's capability
landed a month after the UI shipped the option, the server still does not
set it, and AgentCore is merged in temporal-auto-scaled-workers and the CLI
with no capability field at all.

OpenAPI specs are not regenerated here; buf could not be installed in this
environment. Regenerate before this leaves draft.
Quinn Klassen pointed out on #867 that GetSystemInfo is the wrong carrier:
Cloud serves that endpoint as a fixed response at the reverse proxy layer,
so nothing per-account or per-Namespace can reach a client through it, and
suggested a Namespace-scoped surface instead.

Both halves check out in the client code. temporalio/ui's fetchSystemInfo
returns {} outright when isCloud, and cloud-ui synthesizes the whole
systemInfo object client-side from account feature flags rather than
calling the endpoint. Meanwhile cloud-ui already reads
namespace.namespaceInfo.capabilities in standalone-nexus-guard.svelte, so
NamespaceInfo demonstrably does reach Cloud.

Namespace scope is also the correct scope on the merits, not just a way
around the proxy. A compute provider is tied to the cloud its Namespace
runs in, and cloud-ui already derives the provider list from the
Namespace's RegionID_CloudProvider: an AWS Namespace offers Lambda and
AgentCore, a GCP Namespace offers Cloud Run. One Service-wide list cannot
say that.

- NamespaceInfo.compute_providers = 9
- GetSystemInfoResponse otherwise reverted; the note on
  server_scaled_provider_cloud_run now records why no further per-provider
  booleans belong there
- OpenAPI specs regenerated
@rossnelson rossnelson changed the title RFC: report compute providers and release stage in GetSystemInfo RFC: report compute providers and release stage per Namespace Sep 8, 2026
api#704 carried a typed ProviderDetailAWSLambda before #752 replaced it with
an opaque Payload, so proposing a typed message here invites the obvious
objection. The distinction is that provider config is open-ended and
genuinely provider-specific, while provider status is uniform across every
provider, and the comment now says so where a reviewer reads the message.

Specs regenerated.
rossnelson added a commit to temporalio/ui that referenced this pull request Sep 9, 2026
…in Cloud

Capability gating was the wrong mechanism, and it produced the visible
nonsense: a provider selected, disabled, and badged "Coming Soon" at once.

Self-hosted has no per-account entitlement to express, and a Service that
cannot run a provider rejects the Version with a reason, so gating the picker
only hid a choice behind a badge nobody could act on. The default list is now
every provider, selectable.

Restriction belongs to the caller that has grounds for it. cloud-ui already
passes `providers` derived from the Namespace's own cloud, so an AWS Namespace
offers Lambda and AgentCore and a GCP Namespace offers Cloud Run. That path is
untouched: it always passes the prop, so it never used these defaults.

A Version's provider cannot be changed, so the edit form shows only the
provider in use rather than alternatives that cannot be applied. The previous
commit had this backwards, reading an accepted update-mask path as product
behaviour.

This also removes the local serverScaledProviderAgentCore augmentation on
Capabilities. Nothing gates on it now, so the UI no longer depends on a
capability field that does not exist and that temporalio/api#867 no longer
proposes.
rossnelson added a commit to temporalio/ui that referenced this pull request Sep 9, 2026
…in Cloud

Capability gating was the wrong mechanism, and it produced the visible
nonsense: a provider selected, disabled, and badged "Coming Soon" at once.

Self-hosted has no per-account entitlement to express, and a Service that
cannot run a provider rejects the Version with a reason, so gating the picker
only hid a choice behind a badge nobody could act on. The default list is now
every provider, selectable.

Restriction belongs to the caller that has grounds for it. cloud-ui already
passes `providers` derived from the Namespace's own cloud, so an AWS Namespace
offers Lambda and AgentCore and a GCP Namespace offers Cloud Run. That path is
untouched: it always passes the prop, so it never used these defaults.

A Version's provider cannot be changed, so the edit form shows only the
provider in use rather than alternatives that cannot be applied. The previous
commit had this backwards, reading an accepted update-mask path as product
behaviour.

This also removes the local serverScaledProviderAgentCore augmentation on
Capabilities. Nothing gates on it now, so the UI no longer depends on a
capability field that does not exist and that temporalio/api#867 no longer
proposes.
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