Skip to content

feat: add aiml-access-diagnostics skill for Bedrock and SageMaker - #53

Open
ams-thakkar wants to merge 14 commits into
mainfrom
feature/aiml-access-diagnostics
Open

feat: add aiml-access-diagnostics skill for Bedrock and SageMaker#53
ams-thakkar wants to merge 14 commits into
mainfrom
feature/aiml-access-diagnostics

Conversation

@ams-thakkar

Copy link
Copy Markdown
Contributor

Description

Adds aiml-access-diagnostics, a read-only skill that diagnoses why an Amazon Bedrock or
Amazon SageMaker call was denied. It walks a six-hop authorization chain — caller identity,
iam:PassRole, role trust policy, role permissions, resource policies, organization SCP —
names the hop that denied the call, and proposes a scoped policy for human review.

The problem it targets is that AI/ML access failures are routinely misattributed. The
caller's permissions are frequently correct while the service role's are not, and several
failure modes surface under error codes and messages that point away from the real cause.
Three concrete examples the skill handles, all verified against live failures:

  • A role trust-policy gap returns ValidationException: Could not assume role, not
    AccessDenied. Filtering CloudTrail for access errors never finds it.
  • A SageMaker execution role lacking s3:ListBucket fails with
    ValidationException: No S3 objects found under S3 URL, which reads as missing data.
    SageMaker validates the input path as the execution role, so an object that exists is
    reported absent.
  • A deprecated Bedrock model returns ResourceNotFoundException whose message begins
    "Access denied", which is not a permissions problem at all.

No IAM changes are required. Everything the skill uses is already granted by
AIDevOpsAgentAccessPolicy. cloudformation/devops-agent-skill-policies.yaml is untouched
by this PR.

Design note — why policy reads rather than the policy simulator. An earlier revision
depended on iam:SimulatePrincipalPolicy and shipped a CloudFormation grant for it. Live
testing showed that operation is refused by the DevOps Agent runtime, as is
cloudtrail:LookupEvents, even though both are read-only, are granted in IAM, and sit
inside the agent's permission guardrail. The observed pattern is that operations whose verb
is not Get, List, or Describe are treated as potentially mutating.

Inverting to policy-read-first turned out to be more correct regardless of that constraint:
simulation cannot evaluate trust policies at all (hop 3), and at hop 2 it returns a false
implicitDeny for callers using AWS's own recommended iam:PassedToService scoping.
Policy documents are the only correct evidence for both hops. Simulation now contributes
exactly one thing — AllowedByOrganizations at hop 6 — and where the two disagree the
policy read wins. Both operations are used opportunistically when the environment permits
them, and reported as an environment characteristic when it does not, never as a missing
permission.

Design choices worth reviewer attention:

  • Three-state verdicts with no plain "allowed". The strongest verdict is
    ALLOWED_BUT_UNVERIFIABLE, because readable policies cannot account for session
    policies, conditional SCPs, or service-side gates outside IAM. False reassurance is the
    primary failure mode for this class of tool.
  • The two iam:PassRole failures are separated. The caller missing iam:PassRole and
    the role's trust policy omitting the service principal present near-identically and have
    different fixes in different places.
  • Non-IAM Bedrock causes are ruled out explicitly — model access state, AWS Marketplace
    subscription for third-party models, region mismatch, cross-region inference-profile
    dual-resource requirements — not assumed absent.
  • Proposed policies are split into permissions derived from the observed failure versus
    permissions commonly required but not observed. Wildcard resources are never emitted, and
    no policy is proposed for an explicit-deny root cause, since adding an allow cannot
    override one.

Type of change

  • New skill
  • New custom agent
  • Update to an existing skill or agent
  • Documentation or infrastructure change

Testing

Validated end to end against purpose-built failures in a real Agent Space, not by review
alone. Twelve deliberately broken IAM roles were created in a test account, each producing
a genuine denial, and the skill was run against them through the DevOps Agent chat surface.

Scenarios run and the expected discrimination confirmed:

