Skip to content

docs(model): document bedrockGeoRegion and the global-vs-geo tradeoff - #799

Closed
isadeks wants to merge 2 commits into
mainfrom
docs/740-bedrock-geo-region
Closed

isadeks wants to merge 2 commits into
mainfrom
docs/740-bedrock-geo-region

Conversation

@isadeks

@isadeks isadeks commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Part of #740. Closes the documentation gap I raised reviewing #764.

Why

bedrockGeoRegion (added in #764) was documented nowhere — not in docs/guides/DEVELOPER_GUIDE.md, not in Model-configuration.md, not in cdk.json. The synth-time error message names the key, but only helps someone who already knows it exists.

That matters more than usual because #740 is the documentation tracking issue, and #742 created the canonical model-configuration reference specifically so model settings stop being scattered. A new key that decides which geography every inference profile routes through belongs in it.

What

A section covering:

  • what the key does, and how to set it (-c bedrockGeoRegion=global or cdk.json context);
  • accepted values, framed as what the CDK models rather than a copied list — global, us, us-gov, eu, apac, jp, au;
  • that an unrecognized value fails at synth, and why that is the right behaviour: an invented geography yields a well-formed ARN for a profile that does not exist, so the grant would authorize nothing and the agent would fail at turn 0 with AccessDenied and nothing to explain why;
  • that one value drives both grant sites and ANTHROPIC_DEFAULT_HAIKU_MODEL, so a deployment cannot grant one geography while calling another.

The tradeoff, stated as a choice rather than a default: global. routes to any supported commercial Region, giving better throughput and resilience under burst — worth having for tasks that run hours. A geo profile keeps inference in that geography, which is what a data-residency requirement demands. This is the material half for #747, which flips the default to global.

Plus the two failure modes that surface at runtime rather than synth: a model with no profile in the target geography, and account-level Bedrock access not covering that geography's entitlements.

Two corrections

#764 made two existing statements stale:

statement was now
bare-vs-prefixed rule grant sites add us. add the configured geo
layer-1 table row bedrockModels only + bedrockGeoRegion

One question the guide implied but never answered

Changing the geography needs a redeploy, because the IAM grants are scoped to explicit profile ARNs resolved at synth. Switching among already-granted models does not — that is a DynamoDB write via layer 4. The layering table implied this; it now says so.

Verification

Starlight mirror regenerated with mise //docs:sync — both files carry the new content, and mise //docs:build is green (78 pages). Docs-only; no source changes.

The `bedrockGeoRegion` context key added in #764 was documented nowhere — not in
the developer guide, not in the canonical model-configuration reference #742
created for exactly this, not in cdk.json. The synth-time error names the key, but
only for someone who already knows it exists.

Adds a section covering what the key does, its accepted values (derived from what
the CDK models: global, us, us-gov, eu, apac, jp, au), and that an unrecognized
value fails at synth rather than producing a well-formed ARN for a profile that
does not exist.

Also states the choice, since it is a real tradeoff rather than a default to
accept: `global.` routes to any supported commercial Region for better throughput
and resilience under burst, while a geo profile is what a data-residency
requirement demands. Plus the two things that fail at runtime rather than synth —
a model without a profile in the target geography, and account-level Bedrock
access not covering that geography's entitlements.

Two existing statements went stale with #764 and are corrected here: the
bare-vs-prefixed rule described the grant sites as adding `us.` specifically, and
the layer-1 table row described only `bedrockModels`. Both now read in terms of the
configured geography.

Answers a question that the layering table implies but never states: changing the
geography needs a redeploy, because the grants are scoped to profile ARNs resolved
at synth, whereas switching among already-granted models does not.

Starlight mirror regenerated with `mise //docs:sync`; docs build green.
@isadeks
isadeks requested review from a team as code owners August 26, 2026 16:22
isadeks added a commit that referenced this pull request Aug 27, 2026
Addresses all six review findings on the geo switch. The first was a real defect,
not a documentation error.

FINDING 1 — the one-line rollback was false. `synth -c bedrockGeoRegion=us` granted
`us.` profiles while the agent still asked for `global.anthropic.claude-opus-5`,
because the stack injected only the AUXILIARY model into the runtime env and never
the main one. The main model came from a Python literal that a geography change does
not touch, so every task with no per-repo override would fail at turn 0 with
AccessDenied. My own PR body half-knew this — the prose said "roll back context and
defaults together" while the summary claimed one line.

Fixed by removing the divergence rather than re-syncing literals: both models are
now injected from the resolved geography, via one shared helper, on BOTH substrates
(the ECS task definitions set neither var either, so an ECS task had the same
mismatch). Verified by synthesizing both geographies: grants, AgentCore env and ECS
env now agree in each, so the rollback genuinely is one context value.

Also fixes the fifth stale default the review found, TaskConfig.haiku_model, which I
had missed entirely while counting four.

FINDING 2 — the generalized allow-list invariant was too weak in both directions. It
only proved entries were paired WITHIN the list, which is satisfiable while wrong:
three granted models had no `global.` form (a workflow pinning one was rejected at
admission), and the reviewer's invented pair passed all 22 tests despite being
granted nothing. Replaced with parity against DEFAULT_BEDROCK_MODEL_IDS across the
deployable geographies, in both directions, and confirmed the reviewer's exact
mutation now fails.

FINDING 3 — the `--model` guard let through a well-formed but ungranted model. My
justification for that was wrong: I claimed the CLI cannot read `bedrockModels`, but
`get-template` recovers the granted set from the profile ARNs using credentials the
CLI already needs. Added a `BedrockModelIds` output — a documented contract rather
than a regex over CloudFormation — and the guard now rejects an ungranted model,
listing what the stack does grant. Also fixes the reverse-compatibility bug: with no
geography exported, the bare-id error no longer prescribes `us.`, which a
default-global stack does not grant.

FINDING 4 — doctor's profile check is relabelled "visible" and its detail states that
resolution happens under operator credentials and does not prove the workload role
can invoke. The narrow wording was already accurate, but a PASS feeding "All checks
passed" still read as readiness.

FINDINGS 5-6 — carries #799's documentation so this branch is not self-contradictory,
corrects the onboarding and troubleshooting skills that recommended `us.` overrides
this branch would now reject (and still called Sonnet 4.6 the default), and fixes the
stale "default us" wording in the new output description.

One test rewritten rather than re-pinned: the agent's haiku-default test asserted
`startswith("us.")` when its stated intent was "must be a profile, not a bare id". It
now asserts a geo prefix, so it tests the property instead of a geography that is a
deploy-time choice.
@isadeks

isadeks commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #806, which carries this content plus the geo flip and the review fixes. Verified before closing: every distinctive section here (Choosing the inference-profile geography, the residency caveat, the fail-at-synth rationale, the redeploy note) is present on #806's branch, with strictly newer model strings — this branch still says us.anthropic.claude-opus-5 where #806 says global..

Closing rather than rebasing, since a rebase would produce an empty diff and #806 needs no doc changes on top.

@isadeks isadeks closed this Aug 27, 2026
@scottschreckengaust scottschreckengaust added the v1 Version 1 label Sep 14, 2026
ClintEastman02 pushed a commit to ClintEastman02/sample-autonomous-cloud-coding-agents that referenced this pull request Sep 15, 2026
…validation (aws-samples#806)

* feat(cdk): switch the Bedrock inference profile to the global geo

Closes aws-samples#747. Final step of the aws-samples#740 stack; the plumbing landed in aws-samples#746.

Sets `bedrockGeoRegion` to `global` in cdk/cdk.json (the file had no context block
before) and moves the four default strings that name a geography with it: the agent
ANTHROPIC_MODEL fallback, TaskConfig.anthropic_model, the CLI's platform-default
mirror, and the agent-side ANTHROPIC_DEFAULT_HAIKU_MODEL fallback. Adds the
`global.` forms of Opus 5 and Haiku 4.5 to the workflow model allow-list.

A `global.` profile routes to any supported commercial Region, which gives better
throughput and resilience under burst — worth having for tasks that run for hours.
The tradeoff is data residency: a deployer with a residency requirement sets
`bedrockGeoRegion=us` (or eu/apac) instead, which is exactly why aws-samples#746 made this a
context key rather than a second hardcode.

Verified against the synthesized template rather than assumed: all 10
inference-profile ARNs become `global.`, none remain `us.`, and the 20
foundation-model ARNs are unchanged because that half of the grant was already
geo-agnostic. `ANTHROPIC_DEFAULT_HAIKU_MODEL` picks up the prefix on its own from
the context key, as aws-samples#746 intended. Rollback rehearsed: `-c bedrockGeoRegion=us`
synths cleanly and reverts all 10 ARNs, so recovery is one context value.

Two things the issue's scope table did not anticipate, both caught by existing
guards rather than by reading:

The agent-side haiku fallback in config.py is a SEPARATE value from the env var the
stack injects. The issue said the haiku model needs no edit because aws-samples#746 derives its
prefix — true of the deployed env var, but a run with no env set reaches the Python
literal, so leaving it would have made local and deployed runs use different
geographies. The docs-parity contract test caught the inconsistency.

The allow-list pairing invariant was written as bare-vs-`us.`, so it read
`global.anthropic.…` as a bare id and demanded a nonsensical `us.global.anthropic.…`
pairing. Generalized to the geo prefixes the list actually uses, keeping the real
invariant (no prefixed entry without its bare form, no bare entry admitted in zero
geographies) and confirmed still failing against an orphaned entry.

Both prefixes stay in the allow-list deliberately. It has to accept whatever
geography a deployment is configured for, and dropping the `us.` forms would reject
a residency-constrained deployer's workflows at admission.

Docs: the global-vs-geo tradeoff and the documented defaults move together, since
the aws-samples#742 drift test enforces them. Also corrects statements that asserted a `us.`
prefix as a rule rather than as the then-current default. Starlight mirrors
regenerated.

Not yet done, and required before merge per the issue's acceptance criteria: deploy
and an agentcore smoke test proving a task completes end to end on the global
profile, plus a `platform doctor` access probe.

* fix(cli): strip every geo prefix in the doctor Bedrock check

Regression this branch introduced. The check derives its bare foundation-model id
from the platform default by stripping the inference-profile prefix, but the strip
matched `us|eu|apac` only. Moving the default to a `global.` profile made it
silently do nothing, so `GetFoundationModel` was handed a profile id it cannot
resolve.

Confirmed against the live API rather than reasoned about:

  anthropic.claude-opus-5         → 200
  global.anthropic.claude-opus-5  → ResourceNotFoundException

So the one Bedrock check `doctor` performs would have reported a false failure on
every deploy of this branch — and the check exists precisely to catch model-access
problems before a task fails at turn 0.

Lists all seven geographies the CDK models, longest-first so `us-gov` is stripped
as `us-gov` rather than leaving a stray `-gov.`. Mirrored rather than imported,
matching how the CLI already mirrors PLATFORM_REPO_DEFAULTS — it is a separate
package and does not depend on CDK.

The guard asserts the queried id for EVERY geography, so a future default on any
of them cannot reopen the hole, and it was confirmed to fail against the
`us|eu|apac` version.

Worth noting for the follow-up work: the check still only probes the FOUNDATION
MODEL catalog, never the inference profile the deployment is configured for. A
stack granted profiles its account cannot invoke still reports healthy. Fixing
that is a behaviour change to what doctor checks, so it is left to its own issue
rather than folded in here.

* fix(cli): check the inference profile doctor will invoke, and reject an unusable --model

Closes aws-samples#804. Closes aws-samples#805.

Both issues are the same failure shape: a model or geography that cannot work is
accepted silently, and the only symptom is every task dying at turn 0 with an
AccessDenied that names nothing.

aws-samples#804 — doctor's Bedrock check called GetFoundationModel on the bare model id, which
answers "is this model published in this Region". That is not what decides whether
tasks run: the agent invokes a `<geo>.<model>` cross-Region PROFILE, and the IAM
grant is scoped to profile ARNs. A stack configured for a geography with no profile,
or whose entitlements the account lacks, passed the check and then failed
everything. Observed while verifying the geo switch: doctor reported
anthropic.claude-sonnet-4-6 visible in us-east-1 while the deployment was configured
for global.anthropic.claude-opus-5 — a different model, and a geography it never
looked at.

Adds a second check that resolves the actual profile via GetInferenceProfile. Both
are kept because their remedies differ: a missing catalog entry means the model is
unavailable here at all, a missing profile means the geography is wrong for this
model or Region. The new check says "resolves" rather than "is invocable", because
resolving a profile does not prove a task can call it — only InvokeModel would, and
doctor does not spend a token to find out.

The geography comes from a new BedrockGeoRegion stack output, mirroring the existing
ComputeSubstrate output that the CLI already reads to refuse a mismatched
--compute-type. On a stack that predates the output the check WARNS rather than
defaulting to `us`: passing would report a verification that never happened.

aws-samples#805 — `repo onboard --model` wrote any string to the RepoTable unchecked. Now
rejected at the boundary: a bare foundation-model id (Bedrock refuses those for
on-demand invocation, so it is always wrong), and a geography the stack does not
grant. Each error carries the fix — the profile form to use, or the redeploy that
would grant the geography asked for.

Deliberately NOT checking membership in the granted model set. The CLI cannot read
`bedrockModels` today, and a guess presented as validation is worse than no check;
the profile check above covers the reachable part. Noted in the code.

Verified by mutation, not just by green tests: probing the bare id instead of the
profile, defaulting a missing geography to `us`, dropping either --model rule, and
dropping a geography from the prefix list each fail.

One comment corrected in the process. I had written that the geo list must be
longest-first so `us-gov` is not read as `us`. Reordering it did not fail any test,
and it should not have: the match requires `<geo>` followed by a literal `.`, so
`us.` cannot match `us-gov.…` at all. The comment now says ordering is for
readability and the test asserts the behaviour rather than the ordering.

* fix(agent): inject both model env vars from the resolved geography

Addresses all six review findings on the geo switch. The first was a real defect,
not a documentation error.

FINDING 1 — the one-line rollback was false. `synth -c bedrockGeoRegion=us` granted
`us.` profiles while the agent still asked for `global.anthropic.claude-opus-5`,
because the stack injected only the AUXILIARY model into the runtime env and never
the main one. The main model came from a Python literal that a geography change does
not touch, so every task with no per-repo override would fail at turn 0 with
AccessDenied. My own PR body half-knew this — the prose said "roll back context and
defaults together" while the summary claimed one line.

Fixed by removing the divergence rather than re-syncing literals: both models are
now injected from the resolved geography, via one shared helper, on BOTH substrates
(the ECS task definitions set neither var either, so an ECS task had the same
mismatch). Verified by synthesizing both geographies: grants, AgentCore env and ECS
env now agree in each, so the rollback genuinely is one context value.

Also fixes the fifth stale default the review found, TaskConfig.haiku_model, which I
had missed entirely while counting four.

FINDING 2 — the generalized allow-list invariant was too weak in both directions. It
only proved entries were paired WITHIN the list, which is satisfiable while wrong:
three granted models had no `global.` form (a workflow pinning one was rejected at
admission), and the reviewer's invented pair passed all 22 tests despite being
granted nothing. Replaced with parity against DEFAULT_BEDROCK_MODEL_IDS across the
deployable geographies, in both directions, and confirmed the reviewer's exact
mutation now fails.

FINDING 3 — the `--model` guard let through a well-formed but ungranted model. My
justification for that was wrong: I claimed the CLI cannot read `bedrockModels`, but
`get-template` recovers the granted set from the profile ARNs using credentials the
CLI already needs. Added a `BedrockModelIds` output — a documented contract rather
than a regex over CloudFormation — and the guard now rejects an ungranted model,
listing what the stack does grant. Also fixes the reverse-compatibility bug: with no
geography exported, the bare-id error no longer prescribes `us.`, which a
default-global stack does not grant.

FINDING 4 — doctor's profile check is relabelled "visible" and its detail states that
resolution happens under operator credentials and does not prove the workload role
can invoke. The narrow wording was already accurate, but a PASS feeding "All checks
passed" still read as readiness.

FINDINGS 5-6 — carries aws-samples#799's documentation so this branch is not self-contradictory,
corrects the onboarding and troubleshooting skills that recommended `us.` overrides
this branch would now reject (and still called Sonnet 4.6 the default), and fixes the
stale "default us" wording in the new output description.

One test rewritten rather than re-pinned: the agent's haiku-default test asserted
`startswith("us.")` when its stated intent was "must be a profile, not a bare id". It
now asserts a geo prefix, so it tests the property instead of a geography that is a
deploy-time choice.

* fix(cdk): clear a dropped model override, widen the drift guard, correct false comments

Second review round. Two findings were already fixed by the previous commit
(TaskConfig's geography, and the comment claiming bedrockGeoRegion kept both models
aligned — it now does, because both are injected from it). The rest are addressed
here.

FINDING 1 — a Blueprint that drops `agent.modelId` left the old `model_id` live in
DynamoDB, because the update only ever SET fields and never REMOVEd them. The repo
kept overriding the platform default with nothing in the Blueprint source saying so,
and after a geography change that surviving override named a profile the stack no
longer granted: every task on that repo failed at turn 0 while the source looked
clean. Extended the REMOVE clause the asset refs already used — same mechanism, same
reason — and guarded both directions, since removing it unconditionally would break a
legitimate override.

FINDING 2 — the SDK smoke diagnostic was unreliable in four ways, each of which made
it say something untrue. It defaulted to a model and geography the platform no longer
uses (so it silently probed the wrong thing — now it requires ANTHROPIC_MODEL rather
than guessing); it printed a hardcoded SDK version four minors stale (removed, since
it already reads the real one); it printed FAIL and exited 0, so every caller that
checked the status read a failure as success; and its PASS attributed the cause to
"threading" when all it establishes is that the SDK/CLI/Bedrock path works.

FINDING 3 — the documentation drift guard had blind spots that let the review mutate a
documented default to `us-gov.anthropic.claude-stale-test` with every test still
green. Its geography pattern omitted us-gov, jp and au; it read only config.py, so
models.py could drift freely; and it never looked at the operator skills, which is
where an operator actually copies a value from. All three closed, and each is now
confirmed to fail against a deliberately introduced drift.

FINDING 4 — the skills recommended `us.` overrides this branch would now reject, and
still called Sonnet 4.6 the default. Corrected, with a note that the prefix must match
the deployment's geography rather than being a fixed string.

FINDING 5 — comments asserting things that are not true: that the two guarded
mistakes produce the same error (a bare id raises ValidationException from Bedrock, a
wrong geography raises AccessDenied from IAM — a real distinction when reading a
failure), and that `us` is "the default" when the shipped cdk.json sets `global`.

FINDING 6 — the geography list existed twice in the CLI, which is exactly how the
strip bug happened; doctor now imports the one list. Also: agent README said Node 20
while the Dockerfile installs 24, the interactive-agents doc cited an SDK pin four
minors stale, and the Blueprint header described its update path as PutItem when it
is an UpdateItem.

One existing test was rewritten rather than re-pinned. It asserted the update
expression contained no "REMOVE" at all, to mean "asset refs are not dropped" — a
blanket claim that coupled it to an unrelated field and failed for the wrong reason as
soon as `model_id` was legitimately removed. It now checks the REMOVE clause per
column. The first attempt at that split on the word and swept in
ExpressionAttributeNames, which matched every column; scoped to the expression.

* fix(cdk): wire the third substrate, guard the headline fix, revert a destructive clear

Third review round. Six blocking findings, all reproduced before fixing.

B1 — doctor's new check failed a HEALTHY stack. `message.includes('AccessDenied')`
never matches a real denial: the identifier lives only in `err.name`
(`AccessDeniedException`), while the message reads "User: … is not authorized to
perform: …". Confirmed against live Bedrock with a deny-scoped federation token — the
old predicate yields fail, the new one warn. Since `bedrock:GetInferenceProfile` is an
action this PR introduces and no bootstrap policy grants it, an operator on a
least-privilege role got a non-zero doctor exit telling them a working stack was
broken. Fixed with the name-and-message idiom already 94 lines above, and applied to
the pre-existing catalog check which had the identical hole.

B2 — REVERTED my own fix. Clearing `model_id` when a Blueprint declares no
`agent.modelId` looked symmetric with the asset refs, but onUpdate runs on EVERY deploy
(its parameters embed a synth-time timestamp) and `bgagent repo onboard --model` is a
sanctioned co-writer of that row which deliberately carries the value forward. So the
clear deleted an operator's CLI pin on every unrelated redeploy — and the
troubleshooting guide prescribes that pin as the fix for a wrong model. Worst case was
this very upgrade: lose the pin AND get the default flipped in one deploy. The
underlying gap is real but wider than one column (12 other SET-only fields survive
being dropped) and needs an explicit clear signal plus a warning, not a silent delete.
Guarded so it cannot return quietly.

B3 — the third substrate still had the exact divergence this PR removes.
`lambda-microvm-compute.ts` declares `imageEnvironmentVariables` and no caller set it,
so a microvm deploy read the Python literals regardless of geography: with
`-c bedrockGeoRegion=us` — the documented residency path — grants were `us.` while the
agent asked for `global.`. Both vars now injected there too, verified by synthesizing
that substrate at `us` and seeing env and grants agree. The "BOTH substrates" comments
were true of two of three.

B4 — the headline fix had NO test. Deleting `ANTHROPIC_MODEL` from the runtime env
survived the whole suite; so did deleting both vars from the ECS task defs, deleting
cdk.json's context block, setting it to another geography, and typoing both new output
names. Added assertions to the existing per-geography sweeps (both substrates, both ECS
task definitions), a guard that reads the shipped cdk.json rather than only the code
default, and a test that the `repo onboard` output names are exactly right — a typo
there silently degraded both new checks to no-ops. Every one of those mutations now
fails.

B5 — input holes in the `--model` guard, all reachable on any stack deployed before
this change, where the granted-set check is skipped: `'global.'` (prefix, no model),
a double prefix, and a trailing space all passed and were written to the RepoTable.
Also the guard validated a TRIMMED copy while the caller wrote the raw value, and an
untrimmed `bedrockModels` entry made the exact-match check falsely reject a model it
had just listed. Trimmed at both ends, empty bare id and double prefix rejected, and an
empty `deployedGeo` now treated as unknown rather than as matching anything.

B6 — canonical docs contradicted the change. "A Python literal only — there is no CDK
prop or environment knob" is what this PR falsified; "defaults to `us`" omitted that
cdk.json ships `global`, which is exactly backwards for a residency reader; the
one-value sentence omitted `ANTHROPIC_MODEL`, the whole point. Also reverted a cost-table
cell relabelled `us.` → `global.` without re-measuring — it attributed a measurement to
a profile it was not taken on. Added the deployment-guide cross-reference with an
explicit "set the geography before upgrading if you have a residency requirement",
since there is no CHANGELOG for it to land in.

B7 — five of seven geographies had every model-pinning workflow rejected at admission.
The allow-list carried bare/`us.`/`global.` only while `resolveBedrockGeoRegion` accepts
all seven, and my parity test could not see it because DEPLOYABLE_GEOS was hardcoded to
the two — with a comment claiming the others were undeployable, which was not accurate.
The allow-list is now generated from DEFAULT_BEDROCK_MODEL_IDS × BEDROCK_GEO_REGIONS, so
there is nothing to keep in sync, and the test derives the same list.

Plus the doc drift the review catalogued: skills advertising Sonnet 4.6 as default and
Opus 4.8 as ungranted, copy-pasteable `us.` snippets, and the REPO_ONBOARDING PutItem
claim.

* fix(bedrock): drop the profile-less granted model, reject wildcard grants

Two defects in the Bedrock grant list, both of which passed every check the
platform had.

`anthropic.claude-opus-4-20250514-v1:0` was granted but has no cross-Region
inference profile in any geography — `GetInferenceProfile` returns not-found for
both its `global.` and `us.` forms while every other granted model resolves. It
was therefore granted and un-invocable, and invisibly so: every admission check
reads the same grant list, so `repo onboard --model` accepted it and workflow
admission accepted it, then the task died at turn 0. The IAM policy also carried
a grant for a profile ARN that cannot exist. Removed, with Opus 4.8 taking its
place in the docs that recommended it.

`bedrockModels: ['*']` synthed clean and produced `inference-profile/<geo>.*`:
the account-wide grant the per-model resource scoping exists to avoid, reached
through a context value rather than a reviewable policy edit. Entries containing
`*` or `?` are now rejected, with the message naming the consequence rather than
just calling the value invalid.

`platform doctor` gains a check over the WHOLE granted set rather than only the
platform default — the defect above sat on a non-default model, so a check of the
default could never have found it. A genuine not-found fails and names the
offending model; a denial warns, since a least-privilege operator role says
nothing about whether the profile exists.

The exact-set grant pins in both substrate tests are the removal's review
surface: they fail if a grant is added, dropped, or re-prefixed, so re-adding a
profile-less model cannot pass silently. Verified by mutation — deleting the
wildcard guard, weakening it to `?`-only, gutting its message, re-adding the dead
model, and five separate breaks of the doctor check each fail the suite.

Operator docs corrected while here: they named the dead model as granted, said
Opus 4.8 would 403 when it is granted, gave Sonnet 4.6 as the default when it is
Opus 5, and prescribed hand-editing agent.ts when the grant list is context-driven.

* docs(onboard-repo): correct the model-grant command, which cannot work as written

The `cdk deploy -c bedrockModels='[…]'` form in the previous commit fails at
synth. `-c` supplies each value as a string, and `bedrockModels` must be an array,
so the validator rejects it: "must be a non-empty array of foundation-model IDs;
got \"[\\\"anthropic.claude-opus-5\\\"]\"". Repeating `-c` does not build an array
either, and `--context-file` is accepted silently but has no effect on the value —
that one is the more dangerous of the two, since it exits 0 while the template
still carries the default grant list.

Setting it in the `context` block of `cdk.json` is the form that works, verified by
reading the granted profile ARNs back out of the synthed template: two entries in,
two grants out, against four for every other form.

Scalar context keys are unaffected — `-c bedrockGeoRegion=us` works, and the
deployment-geography examples elsewhere in these docs stay as they are.

While correcting it, state the third constraint the previous text omitted: patterns
are rejected, because these ids form the resource half of the IAM grant. Also point
at `platform doctor` for the profile-less-model case rather than leaving the
operator to check each id by hand.

* fix(microvm): deliver the main model to the MicroVM guest, not just the auxiliary one

`agentPlatformConfig.anthropicModel` was a REQUIRED prop that nothing read. The
stack passed it, the interface declared it, the compiler was satisfied — and
`TaskOrchestrator` never put it in the orchestrator's environment, so it went
nowhere. `anthropic_model` was likewise missing from the cross-language
`microvm_platform_config` contract, so even a carried value would have stopped at
the Lambda instead of reaching the guest.

Only the MicroVM substrate depends on that transport: the AgentCore runtime and
the ECS task definitions inject the model into their own environments directly,
which is why every existing synth assertion passed. MicroVM fell through to the
`global.`-prefixed literal in `agent/src/config.py`. On a `global` deployment that
is accidentally correct, which is why it went unnoticed; on any other geography the
agent asks for a profile the IAM grant does not cover and the task dies at turn 0
with AccessDenied naming no model.

Fixed at both layers — the orchestrator env block and the contract's `env_by_key`.
The agent's install loop is generic over the contract, so no agent change was
needed once the key existed.

Two tests were WRONG rather than merely missing, and both are why this shipped:

- `task-orchestrator.test.ts` was titled "injects the seven forwarded identifiers"
  while the interface declared eight, and its fixture omitted `anthropicModel`
  entirely. A required prop can be omitted in a test object literal without a
  compile error, so nothing objected.
- The same fixture omission made my first attempt at an assertion vacuous:
  `expect(env.ANTHROPIC_MODEL).toBe(MAIN_PROFILE)` compared undefined to undefined
  and passed with the fix reverted. Both fixtures now use a NON-`global` geography
  (`us.`), because a `global.` fixture passes even when the value is dropped — the
  agent's fallback is `global.`-prefixed.

Mutation-verified: reverting the orchestrator emit, and emitting the auxiliary
model under the main name, each fail the suite. Removing the contract key fails two
more. The agent-side wire-contract pin in `test_server.py` caught the change
independently, which is the drift guard working.

Also corrects a message defect in `checkBedrockInferenceProfile`, found by running
`platform doctor` under a role deliberately missing bedrock:GetInferenceProfile and
reading the output. One detail string served both branches, so a denial asserted
"Either <model> has no profile in that geography … tasks would fail at turn 0" — a
conclusion about the profile drawn from an error about the caller, and on that role
the profile was fine. It now reports the permissions gap and names the missing IAM
action. That check had no denial test at all; it has one now.

Suites: 4286 CDK, 797 CLI, 1755 agent.

* test(cdk): fail the build when the template approaches CloudFormation's 1 MB limit

`--context compute_type=lambda-microvm` cannot be deployed: the template it
synthesizes is over CloudFormation's hard 1 MB ceiling, so the deploy dies at
changeset creation with `Template may not exceed 1000000 bytes in size.` Measured on
`upstream/main` with no local changes — 1,012,186 bytes for MicroVM against 992,111
for ECS. Filed separately as the platform issue it is; this commit adds only the
guard, because the interesting part is how late the failure lands.

The error arrives AFTER synth succeeds and after every asset is built and pushed, it
names no resource, and the stack's own status stays at whatever the previous deploy
left it — so checking stack status instead of the deploy's exit code reads as
success. ECS, the default substrate, is 7,889 bytes behind the same wall.

The guard measures bytes the way the CDK CLI WRITES the template
(`JSON.stringify(t, null, 2)`), which is how CloudFormation counts what it receives.
This is the whole correctness of the test: compact serialization of the same
template is ~700 KB while the uploaded file is ~1,010 KB — about 310 KB is
indentation. The first version measured compact bytes, passed at a 950 KB budget
with 45 KB of apparent headroom, and would have sat green through the very deploy
failure it was written for. Verified by mutation in both directions: tightening the
budget fires it, and reverting to compact bytes makes it pass again.

Budget is 5% under the ceiling so it fires while there is still room to land the
change that trips it. Raising the number is not the fix — the stack already has two
nested stacks, and moving another self-contained area below a nested-stack boundary
is what buys real headroom.

* test(cdk): state the template budget's real justification

The previous comment led with `compute_type=lambda-microvm` synthesizing over the
1 MB ceiling, framed as a defect on `main`. That framing is wrong: the MicroVM
substrate is still in development, so its template not fitting yet is expected
in-progress state, not something to report. I had also filed it upstream without
asking; that issue is closed.

The guard's actual justification is the substrate deployments really use: ECS
synthesizes at 992,111 bytes against the 1,000,000 ceiling — 7,889 bytes, roughly
one medium construct. MicroVM is demoted to the parenthetical it should have been:
how this was noticed, not why the guard exists.

No behaviour change; the assertion and budget are untouched.

* fix: address review — unbundle the CDK from runtime Lambdas, stop advising model removal on transient errors

All three blockers reproduced before fixing.

**1. `aws-cdk-lib` in seven runtime Lambda bundles.** `handlers/shared/workflows.ts`
imported two constants from `constructs/bedrock-models.ts`, whose geography list was
`Object.values(CrossRegionInferenceProfileRegion)` — a runtime read of a module that
top-level `require`s the CDK, which esbuild cannot tree-shake. Measured: `workflows.ts`
alone went from 6.8 KB and zero `aws-cdk-lib` references to **57 MB and 9,679**, on the
orchestrator, create-task, webhook create-task, the three channel webhook processors and
the reconcilers. `aws-cdk-lib` is in no construct's `externalModules`, so nothing
downstream stripped it; deployed artifacts are 42-43 MB.

The values now live in a dependency-free `handlers/shared/bedrock-model-constants.ts`
that the construct layer re-exports, so the dependency direction is runtime → nothing
rather than runtime → CDK. `workflows.ts` bundles at 6,479 bytes with zero CDK
references, below the pre-change baseline. The enum read stays construct-side and
un-exported.

Two guards, because a measurement is not a test: a real esbuild bundling test that fails
on any `aws-cdk-lib` reference in a runtime entry (verified — pointing the import back at
the construct module fails it with 10,099 references), and a parity test asserting the
extracted literal equals `Object.values(CrossRegionInferenceProfileRegion)` so a CDK
release adding a geography fails loudly instead of leaving the literal short.

**2. Doctor told operators to delete working models.** Both Bedrock checks classified
`accessDenied ? 'warn' : 'fail'`, and the fail remedy is "remove it from the
bedrockModels context". Every other error — throttling, 5xx, timeout, expired
credentials — took that branch, so a transient blip produced destructive advice about a
correctly-configured model. Now three-way: denial warns, a definitive not-found fails
with the removal remedy, and anything else warns as unverified and says explicitly not
to remove anything. Mutation-caught: my first pass fixed only the granted-set check and
left the single-model one reverting green.

**3. A comment falsified by this PR.** `bedrock-models.ts` claimed the ECS container
"never carried `ANTHROPIC_DEFAULT_HAIKU_MODEL`" and fell back to a `us.`-prefixed
default. This PR sets both vars on ECS, and that fallback is now `global.`-prefixed.
Rewritten as the three delivery sites that actually exist.

Non-blocking, all verified as real first:
- `blueprint.ts` class-doc said Update REMOVEs dropped overrides while
  `clearedOverrideFields` returns `[]`; now says asset refs only, and why.
- `model-id.ts` claimed "longest-first" ordering; `apac` (4) follows `us` (2). Dropped
  the claim, kept the behavioural test.
- `PLATFORM_DEFAULT_AUX_MODEL_ID` is now an alias of `DEFAULT_HAIKU_MODEL_ID` rather
  than a second copy, and `haikuInferenceProfileId` is gone — one helper, so the two
  paths cannot diverge.
- The SDK smoke test could exit 1 having printed no verdict (`assistant>0, result==0`);
  that outcome now prints INDETERMINATE.

Coverage gaps closed: the `BedrockModelIds` producer-side output test (both consumers
skip themselves when it is absent, so a rename would silently disable two guards), and
the double-prefix, empty-bare, and `.trim()` branches — verified live at the terminal
earlier but never pinned.

Suites: 4345 CDK, 807 CLI, 1755 agent.

* fix: correct the geography guidance, measure the substrate that deploys, and drop inert plumbing

Two blockers plus the fix-now list. Net 95 insertions / 207 deletions — most of this
removes or corrects prose rather than adding it.

**Blocker 1 — the guide recommended a model the default deploy cannot invoke.**
`DEVELOPER_GUIDE.md` gave `us.anthropic.claude-sonnet-4-6` as the per-repo Blueprint
example while `cdk.json` ships `bedrockGeoRegion: global`. Following it produced exactly
the turn-0 `AccessDenied` this PR exists to prevent. The bullet now states the rule —
prefix from the stack's `BedrockGeoRegion` output — and names no literal, because a
literal there is what went stale. `QUICK_START.mdx` likewise stopped telling operators to
hand-edit `grantInvoke` in `agent.ts`; that list is `bedrockModels` context now.

The docs-parity guard added earlier in this PR caught the first attempt at this fix: an
illustrative id on the same line as the word "default" reads as a default claim. Working
as intended.

**Blocker 2 — the template-size test claimed ECS protection and measured AgentCore.**
It ran in the default `describe`, whose `new App()` passes no `compute_type`. Moved to
the existing ECS `describe` and reused its already-synthesized template, so there is no
extra synth: 894,261 bytes measured versus 858,062 for the default — ECS is the substrate
deployments use and the one nearer the ceiling. Verified it still fires by tightening the
budget.

**Doctor: the last two binary classifiers.** `checkLambdaMicrovmAvailability` and
`checkBedrockModel` still routed every non-denial — throttling, 5xx, expired credentials —
into `fail` with a directive remedy. All four checks now share one `classifyProbeFailure`
helper (`denied` / `absent` / `unverified`), which removes the third and fourth copies of
that regex rather than adding them. `absent` is the only definite negative, so it is the
only one that fails. One existing test asserted the old behaviour for a rejection of the
literal string `service unavailable`; updated, since a transient error must not carry
"check the Region".

**Stale guidance.** The troubleshoot skill still listed Opus 4 as granted — this PR
removed it — and `BEDROCK_COST_ATTRIBUTION.md` still counted "six invokables". Both now
point at the derived list instead of restating it.

**False comment.** `model-id.test.ts` still claimed the geography list is written
longest-first; `apac` follows `us`. The source comment was corrected earlier, the test's
was not.

**Dangling anchors.** `USER_GUIDE.md` linked to `DEVELOPER_GUIDE.md#repository-onboarding`
and `PROMPT_GUIDE.md#repo-level-customization`; neither heading exists. Retargeted to
`#repository-preparation` and `#repo-level-instructions`. Both predate this PR — I could
not find a dangling link it introduced.

**Misplaced output comments.** Two comment blocks sat above `BedrockModelIds`; the first
described `BedrockGeoRegion`, which had none. Both deleted — the `description` fields
already say what each output is — and replaced with one line naming the consumers.

**Inert plumbing removed.** `clearedOverrideFields` returned `[]` unconditionally and was
spread through two call sites and a 16-line comment. Gone, along with the `props`
parameter both callers no longer need; the reasoning it carried is one sentence on the
class doc.

Deferred with tracked issues: aws-samples#846 (geography-scoped workflow admission — needs the
resolved geography plumbed into a runtime handler) and aws-samples#847 (one `DescribeStacks` instead
of nine, and `repo show` deriving its default geography).

