Skip to content

feat(demo): a runnable AgentCore serverless Worker scenario - #3892

Merged
rossnelson merged 10 commits into
agentcore-compute-providerfrom
agentcore-demo-scenario
Sep 9, 2026
Merged

rossnelson merged 10 commits into
agentcore-compute-providerfrom
agentcore-demo-scenario

Conversation

@rossnelson

@rossnelson rossnelson commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Description & motivation 💭

Makes the AgentCore serverless Worker path a runnable feature demo, and adds the three capabilities the demo tool needed to express it.

Based on agentcore-compute-provider (#3884), so this diff is demo tooling only. #3884 is the UI change.

pnpm demo start agentcore-serverless-worker
agentcore
- Version agentcore-demo.run-1788971559877 was accepted with
  provider "aws-agentcore" and scaler "no-sync".
- The Worker inside AgentCore dialed back through the tunnel and
  registered "agentcore-tq".
- Greet completed on the AgentCore Worker and returned:
  "hello demo from AgentCore worker on localhost"

Nothing polls from this machine. The only Worker is the one AWS started.

What the tool gained

All three are general. None mentions AgentCore.

requires.serverModulesrequires.serverCommit structurally cannot describe a feature that reaches the server through a dependency bump, because the commit lives in another repository. This checks the go.mod of the checkout being built:

serverModules: {
  'go.temporal.io/auto-scaled-workers': 'v0.0.0-20260824233950-312f95fb8b99',
}

Go pseudo-versions order by their embedded commit timestamp, so that is satisfied by anything from that moment on, and a tagged release satisfies it outright. A checkout that falls short fails before the build, naming the module, the floor, and what it found.

requires.commands and a preflight — a missing tool is cheap to detect and expensive to hit halfway through a build, or worse after a provider has already been handed an address. Stages imply their own tools, so the tunnel stage requires ngrok without any definition restating it, and a workspace build requires go and git:

· Preflight: aws, git, go, ngrok, temporal present

A tunnel stage — a server-scaled Worker runs wherever Temporal launched it and dials the frontend back to poll. That inbound leg is the only thing a dev server on localhost cannot offer; the outbound leg to the provider was never the problem. Scenarios read the address as context.publicAddress.

Failures carry remedies

remedy.ts formats every failure as what was attempted → the tool's verbatim output → what to try → where to look. ngrok codes map to concrete fixes where we know them:

ERR_NGROK_4018 → No authtoken is configured. Run: ngrok config add-authtoken <token>
ERR_NGROK_108  → Stop other agents: pkill ngrok, or end the session in the dashboard

For a code we do not recognise it says so and gives https://ngrok.com/docs/errors/<code>/ rather than guessing. There is a test asserting it does not invent a remedy, because an invented fix is worse than none.

Five bugs this found, none visible by reading

The scenario was written before it was run. Running it four times found:

  1. The go.work directive was a literal go 1.26.4. Go refuses a workspace whose directive is below a member module's, and temporalio/cli has since moved to 1.26.5, so every source: 'workspace' scenario was one Go bump from breaking. Now derived as the higher of the two checkouts.
  2. update-agent-runtime replaces rather than patches, so sending only --environment-variables fails argument validation. The scenario now reads the current role, artifact, and network configuration and sends them back unchanged.
  3. tunnel was missing from STAGES, so --skip tunnel and --only tunnel were rejected.
  4. The tunnel handed out a dead address. Children append to their log, and the public address is read back out of it, so a second run matched the previous run's url before its own ngrok had written anything. The stage reported a plausible address, the scenario configured AWS with it, and the failure surfaced inside an AWS container as a connection refused — far from its cause. A stale address is syntactically perfect, so it read as success.
  5. A preflight test helper passed at runtime while failing tsc, which had hidden (3) from the type checker.

(4) is the one worth dwelling on. Removing the stale log fixes that instance; the fix that matters is that the stage now TCP-connects to its own public address before handing it out, which covers the class — a tunnel that reports a url and never established looks identical otherwise.

It was found by running the scenario twice, which the scenario's own preview notes tell reviewers to do. I wrote that note, named the risk, and still shipped a bug in exactly that spot — which is the argument for the demo existing rather than a runbook.

Testing 🧪

  • Manual testing
  • E2E tests added
  • Unit tests added

84 demo tests pass (42 pre-existing, 42 new). tsc reports 0 errors under utilities/demo. eslint and prettier clean.

New coverage: module-version comparison including pseudo-version timestamp ordering and a module path that prefixes another; workspace Go directive resolution including the exact 1.26.4/1.26.5 pair that failed; preflight implication and merging; remedy formatting and the no-guessed-remedy rule; CLI version resolution; and tunnel reachability, which is the regression test for (4).

Steps for others to test 🚶🏽‍♂️

pnpm demo start agentcore-serverless-worker   # needs AGENTCORE_ENDPOINT_ARN
pnpm demo stop agentcore-serverless-worker

Run it twice. The second run gets a fresh tunnel hostname and a fresh build id and must still work.

Checklists

Draft Checklist

Merge Checklist

Docs

utilities/demo/README.md covers the new requires fields, the tunnel stage and why its address is not stable across runs, and provisioning an AgentCore runtime — including the two things that cost a round trip each: a Worker with UseVersioning must register every workflow with a VersioningBehavior or it panics before polling, and the Resource field takes the Runtime Endpoint ARN.

@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
holocene Ready Ready Preview Sep 9, 2026 8:21pm UTC

Request Review

Adds the scenario, plus three capabilities the demo tool needed to express it.

requires.serverModules
  serverCommit cannot describe a feature that reaches the server through a
  dependency bump, because the commit lives in another repository. This checks
  the go.mod of the checkout being built. Pseudo-versions order by their
  embedded commit timestamp, and a tagged release satisfies a pseudo-version
  floor outright.

requires.commands, and a preflight
  A missing tool is cheap to detect and expensive to hit halfway through a
  build, or worse after a provider has been handed an address. Stages imply
  their own tools, so the tunnel stage requires ngrok without the definition
  restating it, and a workspace build requires go and git.

A tunnel stage
  A server-scaled Worker dials the frontend back to poll, and localhost is not
  reachable from a cloud provider. The outbound leg to the provider was never
  the problem. Scenarios read the address as context.publicAddress.

Failures carry remedies. remedy.ts formats what was attempted, the tool's
verbatim output, what to try, and where to look. ngrok error codes map to
concrete fixes where we know them, and to their documentation URL where we do
not: an invented remedy is worse than none.

The scenario resolves its own CLI rather than trusting PATH. The
--aws-agentcore-* flags arrived in 1.8.3, and a package-manager CLI is easily
older than the repository pins; this machine had 1.8.2 on PATH against 1.8.3
in bin/cli, which would have failed on an unknown flag.

It does not provision AWS. An AgentCore runtime bills while it exists, and
starting a demo should not create billable cloud resources as a side effect, so
it takes an endpoint ARN and the README covers provisioning.
…rrectly

Three problems the first real run of agentcore-serverless-worker exposed.

The go.work directive was a literal `go 1.26.4`. Go refuses a workspace whose
directive is below a member module's, and temporalio/cli has since moved to
1.26.5, so every source: 'workspace' scenario was one Go bump away from
failing. It now follows the higher of the two checkouts.

update-agent-runtime replaces a runtime rather than patching it, so sending
only --environment-variables fails argument validation. The scenario now reads
the current role, artifact, and network configuration and sends them back
unchanged, instead of telling the reader to assemble the command by hand.

'tunnel' was missing from STAGES, so --skip tunnel and --only tunnel were
rejected and the stage never appeared in a definition's stage list.

Also fixes a preflight test helper that passed at runtime while failing to
typecheck, which had hidden the STAGES errors from tsc.
Children append to their log, and the tunnel's public address is read back out
of that log, so a previous run's url satisfied the wait before this run's ngrok
had written anything. The stage reported a plausible address, the scenario
configured a provider with it, and the failure surfaced inside an AWS container
as a connection refused, far from its cause. A stale address is syntactically
perfect, so it read as success.

Two fixes, because the second is the one that matters:

- The log is removed before the child starts, so the address can only come from
  this run.
- The address is proved with a TCP connect before it is returned. A tunnel that
  reports a url and carries no traffic is indistinguishable from a working one
  until something remote fails to dial it, and that covers more than this bug:
  a tunnel that never established reports a url too.

Found by running the scenario twice, which its own preview notes call for.
The scenario can now create what it needs instead of refusing when no
endpoint ARN is given. Off by default: an AgentCore runtime bills while it
exists, and starting a demo should not create billable cloud resources by
surprise.

Provisioning is idempotent by name, so repeated runs reuse one runtime rather
than adding another, and it checks that the calling identity can do every part
before creating anything, so a missing permission does not leave an account
half provisioned.

When access is missing it names the policies to attach rather than passing the
raw AccessDenied along. That includes the trap this cost us: the ECR managed
policies are named AmazonEC2ContainerRegistry*, so searching the IAM console
for "ecr" returns nothing useful, and the guidance says to search
ContainerRegistry instead. It also offers the alternative of asking someone for
an endpoint ARN rather than granting the permissions at all.

Nothing is torn down at the end. A scenario's shutdown gets a three second
grace, which is not enough to delete a runtime, and a reviewer wants the demo
to still exist when the run finishes, so the summary lists what was created and
the commands to remove it.

Creating a runtime immediately after creating its execution role fails with an
error that blames ECR permissions rather than IAM propagation, which sends the
reader to the wrong place, so that call retries for a minute.

The Worker image source ships with the scenario, including its own README on
the two requirements that are easy to miss: AgentCore only accepts arm64, and
a Worker with UseVersioning must give every workflow a VersioningBehavior or it
panics before it polls.
requires.serverModules declared what the server needed and then handed the
work back, telling a person to go and edit a checkout this tool had fetched
into its own cache. The point of a requirement is that the tool satisfies it.

A checkout this tool fetched now gets go get and go mod tidy run on it. A
server ref would not solve this: the bump is a one-line go.mod change nobody
has pushed to temporalio/temporal, and requiring a pushed branch to demo a
feature is a heavier prerequisite than bumping a throwaway checkout.
serverModules already names the exact version, so nothing else was needed.

A checkout somebody pointed TEMPORAL_SERVER_REPO at is their working tree and
is never modified. RepoSource carries whether the tool owns it, and the
unowned case fails with advice to bump it there or to unset the variable.

Two ways this could have gone quietly wrong. The bump changes checkout
content, which the build cache is keyed on, so the key folds in the module
requirements or a pre-bump binary is reused against a bumped go.mod. And go
mod tidy can lower a requirement another module constrains, so it is
re-verified rather than assumed.

Verified with TEMPORAL_SERVER_REPO unset: the tool fetched temporalio/temporal
at main (0405f547a), bumped auto-scaled-workers, built, and ran through to a
workflow completing on the AgentCore-hosted Worker.
Running the AgentCore scenario cold with nothing configured reported the
missing endpoint ARN from the scenarios stage, which is after fetching two
repositories, bumping a module, and compiling a server. A person waited five
minutes to be told something a string comparison knew at once.

Scenario gains an optional preflight, called before any stage. The AgentCore
scenario uses it for both of its prerequisites: that it has somewhere to point,
and, when provisioning, that the AWS identity can do every part. Neither needs
a running server, so neither should cost a build. Cold with nothing set now
fails in about two seconds.

AGENTCORE_PROVISION=1 opts in for a single run, so a reviewer does not edit a
tracked file and have to remember to revert it. The source resolution is shared
between preflight and run so they cannot disagree about what the scenario
needs.

Verified from nothing: no AWS resources, no .feature-demo, no environment. The
run fetched both repositories, bumped auto-scaled-workers, built the server,
opened a tunnel, created the ECR repository, built and pushed the arm64 Worker
image, created the execution role, created the runtime, and ran a workflow to
completion on the Worker inside AgentCore. The IAM propagation retry fired,
so it was not defensive.
Three things a fresh run exposed.

Provisioning is on by default for this scenario. It cannot run without a
runtime, and a demo whose first run fails is not a demo. The option remains,
AGENTCORE_PROVISION=1 turns it on for a single run, and an explicit endpoint
ARN still wins.

The tunnel guard moved above provisioning. Running with --only scenarios
created an ECR repository, an IAM role, and an AgentCore runtime, and then
refused because the tunnel stage had not run. Nothing billable should be
created for a run that cannot finish.

Preflight now checks that the runtime an endpoint ARN names actually exists.
An ARN is a string, so having one proves nothing about whether it resolves: a
stale one from a previous run looks entirely valid, silently shadows
provisioning because an explicit ARN takes precedence, and previously failed
only after a server build and a tunnel. It now fails in about two seconds and
says to unset AGENTCORE_ENDPOINT_ARN.
create-agent-runtime returns as soon as the request is accepted, and the
service rejects updates and invokes until the runtime leaves CREATING. The
scenario then immediately pointed the new runtime at the tunnel and got a
ConflictException. Whether that happened depended on how fast the rest of the
run was, so an earlier cold run passed on timing luck.

The tunnel is already open when provisioning runs, so the address goes in at
creation instead. That removes the racing update rather than timing around it,
and saves waiting for a second rollout. A reused runtime carries whatever a
previous run left on it, so that path still updates, and provisioning now
waits for READY either way.

An update that loses a race anyway reports the runtime as busy and says to run
again, rather than listing permissions that are not the problem.
The ui-server embeds the built frontend with //go:embed all:assets, and a Go
embed of a missing directory is a compile error, not an empty filesystem. A
worktree that has never built them cannot build the binary at all:

  ui/embed.go:8:12: pattern all:assets: no matching files found

The stage now runs pnpm build:server, which writes them to server/ui/assets,
when that directory is absent. Any fresh worktree hit this, so it belongs in
the stage rather than in setup instructions.

Both failures also explain themselves now instead of forwarding a compiler
error whose text does not mention the frontend.
create-agent-runtime accepts --environment-variables and then ignores it. The
runtime comes back with environmentVariables null, and the Worker starts with
an empty TEMPORAL_ADDRESS and task queue:

  WORKER starting addr= ns=default tq= deployment=agentcore-demo

So provisioning a runtime from scratch produced a Worker that could not dial
anything, which is worse than the ConflictException it was meant to avoid.

The previous commit conflated two changes. Waiting for READY is what fixes the
race; moving the environment into create was an unnecessary optimisation on
top, and it does not work. Only the wait is kept, and the caller now always
applies the environment through update, which is the path that has been
exercised repeatedly.

This survived because every run after that change reused an existing runtime
and took the update path. The create path was not exercised until a teardown
forced a fresh provision.
@rossnelson
rossnelson force-pushed the agentcore-demo-scenario branch from 0a69c0a to bcf7e8b Compare September 9, 2026 20:20
@rossnelson
rossnelson marked this pull request as ready for review September 9, 2026 22:52
@rossnelson
rossnelson requested a review from a team as a code owner September 9, 2026 22:52
@rossnelson
rossnelson merged commit cc659a5 into agentcore-compute-provider Sep 9, 2026
6 checks passed
@rossnelson
rossnelson deleted the agentcore-demo-scenario branch September 9, 2026 22:52
rossnelson added a commit that referenced this pull request Sep 14, 2026
)

