Setting the file. One moment.
Lambda Shim Py · 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
(opens in a new tab)
assets/ lambda_shim.py.tmpl
TMPL · 158 lines · 8 KB
17
"""
18 # <<< RENDER: delete this whole block after substituting the tokens below.
19 # {{ORIGINAL_LAMBDA_ARN}} - the source action-group Lambda ARN
20 # {{SCHEMA_STYLE}} - "function" (functionSchema) | "openapi" (apiSchema)
21 # {{OP_ROUTES}} - (openapi only) JSON object mapping each operationId to
22 # its {"method","apiPath"} from the SOURCE OpenAPI schema.
23 # apiPath MUST be the literal route TEMPLATE, e.g.
24 # "/customer/{customer_id}" — NOT a value-substituted path.
25 # <<< /RENDER
26 import json
27 import re
28
29 import boto3
30
31 # Rendered at migration time — the Gateway lambda code target has no
32 # environment-variable support, so all config is baked in as literals.
33 _ORIGINAL_ARN = "{{ORIGINAL_LAMBDA_ARN}}"
34 _SCHEMA_STYLE = "{{SCHEMA_STYLE}}" # function | openapi
35 _MAX_ARG_BYTES = 256 * 1024 # cap forwarded payload — reject oversized/abusive input
36
37 # operationId -> {"method": <HTTP method>, "apiPath": <route TEMPLATE>}.
38 # Rendered from the source OpenAPI schema. The apiPath is the template with
39 # placeholders intact (e.g. "/customer/{customer_id}") because the original Bedrock
40 # Lambda dispatches by matching that exact template; path-param VALUES stay in the
41 # parameters array, never substituted into the path.
42 _OP_ROUTES = {{OP_ROUTES}}
43 _lambda = boto3.client("lambda")
44
45
46 def _resolve_tool_and_args(event, context):
47 cc = getattr(context, "client_context", None)
48 custom = getattr(cc, "custom", None) if cc else None
49 raw = (custom or {}).get("bedrockAgentCoreToolName", "") if custom else ""
50 tool = raw.split("___", 1)[1] if "___" in raw else raw
51 args = event if isinstance(event, dict) else {}
52 return tool, args
53
54
55 def _validate_args(args):
56 """Reject unexpected shapes before forwarding to the original Lambda: keys must
57 be strings and the whole payload must stay under a sane size cap. This keeps the
58 shim from injecting oversized or malformed input into the original's envelope."""
59 if not all(isinstance(k, str) for k in args):
60 raise ValueError("All argument keys must be strings.")
61 if len(json.dumps(args, default=str).encode("utf-8")) > _MAX_ARG_BYTES:
62 raise ValueError(f"Arguments exceed {_MAX_ARG_BYTES} bytes.")
63
64
65 def _to_bedrock_event(tool, args):
66 """Build the Bedrock-Agents envelope the original handler expects."""
67 _validate_args(args)
68 if _SCHEMA_STYLE == "openapi":
69 # Look up the route TEMPLATE for this operationId and pass it verbatim.
70 route = _OP_ROUTES.get(tool)
71 if route is None:
72 raise ValueError(
73 f"No OpenAPI route for operationId {tool!r}; _OP_ROUTES must be "
74 "rendered from the source schema."
75 )
76 # In the real Bedrock envelope, path/query params live in `parameters` and
77 # body params in `requestBody` — don't put every arg in both, or a Lambda
78 # that reads both sees duplicated/misplaced values. Path params are the
79 # `{placeholder}` names in the route template. For methods with no request
80 # body (GET/DELETE/HEAD) the remaining args are query params and also belong
81 # in `parameters`; only body-bearing methods route the rest to `requestBody`.
82 path_names = set(re.findall(r"\{(\w+)\}", route["apiPath"]))
83 non_path = {k: v for k, v in args.items() if k not in path_names}
84 base = {
85 "messageVersion": "1.0", "actionGroup": "migrated",
86 "apiPath": route["apiPath"], # literal template — never substitute values
87 "httpMethod": route["method"],
88 }
89 # `parameters` entries declare type "string", so values must be strings —
90 # Gateway may hand us typed JSON (int/bool). `requestBody` keeps native types.
91 if route["method"].upper() in ("GET", "DELETE", "HEAD"):
92 params = [{"name": k, "value": str(v), "type": "string"}
93 for k, v in args.items()] # path + query, all in parameters
94 base["parameters"] = params
95 else:
96 base["parameters"] = [{"name": k, "value": str(v), "type": "string"}
97 for k in path_names for v in [args[k]]]
98 base["requestBody"] = {"content": {"application/json": {
99 "properties": [{"name": k, "value": v} for k, v in non_path.items()]}}}
100 return base
101 # functionSchema style: all args are flat parameters.
102 params = [{"name": k, "value": str(v), "type": "string"} for k, v in args.items()]
103 return {"messageVersion": "1.0", "actionGroup": "migrated",
104 "parameters": params, "function": tool}
105
106
107 def _unwrap(resp):
108 """Pull the tool output out of the Bedrock-Agents response envelope."""
109 if not isinstance(resp, dict):
110 return {"body": resp}
111 r = resp.get("response", {})
112 fr = r.get("functionResponse", {})
113 if fr:
114 body = fr.get("responseBody", {}).get("TEXT", {}).get("body")
115 if body is not None:
116 return {"body": body}
117 api = r.get("apiResponse", {})
118 if api:
119 body = api.get("responseBody", {}).get("application/json", {}).get("body")
120 if body is not None:
121 return {"body": body}
122 return resp # some Lambdas already return plain JSON
123
124
125 def lambda_handler(event, context):
126 # This shim forwards user-provided tool arguments, which may hold PII, financial
127 # data, or other sensitive values. Do NOT log the full event/args/response, and
128 # keep this Lambda's CloudWatch log group KMS-encrypted (see references/deploy.md).
129 tool, args = _resolve_tool_and_args(event, context)
130 bedrock_event = _to_bedrock_event(tool, dict(args))
131 try:
132 out = _lambda.invoke(
133 FunctionName=_ORIGINAL_ARN,
134 InvocationType="RequestResponse",
135 Payload=json.dumps(bedrock_event).encode("utf-8"),
136 )
137 except _lambda.exceptions.ClientError as e:
138 # AccessDenied here means the SOURCE Lambda's resource policy does not yet
139 # allow this shim role to invoke it. This is a source-side grant the builder
140 # must add (see references/deploy.md "Source-side prerequisites") — the
141 # migration must NOT add-permission on the source itself.
142 if e.response.get("Error", {}).get("Code") in ("AccessDeniedException", "AccessDenied"):
143 raise RuntimeError(
144 f"Denied invoking original Lambda {_ORIGINAL_ARN}. The builder must grant "
145 "this shim's role lambda:InvokeFunction on the source (do not modify the "
146 "source from the migration)."
147 ) from e
148 raise
149 payload = json.loads(out["Payload"].read() or b"{}")
150 # A failed original Lambda returns 200 with FunctionError set and the error in
151 # the payload — surface it as a real failure so the Gateway/Harness sees the
152 # tool errored, instead of passing the error dict off as a successful result.
153 if "FunctionError" in out:
154 raise RuntimeError(
155 f"Original Lambda failed ({out['FunctionError']}): "
156 f"{payload.get('errorMessage', 'unknown error')}"
157 )
158 return _unwrap(payload)