Scenario Correct answer Result
Execution role trust policy omits sagemaker.amazonaws.com Hop 3 trust policy, not iam:PassRole Correct — explicitly cleared hop 2 as "correctly authored"
Caller correct, execution role has no S3 Hop 4, and the data is not missing Correct — "making no claim either way about the data"
Allow on * plus a resource-specific Deny Explicit deny; adding permissions cannot fix it Correct — proposed-policy section omitted with reason
Region-scoped grant, call in another region, via a cross-region inference profile Both the region mismatch and the profile dual-resource gap Correct — found both, and resolved all three destination regions

The inference-profile destination regions in the last case were independently verified
against bedrock:GetInferenceProfileus-east-1, us-east-2, us-west-2 — so the
proposed policy is materially correct rather than plausible.

Testing found and fixed nine defects that review had not, including four that produced
confidently wrong output rather than degraded output. The most consequential:

  • Simulating iam:PassRole without an iam:PassedToService context entry returns
    implicitDeny for a correctly configured caller. This would have told users to add a
    permission they already held while the real cause at hop 3 went unreported.
  • Omitting ResourceArns from a simulation errs in both directions — an Allow on *
    with a resource-specific Deny simulates as allowed, while a region-scoped Allow
    simulates as implicitDeny.
  • A runtime-blocked operation was reported as a missing IAM permission, with a
    recommendation to deploy a CloudFormation template that was already deployed and could
    not have helped.
  • The chain table contradicted its own finding bodies, marking a hop "Allows (unverified)"
    while the body stated the job would fail there. Resolved by adding a WOULD_ALSO_DENY
    verdict for a second defect below the root cause.

Each is recorded in CHANGELOG.md with the behaviour that exposed it.

Also verified locally: mkdocs build --strict passes against the merged tree including the
MCP docs hook added in #49/#51; the skill's docs page and catalog entry generate correctly;
the upload bundle is structurally valid with SKILL.md at the archive root; and the
frontmatter description is within the 1024-character upload limit.

Evals are included (.skilleval.yaml, evals/evals.json, evals/eval_queries.json) but
were deprioritised in favour of manual end-to-end validation, which is what surfaced every
defect above.

License confirmation

  • By submitting this pull request, I confirm that my contribution is made under the terms of the Apache License 2.0.

Amrish Thakkar added 14 commits August 12, 2026 14:03
Read-only skill that diagnoses IAM and access failures for Amazon Bedrock and Amazon SageMaker calls. Walks the six-hop authorization chain (caller action, iam:PassRole, role trust policy, role permissions, resource policy, organization SCP) and names the hop that denied the call.

Key design points: CloudTrail is treated as evidence and simulation as explanation, never inverted. Three verdict states with no plain 'allowed', so an unreadable policy is never reported as an absent one. Implicit and explicit denials are distinguished because the remediations differ. The two iam:PassRole failure modes are separated. Bedrock non-IAM causes (model access, Marketplace subscription, propagation, region mismatch) are ruled out explicitly.

Adds iam:SimulatePrincipalPolicy to the CloudFormation skill policies; it is the one action the skill needs that AIDevOpsAgentAccessPolicy does not grant.
…mmunity agent SOPs

Applies five conventions from the UNO-Community agent-sops standard that transfer to this skill:

1. Mandatory AI-generated banner on every report. Their guardrail requires an AI disclaimer on output-generating SOPs; this skill's output proposes IAM policies, making an unreviewed application the highest-consequence failure mode. 2. Output discipline: no narration of API calls, plans, or reasoning, and no post-delivery summary that could be read in place of the report. 3. All arithmetic computed, never estimated, per their 'all math through code' rule; applies to the grant-to-denial interval. 4. Sections are never silently dropped; empty sections render a heading plus a muted explanation, since an absent section is indistinguishable from a check that never ran. 5. User-facing error handling table with explicit graceful degradation on every condition.

Pre-render checks go from 12 to 14, renumbered 1-14 contiguously, with claim, subtotals, and CHANGELOG kept in sync.
…ns found in live testing

End-to-end validation against real Bedrock and SageMaker denials in a test account surfaced four cases the 1.0.0 logic would have diagnosed incorrectly.

- CloudTrail selection filtered on AccessDenied only, but two of the four SageMaker failure modes return ValidationException. Hop 3 (trust policy) returns 'Could not assume role' and hop 4 create-time S3 returns 'No S3 objects found under S3 URL'. Neither would have been found.