* feat(workers): add Amazon Bedrock AgentCore as a compute provider

Adds AgentCore to the serverless Worker forms, following the sequence Cloud
Run used: the UI ships gated behind a capability the api protos do not carry
yet, so OSS shows Coming Soon until a server advertises it, and cloud-ui can
enable it earlier by mapping an account feature flag into its synthesized
systemInfo capabilities.

AgentCore is invoke-based in temporal-auto-scaled-workers, so it pairs with
the no-sync scaler and takes the same assumed-role Access fields as Lambda.
It differs in one payload key: `endpoint_arn` rather than `arn`. The value is
the Runtime *Endpoint* ARN, since the provider parses the runtime id and
endpoint name out of it and rejects a bare Runtime ARN.

- Capability gate via `serverScaledProviderAgentCore`, declared as a local
  intersection on Capabilities until the proto field lands.
- Resource field validates the four-part runtime-endpoint ARN shape.
- The CloudFormation/Terraform role helper stays Lambda-only: it grants
  lambda:InvokeFunction and would hand out a role that cannot invoke a runtime.
- buildComputeConfigFromForm replaces the per-page provider ternaries, so
  mapping a provider to a ComputeConfig is one branch rather than three.
- Adds a feature-demo scenario recording what a reviewer checks and what the
  enabled path additionally requires from api and server.