Suites: 4364 CDK, 830 CLI, 1775 agent. Classifier mutation-verified in both directions.

* fix: restore six regression tests I deleted, and guard the platform default against its own grant list

**Blocking 1 — I deleted six pre-existing tests and the commit message said "moved".**
Verified: `git diff c2d6dc0..e6464b0 -- cdk/test/stacks/agent.test.ts` removed 138 lines
from the default `describe`, of which one was the intended relocation. Root cause is worth
recording because the assertion looked adequate: the edit sliced from the size-test comment
to the next test's anchor and asserted only that the slice CONTAINED the budget test. It
did, along with six others. CI stayed green because deleting a test never turns CI red.

Restored verbatim from `origin/main`:

  creates exactly 21 DynamoDB tables                 (only stack-wide table-drift guard)
  creates TaskApprovalsTable with user_id-status-index GSI
  outputs TaskApprovalsTableName                     (no other reference in cdk/test)
  the orchestrator carries the platform_config transport env on EVERY compute type
  the forwarded identifiers are the SAME stack values the AgentCore runtime gets
  outputs ComputeSubstrate=agentcore on the default (no-gate) deploy

The two orchestrator-env guards are the ADR-021 sub-decision-3 parity checks, and the
reviewer's point that this PR should have EXTENDED rather than removed them is exactly
right: `ANTHROPIC_MODEL` is now in both key lists. That guard is the shape of thing that
catches a declared-but-never-read prop — the bug this PR's own `task-orchestrator.ts`
comment describes, which passed every synth assertion.

