Status update, October 2026: AWS has renamed Amazon Bedrock Agents to Amazon Bedrock Agents Classic and put it in maintenance mode. It is no longer open to new customers as of July 30, 2026; existing customers can keep using it, and AWS recommends migrating workloads to Amazon Bedrock AgentCore. Bedrock models, Knowledge Bases and Guardrails are not affected. This guide is for teams running Agents Classic today: how to keep those agents safe while they run, and how each piece maps to AgentCore when you move.
Most agent incidents don’t need a clever attack. A user writes “cancel all my orders” and the agent does it without asking. A tool Lambda accepts any order ID and returns the full database row, card details included. Or one customer’s agent session reads another customer’s data because the tenant came from the conversation instead of from your application. Each of these has a specific control in Agents Classic, and each control has a counterpart in AgentCore.
⚡ TL;DR
- One narrow function per action-group operation. Validate every parameter in the Lambda and return
REPROMPTfor bad input; return only whitelisted fields.- Take tenant and user identity from
sessionAttributesthat your application sets onInvokeAgent, never from the model’s parameters.- Set
requireConfirmation: ENABLEDon functions with side effects; the agent then returns control so the user can CONFIRM or DENY.- Associate a guardrail with the agent (
guardrailConfigurationon CreateAgent/UpdateAgent). The prompt attack filter applies to input only.- Memory (
SESSION_SUMMARY, 1 to 365 days) is keyed by thememoryIdyou pass, so always pass a per-user value.- Plan the move: action groups become AgentCore gateway tools, return-of-control becomes inline function tools, memory becomes AgentCore memory.
Who this is for: teams with Bedrock Agents Classic in production, or inheriting one. Code is Python with boto3. The Lambda ran against sample events in the documented format, and API requests were validated against the boto3 service models.
How an Agents Classic request flows
Your application calls InvokeAgent with the user’s text, a sessionId and optional session state. The agent’s orchestration loop asks the model what to do; when it decides to call an action-group function, Bedrock invokes your Lambda with an event that includes the function name, parameters and the session attributes. The Lambda’s response goes back into the loop, and the final answer streams back as chunks. Three places in that loop are where the controls belong: the call (what your application puts in session state), the tool (what the Lambda accepts and returns), and the agent configuration (confirmation, guardrail, memory).