* fix(workers): add decodeAgentCoreProviderDetails to the service test double

version-compute-details.svelte calls it, and deployment.svelte.test.ts
swaps the real deployments-service for the client test double, so the
missing export surfaced as an unhandled rejection during that test rather
than a failed assertion.

* fix(workers): stop gating the provider a Version already uses

Editing a Version whose provider the Service does not advertise rendered that
provider selected, disabled, and badged "Coming Soon" all at once, which says
the Version is running on something unavailable.

Capability gating exists to stop somebody choosing a provider the Service
cannot run. A Version already running on one is proof it works, so gating must
not describe it as unavailable. lockProvidersTo makes the provider in use
visible and selectable, hides the rest because a Version's provider cannot be
changed, and keeps the release stage, which stays true either way.

Two causes, both fixed:

The edit page never locked at all. lockComputeProvider was only wired to the
create page, so editing fell through to the capability-gated default list.

lockComputeProvider itself failed closed when the configured provider was
disabled or hidden, which produced the same fallback on the create page. It
now only declines when configuration does not know the provider at all.

Not AgentCore-specific: Cloud Run hits this on OSS today, since no server
advertises server_scaled_provider_cloud_run.

* fix(workers): keep the alternative providers offered when editing a Version

The previous commit ungated the provider a Version uses, but also hid every
alternative on the reasoning that a Version's provider cannot be changed. It
can: provider.type is an accepted update path on
UpdateWorkerDeploymentVersionComputeConfig, so switching a Version to another
provider is a real choice, and hiding them removed a capability the edit form
had before.

