Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ The `run.sh` script overrides the container's default CMD to run `python /app/sr
| `AWS_SECRET_ACCESS_KEY` | Conditional† | | Explicit keys, if you are not using CLI-based resolution |
| `AWS_SESSION_TOKEN` | No | | For temporary credentials |
| `AWS_PROFILE` | No | | Profile for `aws configure export-credentials` in `run.sh`, or default profile when using the `~/.aws` mount fallback |
| `ANTHROPIC_MODEL` | No | `us.anthropic.claude-sonnet-4-6` | Bedrock **inference profile** or model ID for `InvokeModel` (see [inference profiles](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-use.html)) |
| `ANTHROPIC_MODEL` | No | `us.anthropic.claude-opus-4-8` | Bedrock **inference profile** ID for `InvokeModel` (see [inference profiles](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-use.html)). Must be the `us.`-prefixed profile ID, not a bare foundation-model ID — see [Model configuration](../docs/guides/DEVELOPER_GUIDE.md#model-configuration) |
| `MAX_TURNS` | No | `100` | Max agent turns before stopping |
| `MAX_BUDGET_USD` | No | | **Local batch only** (shell env when running `entrypoint.py` directly). Range 0.01–100; agent stops when the budget is reached. For deployed AgentCore **server** mode and production tasks, set **`max_budget_usd`** on task creation (REST API, CLI `--max-budget`, or Blueprint default); the orchestrator sends it in the `/invocations` JSON body — server mode does not read `MAX_BUDGET_USD` from the environment. |
| `DRY_RUN` | No | | Set to `1` to validate config and print the prompt without running the agent |
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | No | `anthropic.claude-haiku-4-5-20251001-v1:0` | Bedrock model ID for the pre-flight safety check (see below) |
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | No | `us.anthropic.claude-haiku-4-5-20251001-v1:0` | Bedrock **inference profile** ID for the small/fast auxiliary model — the pre-flight safety check and WebFetch summarization (see below). Set by the CDK stack (`cdk/src/stacks/agent.ts` (the runtime environment block)); the `us.` prefix is required |
| `NUDGES_TABLE_NAME` | No | | **Phase 2.** DynamoDB table for mid-task user nudges (`<user_nudge>` XML blocks injected between turns). If unset, the agent runs without nudge support — `nudge_reader.read_pending()` returns `[]` and logs a WARN once. Set automatically by the CDK stack on both AgentCore runtimes. |
| `JIRA_APP_ACTOR_PROXY_URL` | No | | Resolved per-task from the Jira tenant secret. Forge v2 web-trigger URL used for app-authored Jira comments and transitions. |
| `JIRA_APP_ACTOR_SHARED_SECRET` | No | | Resolved per-task from the Jira tenant secret. HMAC key for the Forge proxy; redacted from agent diagnostics. |
Expand All @@ -133,7 +133,7 @@ The `run.sh` script overrides the container's default CMD to run `python /app/sr
including non-Jira tasks, so a warm AgentCore process cannot expose one
tenant's OAuth or Forge credential to the next task.

**Bedrock model access (main model):** Configuring `ANTHROPIC_MODEL` and IAM credentials is not enough. Your AWS account must be able to **invoke** that model in Amazon Bedrock: follow [Request access to models](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) (Marketplace permissions on first use, Anthropic first-time use where required, valid payment method for Marketplace-backed models). Use an inference profile ID such as `us.anthropic.claude-sonnet-4-6` when Bedrock requires it. If the CLI stops with a message that the model is not available on your Bedrock deployment, fix model access in the console or switch `ANTHROPIC_MODEL` to an entitled profile, then retry.
**Bedrock model access (main model):** Configuring `ANTHROPIC_MODEL` and IAM credentials is not enough. Your AWS account must be able to **invoke** that model in Amazon Bedrock: follow [Request access to models](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) (Marketplace permissions on first use, Anthropic first-time use where required, valid payment method for Marketplace-backed models). Always use an inference profile ID such as `us.anthropic.claude-opus-4-8`: a bare foundation-model ID cannot be invoked with on-demand throughput and Bedrock rejects it with `ValidationException`. IAM must also grant the model — see [Model configuration](../docs/guides/DEVELOPER_GUIDE.md#model-configuration) for the full layering. If the CLI stops with a message that the model is not available on your Bedrock deployment, fix model access in the console or switch `ANTHROPIC_MODEL` to an entitled profile, then retry.

**Pre-flight check model**: Claude Code runs a quick safety verification using a small Haiku model before executing each tool command. On Bedrock, the default Haiku model ID may not be enabled in your account, causing the check to time out with *"Pre-flight check is taking longer than expected"* warnings. The agent sets `ANTHROPIC_DEFAULT_HAIKU_MODEL` to a known-available Bedrock Haiku model ID to avoid this. If you see pre-flight timeout warnings, verify that this model is enabled in your Bedrock model access settings.

Expand All @@ -145,7 +145,8 @@ tenant's OAuth or Forge credential to the next task.
# Dry run — validate config, fetch issue, print assembled prompt, then exit
DRY_RUN=1 ./agent/run.sh "owner/repo" 42

# Run with a specific model
# Run with a specific model (overrides the us.anthropic.claude-opus-4-8 default).
# Must be a `us.`-prefixed inference profile that IAM grants — see Model configuration.
ANTHROPIC_MODEL="us.anthropic.claude-sonnet-4-6" ./agent/run.sh "owner/repo" 42

# Limit agent to 50 turns
Expand Down
192 changes: 192 additions & 0 deletions cdk/test/contracts/model-default-docs-parity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/**
* MIT No Attribution
*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

import * as fs from 'fs';
import * as path from 'path';

/**
* DOCS CONTRACT: the model defaults the docs advertise must equal the defaults
* the agent actually uses.
*
* The agent's default model is a Python literal in `agent/src/config.py` with no
* CDK prop or environment knob in front of it, so a model bump is a one-line
* source edit — and every doc that quotes the old value silently becomes a lie.
* That is exactly what happened: four docs advertised a Sonnet-4.6 default long
* after the code moved to Opus 4.8, and `agent/README.md` advertised a BARE
* haiku id that cannot be invoked on-demand at all, contradicting the `us.`
* -prefixed inference-profile id the stack actually deploys. Nothing guarded
* either one, so both rotted unnoticed across several releases (#742).
*
* `cdk/test/constructs/bedrock-models.test.ts` already proves this
* cross-language regex-grep pattern for the code→IAM half of the invariant (the
* agent fallback must be in `DEFAULT_BEDROCK_MODEL_IDS` or every task fails at
* turn 0 with AccessDenied). This file closes the code→docs half: the next model
* bump fails CI here instead of quietly rotting the documentation.
*
* Deliberately asserts against the *rendered doc text* (the value inside
* backticks in the env-var tables) rather than a shared constant, because the
* failure mode being guarded is precisely a human reading a stale doc.
*/

const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');

function read(relPath: string): string {
return fs.readFileSync(path.join(REPO_ROOT, relPath), 'utf8');
}

/**
* Extracts an env-var fallback literal from `agent/src/config.py`, i.e. the
* second argument of `os.environ.get("<name>", "<default>")`. Tolerates the
* line wrapping Ruff applies to the call.
*/
function agentDefaultFor(envVar: string): string {
const configPy = read('agent/src/config.py');
const match = configPy.match(new RegExp(`"${envVar}",\\s*"([^"]+)"`));
expect(match).not.toBeNull();
return match![1];
}

/**
* Extracts the `Default` cell of a markdown env-var table row keyed by
* `` `<envVar>` `` — the value the doc advertises to a reader. Returns every
* match so a doc with more than one such table fails loudly rather than having
* the second table silently unguarded.
*/
function documentedDefaults(markdown: string, envVar: string): string[] {
const rows = markdown
.split('\n')
.filter((line) => line.trimStart().startsWith('|') && line.includes(`\`${envVar}\``));
const found: string[] = [];
for (const row of rows) {
// Cells, minus the leading/trailing empties produced by the outer pipes.
const cells = row.split('|').slice(1, -1).map((c) => c.trim());
// The default is the first backticked cell AFTER the one naming the env var.
const nameIdx = cells.findIndex((c) => c === `\`${envVar}\``);
if (nameIdx === -1) continue;
for (const cell of cells.slice(nameIdx + 1)) {
const literal = cell.match(/^`([^`]+)`$/);
if (literal) {
found.push(literal[1]);
break;
}
}
}
return found;
}

describe('documented model defaults match the agent runtime defaults', () => {
// The docs that advertise a default in an env-var table. Both are read by
// humans configuring a deployment, so both must track config.py.
const DOCS_WITH_ENV_TABLES = [
Comment thread
theagenticguy marked this conversation as resolved.
'docs/guides/DEVELOPER_GUIDE.md',
'agent/README.md',
] as const;

it.each(DOCS_WITH_ENV_TABLES)('%s documents the real ANTHROPIC_MODEL default', (docPath) => {
const expected = agentDefaultFor('ANTHROPIC_MODEL');
const documented = documentedDefaults(read(docPath), 'ANTHROPIC_MODEL');
// A doc that stops documenting the default at all is also a regression: the
// guard would silently pass on an empty list.
expect(documented.length).toBeGreaterThan(0);
for (const value of documented) {
expect(value).toBe(expected);
}
});

it('agent/README.md documents the real ANTHROPIC_DEFAULT_HAIKU_MODEL default', () => {
const expected = agentDefaultFor('ANTHROPIC_DEFAULT_HAIKU_MODEL');
const documented = documentedDefaults(read('agent/README.md'), 'ANTHROPIC_DEFAULT_HAIKU_MODEL');
expect(documented.length).toBeGreaterThan(0);
for (const value of documented) {
expect(value).toBe(expected);
}
});

it('both agent defaults are inference-profile ids, not bare foundation-model ids', () => {
// A bare `anthropic.…` id cannot be invoked with on-demand throughput
// (Bedrock returns ValidationException), so documenting one sends readers
// down a dead end. Guards the specific bug fixed in agent/README.md.
for (const envVar of ['ANTHROPIC_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL']) {
expect(agentDefaultFor(envVar)).toMatch(/^(us|eu|apac|global)\./);
}
});

/**
* The env-var-table assertions above only reach the two docs that HAVE such a
* table. Four other places quote a model literal — the `model_id` rows in
* USER_GUIDE / REPO_ONBOARDING and their two generated Starlight mirrors — and
* nothing read them, so on the next model bump CI would force the two guarded
* docs to update while those four quietly went stale again. That is precisely
* the rot this file exists to stop, so sweep every model literal in the doc set
* instead of enumerating table shapes: any `us.`/`eu.`/`apac.`/`global.`-prefixed
* Claude id in a guarded doc must be one the agent actually defaults to.
*
* Deliberately literal-based rather than row-based: it survives someone
* re-wording a table, moving the value into prose, or adding a doc — none of
* which a row parser would follow.
*/
it('no guarded doc presents a stale model id AS the default', () => {
const allowed = new Set([
agentDefaultFor('ANTHROPIC_MODEL'),
agentDefaultFor('ANTHROPIC_DEFAULT_HAIKU_MODEL'),
]);
// Only lines that CLAIM to state the default are in scope. A doc legitimately
// names other models as illustrative examples — a per-repo override snippet, a
// cost-comparison row, "switch to a lighter model such as X" — and failing those
// would make the guard unmaintainable, so it would end up deleted rather than
// fixed. Match on the claim, not on the mere presence of an id.
// Two shapes claim a default: prose/cells saying so, and a `model_id` table row
// whose Default CELL carries the literal with no such word on the line at all
// (REPO_ONBOARDING's blueprint-defaults table and USER_GUIDE's per-repo table
// are both this shape — mutation-tested, and a keyword-only rule misses them).
const CLAIMS_DEFAULT = /\bdefaults?\b|\bfallback\b|^\s*\|\s*`model_id`\s*\|/i;
// Prefixed inference-profile ids only. Bare `anthropic.claude-…` ids appear
// legitimately when the docs explain WHY a bare id is not invocable, and the
// IAM grant list in bedrock-models.ts is bare-by-contract — both out of scope
// here and already covered by bedrock-models.test.ts.
const MODEL_ID = /\b(?:us|eu|apac|global)\.anthropic\.claude-[a-z0-9-]+(?::[0-9]+)?/g;
// Hand-authored sources plus every generated mirror that actually quotes a
// prefixed id (enumerated from the tree, not guessed — `using/Overview.md`
// mirrors USER_GUIDE but carries no literal, so listing it would assert
// nothing while implying coverage).
const GUARDED_DOCS = [
'docs/guides/DEVELOPER_GUIDE.md',
'docs/guides/USER_GUIDE.md',
'docs/design/REPO_ONBOARDING.md',
'agent/README.md',
'docs/src/content/docs/architecture/Repo-onboarding.md',
'docs/src/content/docs/customizing/Per-repo-overrides.md',
'docs/src/content/docs/developer-guide/Model-configuration.md',
'docs/src/content/docs/developer-guide/Repository-preparation.md',
'docs/src/content/docs/developer-guide/Installation.md',
'docs/src/content/docs/getting-started/Quick-start.mdx',
] as const;

const offenders: string[] = [];
for (const docPath of GUARDED_DOCS) {
for (const [i, line] of read(docPath).split('\n').entries()) {
if (!CLAIMS_DEFAULT.test(line)) continue;
for (const id of line.match(MODEL_ID) ?? []) {
if (!allowed.has(id)) offenders.push(`${docPath}:${i + 1} → ${id}`);
}
}
}
expect(offenders).toEqual([]);
});
});
26 changes: 17 additions & 9 deletions cdk/test/stacks/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,15 +294,23 @@ describe('AgentStack', () => {
// and the per-task session role (the coding agent's task-model grants). The
// override replaces the model set for the WORKLOAD; these are its surfaces.
//
// Deliberately EXCLUDES the Linear webhook processor's policy: the
// deterministic-revise interpreter (linear-integration.ts) makes one tiny
// "which plan-edit did they mean?" classification call pinned to a FIXED
// model (DEFAULT_REVISE_MODEL_ID = sonnet), by design independent of the
// per-task ``bedrockModels`` override — you don't want a cheap classification
// running on whatever heavyweight coding model an operator selected. That
// grant is scoped to its single fixed model (asserted in the linear
// integration tests), so it's not a wildcard/drift risk; it just isn't part
// of the override contract this test checks.
// The prefix filter, not a blanket scan, is what this test asserts against.
// On main those two roles are in fact the ONLY policies in the stack holding
// a bedrock:InvokeModel statement — the Linear webhook processor deliberately
// has none (`linear-integration.ts`: "No bedrock:InvokeModel grant: this
// processor never calls a model directly"; its only Bedrock action is
// ApplyGuardrail). So the filter is currently a no-op belt-and-braces guard
// that keeps this assertion honest if a future construct adds an
// InvokeModel grant that the ``bedrockModels`` override is not meant to
// govern — e.g. a cheap fixed-model classification call, which you would not
// want running on whatever heavyweight coding model an operator selected.
//
// (An earlier revision of this comment cited a fixed-model revise grant via a
// `DEFAULT_REVISE_MODEL_ID` constant. That constant and its
// orchestration-plan-revise-interpret module exist only on the unmerged
// #299 branch and never landed on main, so the reference was dangling — see
// #742. Corrected rather than deleted to record that the exclusion describes
// a hypothetical, not a live grant.)
const OVERRIDE_GOVERNED_POLICY_PREFIXES = ['RuntimeExecutionRole', 'AgentSessionRole'];
const policies = overridden.findResources('AWS::IAM::Policy');
const bedrockResources: unknown[] = [];
Expand Down
2 changes: 1 addition & 1 deletion docs/design/REPO_ONBOARDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ From lowest to highest priority:
|---|---|---|
| `compute_type` | `agentcore` | Platform constant |
| `runtime_arn` | Stack-level env var | CDK stack props |
| `model_id` | Claude Sonnet 4 | CDK stack props |
| `model_id` | `us.anthropic.claude-opus-4-8` | Python literal in `agent/src/config.py` (no CDK prop or env knob today) — see [Model configuration](../guides/DEVELOPER_GUIDE.md#model-configuration) |
| `max_turns` | 100 | Platform constant |
| `max_budget_usd` | None (unlimited) | - |
| `memory_token_budget` | 2000 | Platform constant |
Expand Down
Loading
Loading