Step 1: Make every tool Lambda narrow and suspicious
An action group is a typed interface, not access to your AWS account. With a function-details schema, the Lambda receives function, parameters (each with name, type and value) and sessionAttributes. It must return messageVersion 1.0 and a functionResponse whose body uses the TEXT content type. Set responseState to REPROMPT when the input is invalid, so the model gets a chance to fix it, or FAILURE when a dependency failed.
"""Action group Lambda (function-details schema): one narrow capability, tenant from the caller."""
import json
import re
import boto3
table = boto3.resource("dynamodb").Table("orders")
ORDER_ID = re.compile(r"^[A-Z0-9-]{8,32}$")
SAFE_FIELDS = ("orderId", "status", "totalCents", "updatedAt")
def respond(event, body: dict, state: str | None = None) -> dict:
fr = {"responseBody": {"TEXT": {"body": json.dumps(body)}}}
if state:
fr["responseState"] = state # REPROMPT: bad input; FAILURE: dependency failed
return {"messageVersion": "1.0",
"response": {"actionGroup": event["actionGroup"], "function": event["function"],
"functionResponse": fr}}
def handler(event, context=None):
if event.get("function") != "get_order":
return respond(event, {"error": "unsupported_function"}, "REPROMPT")
params = {p["name"]: p["value"] for p in event.get("parameters", [])}
order_id = params.get("orderId", "")
if not ORDER_ID.match(order_id):
return respond(event, {"error": "orderId must be 8-32 characters A-Z, 0-9 or -"}, "REPROMPT")
# Set by YOUR application in InvokeAgent sessionState, never by the model or the end user.
tenant = event.get("sessionAttributes", {}).get("tenantId")
if not tenant:
return respond(event, {"error": "missing tenant context"}, "FAILURE")
item = table.get_item(Key={"pk": f"TENANT#{tenant}", "sk": f"ORDER#{order_id}"}).get("Item")
if not item:
return respond(event, {"orderId": order_id, "status": "NOT_FOUND"})
return respond(event, {k: item[k] for k in SAFE_FIELDS if k in item}) # never raw itemsThree rules in that handler matter more than the rest. The function name and parameters are checked against what you expect, because the model chooses them. The tenant comes from session attributes your application set, not from a parameter the model filled in. And the response is built from a whitelist, so a field like a card number never reaches the model’s context. Give the Lambda’s IAM role only the actions it needs on the specific table; an IAM condition with a wildcard tenant (for example dynamodb:LeadingKeys set to TENANT#*) restricts nothing.
Step 2: Require confirmation for actions with side effects
AWS’s own guidance on prompt injection is direct: if a tool or knowledge base result contains instructions, the agent may follow them. For functions that change something, enable user confirmation. The agent then doesn’t call the Lambda; it returns control to your application with the function and parameters, and waits for a CONFIRM or DENY.
"""Agent configuration: confirmation on risky functions, guardrail, memory, idle timeout."""
import boto3
agent = boto3.client("bedrock-agent", region_name="us-east-1")
AGENT_ID, ROLE = "AGENT12345", "arn:aws:iam::111122223333:role/BedrockAgentRole"
LAMBDA = "arn:aws:lambda:us-east-1:111122223333:function:orders-tools"
def configure_tools():
agent.create_agent_action_group(
agentId=AGENT_ID, agentVersion="DRAFT", actionGroupName="orders",
actionGroupExecutor={"lambda": LAMBDA},
functionSchema={"functions": [
{"name": "get_order", "description": "Look up one order by ID for the current customer.",
"parameters": {"orderId": {"type": "string", "required": True,
"description": "Order ID, 8-32 chars"}}},
{"name": "cancel_order", "description": "Cancel one unshipped order for the current customer.",
"parameters": {"orderId": {"type": "string", "required": True, "description": "Order ID"}},
"requireConfirmation": "ENABLED"}, # user must CONFIRM before the Lambda runs
]})
def configure_agent(guardrail_id: str, guardrail_version: str):
agent.update_agent(
agentId=AGENT_ID, agentName="orders-assistant", agentResourceRoleArn=ROLE,
foundationModel="anthropic.claude-3-5-sonnet-20241022-v2:0",
instruction="You help customers check and cancel their own orders. Use only the provided tools.",
idleSessionTTLInSeconds=900,
guardrailConfiguration={"guardrailIdentifier": guardrail_id, "guardrailVersion": guardrail_version},
memoryConfiguration={"enabledMemoryTypes": ["SESSION_SUMMARY"], "storageDays": 30})
agent.prepare_agent(agentId=AGENT_ID) # then create or update an alias to a new version
def create_guardrail():
bedrock = boto3.client("bedrock", region_name="us-east-1")
return bedrock.create_guardrail(
name="orders-assistant",
contentPolicyConfig={"filtersConfig": [
{"type": "PROMPT_ATTACK", "inputStrength": "HIGH", "outputStrength": "NONE"},
{"type": "MISCONDUCT", "inputStrength": "MEDIUM", "outputStrength": "MEDIUM"}]},
sensitiveInformationPolicyConfig={"piiEntitiesConfig": [
{"type": "CREDIT_DEBIT_CARD_NUMBER", "action": "BLOCK"},
{"type": "EMAIL", "action": "ANONYMIZE"}]},
blockedInputMessaging="Sorry, I can't help with that request.",
blockedOutputsMessaging="Sorry, I can't share that.")Your application handles the returnControl event, shows the user exactly what will happen, and sends the decision back in the next InvokeAgent call as returnControlInvocationResults with the same invocationId:
"""Calling the agent: tenant in sessionAttributes, per-user memoryId, and the confirmation round trip."""
import boto3
runtime = boto3.client("bedrock-agent-runtime", region_name="us-east-1")
def ask(user, session_id, text, confirm=None, session_state=None):
state = session_state or {"sessionAttributes": {"tenantId": user["tenant"]},
"promptSessionAttributes": {"locale": user["locale"]}}
resp = runtime.invoke_agent(agentId="AGENT12345", agentAliasId="PRODALIAS", sessionId=session_id,
memoryId=user["id"], inputText=text, sessionState=state)
return read_stream(resp["completion"], confirm, state)
def read_stream(events, confirm, state):
text = []
for ev in events:
if "chunk" in ev:
text.append(ev["chunk"]["bytes"].decode())
elif "returnControl" in ev: # agent wants confirmation before a risky call
rc = ev["returnControl"]
results = []
for item in rc["invocationInputs"]:
fi = item["functionInvocationInput"]
ok = confirm(fi["function"], {p["name"]: p["value"] for p in fi["parameters"]})
results.append({"functionResult": {"actionGroup": fi["actionGroup"], "function": fi["function"],
"confirmationState": "CONFIRM" if ok else "DENY"}})
return {"pending": {"invocationId": rc["invocationId"], "returnControlInvocationResults": results,
**state}}
return {"text": "".join(text)}Note the memoryId. Memory stores session summaries against the ID you pass and loads them into the next session with the same ID, so a shared or constant value would mix users’ histories. Summaries are written after a session ends (explicitly with endSession or when the idle timeout elapses) and kept for the number of days you configure, from 1 to 365.
Step 3: Test the controls, not just the happy path
from unittest.mock import MagicMock
from botocore.stub import Stubber
import handler, configure, client
handler.table = MagicMock()
handler.table.get_item.return_value = {"Item": {"pk": "TENANT#acme", "sk": "ORDER#AB12CD34",
"orderId": "AB12CD34", "status": "SHIPPED", "totalCents": 4900, "cardLast4": "4242"}}
ev = {"messageVersion": "1.0", "actionGroup": "orders", "function": "get_order",
"parameters": [{"name": "orderId", "type": "string", "value": "AB12CD34"}],
"sessionAttributes": {"tenantId": "acme"}}
print(handler.handler(ev)["response"]["functionResponse"])
print(handler.handler({**ev, "parameters": [{"name": "orderId", "value": "x; DROP"}]})["response"]["functionResponse"])
print(handler.handler({**ev, "sessionAttributes": {}})["response"]["functionResponse"]["responseState"])
s = Stubber(configure.agent)
s.add_response("create_agent_action_group", {"agentActionGroup": {"agentId": "AGENT12345", "agentVersion": "DRAFT",
"actionGroupId": "AG1", "actionGroupName": "orders", "createdAt": "2026-10-10T00:00:00Z",
"updatedAt": "2026-10-10T00:00:00Z", "actionGroupState": "ENABLED"}})
with s:
configure.configure_tools(); print("action group request valid")
events = [{"returnControl": {"invocationId": "inv-1", "invocationInputs": [{"functionInvocationInput": {
"actionGroup": "orders", "function": "cancel_order", "actionInvocationType": "USER_CONFIRMATION",
"parameters": [{"name": "orderId", "type": "string", "value": "AB12CD34"}]}}]}}]
def user_says_no(fn, args):
return False
out = client.read_stream(events, user_says_no, {"sessionAttributes": {"tenantId": "acme"}})
print(out){'responseBody': {'TEXT': {'body': '{"orderId": "AB12CD34", "status": "SHIPPED", "totalCents": 4900}'}}}
{'responseBody': {'TEXT': {'body': '{"error": "orderId must be 8-32 characters A-Z, 0-9 or -"}'}}, 'responseState': 'REPROMPT'}
FAILURE
action group request valid
{'pending': {'invocationId': 'inv-1', 'returnControlInvocationResults': [{'functionResult': {'actionGroup': 'orders', 'function': 'cancel_order', 'confirmationState': 'DENY'}}], 'sessionAttributes': {'tenantId': 'acme'}}}The lookup returned only whitelisted fields (no cardLast4), an injected order ID was sent back for reprompting, a missing tenant failed closed, the action-group request matched the API model, and a user’s refusal produced a DENY result ready to send back. Add cases like these to CI for every action group.
Step 4: Guardrails and limits
- Guardrail: associate it with the agent through
guardrailConfiguration(identifier and version) on CreateAgent or UpdateAgent, then prepare the agent and point an alias at a new version. The prompt attack filter is meant for user input (AWS’s examples set its output strength toNONE). It is one layer: it does not replace Step 2 confirmation or the validation in your tools. Sensitive information filters can block or anonymize PII types such as card numbers and emails. - Idle timeout:
idleSessionTTLInSecondsaccepts 60 to 5,400 seconds in the current API. Shorter sessions mean less stale context. - Tool timeouts: keep Lambda timeouts close to what the lookup really needs, so a slow dependency fails fast instead of holding the conversation.
- Tracing: enable trace on
InvokeAgentin non-production and log the session ID, tenant, function, status and whether the guardrail intervened.
Step 5: Map the design to AgentCore
AWS’s migration guide recommends the AgentCore managed harness (a configuration-based agent where you declare model, tools and instructions) unless you need to own the orchestration loop, in which case you deploy a code-defined agent on AgentCore. The AgentCore CLI can import an existing Agents Classic configuration as a starting point. The guide maps the pieces like this:
| Agents Classic | AgentCore equivalent |
|---|---|
| Action groups (OpenAPI or function schema + Lambda) | Tools exposed through AgentCore gateway as MCP tools wrapping REST APIs, Lambda functions or code-level tools |
| Return of control and user confirmation | Inline function tools: the harness pauses and returns the tool call to your client |
| Session and memory configuration | AgentCore memory, short and long term, with configurable strategies |
| Guardrail on the agent | Guardrail configured in Bedrock, enforced through AgentCore gateway |
| Knowledge bases on the agent | Gateway-fronted knowledge base integration, or a retrieval tool |
| Stage-specific prompt overrides | Not directly replicated in the harness; system prompt plus your own logic, or a code-defined agent |
Because the tool Lambdas above validate their own input, take identity from the caller and whitelist their output, they can sit behind an AgentCore gateway with little change. The parts to redesign are the confirmation round trip in your client and how tenant identity reaches the tools, since sessionAttributes is an Agents Classic feature. Check the AgentCore documentation for the current details before you start; it’s a newer service and changing quickly.
Worked example: “cancel my last order”
- The web app calls
InvokeAgentwithtenantIdin session attributes and the user’s ID asmemoryId. - The agent calls
get_order; the Lambda validates the ID, reads the tenant’s row and returns status and total only. - The agent decides to call
cancel_order. Because confirmation is enabled, the app receivesreturnControland shows “Cancel order AB12CD34 (9.00)?”. - The user taps Cancel. The app sends
DENY; the Lambda is never invoked, and the agent replies that nothing was changed. - When the team later moves to AgentCore, the same two Lambdas become gateway tools and
cancel_orderbecomes an inline function tool with the same client-side confirmation.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
DependencyFailedException in the session | Lambda returned FAILURE or an invalid response shape | Match the documented response format; use REPROMPT for bad input |
| Agent ignores a new action group or guardrail | Agent not prepared, or alias still points to an old version | Run PrepareAgent, create a version, update the alias |
| Users see each other’s history | Same memoryId for different users | Pass a stable per-user ID |
| Confirmation never appears | requireConfirmation not set on that function, or the client ignores returnControl | Enable it per function; handle the event in your stream reader |
Production checklist
- ☐ One function per operation; parameters validated; responses whitelisted
- ☐ Tenant and user identity from application-set session attributes
- ☐ Confirmation on every function with side effects; client handles
returnControl - ☐ Guardrail associated with the agent and tested with real attack prompts
- ☐ Per-user
memoryId; memory retention set deliberately - ☐ Least-privilege Lambda roles; short timeouts; traces in non-production
- ☐ CI tests for injected parameters, missing tenant and denied confirmations
- ☐ Migration plan to AgentCore with an owner and a date
FAQ
Can I still create new Bedrock Agents?
Agents Classic isn’t open to new customers since July 30, 2026. Existing customers can continue to use it; for new agents, AWS points to AgentCore.
Do I have to migrate right away?
AWS describes Agents Classic as being in maintenance mode and recommends migrating. Plan it, but harden what’s running in the meantime; the controls here carry over.
Is a guardrail enough to stop prompt injection?
No. It’s one layer. Confirmation on risky actions, validation in tools and identity from your application are what limit the damage when an injection gets through.
Where should conversation history live?
Let the agent’s session and memory handle it. Don’t paste long transcripts into inputText on every call.
Official documentation
- Amazon Bedrock Agents Classic maintenance mode and migration
- Action group Lambda input and response formats
- User confirmation for action group functions
- Session and prompt session attributes
- Agent memory and associating a guardrail
- Guardrails prompt attack filter
Related on CheatCoders: Strict tool contracts with the Converse API · Idempotent tool calls with DynamoDB
Last updated on October 10, 2026
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.