allowProviderInUse now ungates only the provider in use and leaves the
alternatives exactly as configured, so one the Service can run stays offered
and one it cannot stays refused. lockProvidersTo keeps the hiding behaviour and
is used where the provider is inherited rather than chosen: creating a new
Version in an existing Deployment.

* fix(workers): select any provider in self-hosted, block by Namespace 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.

* feat(demo): a runnable AgentCore serverless Worker scenario (#3892)

* feat(demo): model the AgentCore serverless Worker as a runnable scenario

Adds the scenario, plus three capabilities the demo tool needed to express it.

requires.serverModules
  serverCommit cannot describe a feature that reaches the server through a
  dependency bump, because the commit lives in another repository. This checks
  the go.mod of the checkout being built. Pseudo-versions order by their
  embedded commit timestamp, and a tagged release satisfies a pseudo-version
  floor outright.

requires.commands, and a preflight
  A missing tool is cheap to detect and expensive to hit halfway through a
  build, or worse after a provider has been handed an address. Stages imply
  their own tools, so the tunnel stage requires ngrok without the definition
  restating it, and a workspace build requires go and git.

A tunnel stage
  A server-scaled Worker dials the frontend back to poll, and localhost is not
  reachable from a cloud provider. The outbound leg to the provider was never
  the problem. Scenarios read the address as context.publicAddress.

Failures carry remedies. remedy.ts formats what was attempted, the tool's
verbatim output, what to try, and where to look. ngrok error codes map to
concrete fixes where we know them, and to their documentation URL where we do
not: an invented remedy is worse than none.

The scenario resolves its own CLI rather than trusting PATH. The
--aws-agentcore-* flags arrived in 1.8.3, and a package-manager CLI is easily
older than the repository pins; this machine had 1.8.2 on PATH against 1.8.3
in bin/cli, which would have failed on an unknown flag.

It does not provision AWS. An AgentCore runtime bills while it exists, and
starting a demo should not create billable cloud resources as a side effect, so
it takes an endpoint ARN and the README covers provisioning.

* fix(demo): derive the workspace Go directive, and repoint runtimes correctly

Three problems the first real run of agentcore-serverless-worker exposed.

The go.work directive was a literal `go 1.26.4`. Go refuses a workspace whose
directive is below a member module's, and temporalio/cli has since moved to
1.26.5, so every source: 'workspace' scenario was one Go bump away from
failing. It now follows the higher of the two checkouts.

update-agent-runtime replaces a runtime rather than patching it, so sending
only --environment-variables fails argument validation. The scenario now reads
the current role, artifact, and network configuration and sends them back
unchanged, instead of telling the reader to assemble the command by hand.

'tunnel' was missing from STAGES, so --skip tunnel and --only tunnel were
rejected and the stage never appeared in a definition's stage list.

Also fixes a preflight test helper that passed at runtime while failing to
typecheck, which had hidden the STAGES errors from tsc.

* fix(demo): make the tunnel prove its address before handing it out

Children append to their log, and the tunnel's public address is read back out
of that log, so a previous run's url satisfied the wait before this run's ngrok
had written anything. The stage reported a plausible address, the scenario
configured a provider with it, and the failure surfaced inside an AWS container
as a connection refused, far from its cause. A stale address is syntactically
perfect, so it read as success.

Two fixes, because the second is the one that matters:

- The log is removed before the child starts, so the address can only come from
  this run.
- The address is proved with a TCP connect before it is returned. A tunnel that
  reports a url and carries no traffic is indistinguishable from a working one
  until something remote fails to dial it, and that covers more than this bug:
  a tunnel that never established reports a url too.

Found by running the scenario twice, which its own preview notes call for.

* feat(demo): optionally provision the AgentCore runtime

The scenario can now create what it needs instead of refusing when no
endpoint ARN is given. Off by default: an AgentCore runtime bills while it
exists, and starting a demo should not create billable cloud resources by
surprise.

Provisioning is idempotent by name, so repeated runs reuse one runtime rather
than adding another, and it checks that the calling identity can do every part
before creating anything, so a missing permission does not leave an account
half provisioned.

When access is missing it names the policies to attach rather than passing the
raw AccessDenied along. That includes the trap this cost us: the ECR managed
policies are named AmazonEC2ContainerRegistry*, so searching the IAM console
for "ecr" returns nothing useful, and the guidance says to search
ContainerRegistry instead. It also offers the alternative of asking someone for
an endpoint ARN rather than granting the permissions at all.

Nothing is torn down at the end. A scenario's shutdown gets a three second
grace, which is not enough to delete a runtime, and a reviewer wants the demo
to still exist when the run finishes, so the summary lists what was created and
the commands to remove it.

Creating a runtime immediately after creating its execution role fails with an
error that blames ECR permissions rather than IAM propagation, which sends the
reader to the wrong place, so that call retries for a minute.

The Worker image source ships with the scenario, including its own README on
the two requirements that are easy to miss: AgentCore only accepts arm64, and
a Worker with UseVersioning must give every workflow a VersioningBehavior or it
panics before it polls.

* feat(demo): apply unmet module requirements instead of reporting them

requires.serverModules declared what the server needed and then handed the
work back, telling a person to go and edit a checkout this tool had fetched
into its own cache. The point of a requirement is that the tool satisfies it.

A checkout this tool fetched now gets go get and go mod tidy run on it. A
server ref would not solve this: the bump is a one-line go.mod change nobody
has pushed to temporalio/temporal, and requiring a pushed branch to demo a
feature is a heavier prerequisite than bumping a throwaway checkout.
serverModules already names the exact version, so nothing else was needed.

A checkout somebody pointed TEMPORAL_SERVER_REPO at is their working tree and
is never modified. RepoSource carries whether the tool owns it, and the
unowned case fails with advice to bump it there or to unset the variable.

Two ways this could have gone quietly wrong. The bump changes checkout
content, which the build cache is keyed on, so the key folds in the module
requirements or a pre-bump binary is reused against a bumped go.mod. And go
mod tidy can lower a requirement another module constrains, so it is
re-verified rather than assumed.

Verified with TEMPORAL_SERVER_REPO unset: the tool fetched temporalio/temporal
at main (0405f547a), bumped auto-scaled-workers, built, and ran through to a
workflow completing on the AgentCore-hosted Worker.

* feat(demo): let a scenario refuse before anything expensive runs

Running the AgentCore scenario cold with nothing configured reported the
missing endpoint ARN from the scenarios stage, which is after fetching two
repositories, bumping a module, and compiling a server. A person waited five
minutes to be told something a string comparison knew at once.

Scenario gains an optional preflight, called before any stage. The AgentCore
scenario uses it for both of its prerequisites: that it has somewhere to point,
and, when provisioning, that the AWS identity can do every part. Neither needs
a running server, so neither should cost a build. Cold with nothing set now
fails in about two seconds.

AGENTCORE_PROVISION=1 opts in for a single run, so a reviewer does not edit a
tracked file and have to remember to revert it. The source resolution is shared
between preflight and run so they cannot disagree about what the scenario
needs.

Verified from nothing: no AWS resources, no .feature-demo, no environment. The
run fetched both repositories, bumped auto-scaled-workers, built the server,
opened a tunnel, created the ECR repository, built and pushed the arm64 Worker
image, created the execution role, created the runtime, and ran a workflow to
completion on the Worker inside AgentCore. The IAM propagation retry fired,
so it was not defensive.

* fix(demo): refuse before provisioning, and default provisioning on

Three things a fresh run exposed.

Provisioning is on by default for this scenario. It cannot run without a
runtime, and a demo whose first run fails is not a demo. The option remains,
AGENTCORE_PROVISION=1 turns it on for a single run, and an explicit endpoint
ARN still wins.

The tunnel guard moved above provisioning. Running with --only scenarios
created an ECR repository, an IAM role, and an AgentCore runtime, and then
refused because the tunnel stage had not run. Nothing billable should be
created for a run that cannot finish.

Preflight now checks that the runtime an endpoint ARN names actually exists.
An ARN is a string, so having one proves nothing about whether it resolves: a
stale one from a previous run looks entirely valid, silently shadows
provisioning because an explicit ARN takes precedence, and previously failed
only after a server build and a tunnel. It now fails in about two seconds and
says to unset AGENTCORE_ENDPOINT_ARN.

* fix(demo): create the AgentCore runtime with its environment already set

create-agent-runtime returns as soon as the request is accepted, and the
service rejects updates and invokes until the runtime leaves CREATING. The
scenario then immediately pointed the new runtime at the tunnel and got a
ConflictException. Whether that happened depended on how fast the rest of the
run was, so an earlier cold run passed on timing luck.

The tunnel is already open when provisioning runs, so the address goes in at
creation instead. That removes the racing update rather than timing around it,
and saves waiting for a second rollout. A reused runtime carries whatever a
previous run left on it, so that path still updates, and provisioning now
waits for READY either way.

An update that loses a race anyway reports the runtime as busy and says to run
again, rather than listing permissions that are not the problem.

* fix(demo): build the embedded frontend assets before the ui-server

The ui-server embeds the built frontend with //go:embed all:assets, and a Go
embed of a missing directory is a compile error, not an empty filesystem. A
worktree that has never built them cannot build the binary at all:

  ui/embed.go:8:12: pattern all:assets: no matching files found

The stage now runs pnpm build:server, which writes them to server/ui/assets,
when that directory is absent. Any fresh worktree hit this, so it belongs in
the stage rather than in setup instructions.

Both failures also explain themselves now instead of forwarding a compiler
error whose text does not mention the frontend.

* fix(demo): apply the runtime environment through update, not create

create-agent-runtime accepts --environment-variables and then ignores it. The
runtime comes back with environmentVariables null, and the Worker starts with
an empty TEMPORAL_ADDRESS and task queue:

  WORKER starting addr= ns=default tq= deployment=agentcore-demo

So provisioning a runtime from scratch produced a Worker that could not dial
anything, which is worse than the ConflictException it was meant to avoid.

The previous commit conflated two changes. Waiting for READY is what fixes the
race; moving the environment into create was an unnecessary optimisation on
top, and it does not work. Only the wait is kept, and the caller now always
applies the environment through update, which is the path that has been
exercised repeatedly.

This survived because every run after that change reused an existing runtime
and took the update path. The create path was not exercised until a teardown
forced a fresh provision.

* feat(demo): create the AgentCore Version through the UI, not the CLI

The scenario shelled out to `temporal worker deployment create-version`,
which proves the server accepts an aws-agentcore compute config. That was
never in question: the CLI could do this before any of this work existed.
What is under review is the UI, and a run that never opened the form said
nothing about it.

The Version is now created by driving the real create-version form with
Playwright: select Amazon Bedrock AgentCore, fill the Agent Runtime
Endpoint ARN and the Access fields, submit. `createVersion: 'cli'` keeps
the old path for a machine with no browser, and `headed: true` shows the
browser doing it. Screenshots land per step, so a failure leaves a picture
of where it stopped.

Preflight launches a browser before any stage starts, because Playwright
downloads its browsers separately from the package and finding that out
after a server build costs minutes.

The run also records what it had to type to get there. The form requires
IAM Role ARN and External ID; this server sets
require_role_and_external_id false and discards both. The UI has no
equivalent of the CLI's --aws-agentcore-skip-role-and-external-id, so a
self-hosted operator who turned the requirement off can create a Version
by CLI but not by form. FE-675 tracks that.

Adds `uiUrl` to the scenario context, set to the UI the ui stage started
so a scenario drives the checkout's UI rather than the server's bundled
one.

* fix(demo): drive the real form, and provision the role it needs

Proving the UI can create an AgentCore Version took more than pointing a
browser at the form.

The Access fields are not decorative. Setting
require_role_and_external_id to false makes the role optional, not
ignored: the server assumes whatever role the compute config carries. The
CLI can omit it with --aws-agentcore-skip-role-and-external-id; the form
requires both fields and always sends them, so a placeholder does not get
discarded, it fails at sts:AssumeRole after the form has done its job.
The scenario now provisions TemporalDemoAgentCoreInvoke, trusting this
account under an sts:ExternalId condition and granting
bedrock-agentcore:InvokeAgentRuntime, and waits for it to actually assume
before going on, because IAM is eventually consistent and the server
tries seconds later. The earlier comments claiming the server discards
these fields were wrong and are corrected here.

Each run also gets its own deployment. Versions from earlier runs go
INACTIVE, the server stops reporting their ComputeConfig (FE-672), and
lockComputeProvider cannot name a provider it cannot see, so the
create-version page renders an error where the form should be. Those
Versions cannot be deleted either: AgentCore sessions from earlier runs
keep polling, and the server refuses to delete a Version with active
pollers.

The driver itself had three faults, all found by running it. locator
count() does not wait, and the page renders the form only after it
fetches the deployment, so it looked before the form existed. The page
keeps hidden [role=alert] containers and a closed confirm modal whose
button is also a form submit, so first() waited on elements never shown.
And fullPage screenshots evaluate in the page, which trips over the
__name wrappers esno leaves behind.

Verified end to end against Bedrock AgentCore in us-west-2: the form
created the Version, the Worker Controller invoked the runtime, the
Worker dialed back through the tunnel and registered agentcore-tq, and
Greet returned "hello demo from AgentCore worker on localhost".

* docs(demo): keep internal references out of a public repo

This repository is public. The demo notes named an internal repository
and described how hosted Temporal maps an account entitlement into the
capabilities it synthesizes, and several comments cited internal tracker
ids that mean nothing to anyone reading this here. The observations they
supported are worth keeping, so they are restated without the pointers.

Also corrects a note that this branch had already made false: the
compute-provider scenario still said AgentCore must render disabled and
badged "Coming Soon" on a self-hosted server. Self-hosted now offers
every provider, and a reviewer following that note would have reported a
bug against correct behaviour.

* docs(demo): point the picker scenario at what this branch changed

The scenario listed the fields AgentCore renders but never sent a
reviewer to the two stories that show the actual behaviour change:
every provider selectable on a self-hosted server, and a Version locked
to the provider it already uses.

Those are the cases worth a human looking at. The first is where a
self-hosted server previously gated providers on capabilities it never
advertised, which left Cloud Run permanently unselectable. The second is
where a card used to render selected and disabled at once.

* feat(workers): AgentCore IAM setup material, for Cloud and self-hosted

The Access section required an IAM role and offered no way to create one
for AgentCore. Its CloudFormation and Terraform helpers grant
lambda:InvokeFunction, so sharing them would hand out a role that cannot
invoke a runtime, and the helper was hidden rather than reused.

Adds the AgentCore equivalents and follows the selected provider:

- temporal-agentcore-role.yaml grants bedrock-agentcore:InvokeAgentRuntime
  and GetAgentRuntimeEndpoint. Validation calls GetAgentRuntimeEndpoint
  first, so a role without it fails when the Version is created rather
  than when a Worker is wanted.
- serverless-worker-agentcore.tf points at the aws/agentcore module and
  takes agent_runtime_arns, the same four-part Endpoint ARN the Version
  takes.
- The Launch Stack link, the download filename, and the module link now
  follow the provider rather than always naming Lambda.

The trust policy is the part that differs by deployment. Cloud assumes
the role as a service principal, so it names temporal.io. A self-hosted
server assumes it as whatever IAM identity it runs as, which is an AWS
principal rather than a service and so a different key entirely. The
template takes a TemporalPrincipal parameter and switches on it, since
this PR makes the provider selectable outside Cloud for the first time.

The policy is the one proven by the demo scenario, which provisions the
same permissions and trust shape and only works because the role really
assumes.

Note the aws/agentcore Terraform module does not exist in
temporalio/terraform-modules yet. The snippet names where it will live,
as the Lambda and Cloud Run snippets do for theirs.

* fix(holocene): round the radio card to match the Io system

The card drew a square border while everything around it — buttons,
accordions, inputs — is rounded, so the compute provider picker read as
unfinished next to the rest of the form.

The optional panel beneath a selected card shares its bottom edge, so the
card drops its bottom corners when that panel is showing and the panel
takes them instead. No consumer passes that snippet today, but leaving it
unhandled would have put the sharp corner back the moment one did.

* fix(workers): restore the provider brand colours and round their tile

Two things in the provider picker's logo tile.

The marks lost their colour. Before the icon registry moved to Io, the
picker rendered holocene's aws.svelte, which carried fill="#FF9900". The
refactor remapped it to IconAws, the monochrome glyph, so the AWS and GCP
logos have rendered flat white on dark since 20 Aug. IconAwsColor and
IconGcpColor already existed and were used nowhere outside a Badge story.
These are vendor logos rather than UI icons, so they should keep their own
colour on either theme.

The tile was also explicitly rounded-none, predating the Io work, which
left a square inside a form where everything else is rounded.

* fix(demo): enable the compute provider the scenario needs

The provider allowlist inverted. `workercontroller.compute_providers.enabled`
used to mean "restrict to these", and an unset value allowed everything;
in the auto-scaled-workers release main now pins it means "only these",
and an unset value enables nothing at all.

The scenario never set it, because it never had to. The server now
reports that it could not instantiate the provider, which reads like the
build is missing it — the provider is there and switched off, and the
metric behind that branch says so where the message does not.

Worth knowing beyond this scenario: the same upgrade silently disables
Lambda and Cloud Run for any self-hosted operator who has not set the
list.

Dynamic config values may now be string arrays. The flag already passes
JSON, so only the schema stood in the way.

* fix(deployments): use the brand marks in the compute badge

The last place still rendering the monochrome glyphs. Same cause as the
provider picker: the icon registry move mapped these to IconAws and
IconGcp, so the badge has drawn a flat white AWS smile since August.

These identify a vendor rather than convey UI state, so currentColor is
the wrong default for them.

* fix(workers): use the small badge for the provider stage

The stage badge sits beside a provider name at text-sm, and at the
default size its mono uppercase label read heavier than the name it
qualifies. #3908 added a size variant, so take it rather than override
the padding locally.

The disabled-reason badge occupies the same slot and only ever renders
instead of this one, so it takes the same size.

This branch was successfully deployed

1 active deployment
Preview bcf7e8bd Deployed Sep 9, 2026 by vercel[bot]
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