Setting the file. One moment.
Fetch Bedrock Agent · Amazon Bedrock · aws/agent-toolkit-for-aws · Skills Docs
ContentsBack to the top of the page Kb Shim Py
70
Creating Amazon Aurora Db Cluster With Instances
93
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Creating API Gateway Stage
scripts/ fetch_bedrock_agent.py
Python · 343 lines · 14 KB
16
17 Pass --inline-s3-schemas to fetch action-group OpenAPI schemas stored in S3 and
18 inline them into the manifest under each action group's `apiSchema._inlinedPayload`.
19
20 Requires: boto3 with read-only credentials. Prefer ephemeral, role-based
21 credentials (an assumed IAM role, SSO session, or instance profile) over
22 long-lived IAM user access keys — `--profile` may otherwise resolve to static
23 keys in ~/.aws/credentials.
24
25 Minimum IAM permissions (all read-only; scope Resource as noted):
26 sts:GetCallerIdentity
27 bedrock-agent:GetAgent, ListAgentActionGroups,
28 GetAgentActionGroup, ListAgentKnowledgeBases, GetAgentKnowledgeBase,
29 GetKnowledgeBase, ListDataSources, GetDataSource, ListAgentAliases,
30 GetAgentAlias, ListAgentVersions, ListAgentCollaborators (optional),
31 GetAgentCollaborator (optional)
32 -> scope to the specific agent/KB ARNs where possible
33 iam:GetRole, ListAttachedRolePolicies, ListRolePolicies, GetRolePolicy
34 -> scope to the agent's execution-role ARN
35 s3:GetObject (only with --inline-s3-schemas)
36 -> scope to the OpenAPI schema object(s)
37 No write/mutating permissions are needed; grant none.
38 """
39
40 from __future__ import annotations
41
42 import argparse
43 import json
44 import os
45 import sys
46 from typing import Any, Dict, List, Optional
47
48 try :
49 import boto3
50 from botocore.exceptions import ClientError
51 except ImportError :
52 # boto3 is not in the stdlib. Exit with a distinct code so the caller can
53 # cleanly fall back to the `aws bedrock-agent` CLI path (see
54 # references/discovery.md "Fallback") instead of treating this as a crash.
55 sys.stderr.write(
56 "FALLBACK_REQUIRED: boto3 not available. Use the aws-CLI discovery path "
57 "documented in references/discovery.md. \n "
58 )
59 sys.exit( 3 )
60
61 # Errors we tolerate per-call (record but don't crash). All other ClientErrors propagate.
62 TOLERATED_ERROR_CODES = {
63 "AccessDeniedException" ,
64 "ResourceNotFoundException" ,
65 "ValidationException" ,
66 }
67
68
69 def _safe (call, * args, ** kwargs):
70 """Run a boto3 call, returning {'_error': code, '_message': str} on tolerated errors."""
71 try :
72 return call( * args, ** kwargs)
73 except ClientError as e:
74 code = e.response.get( "Error" , {}).get( "Code" , "" )
75 if code in TOLERATED_ERROR_CODES :
76 return { "_error" : code, "_message" : str (e)}
77 raise
78
79
80 def _strip_response_metadata (d: Any) -> Any:
81 if isinstance (d, dict ):
82 return {k: _strip_response_metadata(v) for k, v in d.items() if k != "ResponseMetadata" }
83 if isinstance (d, list ):
84 return [_strip_response_metadata(x) for x in d]
85 return d
86
87
88 def _maybe_inline_s3_schema (
89 s3_client, api_schema: Optional[Dict[ str , Any]]
90 ) -> Optional[Dict[ str , Any]]:
91 """If apiSchema points to S3, fetch it and inline under _inlinedPayload."""
92 if not api_schema or "s3" not in api_schema:
93 return api_schema
94 s3_ref = api_schema[ "s3" ]
95 bucket = s3_ref.get( "s3BucketName" )
96 key = s3_ref.get( "s3ObjectKey" )
97 if not (bucket and key):
98 return api_schema
99 try :
100 obj = s3_client.get_object( Bucket = bucket, Key = key)
101 body = obj[ "Body" ].read().decode( "utf-8" )
102 return { ** api_schema, "_inlinedPayload" : body, "_inlinedSource" : f "s3:// { bucket } / { key } " }
103 except ClientError as e:
104 return { ** api_schema, "_inlineError" : str (e)}
105
106
107 def fetch_action_groups (
108 bedrock_agent, s3_client, agent_id: str , version: str , inline_s3: bool
109 ) -> List[Dict[ str , Any]]:
110 out: List[Dict[ str , Any]] = []
111 paginator = bedrock_agent.get_paginator( "list_agent_action_groups" )
112 for page in paginator.paginate( agentId = agent_id, agentVersion = version):
113 for summary in page.get( "actionGroupSummaries" , []):
114 detail = _safe(
115 bedrock_agent.get_agent_action_group,
116 agentId = agent_id,
117 agentVersion = version,
118 actionGroupId = summary[ "actionGroupId" ],
119 )
120 ag = _strip_response_metadata(detail).get( "agentActionGroup" , detail)
121 if inline_s3 and isinstance (ag, dict ) and "apiSchema" in ag:
122 ag[ "apiSchema" ] = _maybe_inline_s3_schema(s3_client, ag.get( "apiSchema" ))
123 out.append(ag)
124 return out
125
126
127 def fetch_knowledge_bases (bedrock_agent, agent_id: str , version: str ) -> List[Dict[ str , Any]]:
128 out: List[Dict[ str , Any]] = []
129 paginator = bedrock_agent.get_paginator( "list_agent_knowledge_bases" )
130 for page in paginator.paginate( agentId = agent_id, agentVersion = version):
131 for summary in page.get( "agentKnowledgeBaseSummaries" , []):
132 assoc = _safe(
133 bedrock_agent.get_agent_knowledge_base,
134 agentId = agent_id,
135 agentVersion = version,
136 knowledgeBaseId = summary[ "knowledgeBaseId" ],
137 )
138 kb_detail = _safe(
139 bedrock_agent.get_knowledge_base, knowledgeBaseId = summary[ "knowledgeBaseId" ]
140 )
141 ds_list: List[Dict[ str , Any]] = []
142 ds_paginator = bedrock_agent.get_paginator( "list_data_sources" )
143 try :
144 for ds_page in ds_paginator.paginate( knowledgeBaseId = summary[ "knowledgeBaseId" ]):
145 for ds_summary in ds_page.get( "dataSourceSummaries" , []):
146 ds = _safe(
147 bedrock_agent.get_data_source,
148 knowledgeBaseId = summary[ "knowledgeBaseId" ],
149 dataSourceId = ds_summary[ "dataSourceId" ],
150 )
151 ds_list.append(_strip_response_metadata(ds))
152 except ClientError as e:
153 ds_list.append({ "_error" : str (e)})
154 out.append(
155 {
156 "association" : _strip_response_metadata(assoc).get( "agentKnowledgeBase" , assoc),
157 "knowledgeBase" : _strip_response_metadata(kb_detail).get(
158 "knowledgeBase" , kb_detail
159 ),
160 "dataSources" : ds_list,
161 }
162 )
163 return out
164
165
166 def fetch_aliases_and_versions (bedrock_agent, agent_id: str ) -> Dict[ str , Any]:
167 aliases: List[Dict[ str , Any]] = []
168 versions: List[Dict[ str , Any]] = []
169 try :
170 for page in bedrock_agent.get_paginator( "list_agent_aliases" ).paginate( agentId = agent_id):
171 for s in page.get( "agentAliasSummaries" , []):
172 detail = _safe(
173 bedrock_agent.get_agent_alias, agentId = agent_id, agentAliasId = s[ "agentAliasId" ]
174 )
175 aliases.append(_strip_response_metadata(detail).get( "agentAlias" , detail))
176 except ClientError as e:
177 aliases.append({ "_error" : str (e)})
178 try :
179 for page in bedrock_agent.get_paginator( "list_agent_versions" ).paginate( agentId = agent_id):
180 for s in page.get( "agentVersionSummaries" , []):
181 versions.append(s)
182 except ClientError as e:
183 versions.append({ "_error" : str (e)})
184 return { "aliases" : aliases, "versions" : versions}
185
186
187 def fetch_collaborators (bedrock_agent, agent_id: str , version: str ) -> List[Dict[ str , Any]]:
188 """Multi-agent collaborator agents (if collaboration is enabled on the source)."""
189 out: List[Dict[ str , Any]] = []
190 if not hasattr (bedrock_agent, "list_agent_collaborators" ):
191 return out # SDK too old; collaboration won't be in the manifest
192 try :
193 for page in bedrock_agent.get_paginator( "list_agent_collaborators" ).paginate(
194 agentId = agent_id, agentVersion = version
195 ):
196 for s in page.get( "agentCollaboratorSummaries" , []):
197 detail = _safe(
198 bedrock_agent.get_agent_collaborator,
199 agentId = agent_id,
200 agentVersion = version,
201 collaboratorId = s[ "collaboratorId" ],
202 )
203 out.append(_strip_response_metadata(detail).get( "agentCollaborator" , detail))
204 except (ClientError, AttributeError ) as e:
205 out.append({ "_error" : str (e)})
206 return out
207
208
209 def fetch_iam_role (iam, role_arn: Optional[ str ]) -> Dict[ str , Any]:
210 if not role_arn:
211 return {}
212 role_name = role_arn.split( "/" )[ - 1 ]
213 role = _safe(iam.get_role, RoleName = role_name)
214 attached = _safe(iam.list_attached_role_policies, RoleName = role_name)
215 inline_names = _safe(iam.list_role_policies, RoleName = role_name)
216 inline_policies: List[Dict[ str , Any]] = []
217 if isinstance (inline_names, dict ) and "_error" not in inline_names:
218 for name in inline_names.get( "PolicyNames" , []):
219 doc = _safe(iam.get_role_policy, RoleName = role_name, PolicyName = name)
220 inline_policies.append(_strip_response_metadata(doc))
221 return {
222 "role" : _strip_response_metadata(role).get( "Role" , role),
223 "attachedPolicies" : _strip_response_metadata(attached).get( "AttachedPolicies" , attached),
224 "inlinePolicies" : inline_policies,
225 }
226
227
228 def main () -> int :
229 parser = argparse.ArgumentParser(
230 description = "Fetch a complete Bedrock Agent manifest for migration."
231 )
232 # id only — Phase 1 resolves name/ARN to a confirmed agentId before this runs
233 parser.add_argument( "--agent-id" , required = True )
234 parser.add_argument(
235 "--agent-version" ,
236 default = "DRAFT" ,
237 help = "Agent version to inspect. Default DRAFT, but the skill should resolve a numbered "
238 "version from the production alias before calling this." ,
239 )
240 parser.add_argument(
241 "--agent-alias-id" , help = "Optional alias id, included in manifest for reference"
242 )
243 parser.add_argument(
244 "--region" , required = False , help = "AWS region (defaults to credential default)"
245 )
246 parser.add_argument( "--profile" , required = False , help = "AWS profile" )
247 parser.add_argument(
248 "--inline-s3-schemas" ,
249 action = "store_true" ,
250 help = "Fetch action-group OpenAPI schemas stored in S3 and inline them into the manifest." ,
251 )
252 parser.add_argument( "--out" , required = True , help = "Path to write the JSON manifest" )
253 args = parser.parse_args()
254
255 session = boto3.Session( profile_name = args.profile, region_name = args.region)
256 bedrock_agent = session.client( "bedrock-agent" )
257 iam = session.client( "iam" )
258 sts = session.client( "sts" )
259 s3_client = session.client( "s3" ) if args.inline_s3_schemas else None
260
261 identity = sts.get_caller_identity()
262 region = session.region_name
263 if not region:
264 print (
265 "ERROR: no region resolved from credentials. Pass --region or set AWS_DEFAULT_REGION." ,
266 file = sys.stderr,
267 )
268 return 2
269
270 agent_id = args.agent_id
271 # get_agent is fundamental to discovery — a tolerated error here (e.g.
272 # ResourceNotFoundException) would write a broken manifest whose downstream
273 # field lookups silently produce wrong defaults. So fail hard, not via _safe.
274 try :
275 agent = bedrock_agent.get_agent( agentId = agent_id)
276 except ClientError as e:
277 print ( f "ERROR: failed to fetch agent { agent_id } : { e } " , file = sys.stderr)
278 return 1
279 agent_doc = _strip_response_metadata(agent).get( "agent" , agent)
280 role_info = fetch_iam_role(iam, agent_doc.get( "agentResourceRoleArn" ))
281
282 aliases_and_versions = fetch_aliases_and_versions(bedrock_agent, agent_id)
283
284 manifest: Dict[ str , Any] = {
285 "discovery" : {
286 "account" : identity.get( "Account" ),
287 "region" : region,
288 "callerArn" : identity.get( "Arn" ),
289 "fetchedAgentVersion" : args.agent_version,
290 "fetchedAgentAliasId" : args.agent_alias_id,
291 "warnings" : [],
292 },
293 "agent" : agent_doc,
294 "agentCollaborationMode" : agent_doc.get( "agentCollaboration" ) or "DISABLED" ,
295 "orchestrationType" : agent_doc.get( "orchestrationType" ) or "DEFAULT" ,
296 "executionRole" : role_info,
297 "actionGroups" : fetch_action_groups(
298 bedrock_agent, s3_client, agent_id, args.agent_version, args.inline_s3_schemas
299 ),
300 "knowledgeBases" : fetch_knowledge_bases(bedrock_agent, agent_id, args.agent_version),
301 "collaborators" : fetch_collaborators(bedrock_agent, agent_id, args.agent_version),
302 "aliasesAndVersions" : aliases_and_versions,
303 }
304
305 if args.agent_version == "DRAFT" :
306 prod_aliases = [
307 a
308 for a in aliases_and_versions[ "aliases" ]
309 if isinstance (a, dict ) and a.get( "agentAliasId" ) not in ( None , "TSTALIASID" )
310 ]
311 if prod_aliases:
312 tag = ", " .join(
313 f " { a.get( 'agentAliasName' , '?' ) } ->v { (a.get( 'routingConfiguration' ) or [{}])[ 0 ].get( 'agentVersion' , '?' ) } "
314 for a in prod_aliases
315 )
316 manifest[ "discovery" ][ "warnings" ].append(
317 f "Fetched DRAFT but non-DRAFT aliases exist ( { tag } ). DRAFT may diverge from production."
318 )
319
320 out_dir = os.path.dirname(os.path.abspath(args.out))
321 os.makedirs(out_dir, exist_ok = True )
322 with open (args.out, "w" ) as f:
323 json.dump(manifest, f, indent = 2 , default = str )
324 # Manifest holds sensitive data (account ids, role ARNs, inline IAM policies).
325 # Restrict to owner read/write; prefer writing it to an encrypted volume.
326 try :
327 os.chmod(args.out, 0o 600 )
328 except OSError as e:
329 print (
330 f "WARNING: could not restrict permissions on { args.out } ( { e } ). It holds "
331 "sensitive data (account ids, role ARNs, IAM policies) and may be readable "
332 "by others — secure or delete it manually." ,
333 file = sys.stderr,
334 )
335
336 print ( f "Wrote { args.out } " )
337 for w in manifest[ "discovery" ][ "warnings" ]:
338 print ( f " WARNING: { w } " )
339 return 0
340
341
342 if __name__ == "__main__" :
343 sys.exit(main())