- SageMaker validates the S3 input path using the execution role, so a role lacking s3:ListBucket makes an existing object appear absent. Confirmed with a control job differing only in S3 permissions. Previously diagnosed as missing data.

- cloudtrail:LookupEvents is region-scoped even with a multi-region trail, so the region-mismatch cause the skill claims to diagnose was invisible when querying only the default region.

- A legacy model returns ResourceNotFoundException whose message begins 'Access denied'; added as a non-IAM cause so it is not diagnosed as a permissions gap.

Adds the two ValidationException signals to the activation description so the skill triggers on them. Bumps to 1.1.0.
…ext in simulation

Simulating the live test fixtures exposed two more defects, both of which produce confidently wrong findings rather than degraded ones.

iam:PassRole simulated without an iam:PassedToService context entry returns implicitDeny for a correctly configured caller, because AWS's own recommended scoping pattern conditions on that key. Verified: denied without it, allowed with it, and still correctly denied when the caller's condition names a different service. This is the worst possible failure for this skill - it sends the customer to add a permission they already have while the real cause at hop 3 goes unreported. Hop 2 now refuses to emit a denial when MissingContextValues is non-empty and returns CANNOT_DETERMINE.

Omitting ResourceArns errs in both directions: an Allow on * plus a resource-specific Deny simulates as allowed (false negative), while a region-scoped Allow simulates as implicitDeny (false positive, blaming hop 1). Documented with the measured results.

Also generalizes the rule: a denial carrying MissingContextValues is never evidence of a permission gap.
…load limit

The DevOps Agent upload validator rejects a skill whose frontmatter description exceeds 1024 characters. The description was 1233 after adding the ValidationException trigger phrases, so the upload failed.

Condensed to 998 characters with every trigger and exclusion phrase preserved, including ThrottlingException as a negative trigger. Verified no other skill in the repo exceeds the limit; this was the only one.
…ent can load them

SKILL.md linked its six reference files by absolute github.com URL, so at runtime the agent tried to fetch remote pages instead of reading the bundled files. The reference bundle never loaded.

The absolute URLs came from over-applying a real constraint. Relative .md links do break 'mkdocs build --strict', but only for README.md, which the catalog hook copies verbatim into docs/skills/<id>.md. SKILL.md and references/ never enter the docs build at all - only SKILL.md's frontmatter is read, for catalog metadata. The two consumers want opposite link styles, and this skill had it backwards for the runtime.

Every other skill in the repo with a references/ directory uses relative links or plain text mentions; this was the only one using absolute URLs. Zip layout was already identical to the working skills, so structure was not a factor.

Verified: strict docs build still passes with no reference to this skill, all six relative links resolve on disk, and README.md remains free of relative links.
…t the archive root

The documented zip command archived the parent directory, prefixing every file with 'aiml-access-diagnostics/'. AWS DevOps Agent expects SKILL.md at the root of the archive.

The failure mode is quiet. Upload succeeds and the skill activates, because the platform locates SKILL.md by scanning the archive. But reference files are retrieved by their manifest path (references/access-chain-model.md), which no longer matches the stored path, so every reference fails with 'Failed to get skill resource' and the skill silently runs on SKILL.md alone.

Adds the expected archive layout, an 'unzip -l' verification step, and -D to omit directory entries, which carry no file extension and can trip the extension validator. Verified the documented command reproduces the intended bundle byte-for-byte: 7 files, SKILL.md at root, no README/CHANGELOG/evals leaked.

The same incorrect command appears in the root README, project conventions, and the nine other skills that ship a references/ directory. Reported separately to keep this PR scoped.
…the IAM grant

Running the skill against live denials in a real Agent Space showed that both
operations the previous design treated as central are not callable in the DevOps
Agent runtime, and that the skill misdiagnosed that as its own misconfiguration.

cloudtrail:LookupEvents and iam:SimulatePrincipalPolicy are read-only, are granted
in IAM, and sit inside the DevOps Agent permission guardrail, yet are refused
before reaching AWS. The observed pattern is that operations whose verb is not Get,
List or Describe are treated as potentially mutating. Every other read works.

