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"