From 37cc74c764679b3b6695493b463df11e4a2d5d0d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 17:52:14 -0400 Subject: [PATCH] Diagnose why Bedrock denies the model the product depends on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Luna and Terra both return AccessDeniedException — 'not available for this account ... contact AWS Sales' — on InvokeModel and Converse in us-east-1, while list-foundation-models LISTS all three OpenAI ids. Listing is not access. This matters more than an error normally would. bedrock.tf already records that completeViaBedrock treats AccessDeniedException as PERMANENT, returns null, and every caller in ai.ts degrades to sources-only — so the failure is silent by design and the product looks like it works while Tenure AI never reasons. Three hypotheses, checked separately because each has a different fix: 1. ENTITLEMENT — per-account, per-model, and IAM cannot grant it. 2. REGION — us. profiles route to us-east-1/us-east-2/us-west-2 and entitlement can differ per region, so each is asked directly. 3. POLICY — a permissions boundary or an Organizations SCP denies what an identity policy allows, and neither appears in the role's own document, which is why bedrock-iam.test.ts passes while the call is refused. The earlier probe asked GetFoundationModel about 'us.openai.gpt-5.6-luna' and got ResourceNotFoundException, because that API takes a FOUNDATION model id and 'us.' is an inference PROFILE. This asks the right question of each API. simulate-principal-policy is included because it evaluates identity policy, boundary and SCP together and states the effective decision, rather than leaving it to be inferred from three documents read separately. Read-only: every call is a get/list/describe/simulate. No inference is issued, so unlike bedrock-model-probe.yml this costs nothing. Co-Authored-By: Claude Opus 5 (1M context) --- .../workflows/bedrock-access-diagnosis.yml | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 .github/workflows/bedrock-access-diagnosis.yml diff --git a/.github/workflows/bedrock-access-diagnosis.yml b/.github/workflows/bedrock-access-diagnosis.yml new file mode 100644 index 00000000..5dbadb54 --- /dev/null +++ b/.github/workflows/bedrock-access-diagnosis.yml @@ -0,0 +1,144 @@ +name: Bedrock Access Diagnosis + +# Why can this account not invoke the model the product depends on? +# +# MEASURED 2026-08-21: `us.openai.gpt-5.6-luna` and `us.openai.gpt-5.6-terra` +# both return AccessDeniedException — "not available for this account ... contact +# AWS Sales" — on InvokeModel AND Converse in us-east-1, while +# `list-foundation-models` happily LISTS all three OpenAI ids. Listing is not +# access. +# +# That matters more than an error usually would, because of what bedrock.tf +# already records: `completeViaBedrock` treats AccessDeniedException as +# PERMANENT, returns null, and every caller in ai.ts degrades to sources-only. +# The failure is silent by design, so the product looks like it works while +# Tenure AI is never intelligent on any page. +# +# Three hypotheses, checked separately because they need different fixes: +# 1. ENTITLEMENT — model access is granted per account, per model, and for +# some third-party models needs a commercial agreement. IAM cannot fix it. +# 2. REGION — `us.` profiles route to us-east-1, us-east-2 and us-west-2 +# (bedrock.tf:55). Entitlement can differ per region. +# 3. POLICY — a permissions boundary on the IAM user, or an Organizations SCP, +# can deny what an identity policy allows. Neither shows up in the role's +# own policy document, which is why bedrock-iam.test.ts can pass while the +# call is denied. +# +# READ-ONLY. Every call below is a get/list/describe. No inference is issued, so +# unlike bedrock-model-probe.yml this costs nothing and mutates nothing. + +on: + workflow_dispatch: + +concurrency: + group: bedrock-access-diagnosis + cancel-in-progress: false + +permissions: + contents: read + +jobs: + diagnose: + name: Why is the model denied + runs-on: ubuntu-latest + timeout-minutes: 12 + env: + AWS_REGION: us-east-1 + AWS_DEFAULT_REGION: us-east-1 + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Configure AWS credentials + uses: ./.github/actions/aws-auth + with: + access-key-id: ${{ secrets.ACCESSKEYID }} + secret-access-key: ${{ secrets.SECRETACCESSKEY }} + region: ${{ secrets.AWS_REGION || 'us-east-1' }} + + - name: Who am I + run: | + set -uo pipefail + aws sts get-caller-identity --output json || true + + - name: 1. ENTITLEMENT — the BARE foundation-model ids, which is what GetFoundationModel takes + run: | + set -uo pipefail + # The earlier probe asked about `us.openai...` and got ResourceNotFound, + # because GetFoundationModel takes a FOUNDATION model id and `us.` is an + # inference PROFILE. Ask the right question. + for M in openai.gpt-5.6-luna openai.gpt-5.6-terra openai.gpt-5.6-sol anthropic.claude-haiku-4-5; do + echo "── $M ──" + aws bedrock get-foundation-model --model-identifier "$M" \ + --query 'modelDetails.{id:modelId,lifecycle:modelLifecycle.status,inference:inferenceTypesSupported,streaming:responseStreamingSupported}' \ + --output json 2>&1 || true + done + + - name: 1b. ENTITLEMENT — what does the account list as ON_DEMAND vs INFERENCE_PROFILE + run: | + set -uo pipefail + echo "── models this account lists for ON_DEMAND ──" + aws bedrock list-foundation-models --by-inference-type ON_DEMAND \ + --query 'modelSummaries[?contains(modelId, `openai`)].modelId' --output text 2>&1 || true + echo "" + echo "── inference profiles visible ──" + aws bedrock list-inference-profiles \ + --query 'inferenceProfileSummaries[?contains(inferenceProfileId, `openai`)].{id:inferenceProfileId,status:status,type:type}' \ + --output json 2>&1 || true + + - name: 2. REGION — every region the us. profile routes to + run: | + set -uo pipefail + # bedrock.tf:55 — the `us.` profiles route to us-east-1, us-east-2 and + # us-west-2. Entitlement is per region, so a denial in one is not a + # denial in all. Ask each directly rather than assuming. + for R in us-east-1 us-east-2 us-west-2; do + echo "══ $R ══" + echo " foundation model:" + aws bedrock get-foundation-model --region "$R" --model-identifier openai.gpt-5.6-luna \ + --query 'modelDetails.modelId' --output text 2>&1 | head -2 | sed 's/^/ /' + echo " inference profile:" + aws bedrock get-inference-profile --region "$R" --inference-profile-identifier us.openai.gpt-5.6-luna \ + --query '{id:inferenceProfileId,status:status,models:models[].modelArn}' --output json 2>&1 | head -12 | sed 's/^/ /' + done + + - name: 3. POLICY — permissions boundary and organisation SCPs + run: | + set -uo pipefail + # A boundary or an SCP denies what an identity policy allows, and neither + # appears in the role's own document — which is exactly why + # bedrock-iam.test.ts can pass while the call is refused. + ARN=$(aws sts get-caller-identity --query Arn --output text 2>/dev/null || echo "") + echo "caller: $ARN" + NAME="${ARN##*/}" + echo "── permissions boundary on the calling identity ──" + aws iam get-user --user-name "$NAME" \ + --query 'User.{user:UserName,boundary:PermissionsBoundary}' --output json 2>&1 | head -12 || true + echo "" + echo "── is this account in an Organization? ──" + aws organizations describe-organization --output json 2>&1 | head -12 || true + echo "" + echo "── effective policies AWS itself reports for the Bedrock action ──" + # simulate-principal-policy evaluates identity policy + boundary + SCP + # together. It answers the question directly rather than by inference. + aws iam simulate-principal-policy \ + --policy-source-arn "$ARN" \ + --action-names bedrock:InvokeModel \ + --resource-arns "arn:aws:bedrock:us-east-1::foundation-model/openai.gpt-5.6-luna" \ + --query 'EvaluationResults[].{action:EvalActionName,decision:EvalDecision,matched:MatchedStatements[].SourcePolicyType}' \ + --output json 2>&1 | head -20 || true + + - name: Summary + if: always() + run: | + { + echo "### Bedrock access diagnosis" + echo "" + echo "Read-only. Three hypotheses checked separately: account entitlement," + echo "region, and policy (boundary / SCP). The logs above carry the answers." + echo "" + echo "Reminder of why this matters: \`completeViaBedrock\` treats" + echo "AccessDeniedException as PERMANENT and returns null, and every caller in" + echo "\`ai.ts\` degrades to sources-only — so this failure is SILENT and the" + echo "product looks like it works while Tenure AI never reasons." + } >> "$GITHUB_STEP_SUMMARY"