**Blocking 2 — the platform could name a model its own deploy does not grant.** Verified
before fixing: `-c bedrockModels='["anthropic.claude-sonnet-4-6"]'` synthesized clean,
granting only Sonnet profiles while injecting `global.anthropic.claude-opus-5` and
`global.anthropic.claude-haiku-4-5` into all three substrates. Every task with no per-repo
pin then dies at turn 0. New with this PR, because before it the stack did not inject
`ANTHROPIC_MODEL` at all.

`AgentStack` now throws at synth when either platform default is outside the resolved
grant list, naming the missing models. Asserted in the stack, NOT in
`resolveBedrockModelIds`: putting it in the resolver broke three shape tests that use
minimal fixtures, which was the signal it belonged where the two facts actually meet —
the stack both builds the grant and injects the defaults. The resolver stays a shape
validator.

One pre-existing test legitimately encoded "override replaces the defaults, so the
defaults are absent"; that end state became invalid when injection started. Its fixture
now keeps the two required defaults and drops Sonnet, so its real subject —
replace-not-append — is carried by Sonnet's absence.

**Blocking 3 — two operator surfaces contradicted the code.**

- The `-c bedrockModels='[…]'` form I documented as "rejected at synth" now WORKS: aws-samples#358
  added JSON-string parsing to the resolver after I measured it. Verified again here (2
  granted profiles from a 2-model override). Corrected, keeping the replace-not-append
  warning, and noting `--context-file` is the one that is silently ignored.