Policy documents are now the primary evidence and the diagnosis stands without
CloudTrail or simulation. This is not a downgrade. Simulation cannot evaluate trust
policies at all (hop 3), and at hop 2 it returns a false implicitDeny for callers
using AWS's own recommended iam:PassedToService scoping, so policy reads are the
only correct evidence for both. Simulation now contributes one thing exclusively,
AllowedByOrganizations at hop 6, and where the two disagree the policy read wins.

Removes the CloudFormation grant entirely. iam:SimulatePrincipalPolicy was the sole
action needed beyond AIDevOpsAgentAccessPolicy, and granting it changes nothing.
cloudformation/devops-agent-skill-policies.yaml is restored byte-identical to main,
so this skill now requires no IAM changes.

Fixes found in the same testing:

- A blocked operation is no longer reported as a missing permission. The skill told
  a user to deploy a template that was already deployed and could not have helped.
  Adds a RuntimeUnavailable status distinct from AgentAccessDenied, splits the
  notice in two, and adds a pre-render check that fails the report if it proposes a
  grant for a runtime-blocked operation.
- Adds the WOULD_ALSO_DENY verdict. A run marked hop 4 "Allows (unverified)" in the
  chain table while the body said the job would fail again on S3; there was no
  vocabulary for a second defect below the root cause.
- Closes the verdict vocabulary. SKILL.md specified three verdicts while
  report-format.md defined five markers, and a run emitted NOT_EVALUATED as a
  heading verdict outside the documented set.
- Removes a dependency on s3:ListBucket, which the agent does not have. The finding
  never needed it: a missing S3 list permission on the execution role produces
  "No S3 objects found" whether or not the objects exist.
- Stops inferring one operation's availability from another's failure. After
  s3:ListBucket was refused, s3:GetBucketPolicy was assumed unavailable and hop 5
  was reported undeterminable, while the bucket had a readable aws:SecureTransport
  deny.

Pre-render validation grows from 14 checks to 16.
…rate the two not-run markers

Two findings from the explicit-deny test case.

A run cited the account's SCPs as 'established in earlier diagnoses this session' instead of reading them for that diagnosis. Inherited evidence is not auditable, propagates any error in the earlier read, and may describe a configuration that has since changed. Findings must now cite reads performed for the current diagnosis, and pre-render check 17 rejects phrases such as 'established earlier'.

The same run marked hop 5 NOT_EVALUATED for a plain foundation-model call while explaining in the body that an AWS-owned model carries no customer resource policy. That makes it NOT_APPLICABLE - there was never anything to read. NOT_EVALUATED is reserved for a hop whose evidence exists but was not collected because an earlier hop denied. svc-bedrock.md now states this directly rather than leaving it inferable from the applicability matrix.

Pre-render validation grows from 16 checks to 17.
…ntent after the report

Two validated runs appended a paragraph grading the diagnosis - 'the skill worked end to end' and 'the skill nailed the deceptive case'. Output discipline already barred a post-delivery summary but not self-assessment, and the rule was lost twice out of two runs.

Praising the output lends unearned confidence to findings whose limitations the report has just carefully enumerated, and the reader evaluates the report rather than the report evaluating itself. Nothing may now follow the report except a single line offering a next action.
… architecture

Two functional evals still asserted the pre-1.2.0 design and would have failed for anyone running Agent Skill Eval against the shipped skill.

aiml-access-verdict-states named only three verdicts and justified the absence of a plain allow with 'simulation can diverge from live behavior'. There are now six tokens in a closed vocabulary, and the justification is that reading a policy cannot account for session policies, conditional SCPs, or service-side gates outside IAM.

aiml-access-read-only-boundary credited write prevention to the agent role's IAM permissions alone. The actual guarantee is the DevOps Agent permission guardrail, a session policy capping effective permissions at roughly ReadOnlyAccess.

Adds four evals covering behaviour that had no coverage: the WOULD_ALSO_DENY post-denial verdict, evidence precedence (policy reads primary, policy read wins over simulation except AllowedByOrganizations), the rule that a runtime-blocked operation is not a permission gap and must not prompt a CloudFormation recommendation, and the three access failures that do not surface as AccessDenied.

Functional evals now 12 (was 8). Trigger queries unchanged at 12 with 7 negatives; they are architecture-independent.
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