Pingback: AI PR Reviewers: Diff-Scoped Context Windows That Catch Real Bugs - CheatCoders
Honestly, when I first hired an SEO consultant I expected some secret formula, but it turned out to be mostly research and analysis. They look at your site structure, keywords, backlinks, loading speed, and basically translate all that technical stuff into a plan you can actually follow. Mine also spent a lot of time just reading through analytics reports, which sounds boring but apparently matters a ton.
What surprised me most is how much of the job is just communication, explaining to clients why rankings don’t improve overnight and managing expectations. A lot of consultants also coordinate with whoever writes your content or builds your site, since SEO touches basically everything. Anyone here actually worked with one long term, was it worth the monthly cost?
Been doing my own SEO for years but finally brought in outside help when traffic plateaued. What surprised me was how much time he spent asking about my business goals before even touching keywords. Turns out half my problem wasn’t technical, it was just targeting the wrong audience the whole time.
My advice after going through this is don’t expect overnight miracles, good consultants will actually tell you that upfront. The ones who promise page one in a month are usually the ones you should avoid. Anyone here had a similar experience with consultants overpromising and underdelivering?
SEO consultant advice [url=https://yourhomeyourwayltd.co.uk/what-an-seo-consultant-actually-does-advice/#SEO-consultant-advice]https://yourhomeyourwayltd.co.uk/what-an-seo-consultant-actually-does-advice/[/url] .