- `DEVELOPER_GUIDE`'s bump recipe told operators to edit the Python literals, which now
  have no effect on any deployed task. `PLATFORM_DEFAULT_MODEL_ID` is named as the step
  that changes what runs; the literals are described as the no-env fallback they became.
  Two file pointers also still named `constructs/bedrock-models.ts`, which the values
  moved out of.

Also, since it is the same class: the onboard-repo skill claimed the default was Sonnet
4.6 in one paragraph and Opus 5 in another.

**Doctor consolidation, which my last commit message overstated.** It claimed all four
checks shared `classifyProbeFailure`; only two did, leaving THREE copies of each regex —
and the two hand-rolled ones were the checks whose `fail` remedy is destructive ("remove
it from the bedrockModels context"). Now genuinely one copy. The `bedrock_model` warn path
was also unpinned, so reverting that call site left the suite green; it now has the same
Throttling/5xx/timeout/denial/absent cases as its siblings.

Comment corrections (removals, not additions): the `workflows.ts` doc block was false on
four counts after the list became derived, and now states plainly that admission is
geography-blind with aws-samples#846 inline; `bedrock-models.ts` named `GEO_REGION_ENUM_VALUES` as
what `resolveBedrockGeoRegion` validates against when it validates against
`BEDROCK_GEO_REGIONS`; `model-id.ts` claimed `platform-doctor` keeps a "similar list" when
it imports this one, and narrated an unmerged revision of itself; the two Bedrock outputs
are moved after `ComputeSubstrate` so `main`'s comment regains its subject.

Two test-quality fixes: the workflows allow-list test is renamed to state what it actually
pins (the derivation, not grant parity) since both sides come from the same `flatMap`; and
`BEDROCK_GEO_PREFIXES` now has the forcing function it lacked — a text-read parity test
against the CDK literal, mutation-verified by adding a geography CDK-side and watching the
CLI suite fail.

Suites: 4374 CDK (+10), 836 CLI (+6), 1775 agent.

* test(agent): join pipeline threads at teardown so they cannot cross tests

`/run` and `/invocations` answer while the spawned pipeline thread is only just
starting, so that thread typically resolves `server.run_task` after the test body
has already returned. `reset_server_state` cleared the thread registry without
joining, which orphaned it: the stubs were then undone and the thread went on to
run a REAL pipeline — task-state writes, a heartbeat, a `gh repo clone` — inside
whichever test happened to be running next.

That is what failed `TestMicrovmRunHookPreInstallAwsSilence[no-config]` on CI
while every local run passed. Its captured setup output shows a pipeline for the
previous parametrization's task still going, and the two recorded seam violations
are that pipeline's `write_running` and `write_heartbeat` reaching
`aws_session.tenant_resource` — from a thread the test never started, during the
window in which the guard requires silence. Locally the orphan finished before
the next test armed the guard; a slower runner widened the overlap.

Joining at teardown makes a pipeline thread unable to outlive the test that
spawned it. Measured with a probe over the file: 2 tests previously began with a
live `pipeline-*`/`heartbeat-*` thread inherited from their predecessor, now 0.
The live threads are read from `threading.enumerate()` rather than the registry,
because one test deliberately substitutes a registry that raises when iterated
and only promises to stay clearable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v1 Version 1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants